/******************************************************************************* * Copyright (c) 2004 Composent, Inc. All rights reserved. This * program and the accompanying materials are made available under the terms of * the Eclipse Public License v1.0 which accompanies this distribution, and is * available at http://www.eclipse.org/legal/epl-v10.html * * Contributors: Composent, Inc. - initial API and implementation * Maarten Meijer - bug 237936, added gzip encoded transfer default ******************************************************************************/ package org.eclipse.ecf.provider.filetransfer.retrieve; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.net.Authenticator; import java.net.ConnectException; import java.net.HttpURLConnection; import java.net.PasswordAuthentication; import java.net.URL; import java.net.URLConnection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.Path; import org.eclipse.ecf.core.security.Callback; import org.eclipse.ecf.core.security.CallbackHandler; import org.eclipse.ecf.core.security.IConnectContext; import org.eclipse.ecf.core.security.NameCallback; import org.eclipse.ecf.core.security.ObjectCallback; import org.eclipse.ecf.core.security.UnsupportedCallbackException; import org.eclipse.ecf.core.util.Proxy; import org.eclipse.ecf.filetransfer.IFileRangeSpecification; import org.eclipse.ecf.filetransfer.IFileTransferPausable; import org.eclipse.ecf.filetransfer.IRetrieveFileTransferOptions; import org.eclipse.ecf.filetransfer.IncomingFileTransferException; import org.eclipse.ecf.filetransfer.InvalidFileRangeSpecificationException; import org.eclipse.ecf.internal.provider.filetransfer.Activator; import org.eclipse.ecf.internal.provider.filetransfer.IURLConnectionModifier; import org.eclipse.ecf.internal.provider.filetransfer.Messages; import org.eclipse.ecf.provider.filetransfer.util.JREProxyHelper; import org.eclipse.osgi.util.NLS; public class UrlConnectionRetrieveFileTransfer extends AbstractRetrieveFileTransfer { private static final String USERNAME_PREFIX = Messages.UrlConnectionRetrieveFileTransfer_USERNAME_PROMPT; private static final int HTTP_RANGE_RESPONSE = 206; private static final int OK_RESPONSE_CODE = 200; private static final String JRE_CONNECT_TIMEOUT_PROPERTY = "sun.net.client.defaultConnectTimeout"; //$NON-NLS-1$ // 10/26/2009: Added being able to set with system property with name org.eclipse.ecf.provider.filetransfer.connectTimeout // for https://bugs.eclipse.org/bugs/show_bug.cgi?id=292995 private static final String DEFAULT_CONNECT_TIMEOUT = System.getProperty("org.eclipse.ecf.provider.filetransfer.retrieve.connectTimeout", "15000"); //$NON-NLS-1$ //$NON-NLS-2$ private static final String JRE_READ_TIMEOUT_PROPERTY = "sun.net.client.defaultReadTimeout"; //$NON-NLS-1$ protected URLConnection urlConnection; protected int httpVersion = 1; protected int responseCode = -1; private String remoteFileName; protected String responseMessage = null; private JREProxyHelper proxyHelper = null; protected String username = null; protected String password = null; public UrlConnectionRetrieveFileTransfer() { super(); proxyHelper = new JREProxyHelper(); } /* * (non-Javadoc) * * @see * org.eclipse.ecf.provider.filetransfer.retrieve.AbstractRetrieveFileTransfer * #getRemoteFileName() */ public String getRemoteFileName() { return remoteFileName; } protected void connect() throws IOException { setupTimeouts(); urlConnection = getRemoteFileURL().openConnection(); // set cache to off if using jar protocol // this is for addressing bug // https://bugs.eclipse.org/bugs/show_bug.cgi?id=235933 if (getRemoteFileURL().getProtocol().equalsIgnoreCase("jar")) { //$NON-NLS-1$ urlConnection.setUseCaches(false); } IURLConnectionModifier connectionModifier = Activator.getDefault().getURLConnectionModifier(); if (connectionModifier != null) { connectionModifier.setSocketFactoryForConnection(urlConnection); } } protected boolean isConnected() { return (urlConnection != null); } protected void setResumeRequestHeaderValues() throws IOException { if (this.bytesReceived <= 0 || this.fileLength <= this.bytesReceived) throw new IOException(Messages.UrlConnectionRetrieveFileTransfer_RESUME_START_ERROR); setRangeHeader("bytes=" + this.bytesReceived + "-"); //$NON-NLS-1$ //$NON-NLS-2$ int maxAge = Integer.getInteger("org.eclipse.ecf.http.cache.max-age", 0).intValue(); //$NON-NLS-1$ // set max-age for cache control to 0 for bug https://bugs.eclipse.org/bugs/show_bug.cgi?id=249990 // fix the fix for bug 249990 with bug 410813 if (maxAge == 0) { urlConnection.setRequestProperty("Cache-Control", "max-age=0"); //$NON-NLS-1$//$NON-NLS-2$ } else if (maxAge > 0) { urlConnection.setRequestProperty("Cache-Control", "max-age=" + maxAge); //$NON-NLS-1$//$NON-NLS-2$ } setRequestHeaderValuesFromOptions(); } private void setRequestHeaderValuesFromOptions() { Map localOptions = getOptions(); if (localOptions != null) { Object o = localOptions.get(IRetrieveFileTransferOptions.REQUEST_HEADERS); if (o != null && o instanceof Map) { Map requestHeaders = (Map) o; for (Iterator i = requestHeaders.keySet().iterator(); i.hasNext();) { Object n = i.next(); Object v = requestHeaders.get(n); if (n != null && n instanceof String && v != null && v instanceof String) urlConnection.addRequestProperty((String) n, (String) v); } } } } protected void setRequestHeaderValues() throws InvalidFileRangeSpecificationException { final IFileRangeSpecification rangeSpec = getFileRangeSpecification(); if (rangeSpec != null && isHTTP()) { final long startPosition = rangeSpec.getStartPosition(); final long endPosition = rangeSpec.getEndPosition(); if (startPosition < 0) throw new InvalidFileRangeSpecificationException(Messages.UrlConnectionRetrieveFileTransfer_RESUME_START_POSITION_LESS_THAN_ZERO, rangeSpec); if (endPosition != -1L && endPosition <= startPosition) throw new InvalidFileRangeSpecificationException(Messages.UrlConnectionRetrieveFileTransfer_RESUME_ERROR_END_POSITION_LESS_THAN_START, rangeSpec); setRangeHeader("bytes=" + startPosition + "-" + ((endPosition == -1L) ? "" : ("" + endPosition))); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ } // Add http 1.1 'Connection: close' header in order to potentially avoid // server issue described here // https://bugs.eclipse.org/bugs/show_bug.cgi?id=234916#c13 // See bug https://bugs.eclipse.org/bugs/show_bug.cgi?id=247197 // also see http 1.1 rfc section 14-10 in // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html urlConnection.setRequestProperty("Connection", "close"); //$NON-NLS-1$ //$NON-NLS-2$ int maxAge = Integer.getInteger("org.eclipse.ecf.http.cache.max-age", 0).intValue(); //$NON-NLS-1$ // set max-age for cache control to 0 for bug https://bugs.eclipse.org/bugs/show_bug.cgi?id=249990 // fix the fix for bug 249990 with bug 410813 if (maxAge == 0) { urlConnection.setRequestProperty("Cache-Control", "max-age=0"); //$NON-NLS-1$//$NON-NLS-2$ } else if (maxAge > 0) { urlConnection.setRequestProperty("Cache-Control", "max-age=" + maxAge); //$NON-NLS-1$//$NON-NLS-2$ } setRequestHeaderValuesFromOptions(); } private void setRangeHeader(String value) { urlConnection.setRequestProperty("Range", value); //$NON-NLS-1$ } public int getResponseCode() { if (responseCode != -1) return responseCode; if (isHTTP()) { String response = urlConnection.getHeaderField(0); if (response == null) { responseCode = -1; httpVersion = 1; return responseCode; } if (!response.startsWith("HTTP/")) //$NON-NLS-1$ return -1; response = response.trim(); final int mark = response.indexOf(" ") + 1; //$NON-NLS-1$ if (mark == 0) return -1; if (response.charAt(mark - 2) != '1') httpVersion = 0; int last = mark + 3; if (last > response.length()) last = response.length(); responseCode = Integer.parseInt(response.substring(mark, last)); if (last + 1 <= response.length()) responseMessage = response.substring(last + 1); } else { responseCode = OK_RESPONSE_CODE; responseMessage = "OK"; //$NON-NLS-1$ } return responseCode; } private boolean isHTTP() { final String protocol = getRemoteFileURL().getProtocol(); if (protocol.equalsIgnoreCase("http") || protocol.equalsIgnoreCase("https")) //$NON-NLS-1$ //$NON-NLS-2$ return true; return false; } private boolean isHTTP11() { return (isHTTP() && httpVersion >= 1); } protected void getResponseHeaderValues() throws IOException { if (!isConnected()) throw new ConnectException(Messages.UrlConnectionRetrieveFileTransfer_CONNECT_EXCEPTION_NOT_CONNECTED); if (getResponseCode() == -1) throw new IOException(Messages.UrlConnectionRetrieveFileTransfer_EXCEPTION_INVALID_SERVER_RESPONSE); setLastModifiedTime(urlConnection.getLastModified()); setFileLength(urlConnection.getContentLength()); String contentDispositionValue = urlConnection.getHeaderField(HttpHelper.CONTENT_DISPOSITION_HEADER); if (contentDispositionValue != null) { remoteFileName = HttpHelper.getRemoteFileNameFromContentDispositionHeader(contentDispositionValue); } if (remoteFileName == null) { String pathStr = urlConnection.getURL().getPath(); if (pathStr != null) { IPath path = Path.fromPortableString(pathStr); if (path.segmentCount() > 0) remoteFileName = path.lastSegment(); } if (remoteFileName == null) remoteFileName = super.getRemoteFileName(); } } protected void getResumeResponseHeaderValues() throws IOException { if (!isConnected()) throw new ConnectException(Messages.UrlConnectionRetrieveFileTransfer_CONNECT_EXCEPTION_NOT_CONNECTED); if (getResponseCode() != HTTP_RANGE_RESPONSE) throw new IOException(Messages.UrlConnectionRetrieveFileTransfer_INVALID_SERVER_RESPONSE_TO_PARTIAL_RANGE_REQUEST); if (lastModifiedTime != urlConnection.getLastModified()) throw new IOException(Messages.UrlConnectionRetrieveFileTransfer_EXCEPTION_FILE_MODIFIED_SINCE_LAST_ACCESS); } /** * @param proxy2 * the ECF proxy to setup */ protected void setupProxy(final Proxy proxy2) { proxyHelper.setupProxy(proxy2); } protected void setupAuthentication() throws IOException, UnsupportedCallbackException { if (connectContext == null) return; final CallbackHandler callbackHandler = connectContext.getCallbackHandler(); if (callbackHandler == null) return; final NameCallback usernameCallback = new NameCallback(USERNAME_PREFIX); final ObjectCallback passwordCallback = new ObjectCallback(); // Call callback with username and password callbacks callbackHandler.handle(new Callback[] {usernameCallback, passwordCallback}); username = usernameCallback.getName(); Object o = passwordCallback.getObject(); if (!(o instanceof String)) throw new UnsupportedCallbackException(passwordCallback, Messages.UrlConnectionRetrieveFileTransfer_UnsupportedCallbackException); password = (String) passwordCallback.getObject(); // Now set authenticator to our authenticator with user and password Authenticator.setDefault(new UrlConnectionAuthenticator()); } class UrlConnectionAuthenticator extends Authenticator { /* * (non-Javadoc) * * @see java.net.Authenticator#getPasswordAuthentication() */ protected PasswordAuthentication getPasswordAuthentication() { return new PasswordAuthentication(username, password.toCharArray()); } } /* * (non-Javadoc) * * @seeorg.eclipse.ecf.filetransfer.IRetrieveFileTransferContainerAdapter# * setConnectContextForAuthentication * (org.eclipse.ecf.core.security.IConnectContext) */ public void setConnectContextForAuthentication(IConnectContext connectContext) { super.setConnectContextForAuthentication(connectContext); this.username = null; this.password = null; } /* * (non-Javadoc) * * @see * org.eclipse.ecf.provider.filetransfer.retrieve.AbstractRetrieveFileTransfer * #openStreams() */ protected void openStreams() throws IncomingFileTransferException { int code = -1; try { setupAuthentication(); connect(); setRequestHeaderValues(); // Make actual GET request // need to get response header about encoding before setting stream setCompressionRequestHeader(); setInputStream(getDecompressedStream()); code = getResponseCode(); responseHeaders = getResponseHeaders(); if (isHTTP()) { if (code == HttpURLConnection.HTTP_PARTIAL || code == HttpURLConnection.HTTP_OK) { fireReceiveStartEvent(); } else if (code == HttpURLConnection.HTTP_NOT_FOUND) { throw new IncomingFileTransferException(NLS.bind("File not found: {0}", getRemoteFileURL().toString()), code, responseHeaders); //$NON-NLS-1$ } else if (code == HttpURLConnection.HTTP_UNAUTHORIZED) { throw new IncomingFileTransferException("Unauthorized", code, responseHeaders); //$NON-NLS-1$ } else if (code == HttpURLConnection.HTTP_FORBIDDEN) { throw new IncomingFileTransferException("Forbidden", code, responseHeaders); //$NON-NLS-1$ } else if (code == HttpURLConnection.HTTP_PROXY_AUTH) { throw new IncomingFileTransferException("Proxy authentication required", code, responseHeaders); //$NON-NLS-1$ } else { throw new IncomingFileTransferException(NLS.bind("General connection error with response code={0}", new Integer(code)), code, responseHeaders); //$NON-NLS-1$ } } else { fireReceiveStartEvent(); } } catch (final FileNotFoundException e) { throw new IncomingFileTransferException(NLS.bind("File not found: {0}", getRemoteFileURL().toString()), 404); //$NON-NLS-1$ } catch (final Exception e) { IncomingFileTransferException except = (e instanceof IncomingFileTransferException) ? (IncomingFileTransferException) e : new IncomingFileTransferException(NLS.bind(Messages.UrlConnectionRetrieveFileTransfer_EXCEPTION_COULD_NOT_CONNECT, getRemoteFileURL().toString()), e, code, responseHeaders); hardClose(); throw except; } } private Map getResponseHeaders() { if (responseHeaders != null) return responseHeaders; if (urlConnection == null) return null; Map headerFields = urlConnection.getHeaderFields(); if (headerFields == null) return null; Map result = new HashMap(); for (Iterator i = headerFields.keySet().iterator(); i.hasNext();) { String name = (String) i.next(); List listValue = (List) headerFields.get(name); String val = null; if (listValue != null && listValue.size() > 0) { val = (String) ((listValue.size() > 1) ? listValue.get(listValue.size() - 1) : listValue.get(0)); } if (name != null && val != null) result.put(name, val); } return Collections.unmodifiableMap(result); } /* * (non-Javadoc) * * @see * org.eclipse.ecf.provider.filetransfer.retrieve.AbstractRetrieveFileTransfer * #hardClose() */ protected void hardClose() { super.hardClose(); urlConnection = null; responseCode = -1; if (proxyHelper != null) { proxyHelper.dispose(); proxyHelper = null; } } /* * (non-Javadoc) * * @see * org.eclipse.ecf.provider.filetransfer.retrieve.AbstractRetrieveFileTransfer * #doPause() */ protected boolean doPause() { if (isPaused() || !isConnected() || isDone()) return false; this.paused = true; return this.paused; } /* * (non-Javadoc) * * @see * org.eclipse.ecf.provider.filetransfer.retrieve.AbstractRetrieveFileTransfer * #doResume() */ protected boolean doResume() { if (!isPaused() || isConnected()) return false; return openStreamsForResume(); } /* * (non-Javadoc) * * @see * org.eclipse.ecf.provider.filetransfer.retrieve.AbstractRetrieveFileTransfer * #getAdapter(java.lang.Class) */ public Object getAdapter(Class adapter) { if (adapter == null) return null; if (adapter.equals(IFileTransferPausable.class) && isHTTP11()) return this; return super.getAdapter(adapter); } protected String getConnectTimeout() { String result = DEFAULT_CONNECT_TIMEOUT; Map localOptions = getOptions(); if (localOptions != null) { // See if the connect timeout option is present, if so set Object o = localOptions.get(IRetrieveFileTransferOptions.CONNECT_TIMEOUT); if (o != null) { if (o instanceof Integer) { result = ((Integer) o).toString(); } else if (o instanceof String) { result = (String) o; } return result; } o = localOptions.get("org.eclipse.ecf.provider.filetransfer.httpclient.retrieve.connectTimeout"); //$NON-NLS-1$ if (o != null) { if (o instanceof Integer) { result = ((Integer) o).toString(); } else if (o instanceof String) { result = (String) o; } } } return result; } private void setupTimeouts() { String existingTimeout = System.getProperty(JRE_CONNECT_TIMEOUT_PROPERTY); if (existingTimeout == null) { System.setProperty(JRE_CONNECT_TIMEOUT_PROPERTY, getConnectTimeout()); } existingTimeout = System.getProperty(JRE_READ_TIMEOUT_PROPERTY); if (existingTimeout == null) { System.setProperty(JRE_READ_TIMEOUT_PROPERTY, "" + getSocketReadTimeout()); //$NON-NLS-1$ } } /** * @return <code>true</code> if streams successfully, <code>false</code> * otherwise. */ private boolean openStreamsForResume() { final URL theURL = getRemoteFileURL(); int code = -1; try { remoteFileURL = new URL(theURL.toString()); setupAuthentication(); connect(); setResumeRequestHeaderValues(); // Make actual GET request setInputStream(urlConnection.getInputStream()); code = getResponseCode(); responseHeaders = getResponseHeaders(); if (code == HttpURLConnection.HTTP_PARTIAL || code == HttpURLConnection.HTTP_OK) { getResumeResponseHeaderValues(); this.paused = false; fireReceiveResumedEvent(); return true; } else if (code == HttpURLConnection.HTTP_NOT_FOUND) { throw new IncomingFileTransferException(NLS.bind("File not found: {0}", getRemoteFileURL().toString()), code, responseHeaders); //$NON-NLS-1$ } else if (code == HttpURLConnection.HTTP_UNAUTHORIZED) { throw new IncomingFileTransferException("Unauthorized", code, responseHeaders); //$NON-NLS-1$ } else if (code == HttpURLConnection.HTTP_FORBIDDEN) { throw new IncomingFileTransferException("Forbidden", code, responseHeaders); //$NON-NLS-1$ } else if (code == HttpURLConnection.HTTP_PROXY_AUTH) { throw new IncomingFileTransferException("Proxy authentication required", code, responseHeaders); //$NON-NLS-1$ } else { throw new IncomingFileTransferException(NLS.bind("General connection error with response code={0}", new Integer(code)), code, responseHeaders); //$NON-NLS-1$ } } catch (final Exception e) { this.exception = (e instanceof IncomingFileTransferException) ? e : new IncomingFileTransferException(NLS.bind(Messages.UrlConnectionRetrieveFileTransfer_EXCEPTION_COULD_NOT_CONNECT, getRemoteFileURL().toString()), e, code, responseHeaders); this.done = true; hardClose(); fireTransferReceiveDoneEvent(); return false; } } private static final String ACCEPT_ENCODING = "Accept-encoding"; //$NON-NLS-1$ private static final String CONTENT_ENCODING_GZIP = "gzip"; //$NON-NLS-1$ private static final String CONTENT_ENCODING_ACCEPTED = CONTENT_ENCODING_GZIP; // + private static class Compression { private String type; private Compression(String i) { this.type = i; } static Compression NONE = new Compression("none"); //$NON-NLS-1$ static Compression GZIP = new Compression("gzip"); //$NON-NLS-1$ public String toString() { return type; } } private void setCompressionRequestHeader() { // Set request header for possible gzip encoding, but only if // 1) The file range specification is null (we want the whole file) // 2) The target remote file does *not* end in .gz (see bug https://bugs.eclipse.org/bugs/show_bug.cgi?id=280205) if (getFileRangeSpecification() == null && !targetHasGzSuffix(super.getRemoteFileName())) urlConnection.setRequestProperty(ACCEPT_ENCODING, CONTENT_ENCODING_ACCEPTED); } private Compression getCompressionResponseHeader() { String encoding = urlConnection.getContentEncoding(); if (null == encoding) { return Compression.NONE; // see bug https://bugs.eclipse.org/bugs/show_bug.cgi?id=269018 } else if (encoding.equalsIgnoreCase(CONTENT_ENCODING_GZIP) && !targetHasGzSuffix(remoteFileName)) { return Compression.GZIP; } return Compression.NONE; } private InputStream getDecompressedStream() throws IOException { InputStream input = urlConnection.getInputStream(); getResponseHeaderValues(); Compression type = getCompressionResponseHeader(); if (Compression.GZIP == type) { return new java.util.zip.GZIPInputStream(input); // } else if (Compression.DEFLATE == type) { // return new java.util.zip.InflaterInputStream(input); } return input; } }