/* * Created on Feb 11, 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.core.download; import java.io.*; import java.net.HttpURLConnection; import java.net.PasswordAuthentication; import java.net.URL; import java.util.*; 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.AETemporaryFileHandler; import org.gudy.azureus2.core3.util.Base32; import org.gudy.azureus2.core3.util.Debug; import org.gudy.azureus2.core3.util.FileUtil; import org.gudy.azureus2.core3.util.SHA1Simple; import org.gudy.azureus2.core3.util.UrlUtils; import org.gudy.azureus2.plugins.disk.DiskManagerChannel; import org.gudy.azureus2.plugins.disk.DiskManagerEvent; import org.gudy.azureus2.plugins.disk.DiskManagerFileInfo; import org.gudy.azureus2.plugins.disk.DiskManagerListener; import org.gudy.azureus2.plugins.disk.DiskManagerRandomReadRequest; import org.gudy.azureus2.plugins.disk.DiskManagerRequest; import org.gudy.azureus2.plugins.download.Download; import org.gudy.azureus2.plugins.download.DownloadException; import org.gudy.azureus2.plugins.utils.PooledByteBuffer; import org.gudy.azureus2.pluginsimpl.local.utils.PooledByteBufferImpl; import com.aelitis.azureus.core.util.CopyOnWriteList; import com.aelitis.azureus.plugins.extseed.ExternalSeedException; public class DiskManagerFileInfoURL implements DiskManagerFileInfo, SEPasswordListener { private URL url; private byte[] hash; private File file; private Object lock = new Object(); private URL redirected_url; private int consec_redirect_fails; private volatile boolean file_cached; public DiskManagerFileInfoURL( URL _url ) { url = _url; String url_str = url.toExternalForm(); String id_key = "azcdid="; String dn_key = "azcddn="; int id_pos = url_str.indexOf( id_key ); int dn_pos = url_str.indexOf( dn_key ); int min_pos = id_pos; if ( min_pos == -1 ){ min_pos = dn_pos; }else{ if ( dn_pos != -1 ){ min_pos = Math.min( min_pos, dn_pos ); } } if ( min_pos > 0 ){ try{ url = new URL( url_str.substring( 0, min_pos-1 ) ); }catch( Throwable e ){ Debug.out( e ); } } try{ hash = new SHA1Simple().calculateHash( ( "DiskManagerFileInfoURL" + url.toExternalForm()).getBytes( "UTF-8" )); }catch( Throwable e ){ Debug.out(e); } String file_name; if ( dn_pos != -1 ){ String dn = url_str.substring( dn_pos + dn_key.length()); dn_pos = dn.indexOf( '&' ); if ( dn_pos != -1 ){ dn = dn.substring( 0, dn_pos ); } file_name = UrlUtils.decode( dn ); }else{ String path = url.getPath(); int pos = path.lastIndexOf( "/" ); if ( pos != -1 ){ path = path.substring( pos+1 ); } path = path.trim(); if ( url_str.length() > 0 ){ file_name = UrlUtils.decode( path ); }else{ file_name = Base32.encode( hash ); } } file_name = FileUtil.convertOSSpecificChars( file_name, false ); try{ file = new File( AETemporaryFileHandler.createTempDir(), file_name ); }catch( Throwable e ){ file_name += ".tmp"; file = new File( AETemporaryFileHandler.getTempDirectory(), file_name ); } } public URL getURL() { return( url ); } public void download() { synchronized( lock ){ if ( file_cached ){ return; } try{ channel chan = createChannel(); channel.request req = chan.createRequest(); req.setAll(); final FileOutputStream fos = new FileOutputStream( file ); boolean ok = false; try{ req.addListener( new DiskManagerListener() { public void eventOccurred( DiskManagerEvent event ) { if ( event.getType() == DiskManagerEvent.EVENT_TYPE_FAILED ){ throw( new RuntimeException( event.getFailure())); } PooledByteBuffer buffer = event.getBuffer(); if ( buffer == null ){ throw( new RuntimeException( "eh?" )); } try{ fos.write( buffer.toByteArray()); }catch( IOException e ){ throw( new RuntimeException( "Failed to write to " + file, e )); }finally{ buffer.returnToPool(); } } }); req.run(); ok = true; }finally{ try{ fos.close(); }catch( Throwable e ){ Debug.out( e ); } if ( !ok ){ file.delete(); }else{ file_cached = true; } } }catch( Throwable e ){ Debug.out( "Failed to cache file from " + url, e ); } } } public void setPriority( boolean b ) { } public void setSkipped( boolean b ) { throw( new RuntimeException( "Not supported" )); } public int getNumericPriority() { return( 0 ); } public int getNumericPriorty() { return( 0 ); } public void setNumericPriority( int priority) { throw( new RuntimeException( "Not supported" )); } public void setDeleted(boolean b) { } public void setLink( File link_destination ) { throw( new RuntimeException( "Not supported" )); } public File getLink() { return( null ); } public int getAccessMode() { return( READ ); } public long getDownloaded() { return( getLength()); } public long getLength() { if ( file_cached ){ long len = file.length(); if ( len > 0 ){ return( len ); } } return( -1 ); } public File getFile() { return( file ); } public File getFile( boolean follow_link ) { return( file ); } public int getIndex() { return( 0 ); } public int getFirstPieceNumber() { return( 0 ); } public long getPieceSize() { return( 32*1024 ); } public int getNumPieces() { return( -1 ); } public boolean isPriority() { return( false ); } public boolean isSkipped() { return( false ); } public boolean isDeleted() { return( false ); } public byte[] getDownloadHash() throws DownloadException { return( hash ); } public Download getDownload() throws DownloadException { throw( new DownloadException( "Not supported" )); } public channel createChannel() throws DownloadException { return( new channel()); } public DiskManagerRandomReadRequest createRandomReadRequest( long file_offset, long length, boolean reverse_order, DiskManagerListener listener ) throws DownloadException { throw( new DownloadException( "Not supported" )); } public PasswordAuthentication getAuthentication( String realm, URL tracker ) { return( null ); } public void setAuthenticationOutcome( String realm, URL tracker, boolean success ) { } public void clearPasswords() { } protected class channel implements DiskManagerChannel { private volatile boolean channel_destroyed; private volatile long channel_position; public request createRequest() { return( new request()); } public DiskManagerFileInfo getFile() { return( DiskManagerFileInfoURL.this ); } public long getPosition() { return( channel_position ); } public boolean isDestroyed() { return( channel_destroyed ); } public void destroy() { channel_destroyed = true; } protected class request implements DiskManagerRequest { private long offset; private long length; private boolean do_all_file; private long position; private int max_read_chunk = 128*1024;; private volatile boolean request_cancelled; private CopyOnWriteList<DiskManagerListener> listeners = new CopyOnWriteList<DiskManagerListener>(); public void setType( int type ) { if ( type != DiskManagerRequest.REQUEST_READ ){ throw( new RuntimeException( "Not supported" )); } } public void setAll() { do_all_file = true; offset = 0; setLength( -1 ); } public void setOffset( long _offset ) { offset = _offset; } public void setLength( long _length ) { // length can be -1 here meaning 'to the end' length = _length==-1?Long.MAX_VALUE:_length; } public void setMaximumReadChunkSize( int size ) { if ( size > 16*1024 ){ max_read_chunk = size; } } public long getAvailableBytes() { return( getRemaining()); } public long getRemaining() { return( length==Long.MAX_VALUE?length:(offset + length - position )); } public void run() { try{ byte[] buffer = new byte[max_read_chunk]; long rem = length; long pos = offset; InputStream is = null; try{ SESecurityManager.setThreadPasswordHandler( DiskManagerFileInfoURL.this ); // 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?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" ); if ( !do_all_file ){ connection.setRequestProperty( "Range", "bytes=" + offset + "-" + (offset+length-1)); } connection.setConnectTimeout( 20*1000 ); connection.connect(); connection.setReadTimeout( 10*1000 ); 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 && !url.toExternalForm().equals( final_url.toExternalForm())){ redirected_url = final_url; } is = connection.getInputStream(); while( rem > 0 ){ if ( request_cancelled ){ throw( new Exception( "Cancelled" )); }else if ( channel_destroyed ){ throw( new Exception( "Destroyed" )); } int len = is.read( buffer ); if ( len == -1 ){ if ( length == Long.MAX_VALUE ){ break; }else{ throw( new Exception( "Premature end of stream (complete)" )); } }else if ( len == 0 ){ sendEvent( new event( pos )); }else{ sendEvent( new event( new PooledByteBufferImpl( buffer, 0, len ), pos, len )); rem -= len; pos += len; } } }finally{ SESecurityManager.unsetThreadPasswordHandler(); // System.out.println( "Done to " + url + ": " + Thread.currentThread().getId() + ", outcome=" + outcome ); if ( is != null ){ try{ is.close(); }catch( Throwable e ){ } } } }catch( Throwable e ){ sendEvent( new event( e )); } } public void cancel() { request_cancelled = true; } public void setUserAgent( String agent ) { } protected void sendEvent( event ev ) { for ( DiskManagerListener l: listeners ){ l.eventOccurred( ev ); } } public void addListener( DiskManagerListener listener ) { listeners.add( listener ); } public void removeListener( DiskManagerListener listener ) { listeners.remove( listener ); } protected class event implements DiskManagerEvent { private int event_type; private Throwable error; private PooledByteBuffer buffer; private long event_offset; private int event_length; protected event( Throwable _error ) { event_type = DiskManagerEvent.EVENT_TYPE_FAILED; error = _error; } protected event( long _offset ) { event_type = DiskManagerEvent.EVENT_TYPE_BLOCKED; event_offset = _offset; channel_position = _offset; } protected event( PooledByteBuffer _buffer, long _offset, int _length ) { event_type = DiskManagerEvent.EVENT_TYPE_SUCCESS; buffer = _buffer; event_offset = _offset; event_length = _length; channel_position = _offset + _length - 1; } public int getType() { return( event_type ); } public DiskManagerRequest getRequest() { return( request.this ); } public long getOffset() { return( event_offset ); } public int getLength() { return( event_length ); } public PooledByteBuffer getBuffer() { return( buffer ); } public Throwable getFailure() { return( error ); } } } } }