aboutsummaryrefslogtreecommitdiffstats
path: root/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/FormPostRequestHandler.java
blob: c2b81530e8206292956bf2d94dba957b4a2621cc (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.jdisc.http.server.jetty;

import com.google.common.base.Preconditions;
import com.yahoo.jdisc.Request;
import com.yahoo.jdisc.ResourceReference;
import com.yahoo.jdisc.handler.AbstractRequestHandler;
import com.yahoo.jdisc.handler.CompletionHandler;
import com.yahoo.jdisc.handler.ContentChannel;
import com.yahoo.jdisc.handler.DelegatedRequestHandler;
import com.yahoo.jdisc.handler.RequestHandler;
import com.yahoo.jdisc.handler.ResponseHandler;
import com.yahoo.jdisc.http.HttpRequest;

import java.io.ByteArrayOutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.IllegalCharsetNameException;
import java.nio.charset.StandardCharsets;
import java.nio.charset.UnsupportedCharsetException;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import static com.yahoo.jdisc.Response.Status.UNSUPPORTED_MEDIA_TYPE;
import static com.yahoo.jdisc.http.server.jetty.CompletionHandlerUtils.NOOP_COMPLETION_HANDLER;

/**
 * Request handler that wraps POST requests of application/x-www-form-urlencoded data.
 *
 * The wrapper defers invocation of the "real" request handler until it has read the request content (body),
 * parsed the form parameters and merged them into the request's parameters.
 *
 * @author bakksjo
 */
class FormPostRequestHandler extends AbstractRequestHandler implements ContentChannel, DelegatedRequestHandler {

    private final ByteArrayOutputStream accumulatedRequestContent = new ByteArrayOutputStream();
    private final RequestHandler delegateHandler;
    private final String contentCharsetName;
    private final boolean removeBody;

    private Charset contentCharset;
    private HttpRequest request;
    private ResourceReference requestReference;
    private ResponseHandler responseHandler;

    /**
     * @param delegateHandler the "real" request handler that this handler wraps
     * @param contentCharsetName name of the charset to use when interpreting the content data
     */
    public FormPostRequestHandler(RequestHandler delegateHandler, String contentCharsetName, boolean removeBody) {
        this.delegateHandler = Objects.requireNonNull(delegateHandler);
        this.contentCharsetName = Objects.requireNonNull(contentCharsetName);
        this.removeBody = removeBody;
    }

    @Override
    public ContentChannel handleRequest(Request request, ResponseHandler responseHandler) {
        Preconditions.checkArgument(request instanceof HttpRequest, "Expected HttpRequest, got " + request);
        Objects.requireNonNull(responseHandler, "responseHandler");

        this.contentCharset = getCharsetByName(contentCharsetName);
        this.responseHandler = responseHandler;
        this.request = (HttpRequest) request;
        this.requestReference = request.refer(this);

        return this;
    }

    @Override
    public void write(ByteBuffer buf, CompletionHandler completionHandler) {
        assert buf.hasArray();
        accumulatedRequestContent.write(buf.array(), buf.arrayOffset() + buf.position(), buf.remaining());
        completionHandler.completed();
    }

    @Override
    public void close(CompletionHandler completionHandler) {
        try (ResourceReference ref = requestReference) {
            byte[] requestContentBytes = accumulatedRequestContent.toByteArray();
            String content = new String(requestContentBytes, contentCharset);
            completionHandler.completed();
            Map<String, List<String>> parameterMap = parseFormParameters(content);
            mergeParameters(parameterMap, request.parameters());
            ContentChannel contentChannel = delegateHandler.handleRequest(request, responseHandler);
            if (contentChannel != null) {
                if (!removeBody) {
                    ByteBuffer byteBuffer = ByteBuffer.wrap(requestContentBytes);
                    contentChannel.write(byteBuffer, NOOP_COMPLETION_HANDLER);
                }
                contentChannel.close(NOOP_COMPLETION_HANDLER);
            }
        }
    }

    /**
     * Looks up a Charset given a charset name.
     *
     * @param charsetName the name of the charset to look up
     * @return a valid Charset for the charset name (never returns null)
     * @throws RequestException if the charset name is invalid or unsupported
     */
    private static Charset getCharsetByName(String charsetName) throws RequestException {
        try {
            return Charset.forName(charsetName);
        } catch (IllegalCharsetNameException | UnsupportedCharsetException e) {
            throw new RequestException(UNSUPPORTED_MEDIA_TYPE, "Unsupported charset " + charsetName, e);
        }
    }

    /**
     * Parses application/x-www-form-urlencoded data into a map of parameters.
     *
     * @param formContent raw form content data (body)
     * @return map of decoded parameters
     */
    private static Map<String, List<String>> parseFormParameters(String formContent) {
        if (formContent.isEmpty()) {
            return Collections.emptyMap();
        }

        Map<String, List<String>> parameterMap = new HashMap<>();
        String[] params = formContent.split("&");
        for (String param : params) {
            String[] parts = param.split("=");
            String paramName = urlDecode(parts[0]);
            String paramValue = parts.length > 1 ? urlDecode(parts[1]) : "";
            List<String> currentValues = parameterMap.get(paramName);
            if (currentValues == null) {
                currentValues = new LinkedList<>();
                parameterMap.put(paramName, currentValues);
            }
            currentValues.add(paramValue);
        }
        return parameterMap;
    }

    /**
     * Percent-decoding method that doesn't throw.
     *
     * @param encoded percent-encoded data
     * @return decoded data
     */
    private static String urlDecode(final String encoded) {
        try {
            // Regardless of the charset used to transfer the request body,
            // all percent-escaping of non-ascii characters should use UTF-8 code points.
            return URLDecoder.decode(encoded, StandardCharsets.UTF_8.name());
        } catch (UnsupportedEncodingException e) {
            // Unfortunately, there is no URLDecoder.decode() method that takes a Charset, so we have to deal
            // with this exception.
            throw new IllegalStateException("Whoa, JVM doesn't support UTF-8 today.", e);
        }
    }

    /**
     * Merges source parameters into a destination map.
     *
     * @param source containing the parameters to copy into the destination
     * @param destination receiver of parameters, possibly already containing data
     */
    private static void mergeParameters(Map<String,List<String>> source, Map<String,List<String>> destination) {
        for (Map.Entry<String, List<String>> entry : source.entrySet()) {
            List<String> destinationValues = destination.get(entry.getKey());
            if (destinationValues != null) {
                destinationValues.addAll(entry.getValue());
            } else {
                destination.put(entry.getKey(), entry.getValue());
            }
        }
    }

    @Override
    public RequestHandler getDelegate() {
        return delegateHandler;
    }

}