| /* |
| * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. |
| * |
| * This code is free software; you can redistribute it and/or modify it |
| * under the terms of the GNU General Public License version 2 only, as |
| * published by the Free Software Foundation. Oracle designates this |
| * particular file as subject to the "Classpath" exception as provided |
| * by Oracle in the LICENSE file that accompanied this code. |
| * |
| * This code is distributed in the hope that it will be useful, but WITHOUT |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
| * version 2 for more details (a copy is included in the LICENSE file that |
| * accompanied this code). |
| * |
| * You should have received a copy of the GNU General Public License version |
| * 2 along with this work; if not, write to the Free Software Foundation, |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. |
| * |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA |
| * or visit www.oracle.com if you need additional information or have any |
| * questions. |
| */ |
| |
| package jdk.internal.net.http; |
| |
| import java.io.IOException; |
| import java.net.URI; |
| import java.nio.ByteBuffer; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Map; |
| import java.net.InetSocketAddress; |
| import java.util.Objects; |
| import java.util.concurrent.Flow; |
| import java.util.function.BiPredicate; |
| import java.net.http.HttpHeaders; |
| import java.net.http.HttpRequest; |
| import jdk.internal.net.http.Http1Exchange.Http1BodySubscriber; |
| import jdk.internal.net.http.common.HttpHeadersBuilder; |
| import jdk.internal.net.http.common.Log; |
| import jdk.internal.net.http.common.Logger; |
| import jdk.internal.net.http.common.Utils; |
| |
| import static java.lang.String.format; |
| import static java.nio.charset.StandardCharsets.US_ASCII; |
| |
| /** |
| * An HTTP/1.1 request. |
| */ |
| class Http1Request { |
| |
| private static final String COOKIE_HEADER = "Cookie"; |
| private static final BiPredicate<String,String> NOCOOKIES = |
| (k,v) -> !COOKIE_HEADER.equalsIgnoreCase(k); |
| |
| private final HttpRequestImpl request; |
| private final Http1Exchange<?> http1Exchange; |
| private final HttpConnection connection; |
| private final HttpRequest.BodyPublisher requestPublisher; |
| private final HttpHeaders userHeaders; |
| private final HttpHeadersBuilder systemHeadersBuilder; |
| private volatile boolean streaming; |
| private volatile long contentLength; |
| |
| Http1Request(HttpRequestImpl request, |
| Http1Exchange<?> http1Exchange) |
| throws IOException |
| { |
| this.request = request; |
| this.http1Exchange = http1Exchange; |
| this.connection = http1Exchange.connection(); |
| this.requestPublisher = request.requestPublisher; // may be null |
| this.userHeaders = request.getUserHeaders(); |
| this.systemHeadersBuilder = request.getSystemHeadersBuilder(); |
| } |
| |
| private void logHeaders(String completeHeaders) { |
| if (Log.headers()) { |
| //StringBuilder sb = new StringBuilder(256); |
| //sb.append("REQUEST HEADERS:\n"); |
| //Log.dumpHeaders(sb, " ", systemHeaders); |
| //Log.dumpHeaders(sb, " ", userHeaders); |
| //Log.logHeaders(sb.toString()); |
| |
| String s = completeHeaders.replaceAll("\r\n", "\n"); |
| if (s.endsWith("\n\n")) s = s.substring(0, s.length() - 2); |
| Log.logHeaders("REQUEST HEADERS:\n{0}\n", s); |
| } |
| } |
| |
| |
| private void collectHeaders0(StringBuilder sb) { |
| BiPredicate<String,String> filter = |
| connection.headerFilter(request); |
| |
| // Filter out 'Cookie:' headers, we will collect them at the end. |
| BiPredicate<String,String> nocookies = NOCOOKIES.and(filter); |
| |
| HttpHeaders systemHeaders = systemHeadersBuilder.build(); |
| |
| // If we're sending this request through a tunnel, |
| // then don't send any preemptive proxy-* headers that |
| // the authentication filter may have saved in its |
| // cache. |
| collectHeaders1(sb, systemHeaders, nocookies); |
| |
| // If we're sending this request through a tunnel, |
| // don't send any user-supplied proxy-* headers |
| // to the target server. |
| collectHeaders1(sb, userHeaders, nocookies); |
| |
| // Gather all 'Cookie:' headers and concatenate their |
| // values in a single line. |
| collectCookies(sb, systemHeaders, userHeaders); |
| |
| // terminate headers |
| sb.append('\r').append('\n'); |
| } |
| |
| // Concatenate any 'Cookie:' header in a single line, as mandated |
| // by RFC 6265, section 5.4: |
| // |
| // <<When the user agent generates an HTTP request, the user agent MUST |
| // NOT attach more than one Cookie header field.>> |
| // |
| // This constraint is relaxed for the HTTP/2 protocol, which |
| // explicitly allows sending multiple Cookie header fields. |
| // RFC 7540 section 8.1.2.5: |
| // |
| // <<To allow for better compression efficiency, the Cookie header |
| // field MAY be split into separate header fields, each with one or |
| // more cookie-pairs.>> |
| // |
| // This method will therefore concatenate multiple Cookie header field |
| // values into a single field, in a similar way than was implemented in |
| // the legacy HttpURLConnection. |
| // |
| // Note that at this point this method performs no further validation |
| // on the actual field-values, except to check that they do not contain |
| // any illegal character for header field values. |
| // |
| private void collectCookies(StringBuilder sb, |
| HttpHeaders system, |
| HttpHeaders user) { |
| List<String> systemList = system.allValues(COOKIE_HEADER); |
| List<String> userList = user.allValues(COOKIE_HEADER); |
| boolean found = false; |
| if (systemList != null) { |
| for (String cookie : systemList) { |
| if (!found) { |
| found = true; |
| sb.append(COOKIE_HEADER).append(':').append(' '); |
| } else { |
| sb.append(';').append(' '); |
| } |
| sb.append(cookie); |
| } |
| } |
| if (userList != null) { |
| for (String cookie : userList) { |
| if (!found) { |
| found = true; |
| sb.append(COOKIE_HEADER).append(':').append(' '); |
| } else { |
| sb.append(';').append(' '); |
| } |
| sb.append(cookie); |
| } |
| } |
| if (found) sb.append('\r').append('\n'); |
| } |
| |
| private void collectHeaders1(StringBuilder sb, |
| HttpHeaders headers, |
| BiPredicate<String,String> filter) { |
| for (Map.Entry<String,List<String>> entry : headers.map().entrySet()) { |
| String key = entry.getKey(); |
| List<String> values = entry.getValue(); |
| for (String value : values) { |
| if (!filter.test(key, value)) |
| continue; |
| sb.append(key).append(':').append(' ') |
| .append(value) |
| .append('\r').append('\n'); |
| } |
| } |
| } |
| |
| private String getPathAndQuery(URI uri) { |
| String path = uri.getRawPath(); |
| String query = uri.getRawQuery(); |
| if (path == null || path.equals("")) { |
| path = "/"; |
| } |
| if (query == null) { |
| query = ""; |
| } |
| if (query.equals("")) { |
| return Utils.encode(path); |
| } else { |
| return Utils.encode(path + "?" + query); |
| } |
| } |
| |
| private String authorityString(InetSocketAddress addr) { |
| return addr.getHostString() + ":" + addr.getPort(); |
| } |
| |
| private String hostString() { |
| URI uri = request.uri(); |
| int port = uri.getPort(); |
| String host = uri.getHost(); |
| |
| boolean defaultPort; |
| if (port == -1) { |
| defaultPort = true; |
| } else if (request.secure()) { |
| defaultPort = port == 443; |
| } else { |
| defaultPort = port == 80; |
| } |
| |
| if (defaultPort) { |
| return host; |
| } else { |
| return host + ":" + Integer.toString(port); |
| } |
| } |
| |
| private String requestURI() { |
| URI uri = request.uri(); |
| String method = request.method(); |
| |
| if ((request.proxy() == null && !method.equals("CONNECT")) |
| || request.isWebSocket()) { |
| return getPathAndQuery(uri); |
| } |
| if (request.secure()) { |
| if (request.method().equals("CONNECT")) { |
| // use authority for connect itself |
| return authorityString(request.authority()); |
| } else { |
| // requests over tunnel do not require full URL |
| return getPathAndQuery(uri); |
| } |
| } |
| if (request.method().equals("CONNECT")) { |
| // use authority for connect itself |
| return authorityString(request.authority()); |
| } |
| |
| return uri == null? authorityString(request.authority()) : uri.toString(); |
| } |
| |
| private boolean finished; |
| |
| synchronized boolean finished() { |
| return finished; |
| } |
| |
| synchronized void setFinished() { |
| finished = true; |
| } |
| |
| List<ByteBuffer> headers() { |
| if (Log.requests() && request != null) { |
| Log.logRequest(request.toString()); |
| } |
| String uriString = requestURI(); |
| StringBuilder sb = new StringBuilder(64); |
| sb.append(request.method()) |
| .append(' ') |
| .append(uriString) |
| .append(" HTTP/1.1\r\n"); |
| |
| URI uri = request.uri(); |
| if (uri != null) { |
| systemHeadersBuilder.setHeader("Host", hostString()); |
| } |
| if (requestPublisher == null) { |
| // Not a user request, or maybe a method, e.g. GET, with no body. |
| contentLength = 0; |
| } else { |
| contentLength = requestPublisher.contentLength(); |
| } |
| |
| if (contentLength == 0) { |
| systemHeadersBuilder.setHeader("Content-Length", "0"); |
| } else if (contentLength > 0) { |
| systemHeadersBuilder.setHeader("Content-Length", Long.toString(contentLength)); |
| streaming = false; |
| } else { |
| streaming = true; |
| systemHeadersBuilder.setHeader("Transfer-encoding", "chunked"); |
| } |
| collectHeaders0(sb); |
| String hs = sb.toString(); |
| logHeaders(hs); |
| ByteBuffer b = ByteBuffer.wrap(hs.getBytes(US_ASCII)); |
| return List.of(b); |
| } |
| |
| Http1BodySubscriber continueRequest() { |
| Http1BodySubscriber subscriber; |
| if (streaming) { |
| subscriber = new StreamSubscriber(); |
| requestPublisher.subscribe(subscriber); |
| } else { |
| if (contentLength == 0) |
| return null; |
| |
| subscriber = new FixedContentSubscriber(); |
| requestPublisher.subscribe(subscriber); |
| } |
| return subscriber; |
| } |
| |
| final class StreamSubscriber extends Http1BodySubscriber { |
| |
| StreamSubscriber() { super(debug); } |
| |
| @Override |
| public void onSubscribe(Flow.Subscription subscription) { |
| if (isSubscribed()) { |
| Throwable t = new IllegalStateException("already subscribed"); |
| http1Exchange.appendToOutgoing(t); |
| } else { |
| setSubscription(subscription); |
| } |
| } |
| |
| @Override |
| public void onNext(ByteBuffer item) { |
| Objects.requireNonNull(item); |
| if (complete) { |
| Throwable t = new IllegalStateException("subscription already completed"); |
| http1Exchange.appendToOutgoing(t); |
| } else { |
| int chunklen = item.remaining(); |
| ArrayList<ByteBuffer> l = new ArrayList<>(3); |
| l.add(getHeader(chunklen)); |
| l.add(item); |
| l.add(ByteBuffer.wrap(CRLF)); |
| http1Exchange.appendToOutgoing(l); |
| } |
| } |
| |
| @Override |
| public String currentStateMessage() { |
| return "streaming request body " + (complete ? "complete" : "incomplete"); |
| } |
| |
| @Override |
| public void onError(Throwable throwable) { |
| if (complete) |
| return; |
| |
| cancelSubscription(); |
| http1Exchange.appendToOutgoing(throwable); |
| } |
| |
| @Override |
| public void onComplete() { |
| if (complete) { |
| Throwable t = new IllegalStateException("subscription already completed"); |
| http1Exchange.appendToOutgoing(t); |
| } else { |
| ArrayList<ByteBuffer> l = new ArrayList<>(2); |
| l.add(ByteBuffer.wrap(EMPTY_CHUNK_BYTES)); |
| l.add(ByteBuffer.wrap(CRLF)); |
| complete = true; |
| //setFinished(); |
| http1Exchange.appendToOutgoing(l); |
| http1Exchange.appendToOutgoing(COMPLETED); |
| setFinished(); // TODO: before or after,? does it matter? |
| |
| } |
| } |
| } |
| |
| final class FixedContentSubscriber extends Http1BodySubscriber { |
| |
| private volatile long contentWritten; |
| FixedContentSubscriber() { super(debug); } |
| |
| @Override |
| public void onSubscribe(Flow.Subscription subscription) { |
| if (isSubscribed()) { |
| Throwable t = new IllegalStateException("already subscribed"); |
| http1Exchange.appendToOutgoing(t); |
| } else { |
| setSubscription(subscription); |
| } |
| } |
| |
| @Override |
| public void onNext(ByteBuffer item) { |
| if (debug.on()) debug.log("onNext"); |
| Objects.requireNonNull(item); |
| if (complete) { |
| Throwable t = new IllegalStateException("subscription already completed"); |
| http1Exchange.appendToOutgoing(t); |
| } else { |
| long writing = item.remaining(); |
| long written = (contentWritten += writing); |
| |
| if (written > contentLength) { |
| cancelSubscription(); |
| String msg = connection.getConnectionFlow() |
| + " [" + Thread.currentThread().getName() +"] " |
| + "Too many bytes in request body. Expected: " |
| + contentLength + ", got: " + written; |
| http1Exchange.appendToOutgoing(new IOException(msg)); |
| } else { |
| http1Exchange.appendToOutgoing(List.of(item)); |
| } |
| } |
| } |
| |
| @Override |
| public String currentStateMessage() { |
| return format("fixed content-length: %d, bytes sent: %d", |
| contentLength, contentWritten); |
| } |
| |
| @Override |
| public void onError(Throwable throwable) { |
| if (debug.on()) debug.log("onError"); |
| if (complete) // TODO: error? |
| return; |
| |
| cancelSubscription(); |
| http1Exchange.appendToOutgoing(throwable); |
| } |
| |
| @Override |
| public void onComplete() { |
| if (debug.on()) debug.log("onComplete"); |
| if (complete) { |
| Throwable t = new IllegalStateException("subscription already completed"); |
| http1Exchange.appendToOutgoing(t); |
| } else { |
| complete = true; |
| long written = contentWritten; |
| if (contentLength > written) { |
| cancelSubscription(); |
| Throwable t = new IOException(connection.getConnectionFlow() |
| + " [" + Thread.currentThread().getName() +"] " |
| + "Too few bytes returned by the publisher (" |
| + written + "/" |
| + contentLength + ")"); |
| http1Exchange.appendToOutgoing(t); |
| } else { |
| http1Exchange.appendToOutgoing(COMPLETED); |
| } |
| } |
| } |
| } |
| |
| private static final byte[] CRLF = {'\r', '\n'}; |
| private static final byte[] EMPTY_CHUNK_BYTES = {'0', '\r', '\n'}; |
| |
| /** Returns a header for a particular chunk size */ |
| private static ByteBuffer getHeader(int size) { |
| String hexStr = Integer.toHexString(size); |
| byte[] hexBytes = hexStr.getBytes(US_ASCII); |
| byte[] header = new byte[hexStr.length()+2]; |
| System.arraycopy(hexBytes, 0, header, 0, hexBytes.length); |
| header[hexBytes.length] = CRLF[0]; |
| header[hexBytes.length+1] = CRLF[1]; |
| return ByteBuffer.wrap(header); |
| } |
| |
| final Logger debug = Utils.getDebugLogger(this::toString, Utils.DEBUG); |
| |
| } |