/* * Created on 16-Dec-2005 * Created by Paul Gardner * Copyright (C) 2005, 2006 Aelitis, All Rights Reserved. * * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * * AELITIS, SAS au capital de 46,603.30 euros * 8 Allee Lenotre, La Grille Royale, 78600 Le Mesnil le Roi, France. * */ package com.aelitis.azureus.plugins.extseed.util; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.InetSocketAddress; import java.net.PasswordAuthentication; import java.net.Socket; import java.net.URL; import java.util.HashSet; import java.util.Set; import java.util.StringTokenizer; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLException; import javax.net.ssl.SSLSession; import org.gudy.azureus2.core3.security.SEPasswordListener; import org.gudy.azureus2.core3.security.SESecurityManager; import org.gudy.azureus2.core3.util.Debug; import com.aelitis.azureus.core.networkmanager.admin.NetworkAdmin; import com.aelitis.azureus.plugins.extseed.ExternalSeedException; public class ExternalSeedHTTPDownloaderRange implements ExternalSeedHTTPDownloader, SEPasswordListener { public static final String NL = "\r\n"; private URL original_url; private String user_agent; private URL redirected_url; private int consec_redirect_fails; private int last_response; private int last_response_retry_after_secs; public ExternalSeedHTTPDownloaderRange( URL _url, String _user_agent ) { original_url = _url; user_agent = _user_agent; } public URL getURL() { return( original_url ); } public void download( int length, ExternalSeedHTTPDownloaderListener listener, boolean con_fail_is_perm_fail ) throws ExternalSeedException { download( new String[0], new String[0], length, listener, con_fail_is_perm_fail ); } public void downloadRange( long offset, int length, ExternalSeedHTTPDownloaderListener listener, boolean con_fail_is_perm_fail ) throws ExternalSeedException { download( new String[]{ "Range" }, new String[]{ "bytes=" + offset + "-" + (offset+length-1)}, length, listener, con_fail_is_perm_fail ); } public void download( String[] prop_names, String[] prop_values, int length, ExternalSeedHTTPDownloaderListener listener, boolean con_fail_is_perm_fail ) throws ExternalSeedException { boolean connected = false; InputStream is = null; String outcome = ""; try{ SESecurityManager.setThreadPasswordHandler( this ); if ( NetworkAdmin.getSingleton().hasMissingForcedBind()){ throw( new ExternalSeedException( "Forced bind address is missing" )); } // System.out.println( "Connecting to " + url + ": " + Thread.currentThread().getId()); HttpURLConnection connection; int response; Set<String> redirect_urls = new HashSet<String>(); redirect_loop: while( true ){ URL target = redirected_url==null?original_url:redirected_url; for ( int ssl_loop=0; ssl_loop<2; ssl_loop++ ){ try{ connection = (HttpURLConnection)target.openConnection(); if ( connection instanceof HttpsURLConnection ){ HttpsURLConnection ssl_con = (HttpsURLConnection)connection; // allow for certs that contain IP addresses rather than dns names ssl_con.setHostnameVerifier( new HostnameVerifier() { public boolean verify( String host, SSLSession session ) { return( true ); } }); } connection.setRequestProperty( "Connection", "Keep-Alive" ); connection.setRequestProperty( "User-Agent", user_agent ); for (int i=0;i<prop_names.length;i++){ connection.setRequestProperty( prop_names[i], prop_values[i] ); } int time_remaining = listener.getPermittedTime(); if ( time_remaining > 0 ){ connection.setConnectTimeout( time_remaining ); } connection.connect(); time_remaining = listener.getPermittedTime(); if ( time_remaining < 0 ){ throw( new IOException( "Timeout during connect" )); } connection.setReadTimeout( time_remaining ); connected = true; response = connection.getResponseCode(); if ( response == HttpURLConnection.HTTP_ACCEPTED || response == HttpURLConnection.HTTP_OK || response == HttpURLConnection.HTTP_PARTIAL ){ if ( redirected_url != null ){ consec_redirect_fails = 0; } break redirect_loop; }else if ( response == HttpURLConnection.HTTP_MOVED_TEMP || response == HttpURLConnection.HTTP_MOVED_PERM ){ // auto redirect doesn't work from http to https or vice-versa String move_to = connection.getHeaderField( "location" ); if ( move_to != null ){ if ( redirect_urls.contains( move_to ) || redirect_urls.size() > 32 ){ throw( new ExternalSeedException( "redirect loop" )); } redirect_urls.add( move_to ); redirected_url = new URL( move_to ); continue redirect_loop; } } if ( redirected_url == null ){ break redirect_loop; } // try again with original URL consec_redirect_fails++; redirected_url = null; }catch( SSLException e ){ if ( ssl_loop == 0 ){ if ( SESecurityManager.installServerCertificates( target ) != null ){ // certificate has been installed continue; // retry with new certificate } } throw( e ); } // don't need another SSL loop break; } } URL final_url = connection.getURL(); if ( consec_redirect_fails < 10 && !original_url.toExternalForm().equals( final_url.toExternalForm())){ redirected_url = final_url; } last_response = response; last_response_retry_after_secs = -1; if ( response == 503 ){ // webseed support for temp unavail - read the retry_after long retry_after_date = connection.getHeaderFieldDate("Retry-After", -1L); if ( retry_after_date <= -1 ){ last_response_retry_after_secs = connection.getHeaderFieldInt("Retry-After", -1); }else{ last_response_retry_after_secs = (int)((retry_after_date - System.currentTimeMillis())/1000); if ( last_response_retry_after_secs < 0 ){ last_response_retry_after_secs = -1; } } } is = connection.getInputStream(); if ( response == HttpURLConnection.HTTP_ACCEPTED || response == HttpURLConnection.HTTP_OK || response == HttpURLConnection.HTTP_PARTIAL ){ int pos = 0; byte[] buffer = null; int buffer_pos = 0; int buffer_len = 0; while( pos < length ){ if ( buffer == null ){ buffer = listener.getBuffer(); buffer_pos = listener.getBufferPosition(); buffer_len = listener.getBufferLength(); } listener.setBufferPosition( buffer_pos ); int to_read = buffer_len - buffer_pos; int permitted = listener.getPermittedBytes(); if ( permitted < to_read ){ to_read = permitted; } int len = is.read( buffer, buffer_pos, to_read ); if ( len < 0 ){ break; } listener.reportBytesRead( len ); pos += len; buffer_pos += len; if ( buffer_pos == buffer_len ){ listener.done(); buffer = null; buffer_pos = 0; } } if ( pos != length ){ String log_str; if ( buffer == null ){ log_str = "No buffer assigned"; }else{ log_str = new String( buffer, 0, length ); if ( log_str.length() > 64 ){ log_str = log_str.substring( 0, 64 ); } } outcome = "Connection failed: data too short - " + length + "/" + pos + " [" + log_str + "]"; throw( new ExternalSeedException( outcome )); } outcome = "read " + pos + " bytes"; // System.out.println( "download length: " + pos ); }else{ outcome = "Connection failed: " + connection.getResponseMessage(); ExternalSeedException error = new ExternalSeedException( outcome ); error.setPermanentFailure( true ); throw( error ); } }catch( IOException e ){ if ( con_fail_is_perm_fail && !connected ){ outcome = "Connection failed: " + e.getMessage(); ExternalSeedException error = new ExternalSeedException( outcome ); error.setPermanentFailure( true ); throw( error ); }else{ outcome = "Connection failed: " + Debug.getNestedExceptionMessage( e ); if ( last_response_retry_after_secs >= 0){ outcome += ", Retry-After: " + last_response_retry_after_secs + " seconds"; } ExternalSeedException excep = new ExternalSeedException( outcome, e ); if ( e instanceof FileNotFoundException ){ excep.setPermanentFailure( true ); } throw( excep ); } }catch( Throwable e ){ if ( e instanceof ExternalSeedException ){ throw((ExternalSeedException)e); } outcome = "Connection failed: " + Debug.getNestedExceptionMessage( e ); throw( new ExternalSeedException("Connection failed", e )); }finally{ SESecurityManager.unsetThreadPasswordHandler(); // System.out.println( "Done to " + url + ": " + Thread.currentThread().getId() + ", outcome=" + outcome ); if ( is != null ){ try{ is.close(); }catch( Throwable e ){ } } } } public void downloadSocket( int length, ExternalSeedHTTPDownloaderListener listener, boolean con_fail_is_perm_fail ) throws ExternalSeedException { downloadSocket( new String[0], new String[0], length, listener, con_fail_is_perm_fail ); } public void downloadSocket( String[] prop_names, String[] prop_values, int length, ExternalSeedHTTPDownloaderListener listener, boolean con_fail_is_perm_fail ) throws ExternalSeedException { Socket socket = null; boolean connected = false; try{ String output_header = "GET " + original_url.getPath() + "?" + original_url.getQuery() + " HTTP/1.1" + NL + "Host: " + original_url.getHost() + (original_url.getPort()==-1?"":( ":" + original_url.getPort())) + NL + "Accept: */*" + NL + "Connection: Close" + NL + // if we want to support keep-alive we'll need to implement a socket cache etc. "User-Agent: " + user_agent + NL; for (int i=0;i<prop_names.length;i++){ output_header += prop_names[i] + ":" + prop_values[i] + NL; } output_header += NL; int time_remaining = listener.getPermittedTime(); if ( time_remaining > 0 ){ socket = new Socket(); socket.connect( new InetSocketAddress( original_url.getHost(), original_url.getPort()==-1?original_url.getDefaultPort():original_url.getPort()), time_remaining ); }else{ socket = new Socket( original_url.getHost(), original_url.getPort()==-1?original_url.getDefaultPort():original_url.getPort()); } connected = true; time_remaining = listener.getPermittedTime(); if ( time_remaining < 0 ){ throw( new IOException( "Timeout during connect" )); }else if ( time_remaining > 0 ){ socket.setSoTimeout( time_remaining ); } OutputStream os = socket.getOutputStream(); os.write( output_header.getBytes( "ISO-8859-1" )); os.flush(); InputStream is = socket.getInputStream(); try{ String input_header = ""; while( true ){ byte[] buffer = new byte[1]; int len = is.read( buffer ); if ( len < 0 ){ throw( new IOException( "input too short reading header" )); } input_header += (char)buffer[0]; if ( input_header.endsWith(NL+NL)){ break; } } // HTTP/1.1 403 Forbidden int line_end = input_header.indexOf(NL); if ( line_end == -1 ){ throw( new IOException( "header too short" )); } String first_line = input_header.substring(0,line_end); StringTokenizer tok = new StringTokenizer(first_line, " " ); tok.nextToken(); int response = Integer.parseInt( tok.nextToken()); last_response = response; last_response_retry_after_secs = -1; String response_str = tok.nextToken(); if ( response == HttpURLConnection.HTTP_ACCEPTED || response == HttpURLConnection.HTTP_OK || response == HttpURLConnection.HTTP_PARTIAL ){ byte[] buffer = null; int buffer_pos = 0; int buffer_len = 0; int pos = 0; while( pos < length ){ if ( buffer == null ){ buffer = listener.getBuffer(); buffer_pos = listener.getBufferPosition(); buffer_len = listener.getBufferLength(); } int to_read = buffer_len - buffer_pos; int permitted = listener.getPermittedBytes(); if ( permitted < to_read ){ to_read = permitted; } int len = is.read( buffer, buffer_pos, to_read ); if ( len < 0 ){ break; } listener.reportBytesRead( len ); pos += len; buffer_pos += len; if ( buffer_pos == buffer_len ){ listener.done(); buffer = null; buffer_pos = 0; } } if ( pos != length ){ String log_str; if ( buffer == null ){ log_str = "No buffer assigned"; }else{ log_str = new String( buffer, 0, buffer_pos>64?64:buffer_pos ); } throw( new ExternalSeedException("Connection failed: data too short - " + length + "/" + pos + " [last=" + log_str + "]" )); } // System.out.println( "download length: " + pos ); }else if ( response == 503 ){ // webseed support for temp unavail - read the data String data_str = ""; while( true ){ byte[] buffer = new byte[1]; int len = is.read( buffer ); if ( len < 0 ){ break; } data_str += (char)buffer[0]; } last_response_retry_after_secs = Integer.parseInt( data_str ); // this gets trapped below and turned into an appropriate ExternalSeedException throw( new IOException( "Server overloaded" )); }else{ ExternalSeedException error = new ExternalSeedException("Connection failed: " + response_str ); error.setPermanentFailure( true ); throw( error ); } }finally{ is.close(); } }catch( IOException e ){ if ( con_fail_is_perm_fail && !connected ){ ExternalSeedException error = new ExternalSeedException("Connection failed: " + e.getMessage()); error.setPermanentFailure( true ); throw( error ); }else{ String outcome = "Connection failed: " + Debug.getNestedExceptionMessage( e ); if ( last_response_retry_after_secs >= 0 ){ outcome += ", Retry-After: " + last_response_retry_after_secs + " seconds"; } throw( new ExternalSeedException( outcome, e )); } }catch( Throwable e ){ if ( e instanceof ExternalSeedException ){ throw((ExternalSeedException)e); } throw( new ExternalSeedException("Connection failed", e )); }finally{ if ( socket != null ){ try{ socket.close(); }catch( Throwable e ){ } } } } public void deactivate() { } public PasswordAuthentication getAuthentication( String realm, URL tracker ) { return( null ); } public void setAuthenticationOutcome( String realm, URL tracker, boolean success ) { } public void clearPasswords() { } public int getLastResponse() { return( last_response ); } public int getLast503RetrySecs() { return( last_response_retry_after_secs ); } public static void main( String[] args ) { try{ String url_str = ""; ExternalSeedHTTPDownloader downloader = new ExternalSeedHTTPDownloaderRange( new URL( url_str ), "Azureus" ); downloader.downloadRange( 0, 1, new ExternalSeedHTTPDownloaderListener() { private int position; public byte[] getBuffer() throws ExternalSeedException { return( new byte[1024] ); } public void setBufferPosition( int _position ) { position = _position; } public int getBufferPosition() { return( position ); } public int getBufferLength() { return( 1024 ); } public int getPermittedBytes() throws ExternalSeedException { return( 1024 ); } public int getPermittedTime() { return( Integer.MAX_VALUE ); } public void reportBytesRead( int num ) { System.out.println( "read " + num ); } public boolean isCancelled() { return false; } public void done() { System.out.println( "done" ); } }, true ); }catch( Throwable e ){ e.printStackTrace(); } } }