blob: f0da88b3b5c02e55163e5faf1e3f1bdf1cb34f01 [file] [log] [blame]
/*
* Copyright (c) 2015, 2016, 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
*/
package java.net.http;
import java.io.IOException;
import java.net.URI;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.function.LongConsumer;
import static java.nio.charset.StandardCharsets.US_ASCII;
/**
* A HTTP/1.1 request.
*
* send() -> Writes the request + body to the given channel, in one blocking
* operation.
*/
class Http1Request {
final HttpRequestImpl request;
final HttpConnection chan;
// Multiple buffers are used to hold different parts of request
// See line 206 and below for description
final ByteBuffer[] buffers;
final HttpRequest.BodyProcessor requestProc;
final HttpHeadersImpl userHeaders, systemHeaders;
final LongConsumer flowController;
boolean streaming;
long contentLength;
Http1Request(HttpRequestImpl request, HttpConnection connection)
throws IOException
{
this.request = request;
this.chan = connection;
buffers = new ByteBuffer[5]; // TODO: check
this.requestProc = request.requestProcessor();
this.userHeaders = request.getUserHeaders();
this.systemHeaders = request.getSystemHeaders();
this.flowController = this::dummy;
}
private void logHeaders() throws IOException {
StringBuilder sb = new StringBuilder(256);
sb.append("REQUEST HEADERS:\r\n");
collectHeaders1(sb, request, systemHeaders);
collectHeaders1(sb, request, userHeaders);
Log.logHeaders(sb.toString());
}
private void dummy(long x) {
// not used in this class
}
private void collectHeaders0() throws IOException {
if (Log.headers()) {
logHeaders();
}
StringBuilder sb = new StringBuilder(256);
collectHeaders1(sb, request, systemHeaders);
collectHeaders1(sb, request, userHeaders);
sb.append("\r\n");
String headers = sb.toString();
buffers[1] = ByteBuffer.wrap(headers.getBytes(StandardCharsets.US_ASCII));
}
private void collectHeaders1(StringBuilder sb,
HttpRequestImpl request,
HttpHeadersImpl headers)
throws IOException
{
Map<String,List<String>> h = headers.directMap();
Set<Map.Entry<String,List<String>>> entries = h.entrySet();
for (Map.Entry<String,List<String>> entry : entries) {
String key = entry.getKey();
sb.append(key).append(": ");
List<String> values = entry.getValue();
int num = values.size();
for (String value : values) {
sb.append(value);
if (--num > 0) {
sb.append(',');
}
}
sb.append("\r\n");
}
}
private static final int BUFSIZE = 64 * 1024; // TODO: configurable?
private String getPathAndQuery(URI uri) {
String path = uri.getPath();
String query = uri.getQuery();
if (path == null || path.equals("")) {
path = "/";
}
if (query == null) {
query = "";
}
if (query.equals("")) {
return path;
} else {
return path + "?" + query;
}
}
private String authorityString(InetSocketAddress addr) {
return addr.getHostString() + ":" + addr.getPort();
}
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);
}
}
return uri == null? authorityString(request.authority()) : uri.toString();
}
void sendHeadersOnly() throws IOException {
collectHeaders();
chan.write(buffers, 0, 2);
}
void sendRequest() throws IOException {
collectHeaders();
if (contentLength == 0) {
chan.write(buffers, 0, 2);
} else if (contentLength > 0) {
writeFixedContent(true);
} else {
writeStreamedContent(true);
}
setFinished();
}
private boolean finished;
synchronized boolean finished() {
return finished;
}
synchronized void setFinished() {
finished = true;
}
private void collectHeaders() throws IOException {
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");
String cmd = sb.toString();
buffers[0] = ByteBuffer.wrap(cmd.getBytes(StandardCharsets.US_ASCII));
URI uri = request.uri();
if (uri != null) {
systemHeaders.setHeader("Host", uri.getHost());
}
if (request == null) {
// this is not a user request. No content
contentLength = 0;
} else {
contentLength = requestProc.onRequestStart(request, flowController);
}
if (contentLength == 0) {
systemHeaders.setHeader("Content-Length", "0");
collectHeaders0();
} else if (contentLength > 0) {
/* [0] request line [1] headers [2] body */
systemHeaders.setHeader("Content-Length",
Integer.toString((int) contentLength));
streaming = false;
collectHeaders0();
buffers[2] = chan.getBuffer();
} else {
/* Chunked:
*
* [0] request line [1] headers [2] chunk header [3] chunk data [4]
* final chunk header and trailing CRLF of previous chunks
*
* 2,3,4 used repeatedly */
streaming = true;
systemHeaders.setHeader("Transfer-encoding", "chunked");
collectHeaders0();
buffers[3] = chan.getBuffer();
}
}
// The following two methods used by Http1Exchange to handle expect continue
void continueRequest() throws IOException {
if (streaming) {
writeStreamedContent(false);
} else {
writeFixedContent(false);
}
setFinished();
}
/* Entire request is sent, or just body only */
private void writeStreamedContent(boolean includeHeaders)
throws IOException
{
if (requestProc instanceof HttpRequest.BodyProcessor) {
HttpRequest.BodyProcessor pullproc = requestProc;
int startbuf, nbufs;
if (includeHeaders) {
startbuf = 0;
nbufs = 5;
} else {
startbuf = 2;
nbufs = 3;
}
try {
// TODO: currently each write goes out as one chunk
// TODO: should be collecting data and buffer it.
buffers[3].clear();
boolean done = pullproc.onRequestBodyChunk(buffers[3]);
int chunklen = buffers[3].position();
buffers[2] = getHeader(chunklen);
buffers[3].flip();
buffers[4] = CRLF_BUFFER();
chan.write(buffers, startbuf, nbufs);
while (!done) {
buffers[3].clear();
done = pullproc.onRequestBodyChunk(buffers[3]);
if (done)
break;
buffers[3].flip();
chunklen = buffers[3].remaining();
buffers[2] = getHeader(chunklen);
buffers[4] = CRLF_BUFFER();
chan.write(buffers, 2, 3);
}
buffers[3] = EMPTY_CHUNK_HEADER();
buffers[4] = CRLF_BUFFER();
chan.write(buffers, 3, 2);
} catch (IOException e) {
requestProc.onRequestError(e);
throw e;
}
}
}
/* Entire request is sent, or just body only */
private void writeFixedContent(boolean includeHeaders)
throws IOException
{
try {
int startbuf, nbufs;
if (contentLength == 0) {
return;
}
if (includeHeaders) {
startbuf = 0;
nbufs = 3;
} else {
startbuf = 2;
nbufs = 1;
buffers[0].clear().flip();
buffers[1].clear().flip();
}
buffers[2] = chan.getBuffer();
if (requestProc instanceof HttpRequest.BodyProcessor) {
HttpRequest.BodyProcessor pullproc = requestProc;
boolean done = pullproc.onRequestBodyChunk(buffers[2]);
buffers[2].flip();
long headersLength = buffers[0].remaining() + buffers[1].remaining();
long contentWritten = buffers[2].remaining();
chan.checkWrite(headersLength + contentWritten,
buffers,
startbuf,
nbufs);
while (!done) {
buffers[2].clear();
done = pullproc.onRequestBodyChunk(buffers[2]);
buffers[2].flip();
long len = buffers[2].remaining();
if (contentWritten + len > contentLength) {
break;
}
chan.checkWrite(len, buffers[2]);
contentWritten += len;
}
if (contentWritten != contentLength) {
throw new IOException("wrong content length");
}
}
} catch (IOException e) {
requestProc.onRequestError(e);
throw e;
}
}
private static final byte[] CRLF = {'\r', '\n'};
private static final byte[] EMPTY_CHUNK_BYTES = {'0', '\r', '\n'};
private ByteBuffer CRLF_BUFFER() {
return ByteBuffer.wrap(CRLF);
}
private ByteBuffer EMPTY_CHUNK_HEADER() {
return ByteBuffer.wrap(EMPTY_CHUNK_BYTES);
}
/* 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);
}
}