Redirect browsers to https when forceSsl is set

This commit extends the forceSsl support to redirect any HTML content
to https. Practically speaking, this allows to redirect all browsers to
https when they land on the main page using https. Serving traffic via
http is still allowed as preventing this would break a lot of
existing clients.

To allow to easily run the app locally, forceSsl is false and must be
enabled for any production environment.

Closes gh-473
This commit is contained in:
Stephane Nicoll
2018-07-27 16:56:30 +02:00
parent 9b098a8078
commit e208a9b1f1
18 changed files with 633 additions and 26 deletions

View File

@@ -25,6 +25,8 @@ import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import javax.servlet.http.HttpServletRequest;
import com.samskivert.mustache.Mustache;
import io.spring.initializr.generator.BasicProjectRequest;
import io.spring.initializr.generator.CommandLineHelpGenerator;
@@ -65,6 +67,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.resource.ResourceUrlProvider;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
/**
* The main initializr controller provides access to the configured metadata and serves as
@@ -219,7 +222,12 @@ public class MainController extends AbstractInitializrController {
}
@RequestMapping(path = "/", produces = "text/html")
public String home(Map<String, Object> model) {
public String home(HttpServletRequest request, Map<String, Object> model) {
if (isForceSsl() && !request.isSecure()) {
String securedUrl = ServletUriComponentsBuilder.fromCurrentRequest()
.scheme("https").build().toUriString();
return "redirect:" + securedUrl;
}
renderHome(model);
return "home";
}

View File

@@ -29,6 +29,8 @@ import org.springframework.test.context.ActiveProfiles;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests with custom environment.
*
* @author Stephane Nicoll
*/
@ActiveProfiles({ "test-default", "test-custom-env" })
@@ -44,14 +46,6 @@ public class MainControllerEnvIntegrationTests
assertThat(entity.getHeaders().getLocation()).isEqualTo(new URI(expected));
}
@Test
public void doNotForceSsl() {
ResponseEntity<String> response = invokeHome("curl/1.2.4", "*/*");
String body = response.getBody();
assertThat(body).as("Must not force https").contains("http://start.spring.io/");
assertThat(body).as("Must not force https").doesNotContain("https://");
}
@Test
public void generateProjectWithInvalidName() {
downloadZip("/starter.zip?style=data-jpa&name=Invalid")

View File

@@ -305,6 +305,14 @@ public class MainControllerIntegrationTests
validateCurrentMetadata(getMetadataJson());
}
@Test
public void doNotForceSslByDefault() {
ResponseEntity<String> response = invokeHome("curl/1.2.4", "*/*");
String body = response.getBody();
assertThat(body).as("Must not force https").contains("http://start.spring.io/");
assertThat(body).as("Must not force https").doesNotContain("https://");
}
private void validateCurlHelpContent(ResponseEntity<String> response) {
validateContentType(response, MediaType.TEXT_PLAIN);
assertThat(response.getHeaders().getFirst(HttpHeaders.ETAG)).isNotNull();

View File

@@ -0,0 +1,76 @@
/*
* Copyright 2012-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.spring.initializr.web.project;
import java.net.URI;
import io.spring.initializr.web.AbstractInitializrControllerIntegrationTests;
import io.spring.initializr.web.mapper.InitializrMetadataVersion;
import org.junit.Test;
import org.skyscreamer.jsonassert.JSONCompareMode;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.ActiveProfiles;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests with {@code forceSsl} enabled.
*
* @author Stephane Nicoll
*/
@ActiveProfiles({ "test-default", "test-ssl" })
public class MainControllerSslIntegrationTests
extends AbstractInitializrControllerIntegrationTests {
@Test
public void mainPageRedirectsToHttps() {
ResponseEntity<Void> request = execute("/", Void.class, null,
MediaType.TEXT_HTML_VALUE);
assertThat(request.getStatusCode()).isEqualTo(HttpStatus.FOUND);
// mock tests with start.spring.io host
assertThat(request.getHeaders().getLocation())
.isEqualTo(URI.create("https://start.spring.io/"));
}
@Test
public void forceSsl() {
ResponseEntity<String> response = invokeHome("curl/1.2.4", "*/*");
String body = response.getBody();
assertThat(body).as("Must force https").contains("https://start.spring.io/");
assertThat(body).as("Must force https").doesNotContain("http://");
}
@Test
public void forceSslInMetadata() {
ResponseEntity<String> response = invokeHome(null,
"application/vnd.initializr.v2.1+json");
validateMetadata(response, InitializrMetadataVersion.V2_1.getMediaType(),
"2.1.0-ssl", JSONCompareMode.STRICT);
}
@Test
public void forceSslInMetadataV2() {
ResponseEntity<String> response = invokeHome(null,
"application/vnd.initializr.v2+json");
validateMetadata(response, InitializrMetadataVersion.V2.getMediaType(),
"2.0.0-ssl", JSONCompareMode.STRICT);
}
}

View File

@@ -32,7 +32,7 @@
"configuration": {"env": {
"artifactRepository": "https://repo.spring.io/release/",
"fallbackApplicationName": "Application",
"forceSsl": true,
"forceSsl": false,
"gradle": {
"dependencyManagementPluginVersion": "1.0.0.RELEASE"
},

View File

@@ -0,0 +1,194 @@
{
"_links": {
"maven-build": {
"href": "https://@host@/pom.xml?type=maven-build{&dependencies,packaging,javaVersion,language,bootVersion,groupId,artifactId,version,name,description,packageName}",
"templated": true
},
"maven-project": {
"href": "https://@host@/starter.zip?type=maven-project{&dependencies,packaging,javaVersion,language,bootVersion,groupId,artifactId,version,name,description,packageName}",
"templated": true
},
"gradle-build": {
"href": "https://@host@/build.gradle?type=gradle-build{&dependencies,packaging,javaVersion,language,bootVersion,groupId,artifactId,version,name,description,packageName}",
"templated": true
},
"gradle-project": {
"href": "https://@host@/starter.zip?type=gradle-project{&dependencies,packaging,javaVersion,language,bootVersion,groupId,artifactId,version,name,description,packageName}",
"templated": true
}
},
"dependencies": {
"type": "hierarchical-multi-select",
"values": [
{
"name": "Core",
"values": [
{
"id": "web",
"name": "Web",
"description": "Web dependency description"
},
{
"id": "security",
"name": "Security"
},
{
"id": "data-jpa",
"name": "Data JPA"
}
]
},
{
"name": "Other",
"values": [
{
"id": "org.acme:foo",
"name": "Foo"
},
{
"id": "org.acme:bar",
"name": "Bar"
},
{
"id": "my-api",
"name": "My API"
}
]
}
]
},
"type": {
"type": "action",
"default": "maven-project",
"values": [
{
"id": "maven-build",
"name": "Maven POM",
"action": "/pom.xml",
"tags": {
"build": "maven",
"format": "build"
}
},
{
"id": "maven-project",
"name": "Maven Project",
"action": "/starter.zip",
"tags": {
"build": "maven",
"format": "project"
}
},
{
"id": "gradle-build",
"name": "Gradle Config",
"action": "/build.gradle",
"tags": {
"build": "gradle",
"format": "build"
}
},
{
"id": "gradle-project",
"name": "Gradle Project",
"action": "/starter.zip",
"tags": {
"build": "gradle",
"format": "project"
}
}
]
},
"packaging": {
"type": "single-select",
"default": "jar",
"values": [
{
"id": "jar",
"name": "Jar"
},
{
"id": "war",
"name": "War"
}
]
},
"javaVersion": {
"type": "single-select",
"default": "1.8",
"values": [
{
"id": "1.6",
"name": "1.6"
},
{
"id": "1.7",
"name": "1.7"
},
{
"id": "1.8",
"name": "1.8"
}
]
},
"language": {
"type": "single-select",
"default": "java",
"values": [
{
"id": "groovy",
"name": "Groovy"
},
{
"id": "java",
"name": "Java"
},
{
"id": "kotlin",
"name": "Kotlin"
}
]
},
"bootVersion": {
"type": "single-select",
"default": "1.1.4.RELEASE",
"values": [
{
"id": "1.2.0.BUILD-SNAPSHOT",
"name": "Latest SNAPSHOT"
},
{
"id": "1.1.4.RELEASE",
"name": "1.1.4"
},
{
"id": "1.0.2.RELEASE",
"name": "1.0.2"
}
]
},
"groupId": {
"type": "text",
"default": "com.example"
},
"artifactId": {
"type": "text",
"default": "demo"
},
"version": {
"type": "text",
"default": "0.0.1-SNAPSHOT"
},
"name": {
"type": "text",
"default": "demo"
},
"description": {
"type": "text",
"default": "Demo project for Spring Boot"
},
"packageName": {
"type": "text",
"default": "com.example.demo"
}
}

View File

@@ -1,19 +1,19 @@
{
"_links": {
"maven-build": {
"href": "https://@host@/pom.xml?type=maven-build{&dependencies,packaging,javaVersion,language,bootVersion,groupId,artifactId,version,name,description,packageName}",
"href": "http://@host@/pom.xml?type=maven-build{&dependencies,packaging,javaVersion,language,bootVersion,groupId,artifactId,version,name,description,packageName}",
"templated": true
},
"maven-project": {
"href": "https://@host@/starter.zip?type=maven-project{&dependencies,packaging,javaVersion,language,bootVersion,groupId,artifactId,version,name,description,packageName}",
"href": "http://@host@/starter.zip?type=maven-project{&dependencies,packaging,javaVersion,language,bootVersion,groupId,artifactId,version,name,description,packageName}",
"templated": true
},
"gradle-build": {
"href": "https://@host@/build.gradle?type=gradle-build{&dependencies,packaging,javaVersion,language,bootVersion,groupId,artifactId,version,name,description,packageName}",
"href": "http://@host@/build.gradle?type=gradle-build{&dependencies,packaging,javaVersion,language,bootVersion,groupId,artifactId,version,name,description,packageName}",
"templated": true
},
"gradle-project": {
"href": "https://@host@/starter.zip?type=gradle-project{&dependencies,packaging,javaVersion,language,bootVersion,groupId,artifactId,version,name,description,packageName}",
"href": "http://@host@/starter.zip?type=gradle-project{&dependencies,packaging,javaVersion,language,bootVersion,groupId,artifactId,version,name,description,packageName}",
"templated": true
}
},

View File

@@ -0,0 +1,232 @@
{
"_links": {
"dependencies": {
"href": "https://@host@/dependencies{?bootVersion}",
"templated": true
},
"maven-build": {
"href": "https://@host@/pom.xml?type=maven-build{&dependencies,packaging,javaVersion,language,bootVersion,groupId,artifactId,version,name,description,packageName}",
"templated": true
},
"maven-project": {
"href": "https://@host@/starter.zip?type=maven-project{&dependencies,packaging,javaVersion,language,bootVersion,groupId,artifactId,version,name,description,packageName}",
"templated": true
},
"gradle-build": {
"href": "https://@host@/build.gradle?type=gradle-build{&dependencies,packaging,javaVersion,language,bootVersion,groupId,artifactId,version,name,description,packageName}",
"templated": true
},
"gradle-project": {
"href": "https://@host@/starter.zip?type=gradle-project{&dependencies,packaging,javaVersion,language,bootVersion,groupId,artifactId,version,name,description,packageName}",
"templated": true
}
},
"dependencies": {
"type": "hierarchical-multi-select",
"values": [
{
"name": "Core",
"values": [
{
"id": "web",
"name": "Web",
"description": "Web dependency description",
"_links": {
"guide": {
"href": "https://example.com/guide",
"title": "Building a RESTful Web Service"
},
"reference": {
"href": "https://example.com/doc"
}
}
},
{
"id": "security",
"name": "Security"
},
{
"id": "data-jpa",
"name": "Data JPA"
}
]
},
{
"name": "Other",
"values": [
{
"id": "org.acme:foo",
"name": "Foo",
"_links": {
"guide": [
{
"href": "https://example.com/guide1"
},
{
"href": "https://example.com/guide2",
"title": "Some guide for foo"
}
],
"reference": {
"href": "https://example.com/{bootVersion}/doc",
"templated": true
}
}
},
{
"id": "org.acme:bar",
"name": "Bar"
},
{
"id": "org.acme:biz",
"name": "Biz",
"versionRange": "1.2.0.BUILD-SNAPSHOT"
},
{
"id": "org.acme:bur",
"name": "Bur",
"versionRange": "[1.1.4.RELEASE,1.2.0.BUILD-SNAPSHOT)"
},
{
"id": "my-api",
"name": "My API"
}
]
}
]
},
"type": {
"type": "action",
"default": "maven-project",
"values": [
{
"id": "maven-build",
"name": "Maven POM",
"action": "/pom.xml",
"tags": {
"build": "maven",
"format": "build"
}
},
{
"id": "maven-project",
"name": "Maven Project",
"action": "/starter.zip",
"tags": {
"build": "maven",
"format": "project"
}
},
{
"id": "gradle-build",
"name": "Gradle Config",
"action": "/build.gradle",
"tags": {
"build": "gradle",
"format": "build"
}
},
{
"id": "gradle-project",
"name": "Gradle Project",
"action": "/starter.zip",
"tags": {
"build": "gradle",
"format": "project"
}
}
]
},
"packaging": {
"type": "single-select",
"default": "jar",
"values": [
{
"id": "jar",
"name": "Jar"
},
{
"id": "war",
"name": "War"
}
]
},
"javaVersion": {
"type": "single-select",
"default": "1.8",
"values": [
{
"id": "1.6",
"name": "1.6"
},
{
"id": "1.7",
"name": "1.7"
},
{
"id": "1.8",
"name": "1.8"
}
]
},
"language": {
"type": "single-select",
"default": "java",
"values": [
{
"id": "groovy",
"name": "Groovy"
},
{
"id": "java",
"name": "Java"
},
{
"id": "kotlin",
"name": "Kotlin"
}
]
},
"bootVersion": {
"type": "single-select",
"default": "1.1.4.RELEASE",
"values": [
{
"id": "1.2.0.BUILD-SNAPSHOT",
"name": "Latest SNAPSHOT"
},
{
"id": "1.1.4.RELEASE",
"name": "1.1.4"
},
{
"id": "1.0.2.RELEASE",
"name": "1.0.2"
}
]
},
"groupId": {
"type": "text",
"default": "com.example"
},
"artifactId": {
"type": "text",
"default": "demo"
},
"version": {
"type": "text",
"default": "0.0.1-SNAPSHOT"
},
"name": {
"type": "text",
"default": "demo"
},
"description": {
"type": "text",
"default": "Demo project for Spring Boot"
},
"packageName": {
"type": "text",
"default": "com.example.demo"
}
}

View File

@@ -1,23 +1,23 @@
{
"_links": {
"dependencies": {
"href": "https://@host@/dependencies{?bootVersion}",
"href": "http://@host@/dependencies{?bootVersion}",
"templated": true
},
"maven-build": {
"href": "https://@host@/pom.xml?type=maven-build{&dependencies,packaging,javaVersion,language,bootVersion,groupId,artifactId,version,name,description,packageName}",
"href": "http://@host@/pom.xml?type=maven-build{&dependencies,packaging,javaVersion,language,bootVersion,groupId,artifactId,version,name,description,packageName}",
"templated": true
},
"maven-project": {
"href": "https://@host@/starter.zip?type=maven-project{&dependencies,packaging,javaVersion,language,bootVersion,groupId,artifactId,version,name,description,packageName}",
"href": "http://@host@/starter.zip?type=maven-project{&dependencies,packaging,javaVersion,language,bootVersion,groupId,artifactId,version,name,description,packageName}",
"templated": true
},
"gradle-build": {
"href": "https://@host@/build.gradle?type=gradle-build{&dependencies,packaging,javaVersion,language,bootVersion,groupId,artifactId,version,name,description,packageName}",
"href": "http://@host@/build.gradle?type=gradle-build{&dependencies,packaging,javaVersion,language,bootVersion,groupId,artifactId,version,name,description,packageName}",
"templated": true
},
"gradle-project": {
"href": "https://@host@/starter.zip?type=gradle-project{&dependencies,packaging,javaVersion,language,bootVersion,groupId,artifactId,version,name,description,packageName}",
"href": "http://@host@/starter.zip?type=gradle-project{&dependencies,packaging,javaVersion,language,bootVersion,groupId,artifactId,version,name,description,packageName}",
"templated": true
}
},