From d14c7384ee1d3b611f1f859fa9c11816de98a362 Mon Sep 17 00:00:00 2001 From: Simon Stewart Date: Tue, 9 Jul 2019 14:45:16 +0100 Subject: [PATCH] Provide abstraction for dealing with form encoded http requests --- .../selenium/remote/http/FormEncodedData.java | 100 ++++++++++++ .../remote/http/FormEncodedDataTest.java | 153 ++++++++++++++++++ 2 files changed, 253 insertions(+) create mode 100644 java/client/src/org/openqa/selenium/remote/http/FormEncodedData.java create mode 100644 java/client/test/org/openqa/selenium/remote/http/FormEncodedDataTest.java diff --git a/java/client/src/org/openqa/selenium/remote/http/FormEncodedData.java b/java/client/src/org/openqa/selenium/remote/http/FormEncodedData.java new file mode 100644 index 0000000000000..0bdaaefb3a674 --- /dev/null +++ b/java/client/src/org/openqa/selenium/remote/http/FormEncodedData.java @@ -0,0 +1,100 @@ +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you 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 org.openqa.selenium.remote.http; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.LinkedListMultimap; +import com.google.common.collect.ListMultimap; +import com.google.common.net.MediaType; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.UncheckedIOException; +import java.net.URLDecoder; +import java.nio.charset.Charset; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; + +public class FormEncodedData { + + public static Optional>> getData(HttpRequest request) { + try { + String contentType = request.getHeader("Content-Type"); + MediaType type = MediaType.parse(contentType); + if (!type.is(MediaType.FORM_DATA)) { + return Optional.empty(); + } + } catch (IllegalArgumentException | NullPointerException e) { + return Optional.empty(); + } + + // Maintain ordering of keys. + ListMultimap data = LinkedListMultimap.create(); + AtomicBoolean eof = new AtomicBoolean(false); + Charset encoding = request.getContentEncoding(); + try (InputStream is = request.getContent().get(); + Reader reader = new InputStreamReader(is, request.getContentEncoding())) { + + while (!eof.get()) { + String key = read(reader, encoding, '=', eof); + String value = read(reader, encoding, '&', eof); + + data.put(key, value == null ? "" : value); + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + + // We want to return a Map>, not a Map> so, ugh. + ImmutableMap.Builder> toReturn = ImmutableMap.builder(); + for (Map.Entry> entry : data.asMap().entrySet()) { + toReturn.put(entry.getKey(), ImmutableList.copyOf(entry.getValue())); + } + return Optional.of(toReturn.build()); + } + + private static String read(Reader reader, Charset charSet, char delimiter, AtomicBoolean eof) throws IOException { + if (eof.get()) { + return null; + } + + StringBuilder builder = new StringBuilder(); + for (; ; ) { + int i = reader.read(); + if (i == -1) { + eof.set(true); + break; + } + char c = (char) i; + if (c == delimiter) { + break; + } + builder.append(c); + } + + return URLDecoder.decode(builder.toString(), charSet.toString()); + } + + +} diff --git a/java/client/test/org/openqa/selenium/remote/http/FormEncodedDataTest.java b/java/client/test/org/openqa/selenium/remote/http/FormEncodedDataTest.java new file mode 100644 index 0000000000000..0dea65dfd7f7d --- /dev/null +++ b/java/client/test/org/openqa/selenium/remote/http/FormEncodedDataTest.java @@ -0,0 +1,153 @@ +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you 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 org.openqa.selenium.remote.http; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.net.MediaType; +import org.junit.Test; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Fail.fail; +import static org.openqa.selenium.remote.http.Contents.bytes; +import static org.openqa.selenium.remote.http.Contents.utf8String; +import static org.openqa.selenium.remote.http.HttpMethod.GET; + +public class FormEncodedDataTest { + + @Test + public void shouldRequireCorrectContentType() { + HttpRequest request = createRequest("key", "value").removeHeader("Content-Type"); + Optional>> data = FormEncodedData.getData(request); + + assertThat(data).isEqualTo(Optional.empty()); + } + + @Test + public void canReadASinglePairOfValues() { + HttpRequest request = createRequest("key", "value"); + + Optional>> data = FormEncodedData.getData(request); + + assertThat(data.get()).isEqualTo(ImmutableMap.of("key", ImmutableList.of("value"))); + } + + @Test + public void canReadTwoValues() { + HttpRequest request = createRequest("key", "value", "foo", "bar"); + + Optional>> data = FormEncodedData.getData(request); + + assertThat(data.get()).isEqualTo( + ImmutableMap.of("key", ImmutableList.of("value"), "foo", ImmutableList.of("bar"))); + } + + @Test + public void shouldSetEmptyValuesToTheEmptyString() { + HttpRequest request = createRequest("key", null); + + Optional>> data = FormEncodedData.getData(request); + + assertThat(data.get()).isEqualTo(ImmutableMap.of("key", ImmutableList.of(""))); + } + + @Test + public void shouldDecodeParameterNames() { + HttpRequest request = createRequest("%foo%", "value"); + + Optional>> data = FormEncodedData.getData(request); + + assertThat(data.get()).isEqualTo(ImmutableMap.of("%foo%", ImmutableList.of("value"))); + } + + @Test + public void shouldDecodeParameterValues() { + HttpRequest request = createRequest("key", "%bar%"); + + Optional>> data = FormEncodedData.getData(request); + + assertThat(data.get()).isEqualTo(ImmutableMap.of("key", ImmutableList.of("%bar%"))); + } + + @Test + public void shouldCollectMultipleValuesForTheSameParameterNamePreservingOrder() { + HttpRequest request = createRequest("foo", "bar", "foo", "baz"); + + Optional>> data = FormEncodedData.getData(request); + + assertThat(data.get()).isEqualTo(ImmutableMap.of("foo", ImmutableList.of("bar", "baz"))); + } + + @Test + public void aSingleParameterNameIsEnough() { + HttpRequest request = new HttpRequest(GET, "/example") + .addHeader("Content-Type", MediaType.FORM_DATA.toString()) + .setContent(bytes("param".getBytes())); + + Optional>> data = FormEncodedData.getData(request); + + assertThat(data.get()).isEqualTo(ImmutableMap.of("param", ImmutableList.of(""))); + } + + private HttpRequest createRequest(String key, String value, String... others) { + if (others.length % 2 != 0) { + fail("Other parameters must be of even length"); + } + + List allStrings = new ArrayList<>(); + allStrings.add(key); + allStrings.add(value); + allStrings.addAll(Arrays.asList(others)); + + StringBuilder content = new StringBuilder(); + Iterator iterator = allStrings.iterator(); + boolean isFirst = true; + while (iterator.hasNext()) { + if (!isFirst) { + content.append("&"); + } + try { + content.append(URLEncoder.encode(iterator.next(), UTF_8.toString())); + + String next = iterator.next(); + if (next != null) { + content.append("=").append(URLEncoder.encode(next, UTF_8.toString())); + } + } catch (UnsupportedEncodingException e) { + fail(e.getMessage()); + } + if (isFirst) { + isFirst = false; + } + } + + return new HttpRequest(GET, "/foo") + .addHeader("Content-Type", "application/x-www-form-urlencoded; charset=utf-8") + .setContent(utf8String(content.toString())); + } +}