Skip to content

Commit

Permalink
[ELYWEB-180] Elytron web consumes the InputStream when form parameter…
Browse files Browse the repository at this point in the history
…s are parsed
  • Loading branch information
rmartinc committed Jun 29, 2022
1 parent 19ccd0e commit 346721f
Show file tree
Hide file tree
Showing 13 changed files with 1,221 additions and 13 deletions.
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,12 @@
<version>${version.org.apache.httpcomponents.httpclient}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpmime</artifactId>
<version>${version.org.apache.httpcomponents.httpclient}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpcore</artifactId>
Expand Down
5 changes: 5 additions & 0 deletions undertow-servlet/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,11 @@
<artifactId>httpclient</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpmime</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.json</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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.
Expand Down Expand Up @@ -87,15 +98,26 @@ public Map<String, List<String>> getRequestParameters() {
ServletRequestContext servletRequestContext = httpServerExchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY);
ServletRequest servletRequest = servletRequestContext.getServletRequest();
if (servletRequest instanceof HttpServletRequest) {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
Map<String, String[]> parameters = httpServletRequest.getParameterMap();
Map<String, List<String>> requestParameters = new HashMap<>(parameters.size());
for (Entry<String, String[]> 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<String, String[]> parameterMap = replayRequest.getParameterMap();
Map<String, List<String>> parameters = new HashMap<>(parameterMap.size());
for (Entry<String, String[]> 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<String, List<String>> parameters = new HashMap<>();
Map<String, Deque<String>> queryParameters = httpServerExchange.getQueryParameters();
for (Map.Entry<String, Deque<String>> 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();
}
}
}
Expand Down Expand Up @@ -329,6 +351,49 @@ public void registerForNotification(Consumer<HttpScopeNotification> 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<BytesCallback> 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;
Expand All @@ -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<BytesCallback> future;
private byte[] bytes;
private IOException error;

BytesCallback(CompletableFuture<BytesCallback> 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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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;

/**
* <p>Internal class that wraps the original request and allows the replay
* of the input stream and parsed form params.</p>
*
* @author rmartinc
*/
class ReplayHttpServletRequestWrapper extends HttpServletRequestWrapper {

private final ReplayServletInputStream ris;
private final FormData formData;
private List<Part> 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<String> result = superValues != null? new ArrayList<>(Arrays.asList(superValues)) : new ArrayList<>();
Deque<FormData.FormValue> 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<String, String[]> getParameterMap() {
Map<String, String[]> result = new HashMap<>();
Map<String, String[]> superMap = super.getParameterMap();
if (superMap != null) {
result.putAll(superMap);
}
if (formData != null) {
for (Iterator<String> iter = formData.iterator(); iter.hasNext(); ) {
String paramName = iter.next();
String[] superValues = result.get(paramName);
Deque<FormData.FormValue> fvs = formData.get(paramName);
if (fvs != null) {
List<String> 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<String> getParameterNames() {
Enumeration<String> paramNames = super.getParameterNames();
Set<String> result = new HashSet<>();
while (paramNames.hasMoreElements()) {
result.add(paramNames.nextElement());
}
if (formData != null) {
for (Iterator<String> 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<Part> getParts() throws IOException, ServletException {
Collection<Part> superParts = super.getParts();
if (superParts != null && !superParts.isEmpty()) {
return superParts;
}
if (parts == null) {
loadParts();
}
return parts;
}

private List<String> getValuesFromForm(Deque<FormData.FormValue> formValues) {
ArrayList<String> 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<Part> 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;
}
}
}
Loading

0 comments on commit 346721f

Please sign in to comment.