/*
* Copyright (C) 2015 SoftIndex LLC.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.datakernel.http;
import io.datakernel.async.ResultCallback;
import io.datakernel.bytebuf.ByteBuf;
import io.datakernel.eventloop.AsyncTcpSocket;
import io.datakernel.eventloop.Eventloop;
import io.datakernel.exception.ParseException;
import java.net.InetSocketAddress;
import static io.datakernel.bytebuf.ByteBufStrings.SP;
import static io.datakernel.bytebuf.ByteBufStrings.decodeDecimal;
import static io.datakernel.http.HttpHeaders.CONNECTION;
@SuppressWarnings("ThrowableInstanceNeverThrown")
final class HttpClientConnection extends AbstractHttpConnection {
private static final HttpHeaders.Value CONNECTION_KEEP_ALIVE = HttpHeaders.asBytes(CONNECTION, "keep-alive");
private ResultCallback<HttpResponse> callback;
private HttpResponse response;
private final AsyncHttpClient client;
private final AsyncHttpClient.Inspector inspector;
final InetSocketAddress remoteAddress;
HttpClientConnection addressPrev;
HttpClientConnection addressNext;
private Exception closeError;
HttpClientConnection(Eventloop eventloop, InetSocketAddress remoteAddress,
AsyncTcpSocket asyncTcpSocket, AsyncHttpClient client, char[] headerChars,
int maxHttpMessageSize) {
super(eventloop, asyncTcpSocket, headerChars, maxHttpMessageSize);
this.remoteAddress = remoteAddress;
this.client = client;
this.inspector = client.inspector;
}
@Override
public void onRegistered() {
}
@Override
public void onClosedWithError(Exception e) {
if (inspector != null && e != null) inspector.onHttpError(this, callback == null, e);
readQueue.clear();
if (callback != null) {
callback.postException(eventloop, e);
callback = null;
} else {
closeError = e;
}
onClosed();
}
@Override
protected void onFirstLine(ByteBuf line) throws ParseException {
if (line.peek(0) != 'H' || line.peek(1) != 'T' || line.peek(2) != 'T' || line.peek(3) != 'P' || line.peek(4) != '/' || line.peek(5) != '1') {
line.recycle();
throw new ParseException("Invalid response");
}
keepAlive = false;
int sp1;
if (line.peek(6) == SP) {
sp1 = line.readPosition() + 7;
} else if (line.peek(6) == '.' && (line.peek(7) == '1' || line.peek(7) == '0') && line.peek(8) == SP) {
if (line.peek(7) == '1')
keepAlive = true;
sp1 = line.readPosition() + 9;
} else {
line.recycle();
throw new ParseException("Invalid response: " + new String(line.array(), line.readPosition(), line.readRemaining()));
}
int sp2;
for (sp2 = sp1; sp2 < line.writePosition(); sp2++) {
if (line.at(sp2) == SP) {
break;
}
}
int statusCode = decodeDecimal(line.array(), sp1, sp2 - sp1);
if (!(statusCode >= 100 && statusCode < 600)) {
line.recycle();
throw new ParseException("Invalid HTTP Status Code " + statusCode);
}
response = HttpResponse.ofCode(statusCode);
if (isNoBodyMessage(response)) {
// Reset Content-Length for the case keep-alive connection
contentLength = 0;
}
line.recycle();
}
/**
* RFC 2616, section 4.4
* 1.Any response message which "MUST NOT" include a message-body (such as the 1xx, 204, and 304 responses and any response to a HEAD request) is always
* terminated by the first empty line after the header fields, regardless of the entity-header fields present in the message.
*/
private static boolean isNoBodyMessage(HttpResponse message) {
int messageCode = message.getCode();
return (messageCode >= 100 && messageCode < 200) || messageCode == 204 || messageCode == 304;
}
@Override
protected void onHeader(HttpHeader header, ByteBuf value) throws ParseException {
super.onHeader(header, value);
response.addHeader(header, value);
}
@Override
protected void onHttpMessage(ByteBuf bodyBuf) {
assert !isClosed();
final ResultCallback<HttpResponse> callback = this.callback;
final HttpResponse response = this.response;
this.response = null;
this.callback = null;
response.setBody(bodyBuf);
eventloop.post(new Runnable() {
@Override
public void run() {
callback.setResult(response);
response.recycleBufs();
}
});
if (inspector != null) inspector.onHttpResponse(this, response);
if (keepAlive && client.keepAliveTimeoutMillis != 0) {
reset();
client.returnToKeepAlivePool(this);
} else {
close();
}
}
@Override
public void onReadEndOfStream() {
if (callback != null) {
if (reading == BODY && contentLength == UNKNOWN_LENGTH) {
onHttpMessage(bodyQueue.takeRemaining());
assert callback == null;
} else {
closeWithError(CLOSED_CONNECTION);
assert callback == null;
}
}
close();
}
@Override
protected void reset() {
reading = END_OF_STREAM;
if (response != null) {
response.recycleBufs();
response = null;
}
super.reset();
}
/**
* Sends the request, recycles it and closes connection in case of timeout
*
* @param request request for sending
* @param callback callback for handling result
*/
public void send(HttpRequest request, ResultCallback<HttpResponse> callback) {
this.callback = callback;
assert pool == null;
(pool = client.poolWriting).addLastNode(this);
poolTimestamp = eventloop.currentTimeMillis();
request.addHeader(CONNECTION_KEEP_ALIVE);
asyncTcpSocket.write(request.toByteBuf());
request.recycleBufs();
}
@Override
public void onWrite() {
assert !isClosed();
reading = FIRSTLINE;
assert pool == client.poolWriting;
pool.removeNode(this);
(pool = client.poolReading).addLastNode(this);
poolTimestamp = eventloop.currentTimeMillis();
asyncTcpSocket.read();
}
/**
* After closing this connection it removes it from its connections cache and recycles
* Http response.
*/
protected void onClosed() {
assert callback == null;
if (pool == client.poolKeepAlive) {
AddressLinkedList addresses = client.addresses.get(remoteAddress);
addresses.removeNode(this);
if (addresses.isEmpty()) {
client.addresses.remove(remoteAddress);
}
}
// pool will be null if socket was closed by the peer just before connection.send() invocation
if (pool != null) {
pool.removeNode(this);
pool = null;
}
client.onConnectionClosed();
bodyQueue.clear();
if (response != null) {
response.recycleBufs();
}
}
public Exception getCloseError() {
return closeError;
}
@Override
public String toString() {
return "HttpClientConnection{" +
"callback=" + callback +
", response=" + response +
", httpClient=" + client +
", keepAlive=" + (pool == client.poolKeepAlive) +
// ", lastRequestUrl='" + (request.getFullUrl() == null ? "" : request.getFullUrl()) + '\'' +
", remoteAddress=" + remoteAddress +
',' + super.toString() +
'}';
}
}