/* * Created on Oct 21, 2009 * Created by Paul Gardner * * Copyright 2009 Vuze, Inc. 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; version 2 of the License only. * * 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. */ package com.aelitis.azureus.plugins.extseed.util; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.RandomAccessFile; import java.net.HttpURLConnection; import java.net.PasswordAuthentication; import java.net.URL; import java.util.*; import org.gudy.azureus2.core3.security.SEPasswordListener; import org.gudy.azureus2.core3.security.SESecurityManager; import org.gudy.azureus2.core3.util.AESemaphore; import org.gudy.azureus2.core3.util.AETemporaryFileHandler; import org.gudy.azureus2.core3.util.AEThread2; import org.gudy.azureus2.core3.util.Debug; import com.aelitis.azureus.core.networkmanager.admin.NetworkAdmin; import com.aelitis.azureus.plugins.extseed.ExternalSeedException; public class ExternalSeedHTTPDownloaderLinear implements ExternalSeedHTTPDownloader { private URL original_url; private String user_agent; private int last_response; private int last_response_retry_after_secs; private Downloader downloader; public ExternalSeedHTTPDownloaderLinear( URL _url, String _user_agent ) { original_url = _url; user_agent = _user_agent; } public void downloadRange( long offset, int length, ExternalSeedHTTPDownloaderListener listener, boolean con_fail_is_perm_fail ) throws ExternalSeedException { Request request; synchronized( this ){ if ( downloader == null ){ downloader = new Downloader( listener, con_fail_is_perm_fail ); } request = downloader.addRequest( offset, length, listener ); } while( true ){ if ( request.waitFor(1000)){ return; } if ( listener.isCancelled()){ throw( new ExternalSeedException( "request cancelled" )); } } } public void deactivate() { Downloader to_destroy = null; synchronized( this ){ if ( downloader != null ){ to_destroy = downloader; downloader = null; } } if ( to_destroy != null ){ to_destroy.destroy( new ExternalSeedException( "deactivated" )); } } protected void destoyed( Downloader dead ) { synchronized( this ){ if ( downloader == dead ){ downloader = null; } } } public void download( int length, ExternalSeedHTTPDownloaderListener listener, boolean con_fail_is_perm_fail ) throws ExternalSeedException { throw( new ExternalSeedException( "not supported" )); } public void downloadSocket( int length, ExternalSeedHTTPDownloaderListener listener, boolean con_fail_is_perm_fail ) throws ExternalSeedException { throw( new ExternalSeedException( "not supported" )); } public int getLastResponse() { return( last_response ); } public int getLast503RetrySecs() { return( last_response_retry_after_secs ); } protected class Downloader implements SEPasswordListener { private ExternalSeedHTTPDownloaderListener listener; private boolean con_fail_is_perm_fail; private volatile boolean destroyed; private List<Request> requests = new ArrayList<Request>(); private RandomAccessFile raf = null; private File scratch_file = null; protected Downloader( ExternalSeedHTTPDownloaderListener _listener, boolean _con_fail_is_perm_fail ) { listener = _listener; con_fail_is_perm_fail = _con_fail_is_perm_fail; new AEThread2( "ES:downloader", true ) { public void run() { download(); } }.start(); } protected void download() { boolean connected = false; String outcome = ""; try{ InputStream is = null; try{ SESecurityManager.setThreadPasswordHandler( this ); if ( NetworkAdmin.getSingleton().hasMissingForcedBind()){ throw( new ExternalSeedException( "Forced bind address is missing" )); } synchronized( this ){ if ( destroyed ){ return; } scratch_file = AETemporaryFileHandler.createTempFile(); raf = new RandomAccessFile( scratch_file, "rw" ); } // System.out.println( "Connecting to " + url + ": " + Thread.currentThread().getId()); HttpURLConnection connection; int response; connection = (HttpURLConnection)original_url.openConnection(); connection.setRequestProperty( "Connection", "Keep-Alive" ); connection.setRequestProperty( "User-Agent", user_agent ); 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(); 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 ){ byte[] buffer = new byte[64*1024]; int requests_outstanding = 1; // should be one at least while( !destroyed ){ int permitted = listener.getPermittedBytes(); // idle if no reqs if ( requests_outstanding == 0 || permitted < 1 ){ permitted = 1; Thread.sleep( 100 ); } int len = is.read( buffer, 0, Math.min( permitted, buffer.length )); if ( len <= 0 ){ break; } synchronized( this ){ try{ raf.write( buffer, 0, len ); }catch( Throwable e ){ // assume out of space of something permanent, abandon outcome = "Write failed: " + e.getMessage(); ExternalSeedException error = new ExternalSeedException( outcome, e ); error.setPermanentFailure( true ); throw( error ); } } listener.reportBytesRead( len ); requests_outstanding = checkRequests(); } checkRequests(); }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( ExternalSeedException e ){ throw( e ); }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 ){ } } } }catch( ExternalSeedException e ){ if ( !connected && con_fail_is_perm_fail ){ e.setPermanentFailure( true ); } destroy( e ); } // on successful completion we kill the read thread but leave things 'running' so we continue to service // requests. We will be de-activated when no longer required } protected Request addRequest( long offset, int length, ExternalSeedHTTPDownloaderListener listener ) throws ExternalSeedException { Request request; synchronized( this ){ if ( destroyed ){ throw( new ExternalSeedException( "downloader destroyed" )); } request = new Request( offset, length, listener ); requests.add( request ); } checkRequests(); return( request ); } protected int checkRequests() { try{ synchronized( this ){ if ( raf == null ){ // not yet initialised return( requests.size()); } long pos = raf.getFilePointer(); Iterator<Request> it = requests.iterator(); while( it.hasNext()){ Request request = it.next(); long end = request.getOffset() + request.getLength(); if ( pos >= end ){ ExternalSeedHTTPDownloaderListener listener = request.getListener(); try{ raf.seek( request.getOffset()); int total = 0; while( total < request.getLength()){ byte[] buffer = listener.getBuffer(); int buffer_len = listener.getBufferLength(); if ( raf.read( buffer, 0, buffer_len ) != buffer_len ){ throw( new IOException( "Error reading scratch file" )); } total += buffer_len; listener.done(); } }finally{ raf.seek( pos ); } request.complete(); it.remove(); } } return( requests.size()); } }catch( Throwable e ){ Debug.out( e ); destroy( new ExternalSeedException( "read failed", e )); return( 0 ); } } protected void destroy( ExternalSeedException error ) { synchronized( this ){ if ( destroyed ){ return; } destroyed = true; if ( raf != null ){ try{ raf.close(); }catch( Throwable e ){ } } if ( scratch_file != null ){ scratch_file.delete(); } for ( Request r: requests ){ r.destroy( error ); } requests.clear(); } ExternalSeedHTTPDownloaderLinear.this.destoyed( this ); } public PasswordAuthentication getAuthentication( String realm, URL tracker ) { return( null ); } public void setAuthenticationOutcome( String realm, URL tracker, boolean success ) { } public void clearPasswords() { } } private static class Request { private long offset; private int length; private ExternalSeedHTTPDownloaderListener listener; private AESemaphore sem = new AESemaphore( "ES:wait" ); private volatile ExternalSeedException exception; protected Request( long _offset, int _length, ExternalSeedHTTPDownloaderListener _listener ) { offset = _offset; length = _length; listener = _listener; } protected long getOffset() { return( offset ); } protected int getLength() { return( length ); } protected ExternalSeedHTTPDownloaderListener getListener() { return( listener ); } protected void complete() { sem.release(); } protected void destroy( ExternalSeedException e ) { exception = e; sem.release(); } public boolean waitFor( int timeout ) throws ExternalSeedException { if ( !sem.reserve( timeout )){ return( false ); } if ( exception != null ){ throw( exception ); } return( true ); } } }