|  | 
| 26 | 26 | 
 | 
| 27 | 27 | import org.springframework.core.io.buffer.DataBuffer; | 
| 28 | 28 | import org.springframework.core.io.buffer.DataBufferFactory; | 
|  | 29 | +import org.springframework.core.io.buffer.DataBufferUtils; | 
| 29 | 30 | import org.springframework.http.HttpStatus; | 
| 30 | 31 | import org.springframework.http.MediaType; | 
| 31 | 32 | import org.springframework.http.server.reactive.ServerHttpResponse; | 
|  | 
| 41 | 42 |  * data instead of query string data. | 
| 42 | 43 |  * | 
| 43 | 44 |  * @author Max Batischev | 
|  | 45 | + * @author Steve Riesenberg | 
| 44 | 46 |  * @since 6.5 | 
| 45 | 47 |  */ | 
| 46 |  | -public final class ServerFormPostRedirectStrategy implements ServerRedirectStrategy { | 
|  | 48 | +public final class FormPostServerRedirectStrategy implements ServerRedirectStrategy { | 
| 47 | 49 | 
 | 
| 48 | 50 | 	private static final String CONTENT_SECURITY_POLICY_HEADER = "Content-Security-Policy"; | 
| 49 | 51 | 
 | 
| 50 |  | -	private static final StringKeyGenerator DEFAULT_NONCE_GENERATOR = new Base64StringKeyGenerator( | 
| 51 |  | -			Base64.getUrlEncoder().withoutPadding(), 96); | 
| 52 |  | - | 
| 53 | 52 | 	private static final String REDIRECT_PAGE_TEMPLATE = """ | 
| 54 | 53 | 			<!DOCTYPE html> | 
| 55 | 54 | 			<html lang="en"> | 
| @@ -79,46 +78,46 @@ public final class ServerFormPostRedirectStrategy implements ServerRedirectStrat | 
| 79 | 78 | 			<input name="{{name}}" type="hidden" value="{{value}}" /> | 
| 80 | 79 | 			"""; | 
| 81 | 80 | 
 | 
|  | 81 | +	private static final StringKeyGenerator DEFAULT_NONCE_GENERATOR = new Base64StringKeyGenerator( | 
|  | 82 | +			Base64.getUrlEncoder().withoutPadding(), 96); | 
|  | 83 | + | 
| 82 | 84 | 	@Override | 
| 83 | 85 | 	public Mono<Void> sendRedirect(ServerWebExchange exchange, URI location) { | 
| 84 |  | -		String nonce = DEFAULT_NONCE_GENERATOR.generateKey(); | 
| 85 |  | -		String policyDirective = "script-src 'nonce-%s'".formatted(nonce); | 
| 86 |  | - | 
| 87 |  | -		ServerHttpResponse response = exchange.getResponse(); | 
| 88 |  | -		response.setStatusCode(HttpStatus.OK); | 
| 89 |  | -		response.getHeaders().setContentType(MediaType.TEXT_HTML); | 
| 90 |  | -		response.getHeaders().add(CONTENT_SECURITY_POLICY_HEADER, policyDirective); | 
| 91 |  | -		return response.writeWith(createBuffer(exchange, location, nonce)); | 
| 92 |  | -	} | 
|  | 86 | +		final UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUri(location); | 
| 93 | 87 | 
 | 
| 94 |  | -	private Mono<DataBuffer> createBuffer(ServerWebExchange exchange, URI location, String nonce) { | 
| 95 |  | -		byte[] bytes = createPage(location, nonce); | 
| 96 |  | -		DataBufferFactory bufferFactory = exchange.getResponse().bufferFactory(); | 
| 97 |  | -		return Mono.just(bufferFactory.wrap(bytes)); | 
| 98 |  | -	} | 
| 99 |  | - | 
| 100 |  | -	private byte[] createPage(URI location, String nonce) { | 
| 101 |  | -		UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUri(location); | 
| 102 |  | - | 
| 103 |  | -		StringBuilder hiddenInputsHtmlBuilder = new StringBuilder(); | 
|  | 88 | +		final StringBuilder hiddenInputsHtmlBuilder = new StringBuilder(); | 
| 104 | 89 | 		for (final Map.Entry<String, List<String>> entry : uriComponentsBuilder.build().getQueryParams().entrySet()) { | 
| 105 | 90 | 			final String name = entry.getKey(); | 
| 106 | 91 | 			for (final String value : entry.getValue()) { | 
| 107 | 92 | 				// @formatter:off | 
| 108 | 93 | 				final String hiddenInput = HIDDEN_INPUT_TEMPLATE | 
| 109 |  | -						.replace("{{name}}", HtmlUtils.htmlEscape(name)) | 
| 110 |  | -						.replace("{{value}}", HtmlUtils.htmlEscape(value)); | 
|  | 94 | +					.replace("{{name}}", HtmlUtils.htmlEscape(name)) | 
|  | 95 | +					.replace("{{value}}", HtmlUtils.htmlEscape(value)); | 
| 111 | 96 | 				// @formatter:on | 
| 112 | 97 | 				hiddenInputsHtmlBuilder.append(hiddenInput.trim()); | 
| 113 | 98 | 			} | 
| 114 | 99 | 		} | 
|  | 100 | + | 
|  | 101 | +		// Create the script-src policy directive for the Content-Security-Policy header | 
|  | 102 | +		final String nonce = DEFAULT_NONCE_GENERATOR.generateKey(); | 
|  | 103 | +		final String policyDirective = "script-src 'nonce-%s'".formatted(nonce); | 
|  | 104 | + | 
| 115 | 105 | 		// @formatter:off | 
| 116 |  | -		return REDIRECT_PAGE_TEMPLATE | 
| 117 |  | -				.replace("{{action}}", HtmlUtils.htmlEscape(uriComponentsBuilder.query(null).build().toUriString())) | 
| 118 |  | -				.replace("{{params}}", hiddenInputsHtmlBuilder.toString()) | 
| 119 |  | -				.replace("{{nonce}}", HtmlUtils.htmlEscape(nonce)) | 
| 120 |  | -				.getBytes(StandardCharsets.UTF_8); | 
|  | 106 | +		final String html = REDIRECT_PAGE_TEMPLATE | 
|  | 107 | +			// Clear the query string as we don't want that to be part of the form action URL | 
|  | 108 | +			.replace("{{action}}", HtmlUtils.htmlEscape(uriComponentsBuilder.query(null).build().toUriString())) | 
|  | 109 | +			.replace("{{params}}", hiddenInputsHtmlBuilder.toString()) | 
|  | 110 | +			.replace("{{nonce}}", HtmlUtils.htmlEscape(nonce)); | 
| 121 | 111 | 		// @formatter:on | 
|  | 112 | + | 
|  | 113 | +		final ServerHttpResponse response = exchange.getResponse(); | 
|  | 114 | +		response.setStatusCode(HttpStatus.OK); | 
|  | 115 | +		response.getHeaders().setContentType(MediaType.TEXT_HTML); | 
|  | 116 | +		response.getHeaders().set(CONTENT_SECURITY_POLICY_HEADER, policyDirective); | 
|  | 117 | + | 
|  | 118 | +		final DataBufferFactory bufferFactory = response.bufferFactory(); | 
|  | 119 | +		final DataBuffer buffer = bufferFactory.wrap(html.getBytes(StandardCharsets.UTF_8)); | 
|  | 120 | +		return response.writeWith(Mono.just(buffer)).doOnError((error) -> DataBufferUtils.release(buffer)); | 
| 122 | 121 | 	} | 
| 123 | 122 | 
 | 
| 124 | 123 | } | 
0 commit comments