From f2faf84f317fa01d7316cb77c8405c30570085cf Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Thu, 25 Aug 2016 14:21:25 +0200 Subject: [PATCH] Add RFC5987 support for HTTP header field params This commit adds support for HTTP header field parameters encoding, as described in RFC5987. Note that the default implementation still relies on US-ASCII encoding, as the latest rfc7230 Section 3.2.4 says that: > Newly defined header fields SHOULD limit their field values to US-ASCII octets Issue: SPR-14547 --- .../org/springframework/util/StringUtils.java | 43 +++++++++++++++++++ .../util/StringUtilsTests.java | 18 +++++++- .../org/springframework/http/HttpHeaders.java | 25 ++++++++++- .../http/HttpHeadersTests.java | 5 +++ 4 files changed, 88 insertions(+), 3 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/util/StringUtils.java b/spring-core/src/main/java/org/springframework/util/StringUtils.java index 8fd7b8b178..72e81e4a79 100644 --- a/spring-core/src/main/java/org/springframework/util/StringUtils.java +++ b/spring-core/src/main/java/org/springframework/util/StringUtils.java @@ -16,6 +16,8 @@ package org.springframework.util; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -50,6 +52,7 @@ import java.util.TimeZone; * @author Rick Evans * @author Arjen Poutsma * @author Sam Brannen + * @author Brian Clozel * @since 16 April 2001 */ public abstract class StringUtils { @@ -1193,4 +1196,44 @@ public abstract class StringUtils { return arrayToDelimitedString(arr, ","); } + /** + * Encode the given header field param as describe in the rfc5987. + * @param input the header field param + * @param charset the charset of the header field param string + * @return the encoded header field param + * @see rfc5987 + * @since 5.0 + */ + public static String encodeHttpHeaderFieldParam(String input, Charset charset) { + Assert.notNull(charset, "charset should not be null"); + if(StandardCharsets.US_ASCII.equals(charset)) { + return input; + } + Assert.isTrue(StandardCharsets.UTF_8.equals(charset) || StandardCharsets.ISO_8859_1.equals(charset), + "charset should be UTF-8 or ISO-8859-1"); + final byte[] source = input.getBytes(charset); + final int len = source.length; + final StringBuilder sb = new StringBuilder(len << 1); + sb.append(charset.name()); + sb.append("''"); + for (byte b : source) { + if (isRFC5987AttrChar(b)) { + sb.append((char) b); + } + else { + sb.append('%'); + char hex1 = Character.toUpperCase(Character.forDigit((b >> 4) & 0xF, 16)); + char hex2 = Character.toUpperCase(Character.forDigit(b & 0xF, 16)); + sb.append(hex1); + sb.append(hex2); + } + } + return sb.toString(); + } + + private static boolean isRFC5987AttrChar(byte c) { + return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') + || c == '!' || c == '#' || c == '$' || c == '&' || c == '+' || c == '-' + || c == '.' || c == '^' || c == '_' || c == '`' || c == '|' || c == '~'; + } } diff --git a/spring-core/src/test/java/org/springframework/util/StringUtilsTests.java b/spring-core/src/test/java/org/springframework/util/StringUtilsTests.java index 22e979e455..2b574aaca5 100644 --- a/spring-core/src/test/java/org/springframework/util/StringUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/util/StringUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 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. @@ -16,6 +16,7 @@ package org.springframework.util; +import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Locale; import java.util.Properties; @@ -700,4 +701,19 @@ public class StringUtilsTests { assertEquals("Variant containing country code not extracted correctly", variant, locale.getVariant()); } + // SPR-14547 + @Test + public void encodeHttpHeaderFieldParam() { + String result = StringUtils.encodeHttpHeaderFieldParam("test.txt", StandardCharsets.US_ASCII); + assertEquals("test.txt", result); + + result = StringUtils.encodeHttpHeaderFieldParam("中文.txt", StandardCharsets.UTF_8); + assertEquals("UTF-8''%E4%B8%AD%E6%96%87.txt", result); + } + + @Test(expected = IllegalArgumentException.class) + public void encodeHttpHeaderFieldParamInvalidCharset() { + StringUtils.encodeHttpHeaderFieldParam("test", StandardCharsets.UTF_16); + } + } diff --git a/spring-web/src/main/java/org/springframework/http/HttpHeaders.java b/spring-web/src/main/java/org/springframework/http/HttpHeaders.java index 664f658348..994437acde 100644 --- a/spring-web/src/main/java/org/springframework/http/HttpHeaders.java +++ b/spring-web/src/main/java/org/springframework/http/HttpHeaders.java @@ -19,6 +19,7 @@ package org.springframework.http; import java.io.Serializable; import java.net.URI; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; @@ -672,12 +673,32 @@ public class HttpHeaders implements MultiValueMap, Serializable * @param filename the filename (may be {@code null}) */ public void setContentDispositionFormData(String name, String filename) { + setContentDispositionFormData(name, filename, null); + } + + /** + * Set the (new) value of the {@code Content-Disposition} header + * for {@code form-data}, optionally encoding the filename using the rfc5987. + *

Only the US-ASCII, UTF-8 and ISO-8859-1 charsets are supported. + * @param name the control name + * @param filename the filename (may be {@code null}) + * @param charset the charset used for the filename (may be {@code null}) + * @see rfc7230 Section 3.2.4 + * @since 5.0 + */ + public void setContentDispositionFormData(String name, String filename, Charset charset) { Assert.notNull(name, "'name' must not be null"); StringBuilder builder = new StringBuilder("form-data; name=\""); builder.append(name).append('\"'); if (filename != null) { - builder.append("; filename=\""); - builder.append(filename).append('\"'); + if(charset == null || StandardCharsets.US_ASCII.equals(charset)) { + builder.append("; filename=\""); + builder.append(filename).append('\"'); + } + else { + builder.append("; filename*="); + builder.append(StringUtils.encodeHttpHeaderFieldParam(filename, charset)); + } } set(CONTENT_DISPOSITION, builder.toString()); } diff --git a/spring-web/src/test/java/org/springframework/http/HttpHeadersTests.java b/spring-web/src/test/java/org/springframework/http/HttpHeadersTests.java index 0e7a89ceac..a22f89c70c 100644 --- a/spring-web/src/test/java/org/springframework/http/HttpHeadersTests.java +++ b/spring-web/src/test/java/org/springframework/http/HttpHeadersTests.java @@ -321,6 +321,11 @@ public class HttpHeadersTests { headers.setContentDispositionFormData("name", "filename"); assertEquals("Invalid Content-Disposition header", "form-data; name=\"name\"; filename=\"filename\"", headers.getFirst("Content-Disposition")); + + headers.setContentDispositionFormData("name", "中文.txt", StandardCharsets.UTF_8); + assertEquals("Invalid Content-Disposition header", + "form-data; name=\"name\"; filename*=UTF-8''%E4%B8%AD%E6%96%87.txt", + headers.getFirst("Content-Disposition")); } @Test // SPR-11917