summaryrefslogtreecommitdiffstats
path: root/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/FormPostRequestHandler.java
blob: 5ee4434fd84d8088b68769c47dad13843c5d6932 (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
185
186
187
188
189
190
191
192
193
194
// Copyright 2017 Yahoo Holdings. 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
 * $Id$
 */
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(
            final RequestHandler delegateHandler,
            final String contentCharsetName,
            final boolean removeBody) {
        this.delegateHandler = Objects.requireNonNull(delegateHandler);
        this.contentCharsetName = Objects.requireNonNull(contentCharsetName);
        this.removeBody = removeBody;
    }

    @Override
    public ContentChannel handleRequest(final Request request, final 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(final ByteBuffer buf, final CompletionHandler completionHandler) {
        assert buf.hasArray();
        accumulatedRequestContent.write(buf.array(), buf.arrayOffset() + buf.position(), buf.remaining());
        completionHandler.completed();
    }

    @SuppressWarnings("try")
    @Override
    public void close(final CompletionHandler completionHandler) {
        try (final ResourceReference ref = requestReference) {
            final byte[] requestContentBytes = accumulatedRequestContent.toByteArray();
            final String content = new String(requestContentBytes, contentCharset);
            completionHandler.completed();
            final Map<String, List<String>> parameterMap = parseFormParameters(content);
            mergeParameters(parameterMap, request.parameters());
            final ContentChannel contentChannel = delegateHandler.handleRequest(request, responseHandler);
            if (contentChannel != null) {
                if (!removeBody) {
                    final 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(final String charsetName) throws RequestException {
        try {
            final Charset charset = Charset.forName(charsetName);
            if (charset == null) {
                throw new RequestException(UNSUPPORTED_MEDIA_TYPE, "Unsupported charset " + charsetName);
            }
            return charset;
        } catch (final 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(final String formContent) {
        if (formContent.isEmpty()) {
            return Collections.emptyMap();
        }

        final Map<String, List<String>> parameterMap = new HashMap<>();
        final String[] params = formContent.split("&");
        for (final String param : params) {
            final String[] parts = param.split("=");
            final String paramName = urlDecode(parts[0]);
            final 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 (final 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(
            final Map<String,List<String>> source,
            final Map<String,List<String>> destination) {
        for (Map.Entry<String, List<String>> entry : source.entrySet()) {
            final 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;
    }
}