From 346721fac0d0454db6f03eef694cfd749ee2a163 Mon Sep 17 00:00:00 2001 From: rmartinc Date: Tue, 17 May 2022 09:49:50 +0200 Subject: [PATCH] [ELYWEB-180] Elytron web consumes the InputStream when form parameters are parsed --- pom.xml | 6 + undertow-servlet/pom.xml | 5 + .../servlet/ElytronHttpServletExchange.java | 120 +++++- .../ReplayHttpServletRequestWrapper.java | 205 ++++++++++ .../servlet/ReplayServletInputStream.java | 83 ++++ .../CustomFormServletAuthenticationTest.java | 362 ++++++++++++++++++ ...mFormParamHttpAuthenticationMechanism.java | 119 ++++++ .../util/CustomFormParamMechanismFactory.java | 50 +++ .../servlet/util/InputStreamServlet.java | 47 +++ .../server/servlet/util/MultiPartServlet.java | 81 ++++ .../servlet/util/ParametersServlet.java | 128 +++++++ .../server/servlet/util/TestServlet.java | 19 +- .../servlet/util/UndertowServletServer.java | 9 +- 13 files changed, 1221 insertions(+), 13 deletions(-) create mode 100644 undertow-servlet/src/main/java/org/wildfly/elytron/web/undertow/server/servlet/ReplayHttpServletRequestWrapper.java create mode 100644 undertow-servlet/src/main/java/org/wildfly/elytron/web/undertow/server/servlet/ReplayServletInputStream.java create mode 100644 undertow-servlet/src/test/java/org/wildfly/elytron/web/undertow/server/servlet/CustomFormServletAuthenticationTest.java create mode 100644 undertow-servlet/src/test/java/org/wildfly/elytron/web/undertow/server/servlet/util/CustomFormParamHttpAuthenticationMechanism.java create mode 100644 undertow-servlet/src/test/java/org/wildfly/elytron/web/undertow/server/servlet/util/CustomFormParamMechanismFactory.java create mode 100644 undertow-servlet/src/test/java/org/wildfly/elytron/web/undertow/server/servlet/util/InputStreamServlet.java create mode 100644 undertow-servlet/src/test/java/org/wildfly/elytron/web/undertow/server/servlet/util/MultiPartServlet.java create mode 100644 undertow-servlet/src/test/java/org/wildfly/elytron/web/undertow/server/servlet/util/ParametersServlet.java diff --git a/pom.xml b/pom.xml index 674275c8..e6b3adf1 100644 --- a/pom.xml +++ b/pom.xml @@ -374,6 +374,12 @@ ${version.org.apache.httpcomponents.httpclient} provided + + org.apache.httpcomponents + httpmime + ${version.org.apache.httpcomponents.httpclient} + test + org.apache.httpcomponents httpcore diff --git a/undertow-servlet/pom.xml b/undertow-servlet/pom.xml index a7877ec7..0e7e6aaf 100644 --- a/undertow-servlet/pom.xml +++ b/undertow-servlet/pom.xml @@ -145,6 +145,11 @@ httpclient test + + org.apache.httpcomponents + httpmime + test + org.glassfish javax.json diff --git a/undertow-servlet/src/main/java/org/wildfly/elytron/web/undertow/server/servlet/ElytronHttpServletExchange.java b/undertow-servlet/src/main/java/org/wildfly/elytron/web/undertow/server/servlet/ElytronHttpServletExchange.java index 34f66ffb..02fbedee 100644 --- a/undertow-servlet/src/main/java/org/wildfly/elytron/web/undertow/server/servlet/ElytronHttpServletExchange.java +++ b/undertow-servlet/src/main/java/org/wildfly/elytron/web/undertow/server/servlet/ElytronHttpServletExchange.java @@ -20,12 +20,17 @@ import java.io.IOException; import java.io.InputStream; +import java.nio.ByteBuffer; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.Deque; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; import java.util.function.Consumer; import java.util.function.Function; @@ -46,13 +51,19 @@ import org.wildfly.security.http.HttpScopeNotification; import org.wildfly.security.http.Scope; +import io.undertow.io.Receiver; +import io.undertow.server.Connectors; import io.undertow.server.HttpServerExchange; +import io.undertow.server.handlers.form.FormData; +import io.undertow.server.handlers.form.FormDataParser; import io.undertow.server.session.Session; import io.undertow.server.session.SessionConfig; import io.undertow.server.session.SessionManager; import io.undertow.servlet.api.Deployment; +import io.undertow.servlet.core.ManagedServlet; import io.undertow.servlet.handlers.ServletRequestContext; import io.undertow.servlet.util.SavedRequest; +import io.undertow.util.ImmediatePooledByteBuffer; /** * An extension of {@link ElytronHttpExchange} which adds servlet container specific integrations. @@ -87,15 +98,26 @@ public Map> getRequestParameters() { ServletRequestContext servletRequestContext = httpServerExchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY); ServletRequest servletRequest = servletRequestContext.getServletRequest(); if (servletRequest instanceof HttpServletRequest) { - HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; - Map parameters = httpServletRequest.getParameterMap(); - Map> requestParameters = new HashMap<>(parameters.size()); - for (Entry entry : parameters.entrySet()) { - requestParameters.put(entry.getKey(), Collections.unmodifiableList(Arrays.asList(entry.getValue()))); + HttpServletRequest replayRequest = parseFormDataForReplay(httpServerExchange, servletRequestContext, (HttpServletRequest) servletRequest); + if (replayRequest != null) { + // replay is in place so normal processing + Map parameterMap = replayRequest.getParameterMap(); + Map> parameters = new HashMap<>(parameterMap.size()); + for (Entry entry : parameterMap.entrySet()) { + parameters.put(entry.getKey(), Collections.unmodifiableList(Arrays.asList(entry.getValue()))); + } + this.requestParameters = Collections.unmodifiableMap(parameters); + } else { + // only manage query parameters for this request + HashMap> parameters = new HashMap<>(); + Map> queryParameters = httpServerExchange.getQueryParameters(); + for (Map.Entry> e : queryParameters.entrySet()) { + parameters.put(e.getKey(), Collections.unmodifiableList(new ArrayList<>(e.getValue()))); + } + requestParameters = Collections.unmodifiableMap(parameters); } - this.requestParameters = Collections.unmodifiableMap(requestParameters); } else { - return super.getRequestParameters(); + requestParameters = super.getRequestParameters(); } } } @@ -329,6 +351,49 @@ public void registerForNotification(Consumer consumer) { }; } + private static HttpServletRequest parseFormDataForReplay(final HttpServerExchange exchange, + final ServletRequestContext servletRequestContext, final HttpServletRequest request) { + final int maxBufferSizeToSave = SavedRequest.getMaxBufferSizeToSave(exchange); + if (exchange.getRequestContentLength() > 0 && exchange.getRequestContentLength() <= maxBufferSizeToSave) { + try { + // if the size is allowed to be buffered read bytes for replay + final ManagedServlet originalServlet = servletRequestContext.getCurrentServlet().getManagedServlet(); + final FormDataParser parser = originalServlet.getFormParserFactory().createParser(exchange); + if (parser != null) { + final CompletableFuture future = new CompletableFuture<>(); + BytesCallback callback = new BytesCallback(future); + Receiver receiver = exchange.getRequestReceiver(); + receiver.setMaxBufferSize(maxBufferSizeToSave); + receiver.receiveFullBytes(callback, callback); + + // wait the callback as getRequestParameters is a blocking method + callback = future.get(); + if (callback.isError()) { + throw callback.getError(); + } + + // the bytes are in the callback so replay and parse form data + Connectors.ungetRequestBytes(exchange, new ImmediatePooledByteBuffer(ByteBuffer.wrap(callback.getBytes(), 0, callback.getBytes().length))); + Connectors.resetRequestChannel(exchange); + + // we need to replay InputStream for parsing too + servletRequestContext.setServletRequest(new ReplayHttpServletRequestWrapper(request, null, callback.getBytes())); + FormData data = parser.parseBlocking(); + + // now do the replay for the application + HttpServletRequest replayRequest = new ReplayHttpServletRequestWrapper((HttpServletRequest) request, data, callback.getBytes()); + servletRequestContext.setServletRequest(replayRequest); + + return replayRequest; + } + } catch (IOException | InterruptedException | ExecutionException e) { + log.tracef(e, "Error reading form parameters from exchange %s", exchange); + servletRequestContext.setServletRequest(request); + } + } + return null; + } + private static class FormResponseWrapper extends HttpServletResponseWrapper { private int status = OK; @@ -354,4 +419,45 @@ public int getStatus() { } + /** + * Helper class to receive data bytes and replay them for the InputStream. + */ + private static class BytesCallback implements Receiver.FullBytesCallback, Receiver.ErrorCallback { + + private final CompletableFuture future; + private byte[] bytes; + private IOException error; + + BytesCallback(CompletableFuture future) { + this.future = future; + } + + @Override + public void handle(HttpServerExchange hse, byte[] bytes) { + this.bytes = bytes; + future.complete(this); + } + + @Override + public void error(HttpServerExchange hse, IOException ioe) { + this.error = ioe; + future.complete(this); + } + + public byte[] getBytes() { + return bytes; + } + + public boolean hasBytes() { + return bytes != null; + } + + public IOException getError() { + return error; + } + + public boolean isError() { + return error != null; + } + } } diff --git a/undertow-servlet/src/main/java/org/wildfly/elytron/web/undertow/server/servlet/ReplayHttpServletRequestWrapper.java b/undertow-servlet/src/main/java/org/wildfly/elytron/web/undertow/server/servlet/ReplayHttpServletRequestWrapper.java new file mode 100644 index 00000000..2542e295 --- /dev/null +++ b/undertow-servlet/src/main/java/org/wildfly/elytron/web/undertow/server/servlet/ReplayHttpServletRequestWrapper.java @@ -0,0 +1,205 @@ +/* + * Copyright 2022 JBoss by Red Hat. + * + * 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 org.wildfly.elytron.web.undertow.server.servlet; + +import io.undertow.server.HttpServerExchange; +import io.undertow.server.handlers.form.FormData; +import io.undertow.servlet.handlers.ServletRequestContext; +import io.undertow.servlet.spec.HttpServletRequestImpl; +import io.undertow.servlet.spec.PartImpl; +import io.undertow.servlet.spec.ServletContextImpl; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Deque; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.servlet.ServletException; +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import javax.servlet.http.Part; + +/** + *

Internal class that wraps the original request and allows the replay + * of the input stream and parsed form params.

+ * + * @author rmartinc + */ +class ReplayHttpServletRequestWrapper extends HttpServletRequestWrapper { + + private final ReplayServletInputStream ris; + private final FormData formData; + private List parts = null; + + public ReplayHttpServletRequestWrapper(HttpServletRequest request, FormData formData, byte[] bytes) { + super(request); + this.formData = formData; + ris = new ReplayServletInputStream(bytes); + } + + @Override + public ServletInputStream getInputStream() throws IOException { + return ris; + } + + @Override + public BufferedReader getReader() throws IOException { + return new BufferedReader(new InputStreamReader(ris, getCharacterEncoding())); + } + + @Override + public String getParameter(String name) { + String result = super.getParameter(name); + if (result == null && formData != null) { + FormData.FormValue fv = formData.getFirst(name); + if (fv != null && !fv.isFileItem()) { + result = fv.getValue(); + } + } + return result; + } + + @Override + public String[] getParameterValues(String name) { + String[] superValues = super.getParameterValues(name); + List result = superValues != null? new ArrayList<>(Arrays.asList(superValues)) : new ArrayList<>(); + Deque formValues = formData != null? formData.get(name) : null; + if (formValues != null) { + for (FormData.FormValue fv : formValues) { + if (!fv.isFileItem()) { + result.add(fv.getValue()); + } + } + } + return result.isEmpty()? null : result.toArray(new String[0]); + } + + @Override + public Map getParameterMap() { + Map result = new HashMap<>(); + Map superMap = super.getParameterMap(); + if (superMap != null) { + result.putAll(superMap); + } + if (formData != null) { + for (Iterator iter = formData.iterator(); iter.hasNext(); ) { + String paramName = iter.next(); + String[] superValues = result.get(paramName); + Deque fvs = formData.get(paramName); + if (fvs != null) { + List values = superValues != null? new ArrayList<>(Arrays.asList(superValues)) : new ArrayList<>(); + values.addAll(getValuesFromForm(fvs)); + if (!values.isEmpty()) { + result.put(paramName, values.toArray(new String[0])); + } + } + } + } + return Collections.unmodifiableMap(result); + } + + @Override + public Enumeration getParameterNames() { + Enumeration paramNames = super.getParameterNames(); + Set result = new HashSet<>(); + while (paramNames.hasMoreElements()) { + result.add(paramNames.nextElement()); + } + if (formData != null) { + for (Iterator iter = formData.iterator(); iter.hasNext(); ) { + String name = iter.next(); + for (FormData.FormValue fv : formData.get(name)) { + if (!fv.isFileItem()) { + result.add(name); + break; + } + } + } + } + return Collections.enumeration(result); + } + + @Override + public Part getPart(String name) throws IOException, ServletException { + Part part = super.getPart(name); + if (part != null) { + return part; + } + if (parts == null) { + loadParts(); + } + for (Part p : parts) { + if (p.getName().equals(name)) { + return p; + } + } + return null; + } + + @Override + public Collection getParts() throws IOException, ServletException { + Collection superParts = super.getParts(); + if (superParts != null && !superParts.isEmpty()) { + return superParts; + } + if (parts == null) { + loadParts(); + } + return parts; + } + + private List getValuesFromForm(Deque formValues) { + ArrayList result = new ArrayList<>(); + for (FormData.FormValue fv : formValues) { + if (!fv.isFileItem()) { + result.add(fv.getValue()); + } + } + return result; + } + + private void loadParts() { + HttpServletRequestImpl request = (HttpServletRequestImpl) getRequest(); + HttpServerExchange exchange = request.getExchange(); + ServletContextImpl servletContext = request.getServletContext(); + final ServletRequestContext requestContext = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY); + if (parts == null) { + final List tmp = new ArrayList<>(); + if(formData != null) { + for (final String namedPart : formData) { + for (FormData.FormValue part : formData.get(namedPart)) { + tmp.add(new PartImpl(namedPart, part, + requestContext.getOriginalServletPathMatch().getServletChain().getManagedServlet().getMultipartConfig(), + servletContext, request)); + } + } + } + this.parts = tmp; + } + } +} diff --git a/undertow-servlet/src/main/java/org/wildfly/elytron/web/undertow/server/servlet/ReplayServletInputStream.java b/undertow-servlet/src/main/java/org/wildfly/elytron/web/undertow/server/servlet/ReplayServletInputStream.java new file mode 100644 index 00000000..cd8ddcb6 --- /dev/null +++ b/undertow-servlet/src/main/java/org/wildfly/elytron/web/undertow/server/servlet/ReplayServletInputStream.java @@ -0,0 +1,83 @@ +/* + * Copyright 2022 JBoss by Red Hat. + * + * 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 org.wildfly.elytron.web.undertow.server.servlet; + +import java.io.IOException; +import javax.servlet.ReadListener; +import javax.servlet.ServletInputStream; + +/** + *

Internal class that allows the replay of the InputStream using the + * direct bytes.

+ * + * @author rmartinc + */ +class ReplayServletInputStream extends ServletInputStream { + + private final byte[] bytes; + private int idx; + private ReadListener listener = null; + + public ReplayServletInputStream(byte[] bytes) { + this.bytes = bytes; + this.idx = -1; + } + + @Override + public boolean isFinished() { + return idx >= bytes.length - 1; + } + + @Override + public boolean isReady() { + return !isFinished(); + } + + @Override + public void setReadListener(ReadListener listener) { + this.listener = listener; + if (isReady()) { + try { + listener.onDataAvailable(); + } catch (IOException e) { + listener.onError(e); + } + } else { + try { + listener.onAllDataRead(); + } catch (IOException e) { + listener.onError(e); + } + } + } + + @Override + public int read() throws IOException { + int result = -1; + if (isReady()) { + result = bytes[++idx]; + if (isFinished() && listener != null) { + try { + listener.onAllDataRead(); + } catch (IOException e) { + listener.onError(e); + } + } + } + return result; + } + +} diff --git a/undertow-servlet/src/test/java/org/wildfly/elytron/web/undertow/server/servlet/CustomFormServletAuthenticationTest.java b/undertow-servlet/src/test/java/org/wildfly/elytron/web/undertow/server/servlet/CustomFormServletAuthenticationTest.java new file mode 100644 index 00000000..ecabfc9b --- /dev/null +++ b/undertow-servlet/src/test/java/org/wildfly/elytron/web/undertow/server/servlet/CustomFormServletAuthenticationTest.java @@ -0,0 +1,362 @@ +/* + * Copyright 2022 JBoss by Red Hat. + * + * 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 org.wildfly.elytron.web.undertow.server.servlet; + +import io.undertow.UndertowOptions; +import java.io.File; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.NameValuePair; +import org.apache.http.client.HttpClient; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; +import org.apache.http.entity.mime.HttpMultipartMode; +import org.apache.http.entity.mime.MultipartEntityBuilder; +import org.apache.http.entity.mime.content.FileBody; +import org.apache.http.entity.mime.content.StringBody; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.util.EntityUtils; +import org.hamcrest.CoreMatchers; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.wildfly.elytron.web.undertow.common.AbstractHttpServerMechanismTest; +import org.wildfly.elytron.web.undertow.common.UndertowServer; +import org.wildfly.elytron.web.undertow.server.servlet.util.CustomFormParamHttpAuthenticationMechanism; +import org.wildfly.elytron.web.undertow.server.servlet.util.CustomFormParamMechanismFactory; +import org.wildfly.elytron.web.undertow.server.servlet.util.UndertowServletServer; +import org.wildfly.security.auth.permission.LoginPermission; +import org.wildfly.security.auth.realm.SimpleMapBackedSecurityRealm; +import org.wildfly.security.auth.realm.SimpleRealmEntry; +import org.wildfly.security.auth.server.SecurityDomain; +import org.wildfly.security.credential.PasswordCredential; +import org.wildfly.security.http.util.FilterServerMechanismFactory; +import org.wildfly.security.http.util.PropertiesServerMechanismFactory; +import org.wildfly.security.password.PasswordFactory; +import org.wildfly.security.password.interfaces.ClearPassword; +import org.wildfly.security.password.spec.ClearPasswordSpec; +import org.wildfly.security.permission.PermissionVerifier; + +/** + *

Test that uses a custom form mechanism in order to check that + * parameters and the input stream are available after parsing and can be + * replayed.

+ * + * @author rmartinc + */ +public class CustomFormServletAuthenticationTest extends AbstractHttpServerMechanismTest { + + @Rule + public UndertowServer server = createUndertowServer(); + + @Rule + public TemporaryFolder folder = new TemporaryFolder(); + + public CustomFormServletAuthenticationTest() throws Exception { + } + + @Test + public void testLogin() throws Exception { + HttpClient httpClient = HttpClientBuilder.create().build(); + HttpPost httpAuthenticate = new HttpPost(server.createUri("/secured")); + List parameters = new ArrayList<>(2); + + parameters.add(new BasicNameValuePair(CustomFormParamHttpAuthenticationMechanism.USERNAME_PARAM, "ladybird")); + parameters.add(new BasicNameValuePair(CustomFormParamHttpAuthenticationMechanism.PASSWORD_PARAM, "Coleoptera")); + + httpAuthenticate.setEntity(new UrlEncodedFormEntity(parameters)); + + assertSuccessfulResponse(httpClient.execute(httpAuthenticate), "ladybird"); + assertSuccessfulResponse(httpClient.execute(httpAuthenticate), "ladybird"); + } + + @Test + public void testInputStream() throws Exception { + HttpClient httpClient = HttpClientBuilder.create().build(); + HttpPost httpAuthenticate = new HttpPost(server.createUri("/input-stream")); + List parameters = new ArrayList<>(2); + + parameters.add(new BasicNameValuePair(CustomFormParamHttpAuthenticationMechanism.USERNAME_PARAM, "ladybird")); + parameters.add(new BasicNameValuePair(CustomFormParamHttpAuthenticationMechanism.PASSWORD_PARAM, "Coleoptera")); + parameters.add(new BasicNameValuePair("other", "value")); + + httpAuthenticate.setEntity(new UrlEncodedFormEntity(parameters)); + + HttpResponse response = httpClient.execute(httpAuthenticate); + assertSuccessfulResponse(response, "ladybird"); + Assert.assertEquals("X-USERNAME=ladybird&X-PASSWORD=Coleoptera&other=value", + EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8)); + } + + @Test + public void testParameterNames() throws Exception { + HttpClient httpClient = HttpClientBuilder.create().build(); + HttpPost httpAuthenticate = new HttpPost(new URI(server.createUri("/parameters").toString() + "?op=names")); + List parameters = new ArrayList<>(2); + + parameters.add(new BasicNameValuePair(CustomFormParamHttpAuthenticationMechanism.USERNAME_PARAM, "ladybird")); + parameters.add(new BasicNameValuePair(CustomFormParamHttpAuthenticationMechanism.PASSWORD_PARAM, "Coleoptera")); + parameters.add(new BasicNameValuePair("other", "value1")); + parameters.add(new BasicNameValuePair("other", "value2")); + + httpAuthenticate.setEntity(new UrlEncodedFormEntity(parameters)); + + HttpResponse response = httpClient.execute(httpAuthenticate); + assertSuccessfulResponse(response, "ladybird"); + String output = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); + Assert.assertEquals(4, output.codePoints().filter(ch -> ch == '\n').count()); + Assert.assertThat(output, CoreMatchers.containsString("op\r\n")); + Assert.assertThat(output, CoreMatchers.containsString("other\r\n")); + Assert.assertThat(output, CoreMatchers.containsString(CustomFormParamHttpAuthenticationMechanism.USERNAME_PARAM + "\r\n")); + Assert.assertThat(output, CoreMatchers.containsString(CustomFormParamHttpAuthenticationMechanism.PASSWORD_PARAM + "\r\n")); + } + + @Test + public void testParameterValues() throws Exception { + HttpClient httpClient = HttpClientBuilder.create().build(); + HttpPost httpAuthenticate = new HttpPost(new URI(server.createUri("/parameters").toString() + "?op=values&other=value1")); + List parameters = new ArrayList<>(2); + + parameters.add(new BasicNameValuePair(CustomFormParamHttpAuthenticationMechanism.USERNAME_PARAM, "ladybird")); + parameters.add(new BasicNameValuePair(CustomFormParamHttpAuthenticationMechanism.PASSWORD_PARAM, "Coleoptera")); + parameters.add(new BasicNameValuePair("other", "value2")); + parameters.add(new BasicNameValuePair("other", "value3")); + + httpAuthenticate.setEntity(new UrlEncodedFormEntity(parameters)); + + HttpResponse response = httpClient.execute(httpAuthenticate); + assertSuccessfulResponse(response, "ladybird"); + String output = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); + Assert.assertEquals(4, output.codePoints().filter(ch -> ch == '\n').count()); + Assert.assertThat(output, CoreMatchers.containsString("op=values\r\n")); + Assert.assertThat(output, CoreMatchers.containsString("other=value1,value2,value3\r\n")); + Assert.assertThat(output, CoreMatchers.containsString(CustomFormParamHttpAuthenticationMechanism.USERNAME_PARAM + "=ladybird\r\n")); + Assert.assertThat(output, CoreMatchers.containsString(CustomFormParamHttpAuthenticationMechanism.PASSWORD_PARAM + "=Coleoptera\r\n")); + } + + @Test + public void testParameterMap() throws Exception { + HttpClient httpClient = HttpClientBuilder.create().build(); + HttpPost httpAuthenticate = new HttpPost(new URI(server.createUri("/parameters").toString() + "?op=map&other=value1")); + List parameters = new ArrayList<>(2); + + parameters.add(new BasicNameValuePair(CustomFormParamHttpAuthenticationMechanism.USERNAME_PARAM, "ladybird")); + parameters.add(new BasicNameValuePair(CustomFormParamHttpAuthenticationMechanism.PASSWORD_PARAM, "Coleoptera")); + parameters.add(new BasicNameValuePair("other", "value2")); + parameters.add(new BasicNameValuePair("other", "value3")); + + httpAuthenticate.setEntity(new UrlEncodedFormEntity(parameters)); + + HttpResponse response = httpClient.execute(httpAuthenticate); + assertSuccessfulResponse(response, "ladybird"); + String output = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); + Assert.assertEquals(4, output.codePoints().filter(ch -> ch == '\n').count()); + Assert.assertThat(output, CoreMatchers.containsString("op=map\r\n")); + Assert.assertThat(output, CoreMatchers.containsString("other=value1,value2,value3\r\n")); + Assert.assertThat(output, CoreMatchers.containsString(CustomFormParamHttpAuthenticationMechanism.USERNAME_PARAM + "=ladybird\r\n")); + Assert.assertThat(output, CoreMatchers.containsString(CustomFormParamHttpAuthenticationMechanism.PASSWORD_PARAM + "=Coleoptera\r\n")); + } + + @Test + public void testParameterValue() throws Exception { + HttpClient httpClient = HttpClientBuilder.create().build(); + HttpPost httpAuthenticate = new HttpPost(new URI(server.createUri("/parameters").toString() + "?op=value")); + List parameters = new ArrayList<>(2); + + parameters.add(new BasicNameValuePair(CustomFormParamHttpAuthenticationMechanism.USERNAME_PARAM, "ladybird")); + parameters.add(new BasicNameValuePair(CustomFormParamHttpAuthenticationMechanism.PASSWORD_PARAM, "Coleoptera")); + parameters.add(new BasicNameValuePair("other", "value1")); + parameters.add(new BasicNameValuePair("other", "value2")); + + httpAuthenticate.setEntity(new UrlEncodedFormEntity(parameters)); + + HttpResponse response = httpClient.execute(httpAuthenticate); + assertSuccessfulResponse(response, "ladybird"); + String output = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); + Assert.assertEquals(4, output.codePoints().filter(ch -> ch == '\n').count()); + Assert.assertThat(output, CoreMatchers.containsString("op=value\r\n")); + Assert.assertThat(output, CoreMatchers.containsString("other=value1\r\n")); + Assert.assertThat(output, CoreMatchers.containsString(CustomFormParamHttpAuthenticationMechanism.USERNAME_PARAM + "=ladybird\r\n")); + Assert.assertThat(output, CoreMatchers.containsString(CustomFormParamHttpAuthenticationMechanism.PASSWORD_PARAM + "=Coleoptera\r\n")); + } + + @Test + public void testMultiPart() throws Exception { + HttpClient httpClient = HttpClientBuilder.create().build(); + HttpPost httpAuthenticate = new HttpPost(server.createUri("/multipart")); + + MultipartEntityBuilder builder = MultipartEntityBuilder.create(); + builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE); + File file = folder.newFile("myfile.txt"); + Files.write(file.toPath(), "file-content".getBytes(StandardCharsets.UTF_8)); + builder.addPart("myfile.txt", new FileBody(file, ContentType.DEFAULT_TEXT)); + builder.addPart(CustomFormParamHttpAuthenticationMechanism.USERNAME_PARAM, new StringBody("ladybird", ContentType.MULTIPART_FORM_DATA)); + builder.addPart(CustomFormParamHttpAuthenticationMechanism.PASSWORD_PARAM, new StringBody("Coleoptera", ContentType.MULTIPART_FORM_DATA)); + builder.addPart("param1", new StringBody("value1", ContentType.MULTIPART_FORM_DATA)); + builder.addPart("param2", new StringBody("value2", ContentType.MULTIPART_FORM_DATA)); + HttpEntity entity = builder.build(); + + httpAuthenticate.setEntity(entity); + + HttpResponse response = httpClient.execute(httpAuthenticate); + assertSuccessfulResponse(response, "ladybird"); + String output = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); + Assert.assertEquals(5, output.codePoints().filter(ch -> ch == '\n').count()); + Assert.assertThat(output, CoreMatchers.containsString("myfile.txt:myfile.txt:text/plain; charset=ISO-8859-1:12:file-content\r\n")); + Assert.assertThat(output, CoreMatchers.containsString("X-USERNAME:null:null:8:ladybird\r\n")); + Assert.assertThat(output, CoreMatchers.containsString("X-PASSWORD:null:null:10:Coleoptera\r\n")); + Assert.assertThat(output, CoreMatchers.containsString("param1:null:null:6:value1\r\n")); + Assert.assertThat(output, CoreMatchers.containsString("param2:null:null:6:value2\r\n")); + } + + @Test + public void testMultiPartValues() throws Exception { + HttpClient httpClient = HttpClientBuilder.create().build(); + HttpPost httpAuthenticate = new HttpPost(new URI(server.createUri("/multipart").toString() + "?op=values")); + + MultipartEntityBuilder builder = MultipartEntityBuilder.create(); + builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE); + File file= folder.newFile("myfile.txt"); + Files.write(file.toPath(), "file-content".getBytes(StandardCharsets.UTF_8)); + builder.addPart("myfile.txt", new FileBody(file, ContentType.DEFAULT_TEXT)); + builder.addPart(CustomFormParamHttpAuthenticationMechanism.USERNAME_PARAM, new StringBody("ladybird", ContentType.MULTIPART_FORM_DATA)); + builder.addPart(CustomFormParamHttpAuthenticationMechanism.PASSWORD_PARAM, new StringBody("Coleoptera", ContentType.MULTIPART_FORM_DATA)); + builder.addPart("param1", new StringBody("value1", ContentType.MULTIPART_FORM_DATA)); + builder.addPart("param2", new StringBody("value2", ContentType.MULTIPART_FORM_DATA)); + HttpEntity entity = builder.build(); + + httpAuthenticate.setEntity(entity); + + HttpResponse response = httpClient.execute(httpAuthenticate); + assertSuccessfulResponse(response, "ladybird"); + String output = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); + Assert.assertEquals(5, output.codePoints().filter(ch -> ch == '\n').count()); + Assert.assertThat(output, CoreMatchers.containsString("op=values\r\n")); + Assert.assertThat(output, CoreMatchers.containsString("param1=value1\r\n")); + Assert.assertThat(output, CoreMatchers.containsString("param2=value2\r\n")); + Assert.assertThat(output, CoreMatchers.containsString(CustomFormParamHttpAuthenticationMechanism.USERNAME_PARAM + "=ladybird\r\n")); + Assert.assertThat(output, CoreMatchers.containsString(CustomFormParamHttpAuthenticationMechanism.PASSWORD_PARAM + "=Coleoptera\r\n")); + } + + @Test + public void testFailureTooLong() throws Exception { + StringBuilder sb = new StringBuilder(UndertowOptions.DEFAULT_MAX_BUFFERED_REQUEST_SIZE + 1); + for (int i = 0; i <= UndertowOptions.DEFAULT_MAX_BUFFERED_REQUEST_SIZE; i++) { + sb.append(i % 10); + } + + HttpClient httpClient = HttpClientBuilder.create().build(); + HttpPost httpAuthenticate = new HttpPost(server.createUri("/parameters")); + List parameters = new ArrayList<>(2); + + parameters.add(new BasicNameValuePair(CustomFormParamHttpAuthenticationMechanism.USERNAME_PARAM, "ladybird")); + parameters.add(new BasicNameValuePair(CustomFormParamHttpAuthenticationMechanism.PASSWORD_PARAM, "Coleoptera")); + parameters.add(new BasicNameValuePair("long", sb.toString())); + + httpAuthenticate.setEntity(new UrlEncodedFormEntity(parameters)); + + HttpResponse response = httpClient.execute(httpAuthenticate); + assertSuccessfulUnconstraintResponse(response, null); + String output = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); + Assert.assertEquals(3, output.codePoints().filter(ch -> ch == '\n').count()); + Assert.assertThat(output, CoreMatchers.containsString("long=0123456789")); + Assert.assertThat(output, CoreMatchers.containsString(CustomFormParamHttpAuthenticationMechanism.USERNAME_PARAM + "=ladybird\r\n")); + Assert.assertThat(output, CoreMatchers.containsString(CustomFormParamHttpAuthenticationMechanism.PASSWORD_PARAM + "=Coleoptera\r\n")); + } + + @Test + public void testQueryParametersIfTooLong() throws Exception { + StringBuilder sb = new StringBuilder(UndertowOptions.DEFAULT_MAX_BUFFERED_REQUEST_SIZE + 1); + for (int i = 0; i <= UndertowOptions.DEFAULT_MAX_BUFFERED_REQUEST_SIZE; i++) { + sb.append(i % 10); + } + + HttpClient httpClient = HttpClientBuilder.create().build(); + HttpPost httpAuthenticate = new HttpPost(new URI(server.createUri("/parameters").toString() + "?" + + CustomFormParamHttpAuthenticationMechanism.USERNAME_PARAM + "=" + "ladybird&" + + CustomFormParamHttpAuthenticationMechanism.PASSWORD_PARAM + "=" + "Coleoptera")); + List parameters = new ArrayList<>(2); + + parameters.add(new BasicNameValuePair("long", sb.toString())); + + httpAuthenticate.setEntity(new UrlEncodedFormEntity(parameters)); + + HttpResponse response = httpClient.execute(httpAuthenticate); + assertSuccessfulResponse(response, "ladybird"); + String output = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); + Assert.assertEquals(3, output.codePoints().filter(ch -> ch == '\n').count()); + Assert.assertThat(output, CoreMatchers.containsString("long=0123456789")); + Assert.assertThat(output, CoreMatchers.containsString(CustomFormParamHttpAuthenticationMechanism.USERNAME_PARAM + "=ladybird\r\n")); + Assert.assertThat(output, CoreMatchers.containsString(CustomFormParamHttpAuthenticationMechanism.PASSWORD_PARAM + "=Coleoptera\r\n")); + } + + @Test + public void testQueryParametersIfOtherContentType() throws Exception { + HttpClient httpClient = HttpClientBuilder.create().build(); + HttpPost httpAuthenticate = new HttpPost(new URI(server.createUri("/input-stream").toString() + "?" + + CustomFormParamHttpAuthenticationMechanism.USERNAME_PARAM + "=" + "ladybird&" + + CustomFormParamHttpAuthenticationMechanism.PASSWORD_PARAM + "=" + "Coleoptera")); + + StringEntity json = new StringEntity("{\"id\":1,\"name\":\"name\"}"); + httpAuthenticate.setEntity(json); + httpAuthenticate.setHeader("Content-type", "application/json"); + + HttpResponse response = httpClient.execute(httpAuthenticate); + assertSuccessfulResponse(response, "ladybird"); + Assert.assertEquals("{\"id\":1,\"name\":\"name\"}", + EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8)); + } + + @Override + protected String getMechanismName() { + return CustomFormParamMechanismFactory.CUSTOM_NAME; + } + + @Override + protected SecurityDomain doCreateSecurityDomain() throws Exception { + PasswordFactory passwordFactory = PasswordFactory.getInstance(ClearPassword.ALGORITHM_CLEAR); + Map passwordMap = new HashMap<>(); + passwordMap.put("ladybird", new SimpleRealmEntry(Collections.singletonList(new PasswordCredential(passwordFactory.generatePassword(new ClearPasswordSpec("Coleoptera".toCharArray())))))); + SimpleMapBackedSecurityRealm realm = new SimpleMapBackedSecurityRealm(); + realm.setIdentityMap(passwordMap); + SecurityDomain.Builder builder = SecurityDomain.builder().setDefaultRealmName("TestRealm"); + builder.addRealm("TestRealm", realm).build(); + builder.setPermissionMapper((principal, roles) -> PermissionVerifier.from(new LoginPermission())); + return builder.build(); + } + + protected UndertowServer createUndertowServer() throws Exception { + return UndertowServletServer.builder() + .setAuthenticationMechanism(getMechanismName()) + .setSecurityDomain(getSecurityDomain()) + .setHttpServerAuthenticationMechanismFactory(new PropertiesServerMechanismFactory( + new FilterServerMechanismFactory(new CustomFormParamMechanismFactory(), true, getMechanismName()), + Collections.emptyMap())) + .build(); + } +} diff --git a/undertow-servlet/src/test/java/org/wildfly/elytron/web/undertow/server/servlet/util/CustomFormParamHttpAuthenticationMechanism.java b/undertow-servlet/src/test/java/org/wildfly/elytron/web/undertow/server/servlet/util/CustomFormParamHttpAuthenticationMechanism.java new file mode 100644 index 00000000..fedd5f20 --- /dev/null +++ b/undertow-servlet/src/test/java/org/wildfly/elytron/web/undertow/server/servlet/util/CustomFormParamHttpAuthenticationMechanism.java @@ -0,0 +1,119 @@ +/* + * Copyright 2022 JBoss by Red Hat. + * + * 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 org.wildfly.elytron.web.undertow.server.servlet.util; + +import java.io.IOException; + +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.NameCallback; +import javax.security.auth.callback.UnsupportedCallbackException; +import javax.security.sasl.AuthorizeCallback; + +import org.wildfly.security.auth.callback.AuthenticationCompleteCallback; +import org.wildfly.security.auth.callback.EvidenceVerifyCallback; +import org.wildfly.security.auth.callback.IdentityCredentialCallback; +import org.wildfly.security.credential.PasswordCredential; +import org.wildfly.security.evidence.PasswordGuessEvidence; +import org.wildfly.security.http.HttpAuthenticationException; +import org.wildfly.security.http.HttpServerAuthenticationMechanism; +import org.wildfly.security.http.HttpServerMechanismsResponder; +import org.wildfly.security.http.HttpServerRequest; +import org.wildfly.security.http.HttpServerResponse; +import org.wildfly.security.password.interfaces.ClearPassword; + +/** + *

Custom Form mechanism. It uses two form parameters to obtain the + * username and password (X-USERNAME and X-PASSWORD). It is used to test that + * replay is done OK.

+ * + * @author rmartinc + */ +public class CustomFormParamHttpAuthenticationMechanism implements HttpServerAuthenticationMechanism { + + public static final String USERNAME_PARAM = "X-USERNAME"; + public static final String PASSWORD_PARAM = "X-PASSWORD"; + public static final String MESSAGE_HEADER = "X-MESSAGE"; + + private static final HttpServerMechanismsResponder RESPONDER = new HttpServerMechanismsResponder() { + @Override + public void sendResponse(HttpServerResponse response) throws HttpAuthenticationException { + response.addResponseHeader(MESSAGE_HEADER, "Please resubmit the request with a username specified using the X-USERNAME and a password specified using the X-PASSWORD form attributes."); + response.setStatusCode(401); + } + }; + + private final CallbackHandler callbackHandler; + + CustomFormParamHttpAuthenticationMechanism(final CallbackHandler callbackHandler) { + this.callbackHandler = callbackHandler; + } + + @Override + public void evaluateRequest(HttpServerRequest request) throws HttpAuthenticationException { + final String username = request.getFirstParameterValue(USERNAME_PARAM); + final String password = request.getFirstParameterValue(PASSWORD_PARAM); + + if (username == null || username.length() == 0 || password == null || password.length() == 0) { + request.noAuthenticationInProgress(RESPONDER); + return; + } + + NameCallback nameCallback = new NameCallback("Remote Authentication Name", username); + nameCallback.setName(username); + final PasswordGuessEvidence evidence = new PasswordGuessEvidence(password.toCharArray()); + EvidenceVerifyCallback evidenceVerifyCallback = new EvidenceVerifyCallback(evidence); + + try { + callbackHandler.handle(new Callback[] { nameCallback, evidenceVerifyCallback }); + } catch (IOException | UnsupportedCallbackException e) { + throw new HttpAuthenticationException(e); + } + + if (evidenceVerifyCallback.isVerified() == false) { + request.authenticationFailed("Username / Password Validation Failed", RESPONDER); + return; + } + + try { + callbackHandler.handle(new Callback[] {new IdentityCredentialCallback(new PasswordCredential(ClearPassword.createRaw(ClearPassword.ALGORITHM_CLEAR, password.toCharArray())), true)}); + } catch (IOException | UnsupportedCallbackException e) { + throw new HttpAuthenticationException(e); + } + + AuthorizeCallback authorizeCallback = new AuthorizeCallback(username, username); + + try { + callbackHandler.handle(new Callback[] {authorizeCallback}); + + if (authorizeCallback.isAuthorized()) { + callbackHandler.handle(new Callback[] { AuthenticationCompleteCallback.SUCCEEDED }); + request.authenticationComplete(); + } else { + callbackHandler.handle(new Callback[] { AuthenticationCompleteCallback.FAILED }); + request.authenticationFailed("Authorization check failed.", RESPONDER); + } + } catch (IOException | UnsupportedCallbackException e) { + throw new HttpAuthenticationException(e); + } + } + + @Override + public String getMechanismName() { + return CustomFormParamMechanismFactory.CUSTOM_NAME; + } +} \ No newline at end of file diff --git a/undertow-servlet/src/test/java/org/wildfly/elytron/web/undertow/server/servlet/util/CustomFormParamMechanismFactory.java b/undertow-servlet/src/test/java/org/wildfly/elytron/web/undertow/server/servlet/util/CustomFormParamMechanismFactory.java new file mode 100644 index 00000000..3e30fc93 --- /dev/null +++ b/undertow-servlet/src/test/java/org/wildfly/elytron/web/undertow/server/servlet/util/CustomFormParamMechanismFactory.java @@ -0,0 +1,50 @@ +/* + * Copyright 2022 JBoss by Red Hat. + * + * 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 org.wildfly.elytron.web.undertow.server.servlet.util; + +import java.util.Map; + +import javax.security.auth.callback.CallbackHandler; + +import org.wildfly.security.http.HttpAuthenticationException; +import org.wildfly.security.http.HttpServerAuthenticationMechanism; +import org.wildfly.security.http.HttpServerAuthenticationMechanismFactory; + +/** + *

Form mechanism factory.

+ * + * @author rmartinc + */ +public class CustomFormParamMechanismFactory implements HttpServerAuthenticationMechanismFactory { + + public static final String CUSTOM_NAME = "CUSTOM_FORM_MECHANISM"; + + @Override + public HttpServerAuthenticationMechanism createAuthenticationMechanism(String name, Map properties, + CallbackHandler handler) throws HttpAuthenticationException { + if (CUSTOM_NAME.equals(name)) { + return new CustomFormParamHttpAuthenticationMechanism(handler); + } + return null; + } + + @Override + public String[] getMechanismNames(Map properties) { + return new String[] { CUSTOM_NAME }; + } + +} diff --git a/undertow-servlet/src/test/java/org/wildfly/elytron/web/undertow/server/servlet/util/InputStreamServlet.java b/undertow-servlet/src/test/java/org/wildfly/elytron/web/undertow/server/servlet/util/InputStreamServlet.java new file mode 100644 index 00000000..bd6d77a6 --- /dev/null +++ b/undertow-servlet/src/test/java/org/wildfly/elytron/web/undertow/server/servlet/util/InputStreamServlet.java @@ -0,0 +1,47 @@ +/* + * Copyright 2022 JBoss by Red Hat. + * + * 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 org.wildfly.elytron.web.undertow.server.servlet.util; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + *

A Servlet that gets the input stream and copies it back to the output.

+ * + * @author rmartinc + */ +public class InputStreamServlet extends HttpServlet { + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + TestServlet.manageLoginHeaders(req, resp); + resp.setContentType("text/plain;charset=UTF-8"); + try (OutputStream out = resp.getOutputStream(); + InputStream in = req.getInputStream()) { + byte[] buf = new byte[512]; + int length; + while ((length = in.read(buf)) != -1) { + out.write(buf, 0, length); + } + } + } + +} diff --git a/undertow-servlet/src/test/java/org/wildfly/elytron/web/undertow/server/servlet/util/MultiPartServlet.java b/undertow-servlet/src/test/java/org/wildfly/elytron/web/undertow/server/servlet/util/MultiPartServlet.java new file mode 100644 index 00000000..cba419cd --- /dev/null +++ b/undertow-servlet/src/test/java/org/wildfly/elytron/web/undertow/server/servlet/util/MultiPartServlet.java @@ -0,0 +1,81 @@ +/* + * Copyright 2022 JBoss by Red Hat. + * + * 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 org.wildfly.elytron.web.undertow.server.servlet.util; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.Part; + +/** + *

A MultiPartServlet that displays information for common parameters and + * multi parts. Same op parameter is used:

+ * + *
    + *
  • names: getParameterNames
  • + *
  • map: getParameterMap
  • + *
  • value: getParameterNames + getParameter
  • + *
  • values: getParameterNames + getParameterValues
  • + *
  • parts: getParts
  • + *
+ * + * @author rmartinc + */ +public class MultiPartServlet extends HttpServlet { + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + TestServlet.manageLoginHeaders(req, resp); + String op = req.getParameter("op"); + if (op == null) { + op = "parts"; + } + resp.setContentType("text/plain;charset=UTF-8"); + try (PrintWriter out = resp.getWriter()) { + if (op.equals("parts")) { + for (Part p : req.getParts()) { + out.print(p.getName()); + out.print(":"); + out.print(p.getSubmittedFileName()); + out.print(":"); + out.print(p.getContentType()); + out.print(":"); + out.print(p.getSize()); + out.print(":"); + out.println(readToString(p.getInputStream())); + } + } else { + ParametersServlet.processParameters(op, out, req); + } + } + } + + private String readToString(InputStream is) throws IOException { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + int read; + byte[] data = new byte[512]; + while ((read = is.read(data, 0, data.length)) != -1) { + buffer.write(data, 0, read); + } + return new String(buffer.toByteArray(), StandardCharsets.UTF_8); + } +} diff --git a/undertow-servlet/src/test/java/org/wildfly/elytron/web/undertow/server/servlet/util/ParametersServlet.java b/undertow-servlet/src/test/java/org/wildfly/elytron/web/undertow/server/servlet/util/ParametersServlet.java new file mode 100644 index 00000000..efb91925 --- /dev/null +++ b/undertow-servlet/src/test/java/org/wildfly/elytron/web/undertow/server/servlet/util/ParametersServlet.java @@ -0,0 +1,128 @@ +/* + * Copyright 2022 JBoss by Red Hat. + * + * 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 org.wildfly.elytron.web.undertow.server.servlet.util; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Enumeration; +import java.util.Map; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + *

A Servlet that displays information for common parameters in the request. + * The op parameter can be used to test a different method:

+ * + *
    + *
  • names: getParameterNames
  • + *
  • map: getParameterMap
  • + *
  • value: getParameterNames + getParameter
  • + *
  • values: getParameterNames + getParameterValues
  • + *
+ * + * @author rmartinc + */ +public class ParametersServlet extends HttpServlet { + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + TestServlet.manageLoginHeaders(req, resp); + resp.setContentType("text/plain;charset=UTF-8"); + String op = req.getParameter("op"); + if (op == null) { + op = "values"; + } + try (PrintWriter out = resp.getWriter()) { + processParameters(op, out, req); + } + } + + static void processParameters(String op, PrintWriter out, HttpServletRequest req) { + switch (op) { + case "names": + writeParameterNames(out, req); + break; + case "map": + writeParameterMap(out, req); + break; + case "value": + writeParameterValue(out, req); + break; + case "values": + writeParameterValues(out, req); + break; + default: + break; + } + } + + private static void writeParameterNames(PrintWriter out, HttpServletRequest req) { + Enumeration e = req.getParameterNames(); + while (e.hasMoreElements()) { + out.println(e.nextElement()); + } + } + + private static void writeParameterMap(PrintWriter out, HttpServletRequest req) { + Map map = req.getParameterMap(); + if (map != null) { + for (Map.Entry e : map.entrySet()) { + out.print(e.getKey()); + out.print("="); + for (int i = 0; i < e.getValue().length; i++) { + if (i == e.getValue().length - 1) { + out.println(e.getValue()[i]); + } else { + out.print(e.getValue()[i]); + out.print(","); + } + } + } + } + } + + private static void writeParameterValues(PrintWriter out, HttpServletRequest req) { + Enumeration e = req.getParameterNames(); + while (e.hasMoreElements()) { + String name = e.nextElement(); + out.print(name); + out.print("="); + String[] values = req.getParameterValues(name); + if (values != null) { + for (int i = 0; i < values.length; i++) { + if (i == values.length - 1) { + out.println(values[i]); + } else { + out.print(values[i]); + out.print(","); + } + } + } + } + } + + private static void writeParameterValue(PrintWriter out, HttpServletRequest req) { + Enumeration e = req.getParameterNames(); + while (e.hasMoreElements()) { + String name = e.nextElement(); + out.print(name); + out.print("="); + out.println(req.getParameter(name)); + } + } +} diff --git a/undertow-servlet/src/test/java/org/wildfly/elytron/web/undertow/server/servlet/util/TestServlet.java b/undertow-servlet/src/test/java/org/wildfly/elytron/web/undertow/server/servlet/util/TestServlet.java index c2922d16..d212b624 100644 --- a/undertow-servlet/src/test/java/org/wildfly/elytron/web/undertow/server/servlet/util/TestServlet.java +++ b/undertow-servlet/src/test/java/org/wildfly/elytron/web/undertow/server/servlet/util/TestServlet.java @@ -41,6 +41,18 @@ public class TestServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + manageLoginHeaders(req, resp); + if (req.getParameter("logout") != null) { + req.logout(); + } + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + doGet(req, resp); + } + + static void manageLoginHeaders(HttpServletRequest req, HttpServletResponse resp) { resp.addHeader(PROCESSED_BY, "ResponseHandler"); String undertowUser = getUndertowUser(req); if (undertowUser != null) { @@ -50,17 +62,14 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws Se if (elytronUser != null) { resp.addHeader(ELYTRON_USER, elytronUser); } - if (req.getParameter("logout") != null) { - req.logout(); - } } - private String getUndertowUser(final HttpServletRequest request) { + private static String getUndertowUser(final HttpServletRequest request) { Principal principal = request.getUserPrincipal(); return principal != null ? principal.getName() : null; } - private String getElytronUser() { + private static String getElytronUser() { SecurityDomain securityDomain = SecurityDomain.getCurrent(); if (securityDomain != null) { SecurityIdentity securityIdentity = securityDomain.getCurrentSecurityIdentity(); diff --git a/undertow-servlet/src/test/java/org/wildfly/elytron/web/undertow/server/servlet/util/UndertowServletServer.java b/undertow-servlet/src/test/java/org/wildfly/elytron/web/undertow/server/servlet/util/UndertowServletServer.java index 31a151ae..101f2841 100644 --- a/undertow-servlet/src/test/java/org/wildfly/elytron/web/undertow/server/servlet/util/UndertowServletServer.java +++ b/undertow-servlet/src/test/java/org/wildfly/elytron/web/undertow/server/servlet/util/UndertowServletServer.java @@ -82,7 +82,14 @@ protected void before() throws Throwable { Servlets.servlet(LoginServlet.class) .addMapping("/login"), Servlets.servlet(LogoutServlet.class) - .addMapping("/logout")); + .addMapping("/logout"), + Servlets.servlet(InputStreamServlet.class) + .addMapping("/input-stream"), + Servlets.servlet(ParametersServlet.class) + .addMapping("/parameters"), + Servlets.servlet(MultiPartServlet.class) + .addMapping("/multipart") + .setMultipartConfig(Servlets.multipartConfig(null, 0, 0, 0))); HttpAuthenticationFactory httpAuthenticationFactory = HttpAuthenticationFactory.builder() .setFactory(httpServerAuthenticationMechanismFactory)