aboutsummaryrefslogtreecommitdiffstats
path: root/container-core/src/main/java/com/yahoo/jdisc/http/HttpRequest.java
blob: 6a25937592b457e35361004d97d737110c265937 (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
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.jdisc.http;

import com.yahoo.jdisc.HeaderFields;
import com.yahoo.jdisc.Request;
import com.yahoo.jdisc.handler.CompletionHandler;
import com.yahoo.jdisc.handler.ContentChannel;
import com.yahoo.jdisc.handler.RequestHandler;
import com.yahoo.jdisc.handler.ResponseHandler;
import com.yahoo.jdisc.service.CurrentContainer;
import org.eclipse.jetty.util.MultiMap;
import org.eclipse.jetty.util.UrlEncoded;

import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.URI;
import java.security.Principal;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * A HTTP request.
 *
 * @author Anirudha Khanna
 * @author Einar M R Rosenvinge
 */
public class HttpRequest extends Request {

    public enum Method {
        OPTIONS,
        GET,
        HEAD,
        POST,
        PUT,
        PATCH,
        DELETE,
        TRACE,
        CONNECT
    }

    public enum Version {
        HTTP_1_0("HTTP/1.0"),
        HTTP_1_1("HTTP/1.1"),
        HTTP_2_0("HTTP/2.0");

        private final String str;

        Version(String str) {
            this.str = str;
        }

        @Override
        public String toString() {
            return str;
        }

        public static Version fromString(String str) {
            for (Version version : values()) {
                if (version.str.equals(str)) {
                    return version;
                }
            }
            throw new IllegalArgumentException(str);
        }
    }

    private final long jvmRelativeCreatedAt = System.nanoTime();
    private final HeaderFields trailers = new HeaderFields();
    private final Map<String, List<String>> parameters = new HashMap<>();
    private volatile Principal principal;
    private final long connectedAt;
    private Method method;
    private Version version;
    private SocketAddress remoteAddress;
    private URI proxyServer;
    private Long connectionTimeout;

    protected HttpRequest(CurrentContainer container, URI uri, Method method, Version version,
                          SocketAddress remoteAddress, Long connectedAtMillis, long creationTime)
    {
        super(container, uri, true, creationTime);
        try {
            this.method = method;
            this.version = version;
            this.remoteAddress = remoteAddress;
            this.parameters.putAll(getUriQueryParameters(uri));
            if (connectedAtMillis != null) {
                this.connectedAt = connectedAtMillis;
            } else {
                this.connectedAt = creationTime(TimeUnit.MILLISECONDS);
            }
        } catch (Throwable e) {
            release();
            throw e;
        }
    }

    private HttpRequest(Request parent, URI uri, Method method, Version version) {
        super(parent, uri);
        try {
            this.method = method;
            this.version = version;
            this.remoteAddress = null;
            this.parameters.putAll(getUriQueryParameters(uri));
            this.connectedAt = creationTime(TimeUnit.MILLISECONDS);
        } catch (Throwable e) {
            release();
            throw e;
        }
    }

    private static Map<String, List<String>> getUriQueryParameters(URI uri) {
        if (uri.getRawQuery() == null) return Map.of();
        MultiMap<String> params = new MultiMap<>();
        UrlEncoded.decodeUtf8To(uri.getRawQuery(), params);
        return Map.copyOf(params);
    }

    public Method getMethod() {
        return method;
    }

    public void setMethod(Method method) {
        this.method = method;
    }

    public Version getVersion() {
        return version;
    }

    /** Returns the remote address, or null if unresolved */
    public String getRemoteHostAddress() {
        if (remoteAddress instanceof InetSocketAddress) {
            InetAddress remoteInetAddress =  ((InetSocketAddress) remoteAddress).getAddress();
            if (remoteInetAddress == null)
                return null;
            return remoteInetAddress.getHostAddress();
        }
        else {
            throw new RuntimeException("Unknown SocketAddress class: " + remoteAddress.getClass().getName());
        }
    }

    public String getRemoteHostName() {
        if (remoteAddress instanceof InetSocketAddress) {
            InetAddress remoteInetAddress = ((InetSocketAddress) remoteAddress).getAddress();
            if (remoteInetAddress == null) return null; // not resolved; we have no network
            return remoteInetAddress.getHostName();
        }
        else {
            throw new RuntimeException("Unknown SocketAddress class: " + remoteAddress.getClass().getName());
        }
    }

    public int getRemotePort() {
        if (remoteAddress instanceof InetSocketAddress)
            return ((InetSocketAddress) remoteAddress).getPort();
        else
            throw new RuntimeException("Unknown SocketAddress class: " + remoteAddress.getClass().getName());
    }

    public void setVersion(Version version) {
        this.version = version;
    }

    public SocketAddress getRemoteAddress() {
        return remoteAddress;
    }

    public void setRemoteAddress(SocketAddress remoteAddress) {
        this.remoteAddress = remoteAddress;
    }

    public URI getProxyServer() {
        return proxyServer;
    }

    public void setProxyServer(URI proxyServer) {
        this.proxyServer = proxyServer;
    }

    /**
     * <p>For server requests, this returns the timestamp of when the underlying HTTP channel was connected.
     *
     * <p>For client requests, this returns the same value as {@link #creationTime(java.util.concurrent.TimeUnit)}.</p>
     *
     * @param unit the unit to return the time in
     * @return the timestamp of when the underlying HTTP channel was connected, or request creation time
     */
    public long getConnectedAt(TimeUnit unit) {
        return unit.convert(connectedAt, TimeUnit.MILLISECONDS);
    }

    public Long getConnectionTimeout(TimeUnit unit) {
        if (connectionTimeout == null) {
            return null;
        }
        return unit.convert(connectionTimeout, TimeUnit.MILLISECONDS);
    }

    /**
     * <p>Sets the allocated time that this HttpRequest is allowed to spend trying to connect to a remote host. This has
     * no effect on an HttpRequest received by a {@link RequestHandler}. If no connection timeout is assigned to an
     * HttpRequest, it defaults the connection-timeout in the client configuration.</p>
     *
     * <p><b>NOTE:</b> Where {@link Request#setTimeout(long, TimeUnit)} sets the expiration time between calling a
     * RequestHandler and a {@link ResponseHandler}, this method sets the expiration time of the connect-operation as
     * performed by the client.</p>
     *
     * @param timeout The allocated amount of time.
     * @param unit    The time unit of the <em>timeout</em> argument.
     */
    public void setConnectionTimeout(long timeout, TimeUnit unit) {
        this.connectionTimeout = unit.toMillis(timeout);
    }

    public Map<String, List<String>> parameters() {
        return parameters;
    }

    public void copyHeaders(HeaderFields target) {
        target.addAll(headers());
    }

    public List<Cookie> decodeCookieHeader() {
        List<String> cookies = headers().get(HttpHeaders.Names.COOKIE);
        if (cookies == null) {
            return List.of();
        }
        List<Cookie> ret = new LinkedList<>();
        for (String cookie : cookies) {
            ret.addAll(Cookie.fromCookieHeader(cookie));
        }
        return ret;
    }

    public void encodeCookieHeader(List<Cookie> cookies) {
        headers().put(HttpHeaders.Names.COOKIE, Cookie.toCookieHeader(cookies));
    }

    /**
     * <p>Returns the set of trailer header fields of this HttpRequest. These are typically meta-data that should have
     * been part of {@link #headers()}, but were not available prior to calling {@link #connect(ResponseHandler)}. You
     * must NOT WRITE to these headers AFTER calling {@link ContentChannel#close(CompletionHandler)}, and you must NOT
     * READ from these headers BEFORE {@link ContentChannel#close(CompletionHandler)} has been called.</p>
     *
     * <p><b>NOTE:</b> These headers are NOT thread-safe. You need to explicitly synchronized on the returned object to
     * prevent concurrency issues such as ConcurrentModificationExceptions.</p>
     *
     * @return The trailer headers of this HttpRequest.
     */
    public HeaderFields trailers() {
        return trailers;
    }

    /**
     * Returns whether this request was <em>explicitly</em> chunked from the client.&nbsp;NOTE that there are cases
     * where the underlying HTTP server library (Netty for the time being) will read the request in a chunked manner. An
     * application MUST wait for {@link com.yahoo.jdisc.handler.ContentChannel#close(com.yahoo.jdisc.handler.CompletionHandler)}
     * before it can actually know that it has received the entire request.
     *
     * @return true if this request was chunked from the client.
     */
    public boolean isChunked() {
        return version == Version.HTTP_1_1 &&
               headers().containsIgnoreCase(HttpHeaders.Names.TRANSFER_ENCODING, HttpHeaders.Values.CHUNKED);
    }

    public boolean hasChunkedResponse() {
        return version == Version.HTTP_1_1 &&
               !headers().isTrue(HttpHeaders.Names.X_DISABLE_CHUNKING);
    }

    public boolean isKeepAlive() {
        if (headers().containsIgnoreCase(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.KEEP_ALIVE)) {
            return true;
        }
        if (headers().containsIgnoreCase(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.CLOSE)) {
            return false;
        }
        return version == Version.HTTP_1_1;
    }

    /**
     * @return the relative created timestamp (using {@link System#nanoTime()}
     */
    public long relativeCreatedAtNanoTime() { return jvmRelativeCreatedAt; }

    public Principal getUserPrincipal() {
        return principal;
    }

    public void setUserPrincipal(Principal principal) {
        this.principal = principal;
    }

    public static HttpRequest newServerRequest(CurrentContainer container, URI uri) {
        return newServerRequest(container, uri, Method.GET);
    }

    public static HttpRequest newServerRequest(CurrentContainer container, URI uri, Method method) {
        return newServerRequest(container, uri, method, Version.HTTP_1_1);
    }

    public static HttpRequest newServerRequest(CurrentContainer container, URI uri, Method method, Version version) {
        return newServerRequest(container, uri, method, version, null);
    }

    public static HttpRequest newServerRequest(CurrentContainer container, URI uri, Method method, Version version,
                                               SocketAddress remoteAddress) {
        return new HttpRequest(container, uri, method, version, remoteAddress, null, -1);
    }

    public static HttpRequest newServerRequest(CurrentContainer container, URI uri, Method method, Version version,
                                               SocketAddress remoteAddress, long connectedAtMillis, long creationTime)
    {
        return new HttpRequest(container, uri, method, version, remoteAddress, connectedAtMillis, creationTime);
    }

    public static HttpRequest newClientRequest(Request parent, URI uri) {
        return newClientRequest(parent, uri, Method.GET);
    }

    public static HttpRequest newClientRequest(Request parent, URI uri, Method method) {
        return newClientRequest(parent, uri, method, Version.HTTP_1_1);
    }

    public static HttpRequest newClientRequest(Request parent, URI uri, Method method, Version version) {
        return new HttpRequest(parent, uri, method, version);
    }

}