/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF 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 com.sun.jini.jeri.internal.http; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.ConnectException; import java.net.HttpURLConnection; import java.net.Socket; import java.security.AccessController; import java.security.PrivilegedAction; import java.util.Collection; import net.jini.core.constraint.InvocationConstraints; import net.jini.jeri.OutboundRequest; import net.jini.io.context.AcknowledgmentSource; /** * Class representing a client-side HTTP connection used to send HTTP requests. * * @author Sun Microsystems, Inc. * */ public class HttpClientConnection implements TimedConnection { private static final int HTTP_MAJOR = 1; private static final int HTTP_MINOR = 1; private static final String clientString = (String) AccessController.doPrivileged(new PrivilegedAction() { public Object run() { return "Java/" + System.getProperty("java.version", "???") + " " + HttpClientConnection.class.getName(); } }); /* modes */ private static final int DIRECT = 0; private static final int PROXIED = 1; private static final int TUNNELED = 2; /* states */ private static final int IDLE = 0; private static final int BUSY = 1; private static final int CLOSED = 2; private final int mode; private final Object stateLock = new Object(); private int state = IDLE; private final HttpClientManager manager; private ServerInfo targetInfo; private ServerInfo proxyInfo; private final boolean persist; private String[] acks; private Socket sock; private OutputStream out; private InputStream in; /** * Creates HttpClientConnection which sends requests directly to given * host/port through a socket obtained from the given socket factory. * The createSocket method of the given socket factory may be called * more than once in cases where connection establishment involves multiple * HTTP message exchanges. */ public HttpClientConnection(String host, int port, HttpClientSocketFactory factory, HttpClientManager manager) throws IOException { this.manager = manager; mode = DIRECT; targetInfo = manager.getServerInfo(host, port); persist = true; setupConnection(factory); } /** * Creates HttpClientConnection which sends requests to given target * host/port through the indicated HTTP proxy over a socket provided by the * specified socket factory. If tunnel is true, requests are tunneled * through the proxy over an additional layered socket (also provided by * the socket factory). If persist and tunnel are both false, the * connection can only be used for a single request. If persist is true * and tunnel is false, a persistent connection is maintained if possible. * The createSocket and createTunnelSocket methods of the given socket * factory may be called more than once in cases where connection * establishment involves multiple HTTP message exchanges. */ public HttpClientConnection(String targetHost, int targetPort, String proxyHost, int proxyPort, boolean tunnel, boolean persist, HttpClientSocketFactory factory, HttpClientManager manager) throws IOException { this.manager = manager; mode = tunnel ? TUNNELED : PROXIED; targetInfo = manager.getServerInfo(targetHost, targetPort); proxyInfo = manager.getServerInfo(proxyHost, proxyPort); this.persist = persist || tunnel; setupConnection(factory); } /** * Pings target. Returns true if ping succeeded, false if it fails * "cleanly" (i.e., if a valid HTTP response indicating request failure is * received). */ public boolean ping() throws IOException { markBusy(); fetchServerInfo(); try { return ping(false); } finally { markIdle(); } } /** * Initiates new request to connection target. Throws an IOException if * the connection is currently busy. */ public OutboundRequest newRequest() throws IOException { OutboundRequest req = null; markBusy(); fetchServerInfo(); try { req = new OutboundRequestImpl(); return req; } finally { if (req == null) { markIdle(); } } } /** * Upcall indicating that connection has become idle. Subclasses may * override this method to perform an appropriate action, such as * scheduling an idle timeout. */ protected void idle() { } /** * Returns socket over which requests are sent. */ public Socket getSocket() { return sock; } /** * Attempts to shut down connection, returning true if connection is * closed. If force is true, connection is always shut down; if force is * false, connection is only shut down if idle. */ public boolean shutdown(boolean force) { synchronized (stateLock) { if (state == CLOSED) { return true; } if (!force && state == BUSY) { return false; } state = CLOSED; } disconnect(); return true; } /** * Fetches latest server/proxy HTTP information from cache. */ private void fetchServerInfo() { ServerInfo sinfo = manager.getServerInfo(targetInfo.host, targetInfo.port); if (sinfo.timestamp > targetInfo.timestamp) { targetInfo = sinfo; } if (mode != DIRECT) { sinfo = manager.getServerInfo(proxyInfo.host, proxyInfo.port); if (sinfo.timestamp > proxyInfo.timestamp) { proxyInfo = sinfo; } } } /** * Flushes current copy of server/proxy HTTP information to cache. */ private void flushServerInfo() { manager.cacheServerInfo(targetInfo); if (mode != DIRECT) { manager.cacheServerInfo(proxyInfo); } } /** * Marks connection busy. Throws IOException if connection closed. */ private void markBusy() throws IOException { synchronized (stateLock) { if (state == BUSY) { throw new IOException("connection busy"); } else if (state == CLOSED) { throw new IOException("connection closed"); } state = BUSY; } } /** * Marks connection idle. Does nothing if connection closed. */ private void markIdle() { synchronized (stateLock) { if (state == CLOSED) { return; } state = IDLE; } idle(); } /** * Establishes connection using sockets from the given socket factory. * Throws IOException if connection setup fails. */ private void setupConnection(HttpClientSocketFactory factory) throws IOException { boolean ok = false; try { /* * 4 cycles required in worst-case (proxied) scenario: * i = 0: send OPTIONS request to proxy * i = 1: send ping, fails with 407 (proxy auth required) * i = 2: send ping, fails with 401 (unauthorized) * i = 3: return */ for (int i = 0; i < 4; i++) { if (sock == null) { connect(factory); } if (mode == PROXIED && proxyInfo.timestamp == ServerInfo.NO_TIMESTAMP) { requestProxyOptions(); } else if (targetInfo.timestamp == ServerInfo.NO_TIMESTAMP) { ping(true); } else { ok = true; return; } } } finally { if (!ok) { disconnect(); } } throw new ConnectException("failed to establish HTTP connection"); } /** * Opens underlying connection. If tunneling through an HTTP proxy, * attempts CONNECT request. */ private void connect(HttpClientSocketFactory factory) throws IOException { disconnect(); for (int i = 0; i < 2; i++) { if (sock == null) { ServerInfo sinfo = (mode == DIRECT) ? targetInfo : proxyInfo; sock = factory.createSocket(sinfo.host, sinfo.port); out = new BufferedOutputStream(sock.getOutputStream()); in = new BufferedInputStream(sock.getInputStream()); } if (mode != TUNNELED) { return; } if (requestProxyConnect()) { sock = factory.createTunnelSocket(sock); out = new BufferedOutputStream(sock.getOutputStream()); in = new BufferedInputStream(sock.getInputStream()); return; } } throw new ConnectException("failed to establish proxy tunnel"); } /** * Closes and releases reference to underlying socket. */ private void disconnect() { if (sock != null) { try { sock.close(); } catch (IOException ex) {} sock = null; out = null; in = null; } } /** * Pings target. Returns true if succeeded, false if failed "cleanly". */ private boolean ping(boolean setup) throws IOException { StartLine outLine = createPostLine(); Header outHeader = createPostHeader(outLine); outHeader.setField("RMI-Request-Type", "ping"); MessageWriter writer = new MessageWriter(out, false); writer.writeStartLine(outLine); writer.writeHeader(outHeader); writer.writeTrailer(null); MessageReader reader; StartLine inLine; Header inHeader; do { reader = new MessageReader(in, false); inLine = reader.readStartLine(); inHeader = reader.readHeader(); inHeader.merge(reader.readTrailer()); } while (inLine.status / 100 == 1); analyzePostResponse(inLine, inHeader); if (!supportsPersist(inLine, inHeader)) { if (setup) { disconnect(); } else { shutdown(true); } } return (inLine.status / 100) == 2; } /** * Sends OPTIONS request to proxy. Returns true if OPTIONS succeeded, * false otherwise. */ private boolean requestProxyOptions() throws IOException { MessageWriter writer = new MessageWriter(out, false); writer.writeStartLine( new StartLine(HTTP_MAJOR, HTTP_MINOR, "OPTIONS", "*")); writer.writeHeader(createOptionsHeader()); writer.writeTrailer(null); MessageReader reader; StartLine inLine; Header inHeader; do { reader = new MessageReader(in, false); inLine = reader.readStartLine(); inHeader = reader.readHeader(); inHeader.merge(reader.readTrailer()); } while (inLine.status / 100 == 1); analyzeProxyResponse(inLine, inHeader); if (!supportsPersist(inLine, inHeader)) { disconnect(); } return (inLine.status / 100) == 2; } /** * Sends CONNECT request to proxy. Returns true if CONNECT succeeded, * false otherwise. */ private boolean requestProxyConnect() throws IOException { StartLine outLine = new StartLine( HTTP_MAJOR, HTTP_MINOR, "CONNECT", targetInfo.host + ":" + targetInfo.port); // REMIND: eliminate hardcoded protocol string Header outHeader = createConnectHeader(); String auth = proxyInfo.getAuthString("http", outLine.method, outLine.uri); if (auth != null) { outHeader.setField("Proxy-Authorization", auth); } MessageWriter writer = new MessageWriter(out, false); writer.writeStartLine(outLine); writer.writeHeader(outHeader); writer.writeTrailer(null); MessageReader reader; StartLine inLine; Header inHeader; do { reader = new MessageReader(in, true); inLine = reader.readStartLine(); inHeader = reader.readHeader(); inHeader.merge(reader.readTrailer()); } while (inLine.status / 100 == 1); analyzeProxyResponse(inLine, inHeader); if ((inLine.status / 100) == 2) { return true; } if (!supportsPersist(inLine, inHeader)) { disconnect(); } return false; } /** * Creates start line for outbound HTTP POST message. */ private StartLine createPostLine() { String uri = (mode == PROXIED) ? "http://" + targetInfo.host + ":" + targetInfo.port + "/" : "/"; return new StartLine(HTTP_MAJOR, HTTP_MINOR, "POST", uri); } /** * Creates base header containing fields common to all HTTP messages sent * by this connection. */ private Header createBaseHeader() { Header header = new Header(); long now = System.currentTimeMillis(); header.setField("Date", Header.getDateString(now)); header.setField("User-Agent", clientString); return header; } /** * Creates header for use in CONNECT messages sent to proxies. */ private Header createConnectHeader() { Header header = createBaseHeader(); header.setField("Host", targetInfo.host + ":" + targetInfo.port); return header; } /** * Creates header for use in OPTIONS messages sent to proxies. */ private Header createOptionsHeader() { Header header = createBaseHeader(); header.setField("Host", proxyInfo.host + ":" + proxyInfo.port); if (!persist) { header.setField("Connection", "close"); } return header; } /** * Creates header for outbound HTTP POST message with given start line. */ private Header createPostHeader(StartLine sline) { Header header = createBaseHeader(); header.setField("Host", targetInfo.host + ":" + targetInfo.port); header.setField("Connection", persist ? "TE" : "close, TE"); header.setField("TE", "trailers"); // REMIND: eliminate hardcoded protocol string String auth = targetInfo.getAuthString("http", sline.method, sline.uri); if (auth != null) { header.setField("Authorization", auth); } if (mode == PROXIED) { auth = proxyInfo.getAuthString("http", sline.method, sline.uri); if (auth != null) { header.setField("Proxy-Authorization", auth); } } acks = manager.getUnsentAcks(targetInfo.host, targetInfo.port); if (acks.length > 0) { String ackList = acks[0]; for (int i = 1; i < acks.length; i++) { ackList += ", " + acks[i]; } header.setField("RMI-Response-Ack", ackList); } return header; } /** * Analyzes POST response message start line and header, updating cached * target/proxy server information if necessary. */ private void analyzePostResponse(StartLine inLine, Header inHeader) { String str; long now = System.currentTimeMillis(); if ((str = inHeader.getField("WWW-Authenticate")) != null) { try { targetInfo.setAuthInfo(str); } catch (HttpParseException ex) { } targetInfo.timestamp = now; } else if ((str = inHeader.getField("Authentication-Info")) != null) { try { targetInfo.updateAuthInfo(str); } catch (HttpParseException ex) { } targetInfo.timestamp = now; } if (mode != DIRECT) { if ((str = inHeader.getField("Proxy-Authenticate")) != null) { try { proxyInfo.setAuthInfo(str); } catch (HttpParseException ex) { } proxyInfo.timestamp = now; } else if ((str = inHeader.getField( "Proxy-Authentication-Info")) != null) { try { proxyInfo.updateAuthInfo(str); } catch (HttpParseException ex) { } proxyInfo.timestamp = now; } } if (mode != PROXIED) { targetInfo.major = inLine.major; targetInfo.minor = inLine.minor; targetInfo.timestamp = now; } else { /* Return message was sent by proxy; however, since some proxies * incorrectly relay the target server's version numbers instead of * their own, we can only rely on version numbers which could not * have been sent from target server. */ if (inLine.status == HttpURLConnection.HTTP_PROXY_AUTH) { proxyInfo.major = inLine.major; proxyInfo.minor = inLine.minor; } proxyInfo.timestamp = now; } if ((inLine.status / 100) == 2) { manager.clearUnsentAcks(targetInfo.host, targetInfo.port, acks); targetInfo.timestamp = now; } flushServerInfo(); } /** * Analyzes CONNECT or OPTIONS response message start line and header sent * by proxy, updating proxy server information if necessary. */ private void analyzeProxyResponse(StartLine inLine, Header inHeader) { proxyInfo.major = inLine.major; proxyInfo.minor = inLine.minor; proxyInfo.timestamp = System.currentTimeMillis(); String str; if ((str = inHeader.getField("Proxy-Authenticate")) != null) { try { proxyInfo.setAuthInfo(str); } catch (HttpParseException ex) { } } else if ((str = inHeader.getField( "Proxy-Authentication-Info")) != null) { try { proxyInfo.updateAuthInfo(str); } catch (HttpParseException ex) { } } flushServerInfo(); } /** * Returns true if requests sent over this connection should chunk output. */ private boolean supportsChunking() { ServerInfo si = (mode == PROXIED) ? proxyInfo : targetInfo; return StartLine.compareVersions(si.major, si.minor, 1, 1) >= 0; } /** * Returns true if the given response line and header indicate that the * connection can be persisted, and use of persistent connections has * not been disabled. */ private boolean supportsPersist(StartLine sline, Header header) { if (header.containsValue("Connection", "close", true)) { return false; } else if (!persist) { return false; } else if (header.containsValue("Connection", "Keep-Alive", true)) { return true; } else { int c = StartLine.compareVersions(sline.major, sline.minor, 1, 1); return c >= 0; } } /** * HTTP-based implementation of OutboundRequest abstraction. */ private class OutboundRequestImpl extends Request implements OutboundRequest { private final MessageWriter writer; private MessageReader reader; private StartLine inLine; private Header inHeader; private boolean persist = false; OutboundRequestImpl() throws IOException { StartLine outLine = createPostLine(); Header outHeader = createPostHeader(outLine); outHeader.setField("RMI-Request-Type", "standard"); writer = new MessageWriter(out, supportsChunking()); writer.writeStartLine(outLine); writer.writeHeader(outHeader); writer.flush(); } public void populateContext(Collection context) { if (context == null) { throw new NullPointerException(); } } public InvocationConstraints getUnfulfilledConstraints() { /* * NYI: With no request-specific hook, we depend on * OutboundRequest wrapping for this method. */ throw new AssertionError(); } public OutputStream getRequestOutputStream() { return getOutputStream(); } public InputStream getResponseInputStream() { return getInputStream(); } void startOutput() throws IOException { // start line, header already written } void write(byte[] b, int off, int len) throws IOException { writer.writeContent(b, off, len); } void endOutput() throws IOException { writer.writeTrailer(null); } boolean startInput() throws IOException { for (;;) { reader = new MessageReader(in, false); inLine = reader.readStartLine(); inHeader = reader.readHeader(); if (inLine.status / 100 != 1) { return inLine.status / 100 == 2; } reader.readTrailer(); } } int read(byte[] b, int off, int len) throws IOException { return reader.readContent(b, off, len); } int available() throws IOException { return reader.availableContent(); } void endInput() throws IOException { inHeader.merge(reader.readTrailer()); analyzePostResponse(inLine, inHeader); persist = supportsPersist(inLine, inHeader); } void addAckListener(AcknowledgmentSource.Listener listener) { throw new UnsupportedOperationException(); } void done(boolean corrupt) { if (corrupt || !persist) { shutdown(true); } else { markIdle(); } } } }