// // $Id: HttpConnect.java 286 2007-06-17 09:03:29 +0000 (dim., 17 juin 2007) // felfert $ // // jupload - A file upload applet. // // Copyright 2007 The JUpload Team // // Created: 07.05.2007 // Creator: felfert // Last modified: $Date: 2008-06-05 05:16:05 -0700 (Thu, 05 Jun 2008) $ // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation; either version 2 of the License, or // (at your option) any later version. // // This program 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 for more details. // // You should have received a copy of the GNU General Public License // along with this program; if not, write to the Free Software // Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. package wjhk.jupload2.upload; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.ConnectException; import java.net.InetSocketAddress; import java.net.Proxy; import java.net.ProxySelector; import java.net.Socket; import java.net.URISyntaxException; import java.net.URL; import java.net.UnknownHostException; import java.security.KeyManagementException; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocket; import wjhk.jupload2.exception.JUploadException; import wjhk.jupload2.policies.UploadPolicy; /** * This class implements the task of connecting to a HTTP(S) url using a proxy. * * @author felfert */ public class HttpConnect { private final static String DEFAULT_PROTOCOL = "HTTP/1.1"; /** * The current upload policy. Used for logging, and to get the post URL. * Also used to change this URL, when it has moved (301, 302 or 303 return * code) */ private UploadPolicy uploadPolicy; /** * Helper function for perforing a proxy CONNECT request. * * @param proxy The proxy to use. * @param host The destination's hostname. * @param port The destination's port * @return An established socket connection to the proxy. * @throws ConnectException if the proxy response code is not 200 * @throws UnknownHostException * @throws IOException */ private Socket HttpProxyConnect(Proxy proxy, String host, int port) throws UnknownHostException, IOException, ConnectException { InetSocketAddress sa = (InetSocketAddress) proxy.address(); String phost = (sa.isUnresolved()) ? sa.getHostName() : sa.getAddress() .getHostAddress(); int pport = sa.getPort(); // Socket proxysock = new Socket(phost, pport); String req = "CONNECT " + host + ":" + port + " " + DEFAULT_PROTOCOL + "\r\n\r\n"; proxysock.getOutputStream().write(req.getBytes()); BufferedReader proxyIn = new BufferedReader(new InputStreamReader( proxysock.getInputStream())); // We expect exactly one line: the proxy response String line = proxyIn.readLine(); if (!line.matches("^HTTP/\\d\\.\\d\\s200\\s.*")) throw new ConnectException("Proxy response: " + line); this.uploadPolicy.displayDebug("Proxy response: " + line, 40); proxyIn.readLine(); // eat the header delimiter // we now are connected ... return proxysock; } /** * Connects to a given URL. * * @param url The URL to connect to * @param proxy The proxy to be used, may be null if direct connection is * needed * @return A socket, connected to the specified URL. May be null if an error * occurs. * @throws NoSuchAlgorithmException * @throws KeyManagementException * @throws IOException * @throws UnknownHostException * @throws ConnectException * @throws CertificateException * @throws KeyStoreException * @throws UnrecoverableKeyException * @throws IllegalArgumentException */ public Socket Connect(URL url, Proxy proxy) throws NoSuchAlgorithmException, KeyManagementException, ConnectException, UnknownHostException, IOException, KeyStoreException, CertificateException, IllegalArgumentException, UnrecoverableKeyException { // Temporary socket for SOCKS support Socket tsock; Socket ret = null; String host = url.getHost(); int port; boolean useProxy = ((proxy != null) && (proxy.type() != Proxy.Type.DIRECT)); // Check if SSL connection is needed if (url.getProtocol().equals("https")) { port = (-1 == url.getPort()) ? 443 : url.getPort(); SSLContext context = SSLContext.getInstance("SSL"); // Allow all certificates InteractiveTrustManager tm = new InteractiveTrustManager( this.uploadPolicy, url.getHost(), null); context.init(tm.getKeyManagers(), tm.getTrustManagers(), SecureRandom.getInstance("SHA1PRNG")); if (useProxy) { if (proxy.type() == Proxy.Type.HTTP) { // First establish a CONNECT, then do a normal SSL // thru that connection. this.uploadPolicy.displayDebug( "Using SSL socket, via HTTP proxy", 20); ret = context.getSocketFactory().createSocket( HttpProxyConnect(proxy, host, port), host, port, true); } else if (proxy.type() == Proxy.Type.SOCKS) { this.uploadPolicy.displayDebug( "Using SSL socket, via SOCKS proxy", 20); tsock = new Socket(proxy); tsock.connect(new InetSocketAddress(host, port)); ret = context.getSocketFactory().createSocket(tsock, host, port, true); } else throw new ConnectException("Unkown proxy type " + proxy.type()); } else { // If port not specified then use default https port // 443. this.uploadPolicy.displayDebug( "Using SSL socket, direct connection", 20); ret = context.getSocketFactory().createSocket(host, port); } } else { // If we are not in SSL, just use the old code. port = (-1 == url.getPort()) ? 80 : url.getPort(); if (useProxy) { if (proxy.type() == Proxy.Type.HTTP) { InetSocketAddress sa = (InetSocketAddress) proxy.address(); host = (sa.isUnresolved()) ? sa.getHostName() : sa .getAddress().getHostAddress(); port = sa.getPort(); this.uploadPolicy.displayDebug( "Using non SSL socket, proxy=" + host + ":" + port, 20); ret = new Socket(host, port); } else if (proxy.type() == Proxy.Type.SOCKS) { this.uploadPolicy.displayDebug( "Using non SSL socket, via SOCKS proxy", 20); tsock = new Socket(proxy); tsock.connect(new InetSocketAddress(host, port)); ret = tsock; } else throw new ConnectException("Unkown proxy type " + proxy.type()); } else { this.uploadPolicy.displayDebug( "Using non SSL socket, direct connection", 20); ret = new Socket(host, port); } } return ret; } /** * Connects to a given URL automatically using a proxy. * * @param url The URL to connect to * @return A socket, connected to the specified URL. May be null if an error * occurs. * @throws NoSuchAlgorithmException * @throws KeyManagementException * @throws IOException * @throws UnknownHostException * @throws ConnectException * @throws URISyntaxException * @throws UnrecoverableKeyException * @throws CertificateException * @throws KeyStoreException * @throws UnrecoverableKeyException * @throws IllegalArgumentException */ public Socket Connect(URL url) throws NoSuchAlgorithmException, KeyManagementException, ConnectException, UnknownHostException, IOException, URISyntaxException, KeyStoreException, CertificateException, IllegalArgumentException, UnrecoverableKeyException { Proxy proxy = ProxySelector.getDefault().select(url.toURI()).get(0); return Connect(url, proxy); } /** * Retrieve the protocol to be used for the postURL of the current policy. * This method issues a HEAD request to the postURL and then examines the * protocol version returned in the response. * * @return The string, describing the protocol (e.g. "HTTP/1.1") * @throws URISyntaxException * @throws IOException * @throws UnrecoverableKeyException * @throws IllegalArgumentException * @throws CertificateException * @throws KeyStoreException * @throws UnknownHostException * @throws NoSuchAlgorithmException * @throws KeyManagementException * @throws JUploadException */ public String getProtocol() throws URISyntaxException, KeyManagementException, NoSuchAlgorithmException, UnknownHostException, KeyStoreException, CertificateException, IllegalArgumentException, UnrecoverableKeyException, IOException, JUploadException { String protocol = DEFAULT_PROTOCOL; String returnCode = null; // bRedirect indicates a return code of 301, 302 or 303. boolean bRedirect = false; URL url = new URL(this.uploadPolicy.getPostURL()); Proxy proxy = ProxySelector.getDefault().select(url.toURI()).get(0); boolean useProxy = ((proxy != null) && (proxy.type() != Proxy.Type.DIRECT)); boolean useSSL = url.getProtocol().equals("https"); Socket s = Connect(url, proxy); // BufferedReader in = new BufferedReader(new // InputStreamReader(s.getInputStream())); InputStream in = s.getInputStream(); StringBuffer req = new StringBuffer(); req.append("HEAD "); if (useProxy && (!useSSL)) { // with a proxy we need the absolute URL, but only if not // using SSL. (with SSL, we first use the proxy CONNECT method, // and then a plain request.) req.append(url.getProtocol()).append("://").append(url.getHost()); } req.append(url.getPath()); /* * if (null != url.getQuery() && !"".equals(url.getQuery())) * req.append("?").append(url.getQuery()); */ req.append(" ").append(DEFAULT_PROTOCOL).append("\r\n"); req.append("Host: ").append(url.getHost()).append("\r\n"); req.append("Connection: close\r\n\r\n"); OutputStream os = s.getOutputStream(); this.uploadPolicy.displayDebug("Request: " + req.toString(), 40); os.write(req.toString().getBytes()); os.flush(); // Let's read the first line, and try to guess the HTTP protocol, and // look for 301, 302 or 303 HTTP Return code. String firstLine = FileUploadThreadHTTP.readLine(in, "US-ASCII", false); if (null == firstLine) { // Using default value. Already initialized. //This can occur, for instance, when Kaspersky antivirus is on ! this.uploadPolicy.displayWarn("EMPTY HEAD response"); } else { Matcher m = Pattern.compile("^(HTTP/\\d\\.\\d)\\s(.*)\\s.*") .matcher(firstLine); if (!m.matches()) { // Using default value. Already initialized. this.uploadPolicy.displayErr("Unexpected HEAD response: '" + firstLine + "'"); } this.uploadPolicy.displayDebug("HEAD response: " + firstLine, 40); // We will return the found protocol. protocol = m.group(1); // Do we have some URL to change ? returnCode = m.group(2); if (returnCode.equals("301") || returnCode.equals("302") || returnCode.equals("303")) { bRedirect = true; this.uploadPolicy.displayInfo("Received " + returnCode + " (current postURL: " + this.uploadPolicy.getPostURL() + ")"); } } // Let's check if we're facing an IIS server. The applet is compatible // with IIS, only if allowHttpPersistent is false. String nextLine = FileUploadThreadHTTP.readLine(in, "US-ASCII", false); Pattern pLocation = Pattern.compile("^Location: (.*)$"); Matcher mLocation; if (null != nextLine) { while ((nextLine = FileUploadThreadHTTP.readLine(in, "US-ASCII", false))!=null && (nextLine.length() > 0)) { if (nextLine.matches("^Server: .*IIS")) { try { uploadPolicy.setProperty( UploadPolicy.PROP_ALLOW_HTTP_PERSISTENT, "false"); uploadPolicy .displayWarn(UploadPolicy.PROP_ALLOW_HTTP_PERSISTENT + "' forced to false, for IIS compatibility (in HttpConnect.getProtocol())"); } catch (JUploadException e) { uploadPolicy.displayWarn("Can't set property '" + UploadPolicy.PROP_ALLOW_HTTP_PERSISTENT + "' to false, in HttpConnect.getProtocol()"); } break; } else if (bRedirect) { mLocation = pLocation.matcher(nextLine); if (mLocation.matches()) { // We found the location where we should go instead of the // original postURL this.uploadPolicy.displayDebug("Location read: " + mLocation.group(1), 50); changePostURL(mLocation.group(1)); } } } } if (!(s instanceof SSLSocket)) { s.shutdownOutput(); } // Let's look for the web server kind: the applet works IIS only if // allowHttpPersistent is false s.close(); return protocol; } // getProtocol() /** * Reaction of the applet when a 301, 302 et 303 return code is returned. * The postURL is changed according to the Location header returned. * * @param newLocation This new location may contain the * http://host.name.domain part of the URL ... or not */ private void changePostURL(String newLocation) throws JUploadException { String currentPostURL = this.uploadPolicy.getPostURL(); String newPostURL; Pattern pHostName = Pattern.compile("http://([^/]*)/.*"); Matcher mOldPostURL = Pattern.compile("(.*)\\?(.*)").matcher( currentPostURL); // If there is an interrogation point in the original postURL, we'll // keep the parameters, and just changed the URI part. if (mOldPostURL.matches()) { newPostURL = newLocation + '?' + mOldPostURL.group(2); // Otherwise, we change the whole URL. } else { newPostURL = newLocation; } // There are three main cases or newLocation: // 1- It's a full URL, with host name... // 2- It's a local full path on the same server (begins with /) // 3- It's a relative path (for instance, add of a prefix in the // filename) (doesn't begin with /) Matcher mHostOldPostURL = pHostName.matcher(currentPostURL); if (!mHostOldPostURL.matches()) { // Oups ! There is a little trouble here ! throw new JUploadException( "[HttpConnect.changePostURL()] No host found in the old postURL !"); } // Let's analyze the given newLocation for these three cases. Matcher mHostNewLocation = pHostName.matcher(newLocation); if (mHostNewLocation.matches()) { // 1- It's a full URL, with host name. We already got this URL, in // the newPostURL initialization. } else if (newLocation.startsWith("/")) { // 2- It's a local full path on the same server (begins with /) newPostURL = "http://" + mHostOldPostURL.group(1) + newPostURL; } else { // 3- It's a relative path (for instance, add of a prefix in the // filename) (doesn't begin with /) Matcher mOldPostURLAllButFilename = Pattern .compile("(.*)/([^/]*)$").matcher(currentPostURL); if (!mOldPostURLAllButFilename.matches()) { // Hum, that won't be easy. throw new JUploadException( "[HttpConnect.changePostURL()] Can't find the filename in the URL !"); } newPostURL = mOldPostURLAllButFilename.group(1) + "/" + newPostURL; } // Let's store this new postURL, and display some info about the change this.uploadPolicy.setPostURL(newPostURL); this.uploadPolicy.displayInfo("postURL switched from " + currentPostURL + " to " + newPostURL); } /** * Creates a new instance. * * @param policy The UploadPolicy to be used for logging. */ public HttpConnect(UploadPolicy policy) { this.uploadPolicy = policy; } }