Skip to content

Commit

Permalink
Allow non-ascii header values & add utf-8 filename fallback (#35060)
Browse files Browse the repository at this point in the history
Summary:
Fix #31537: [Android] React Native strips non-ASCII characters from HTTP headers

## Changelog

<!-- Help reviewers and the release process by writing your own changelog entry. For an example, see:
https://reactnative.dev/contributing/changelogs-in-pull-requests
-->

[Android] [Changed] - Allow non-ascii header values on Android and add utf-8 filename fallback in FormData

Pull Request resolved: #35060

Test Plan:
1. Clone the `react-native` repo.
2. Build the rn-tester app.
3. Prepare tests
   1. Add `android:usesCleartextTraffic="true"` to AndroidManifest.xml
   2. Use the following code as a server:
       ```javascript
		const http = require('http');

		const requestListener = function (req, res) {
		    // raw header value
		    console.log(req.headers['content-disposition']);
		    // nodejs assumes the header value is ISO-8859-1 encoded
		    console.log(Buffer.from(req.headers['content-disposition'], 'latin1').toString('utf-8'));
		    // decode encoded header value if it's sent as UTF-8
		    console.log(decodeURI(req.headers['content-disposition']));
		    res.writeHead(200);
		    res.end();
		};

		const server = http.createServer(requestListener);
		server.listen(3000);
       ```
  	3. Run `adb reverse tcp:3000 tcp:3000` to connect the 3000 port on the emulator if necessary.
4. Edit `RNTesterAppShared.js` to include test code:
    ```javascript
	  useEffect(() => {
	    fetch('http://localhost:3000/', {
	      headers: {
	        'Content-Type': 'multipart/form-data; charset=utf-8',
	        'Content-Disposition': `attachment; filename*=utf-8''${encodeURI(
	          'filename测试abc.jpg',
	        )}`,
	      },
	    }).then(res => {
	      console.log(res.ok);
	    });
	    fetch('http://localhost:3000/', {
	      headers: {
	        'Content-Type': 'multipart/form-data; charset=utf-8',
	        'Content-Disposition': `attachment; filename="filename测试abc.jpg"`,
	      },
	    }).then(res => {
	      console.log(res.ok);
	    });
	  }, []);
    ```
5. Both requests should succeed; without the fix, the second request received by the server will not have the utf-8 characters "测试" in the header value.

Reviewed By: NickGerleman

Differential Revision: D40639985

Pulled By: cortinico

fbshipit-source-id: 005f2481976046a92a26239ad704780ac58d4a44
  • Loading branch information
robertying authored and facebook-github-bot committed Sep 26, 2023
1 parent 8b2f324 commit 7c7e9e6
Show file tree
Hide file tree
Showing 6 changed files with 33 additions and 45 deletions.
4 changes: 3 additions & 1 deletion packages/react-native/Libraries/Network/FormData.js
Expand Up @@ -82,7 +82,9 @@ class FormData {
// content type (cf. web Blob interface.)
if (typeof value === 'object' && !Array.isArray(value) && value) {
if (typeof value.name === 'string') {
headers['content-disposition'] += '; filename="' + value.name + '"';
headers['content-disposition'] += `; filename="${
value.name
}"; filename*=utf-8''${encodeURI(value.name)}`;
}
if (typeof value.type === 'string') {
headers['content-type'] = value.type;
Expand Down
Expand Up @@ -48,7 +48,29 @@ describe('FormData', function () {
type: 'image/jpeg',
name: 'photo.jpg',
headers: {
'content-disposition': 'form-data; name="photo"; filename="photo.jpg"',
'content-disposition':
'form-data; name="photo"; filename="photo.jpg"; filename*=utf-8\'\'photo.jpg',
'content-type': 'image/jpeg',
},
fieldName: 'photo',
};
expect(formData.getParts()[0]).toMatchObject(expectedPart);
});

it('should return blob with the correct utf-8 handling', function () {
formData.append('photo', {
uri: 'arbitrary/path',
type: 'image/jpeg',
name: '测试photo.jpg',
});

const expectedPart = {
uri: 'arbitrary/path',
type: 'image/jpeg',
name: '测试photo.jpg',
headers: {
'content-disposition':
'form-data; name="photo"; filename="测试photo.jpg"; filename*=utf-8\'\'%E6%B5%8B%E8%AF%95photo.jpg',
'content-type': 'image/jpeg',
},
fieldName: 'photo',
Expand Down
Expand Up @@ -28,18 +28,4 @@ public static String stripHeaderName(String name) {
}
return modified ? builder.toString() : name;
}

public static String stripHeaderValue(String value) {
StringBuilder builder = new StringBuilder(value.length());
boolean modified = false;
for (int i = 0, length = value.length(); i < length; i++) {
char c = value.charAt(i);
if ((c > '\u001f' && c < '\u007f') || c == '\t') {
builder.append(c);
} else {
modified = true;
}
}
return modified ? builder.toString() : value;
}
}
Expand Up @@ -761,11 +761,11 @@ public void removeListeners(double count) {}
return null;
}
String headerName = HeaderUtil.stripHeaderName(header.getString(0));
String headerValue = HeaderUtil.stripHeaderValue(header.getString(1));
String headerValue = header.getString(1);
if (headerName == null || headerValue == null) {
return null;
}
headersBuilder.add(headerName, headerValue);
headersBuilder.addUnsafeNonAscii(headerName, headerValue);
}
if (headersBuilder.get(USER_AGENT_HEADER_NAME) == null && mDefaultUserAgent != null) {
headersBuilder.add(USER_AGENT_HEADER_NAME, mDefaultUserAgent);
Expand Down
Expand Up @@ -27,46 +27,21 @@ class HeaderUtilTest {
assertEquals(ALPHABET_TEST, HeaderUtil.stripHeaderName(ALPHABET_TEST))
}

@Test
fun valueStripKeepsLetters() {
assertEquals(ALPHABET_TEST, HeaderUtil.stripHeaderValue(ALPHABET_TEST))
}

@Test
fun nameStripKeepsNumbers() {
assertEquals(NUMBERS_TEST, HeaderUtil.stripHeaderName(NUMBERS_TEST))
}

@Test
fun valueStripKeepsNumbers() {
assertEquals(NUMBERS_TEST, HeaderUtil.stripHeaderValue(NUMBERS_TEST))
}

@Test
fun valueStripKeepsSpecials() {
assertEquals(SPECIALS_TEST, HeaderUtil.stripHeaderValue(SPECIALS_TEST))
}

@Test
fun nameStripKeepsSpecials() {
assertEquals(SPECIALS_TEST, HeaderUtil.stripHeaderName(SPECIALS_TEST))
}

@Test
fun valueStripKeepsTabs() {
assertEquals(TABULATION_TEST, HeaderUtil.stripHeaderValue(TABULATION_TEST))
}

@Test
fun nameStripDeletesTabs() {
assertEquals(TABULATION_STRIP_EXPECTED, HeaderUtil.stripHeaderName(TABULATION_TEST))
}

@Test
fun valueStripRemovesExtraSymbols() {
assertEquals(BANNED_TEST_EXPECTED, HeaderUtil.stripHeaderValue(VALUE_BANNED_SYMBOLS_TEST))
}

@Test
fun nameStripRemovesExtraSymbols() {
assertEquals(BANNED_TEST_EXPECTED, HeaderUtil.stripHeaderName(NAME_BANNED_SYMBOLS_TEST))
Expand Down
Expand Up @@ -482,7 +482,9 @@ public Object answer(InvocationOnMock invocation) throws Throwable {
JavaOnlyArray.from(
Arrays.asList(
JavaOnlyArray.of("content-type", "image/jpg"),
JavaOnlyArray.of("content-disposition", "filename=photo.jpg"))));
JavaOnlyArray.of(
"content-disposition",
"filename=\"测试photo.jpg\"; filename*=utf-8''%E6%B5%8B%E8%AF%95photo.jpg"))));
formData.pushMap(imageBodyPart);

mNetworkingModule.sendRequest(
Expand Down Expand Up @@ -521,7 +523,8 @@ public Object answer(InvocationOnMock invocation) throws Throwable {
assertThat(bodyHeaders.get(0).get("content-disposition")).isEqualTo("user");
assertThat(bodyRequestBody.get(0).contentType()).isNull();
assertThat(bodyRequestBody.get(0).contentLength()).isEqualTo("locale".getBytes().length);
assertThat(bodyHeaders.get(1).get("content-disposition")).isEqualTo("filename=photo.jpg");
assertThat(bodyHeaders.get(1).get("content-disposition"))
.isEqualTo("filename=\"测试photo.jpg\"; filename*=utf-8''%E6%B5%8B%E8%AF%95photo.jpg");
assertThat(bodyRequestBody.get(1).contentType()).isEqualTo(MediaType.parse("image/jpg"));
assertThat(bodyRequestBody.get(1).contentLength()).isEqualTo("imageUri".getBytes().length);
}
Expand Down

0 comments on commit 7c7e9e6

Please sign in to comment.