Java tutorial
/* * Copyright 2010-2013 Ning, Inc. * * Ning licenses this file to you 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 org.asynchttpclient.providers.netty.handler; import static io.netty.handler.codec.http.HttpResponseStatus.CONTINUE; import static io.netty.handler.codec.http.HttpResponseStatus.OK; import static io.netty.handler.codec.http.HttpResponseStatus.PROXY_AUTHENTICATION_REQUIRED; import static io.netty.handler.codec.http.HttpResponseStatus.UNAUTHORIZED; import static org.asynchttpclient.providers.netty.util.HttpUtil.isNTLM; import static org.asynchttpclient.util.MiscUtil.isNonEmpty; import org.asynchttpclient.AsyncHandler; import org.asynchttpclient.AsyncHandler.STATE; import org.asynchttpclient.AsyncHttpClientConfig; import org.asynchttpclient.FluentCaseInsensitiveStringsMap; import org.asynchttpclient.HttpResponseBodyPart; import org.asynchttpclient.HttpResponseHeaders; import org.asynchttpclient.HttpResponseStatus; import org.asynchttpclient.ProxyServer; import org.asynchttpclient.Realm; import org.asynchttpclient.Request; import org.asynchttpclient.RequestBuilder; import org.asynchttpclient.ntlm.NTLMEngine; import org.asynchttpclient.ntlm.NTLMEngineException; import org.asynchttpclient.providers.netty.Callback; import org.asynchttpclient.providers.netty.NettyAsyncHttpProviderConfig; import org.asynchttpclient.providers.netty.channel.Channels; import org.asynchttpclient.providers.netty.future.NettyResponseFuture; import org.asynchttpclient.providers.netty.request.NettyRequest; import org.asynchttpclient.providers.netty.request.NettyRequestSender; import org.asynchttpclient.providers.netty.response.NettyResponseBodyPart; import org.asynchttpclient.providers.netty.response.ResponseHeaders; import org.asynchttpclient.providers.netty.response.ResponseStatus; import org.asynchttpclient.spnego.SpnegoEngine; import org.asynchttpclient.util.AsyncHttpProviderUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.netty.buffer.ByteBuf; import io.netty.channel.Channel; import io.netty.handler.codec.http.HttpContent; import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.LastHttpContent; import java.io.IOException; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.util.List; final class HttpProtocol extends Protocol { private static final Logger LOGGER = LoggerFactory.getLogger(HttpProtocol.class); public HttpProtocol(Channels channels, AsyncHttpClientConfig config, NettyAsyncHttpProviderConfig nettyConfig, NettyRequestSender requestSender) { super(channels, config, nettyConfig, requestSender); } private Realm.RealmBuilder newRealmBuilder(Realm realm) { return realm != null ? new Realm.RealmBuilder().clone(realm) : new Realm.RealmBuilder(); } private Realm kerberosChallenge(List<String> proxyAuth, Request request, ProxyServer proxyServer, FluentCaseInsensitiveStringsMap headers, Realm realm, NettyResponseFuture<?> future, boolean proxyInd) throws NTLMEngineException { URI uri = request.getURI(); String host = request.getVirtualHost() == null ? AsyncHttpProviderUtils.getHost(uri) : request.getVirtualHost(); String server = proxyServer == null ? host : proxyServer.getHost(); try { String challengeHeader = SpnegoEngine.instance().generateToken(server); headers.remove(HttpHeaders.Names.AUTHORIZATION); headers.add(HttpHeaders.Names.AUTHORIZATION, "Negotiate " + challengeHeader); return newRealmBuilder(realm)// .setUri(uri.getRawPath())// .setMethodName(request.getMethod())// .setScheme(Realm.AuthScheme.KERBEROS)// .build(); } catch (Throwable throwable) { if (isNTLM(proxyAuth)) { return ntlmChallenge(proxyAuth, request, proxyServer, headers, realm, future, proxyInd); } channels.abort(future, throwable); return null; } } private String authorizationHeaderName(boolean proxyInd) { return proxyInd ? HttpHeaders.Names.PROXY_AUTHORIZATION : HttpHeaders.Names.AUTHORIZATION; } private void addNTLMAuthorizationHeader(FluentCaseInsensitiveStringsMap headers, String challengeHeader, boolean proxyInd) { headers.add(authorizationHeaderName(proxyInd), "NTLM " + challengeHeader); } private Realm ntlmChallenge(List<String> wwwAuth, Request request, ProxyServer proxyServer, FluentCaseInsensitiveStringsMap headers, Realm realm, NettyResponseFuture<?> future, boolean proxyInd) throws NTLMEngineException { boolean useRealm = proxyServer == null && realm != null; String ntlmDomain = useRealm ? realm.getNtlmDomain() : proxyServer.getNtlmDomain(); String ntlmHost = useRealm ? realm.getNtlmHost() : proxyServer.getHost(); String principal = useRealm ? realm.getPrincipal() : proxyServer.getPrincipal(); String password = useRealm ? realm.getPassword() : proxyServer.getPassword(); if (realm != null && !realm.isNtlmMessageType2Received()) { String challengeHeader = NTLMEngine.INSTANCE.generateType1Msg(ntlmDomain, ntlmHost); URI uri = request.getURI(); addNTLMAuthorizationHeader(headers, challengeHeader, proxyInd); future.getAndSetAuth(false); return newRealmBuilder(realm)// .setScheme(realm.getAuthScheme())// .setUri(uri.getRawPath())// .setMethodName(request.getMethod())// .setNtlmMessageType2Received(true)// .build(); } else { addType3NTLMAuthorizationHeader(wwwAuth, headers, principal, password, ntlmDomain, ntlmHost, proxyInd); Realm.AuthScheme authScheme = realm != null ? realm.getAuthScheme() : Realm.AuthScheme.NTLM; return newRealmBuilder(realm)// .setScheme(authScheme)// .setUri(request.getURI().getPath())// .setMethodName(request.getMethod())// .build(); } } private Realm ntlmProxyChallenge(List<String> wwwAuth, Request request, ProxyServer proxyServer, FluentCaseInsensitiveStringsMap headers, Realm realm, NettyResponseFuture<?> future, boolean proxyInd) throws NTLMEngineException { future.getAndSetAuth(false); headers.remove(HttpHeaders.Names.PROXY_AUTHORIZATION); addType3NTLMAuthorizationHeader(wwwAuth, headers, proxyServer.getPrincipal(), proxyServer.getPassword(), proxyServer.getNtlmDomain(), proxyServer.getHost(), proxyInd); return newRealmBuilder(realm)// // .setScheme(realm.getAuthScheme()) .setUri(request.getURI().getPath())// .setMethodName(request.getMethod()).build(); } private void addType3NTLMAuthorizationHeader(List<String> auth, FluentCaseInsensitiveStringsMap headers, String username, String password, String domain, String workstation, boolean proxyInd) throws NTLMEngineException { headers.remove(authorizationHeaderName(proxyInd)); if (isNonEmpty(auth) && auth.get(0).startsWith("NTLM ")) { String serverChallenge = auth.get(0).trim().substring("NTLM ".length()); String challengeHeader = NTLMEngine.INSTANCE.generateType3Msg(username, password, domain, workstation, serverChallenge); addNTLMAuthorizationHeader(headers, challengeHeader, proxyInd); } } private void finishUpdate(final NettyResponseFuture<?> future, Channel channel, boolean lastValidChunk) throws IOException { if (lastValidChunk && future.isKeepAlive()) { channels.drainChannel(channel, future); } else { if (future.isKeepAlive() && channel.isActive() && channels.offerToPool(channels.getPoolKey(future), channel)) { markAsDone(future, channel); return; } channels.finishChannel(channel); } markAsDone(future, channel); } private final boolean updateBodyAndInterrupt(NettyResponseFuture<?> future, AsyncHandler<?> handler, HttpResponseBodyPart bodyPart) throws Exception { boolean state = handler.onBodyPartReceived(bodyPart) != STATE.CONTINUE; if (bodyPart.isUnderlyingConnectionToBeClosed()) { future.setKeepAlive(false); } return state; } private void markAsDone(NettyResponseFuture<?> future, final Channel channel) throws MalformedURLException { // We need to make sure everything is OK before adding the // connection back to the pool. try { future.done(); } catch (Throwable t) { // Never propagate exception once we know we are done. LOGGER.debug(t.getMessage(), t); } if (!future.isKeepAlive() || !channel.isActive()) { channels.closeChannel(channel); } } private final String computeRealmURI(Realm realm, URI requestURI) throws URISyntaxException { if (realm.isUseAbsoluteURI()) { if (realm.isOmitQuery() && isNonEmpty(requestURI.getQuery())) { return new URI(requestURI.getScheme(), requestURI.getAuthority(), requestURI.getPath(), null, null) .toString(); } else { return requestURI.toString(); } } else { if (realm.isOmitQuery() || !isNonEmpty(requestURI.getQuery())) { return requestURI.getPath(); } else { return requestURI.getPath() + "?" + requestURI.getQuery(); } } } private boolean handleUnauthorizedAndExit(int statusCode, Realm realm, final Request request, HttpResponse response, final NettyResponseFuture<?> future, ProxyServer proxyServer, final Channel channel) throws Exception { if (statusCode == UNAUTHORIZED.code() && realm != null) { List<String> authenticateHeaders = response.headers().getAll(HttpHeaders.Names.WWW_AUTHENTICATE); if (!authenticateHeaders.isEmpty() && !future.getAndSetAuth(true)) { future.setState(NettyResponseFuture.STATE.NEW); Realm newRealm = null; // NTLM boolean negociate = authenticateHeaders.contains("Negotiate"); if (!authenticateHeaders.contains("Kerberos") && (isNTLM(authenticateHeaders) || negociate)) { newRealm = ntlmChallenge(authenticateHeaders, request, proxyServer, request.getHeaders(), realm, future, false); // SPNEGO KERBEROS } else if (negociate) { newRealm = kerberosChallenge(authenticateHeaders, request, proxyServer, request.getHeaders(), realm, future, false); if (newRealm == null) { return true; } } else { newRealm = new Realm.RealmBuilder().clone(realm).setScheme(realm.getAuthScheme()) .setUri(request.getURI().getPath()).setMethodName(request.getMethod()) .setUsePreemptiveAuth(true).parseWWWAuthenticateHeader(authenticateHeaders.get(0)) .build(); } String realmURI = computeRealmURI(newRealm, request.getURI()); Realm nr = new Realm.RealmBuilder().clone(newRealm).setUri(realmURI).build(); final Request nextRequest = new RequestBuilder(future.getRequest()).setHeaders(request.getHeaders()) .setRealm(nr).build(); LOGGER.debug("Sending authentication to {}", request.getURI()); Callback callback = new Callback(future) { public void call() throws Exception { channels.drainChannel(channel, future); requestSender.sendNextRequest(nextRequest, future); } }; if (future.isKeepAlive() && HttpHeaders.isTransferEncodingChunked(response)) { // We must make sure there is no bytes left // before executing the next request. Channels.setDefaultAttribute(channel, callback); } else { callback.call(); } return true; } } return false; } private boolean handleContinueAndExit(final Channel channel, final NettyResponseFuture<?> future, int statusCode) { if (statusCode == CONTINUE.code()) { future.setHeadersAlreadyWrittenOnContinue(true); future.setDontWriteBodyBecauseExpectContinue(false); // FIXME why not reuse the channel? requestSender.writeRequest(future, channel); return true; } return false; } private boolean handleProxyAuthenticationRequiredAndExit(int statusCode, // Realm realm, // final Request request, // HttpResponse response, // final NettyResponseFuture<?> future, // ProxyServer proxyServer) throws Exception { if (statusCode == PROXY_AUTHENTICATION_REQUIRED.code() && realm != null) { List<String> proxyAuthenticateHeaders = response.headers().getAll(HttpHeaders.Names.PROXY_AUTHENTICATE); if (!proxyAuthenticateHeaders.isEmpty() && !future.getAndSetAuth(true)) { LOGGER.debug("Sending proxy authentication to {}", request.getURI()); future.setState(NettyResponseFuture.STATE.NEW); Realm newRealm = null; boolean negociate = proxyAuthenticateHeaders.contains("Negotiate"); if (!proxyAuthenticateHeaders.contains("Kerberos") && (isNTLM(proxyAuthenticateHeaders) || negociate)) { newRealm = ntlmProxyChallenge(proxyAuthenticateHeaders, request, proxyServer, request.getHeaders(), realm, future, true); // SPNEGO KERBEROS } else if (negociate) { newRealm = kerberosChallenge(proxyAuthenticateHeaders, request, proxyServer, request.getHeaders(), realm, future, true); if (newRealm == null) return true; } else { newRealm = new Realm.RealmBuilder().clone(realm)// .setScheme(realm.getAuthScheme())// .setUri("/")// .setMethodName(HttpMethod.CONNECT.name())// .setUsePreemptiveAuth(true)// .parseProxyAuthenticateHeader(proxyAuthenticateHeaders.get(0))// .build(); } future.setReuseChannel(true); future.setConnectAllowed(true); Request nextRequest = new RequestBuilder(future.getRequest()).setHeaders(request.getHeaders()) .setRealm(newRealm).build(); requestSender.sendNextRequest(nextRequest, future); return true; } } return false; } private boolean handleConnectOKAndExit(int statusCode, Realm realm, final Request request, HttpRequest httpRequest, HttpResponse response, final NettyResponseFuture<?> future, ProxyServer proxyServer, final Channel channel) throws IOException { if (statusCode == OK.code() && httpRequest.getMethod() == HttpMethod.CONNECT) { LOGGER.debug("Connected to {}:{}", proxyServer.getHost(), proxyServer.getPort()); if (future.isKeepAlive()) { future.attachChannel(channel, true); } try { URI requestURI = request.getURI(); String scheme = requestURI.getScheme(); LOGGER.debug("Connecting to proxy {} for scheme {}", proxyServer, scheme); String host = AsyncHttpProviderUtils.getHost(requestURI); int port = AsyncHttpProviderUtils.getPort(requestURI); channels.upgradeProtocol(channel.pipeline(), scheme, host, port); } catch (Throwable ex) { channels.abort(future, ex); } future.setReuseChannel(true); future.setConnectAllowed(false); requestSender.sendNextRequest(new RequestBuilder(future.getRequest()).build(), future); return true; } return false; } private boolean handleHanderAndExit(Channel channel, NettyResponseFuture<?> future, AsyncHandler<?> handler, HttpResponseStatus status, HttpResponseHeaders responseHeaders, HttpResponse response) throws Exception { if (!future.getAndSetStatusReceived(true) && (handler.onStatusReceived(status) != STATE.CONTINUE || handler.onHeadersReceived(responseHeaders) != STATE.CONTINUE)) { finishUpdate(future, channel, HttpHeaders.isTransferEncodingChunked(response)); return true; } return false; } private boolean handleResponseAndExit(final Channel channel, final NettyResponseFuture<?> future, AsyncHandler<?> handler, HttpRequest httpRequest, ProxyServer proxyServer, HttpResponse response) throws Exception { // store the original headers so we can re-send all them to // the handler in case of trailing headers future.setHttpHeaders(response.headers()); future.setKeepAlive( !HttpHeaders.Values.CLOSE.equalsIgnoreCase(response.headers().get(HttpHeaders.Names.CONNECTION))); HttpResponseStatus status = new ResponseStatus(future.getURI(), response, config); int statusCode = response.getStatus().code(); Request request = future.getRequest(); Realm realm = request.getRealm() != null ? request.getRealm() : config.getRealm(); HttpResponseHeaders responseHeaders = new ResponseHeaders(future.getURI(), response.headers()); return handleResponseFiltersReplayRequestAndExit(channel, future, status, responseHeaders)// || handleUnauthorizedAndExit(statusCode, realm, request, response, future, proxyServer, channel)// || handleContinueAndExit(channel, future, statusCode)// || handleProxyAuthenticationRequiredAndExit(statusCode, realm, request, response, future, proxyServer) || handleConnectOKAndExit(statusCode, realm, request, httpRequest, response, future, proxyServer, channel)// || handleRedirectAndExit(request, future, response, channel)// || handleHanderAndExit(channel, future, handler, status, responseHeaders, response); } @Override public void handle(final Channel channel, final NettyResponseFuture<?> future, final Object e) throws Exception { future.touch(); // The connect timeout occurred. if (future.isCancelled() || future.isDone()) { channels.finishChannel(channel); return; } NettyRequest nettyRequest = future.getNettyRequest(); AsyncHandler<?> handler = future.getAsyncHandler(); ProxyServer proxyServer = future.getProxyServer(); try { if (e instanceof HttpResponse) { HttpResponse response = (HttpResponse) e; LOGGER.debug("\n\nRequest {}\n\nResponse {}\n", nettyRequest.getHttpRequest(), response); future.setPendingResponse(response); return; } if (e instanceof HttpContent) { HttpResponse response = future.getPendingResponse(); future.setPendingResponse(null); if (handler != null) { if (response != null && handleResponseAndExit(channel, future, handler, nettyRequest.getHttpRequest(), proxyServer, response)) { return; } HttpContent chunk = (HttpContent) e; boolean interrupt = false; boolean last = chunk instanceof LastHttpContent; if (last) { LastHttpContent lastChunk = (LastHttpContent) chunk; HttpHeaders trailingHeaders = lastChunk.trailingHeaders(); if (!trailingHeaders.isEmpty()) { ResponseHeaders responseHeaders = new ResponseHeaders(future.getURI(), future.getHttpHeaders(), trailingHeaders); interrupt = handler.onHeadersReceived(responseHeaders) != STATE.CONTINUE; } } ByteBuf buf = chunk.content(); try { if (!interrupt && (buf.readableBytes() > 0 || last)) { NettyResponseBodyPart part = nettyConfig.getBodyPartFactory().newResponseBodyPart(buf, last); interrupt = updateBodyAndInterrupt(future, handler, part); } } finally { // FIXME we shouldn't need this, should we? But a leak was reported there without it?! buf.release(); } if (interrupt || last) { finishUpdate(future, channel, !last); } } } } catch (Exception t) { if (hasIOExceptionFilters// && t instanceof IOException// && requestSender.applyIoExceptionFiltersAndReplayRequest(future, IOException.class.cast(t), channel)) { return; } try { channels.abort(future, t); } catch (Exception abortException) { LOGGER.debug("Abort failed", abortException); } finally { finishUpdate(future, channel, false); } throw t; } } @Override public void onError(Channel channel, Throwable error) { } @Override public void onClose(Channel channel) { } }