/* * File : TRTrackerServerProcessor.java * Created : 5 Oct. 2003 * By : Parg * * Azureus - a Java Bittorrent client * * 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. * * 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 ( see the LICENSE file ). * * 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 org.gudy.azureus2.core3.tracker.server.impl.tcp; import java.io.*; import java.net.*; import java.util.*; import java.util.zip.GZIPOutputStream; import org.gudy.azureus2.core3.tracker.server.*; import org.gudy.azureus2.core3.tracker.server.impl.*; import org.gudy.azureus2.core3.util.*; import org.minicastle.util.encoders.Base64; import com.aelitis.azureus.core.dht.netcoords.DHTNetworkPosition; import com.aelitis.azureus.core.dht.netcoords.DHTNetworkPositionManager; import com.aelitis.azureus.core.util.HTTPUtils; public abstract class TRTrackerServerProcessorTCP extends TRTrackerServerProcessor { protected static final int SOCKET_TIMEOUT = 5000; protected static final char CR = '\015'; protected static final char FF = '\012'; protected static final String NL = "\015\012"; private static final String lc_azureus_name = Constants.AZUREUS_NAME.toLowerCase(); protected static final byte[] HTTP_RESPONSE_START = ( "HTTP/1.1 200 OK" + NL + "Content-Type: text/html" + NL + "Server: " + Constants.AZUREUS_NAME + " " + Constants.AZUREUS_VERSION + NL + "Connection: close" + NL + "Content-Length: ").getBytes(); protected static final byte[] HTTP_RESPONSE_XML_START = ( "HTTP/1.1 200 OK" + NL + "Content-Type: text/xml; charset=\"utf-8\"" + NL + "Server: " + Constants.AZUREUS_NAME + " " + Constants.AZUREUS_VERSION + NL + "Connection: close" + NL + "Content-Length: ").getBytes(); protected static final byte[] HTTP_RESPONSE_END_GZIP = (NL + "Content-Encoding: gzip" + NL + NL).getBytes(); protected static final byte[] HTTP_RESPONSE_END_NOGZIP = (NL + NL).getBytes(); private TRTrackerServerTCP server; private String server_url; private boolean disable_timeouts = false; protected TRTrackerServerProcessorTCP( TRTrackerServerTCP _server ) { server = _server; server_url = (server.isSSL()?"https":"http") + "://" + UrlUtils.convertIPV6Host(server.getHost()) + ":" + server.getPort(); } public boolean areTimeoutsDisabled() { return( disable_timeouts ); } public void setTimeoutsDisabled( boolean d ) { disable_timeouts = d; } protected TRTrackerServerTCP getServer() { return( server ); } protected boolean processRequest( String input_header, String lowercase_input_header, String url_path, InetSocketAddress local_address, InetSocketAddress remote_address, boolean announce_and_scrape_only, boolean keep_alive, InputStream is, OutputStream os, AsyncController async ) throws IOException { String str = url_path; int request_type = TRTrackerServerRequest.RT_UNKNOWN; boolean compact_enabled = server.isCompactEnabled(); try{ Map root = null; TRTrackerServerTorrentImpl specific_torrent = null; boolean gzip_reply = false; boolean xml_output = false; try{ if ( str.startsWith( "/announce?" )){ request_type = TRTrackerServerRequest.RT_ANNOUNCE; str = str.substring(10); }else if ( str.startsWith( "/scrape?" )){ request_type = TRTrackerServerRequest.RT_SCRAPE; str = str.substring(8); }else if ( str.equals( "/scrape" )){ request_type = TRTrackerServerRequest.RT_FULL_SCRAPE; str = ""; }else if ( str.startsWith( "/query?" )){ request_type = TRTrackerServerRequest.RT_QUERY; str = str.substring(7); }else{ String redirect = TRTrackerServerImpl.redirect_on_not_found; if ( announce_and_scrape_only ){ if ( redirect.length() == 0 ){ throw( new Exception( "Tracker only supports announce and scrape functions" )); } }else{ setTaskState( "external request" ); disable_timeouts = true; // check non-tracker authentication String user = doAuthentication( remote_address, url_path, input_header, os, false ); if ( user == null ){ return( false ); } boolean[] ka = new boolean[]{ keep_alive }; if ( handleExternalRequest( local_address, remote_address, user, str, input_header, is, os, async, ka )){ return( ka[0] ); } } if ( redirect.length() > 0 ){ os.write( ("HTTP/1.1 301 Moved Permanently" + NL + "Location: " + redirect + NL + "Connection: close" + NL + "Content-Length: 0" + NL + NL).getBytes() ); }else{ os.write( ("HTTP/1.1 404 Not Found" + NL + "Connection: close" + NL + "Content-Length: 0" + NL + NL ).getBytes() ); } os.flush(); return( false ); // throw( new Exception( "Unsupported Request Type")); } // OK, here its an announce, scrape or full scrape // check tracker authentication if ( doAuthentication( remote_address, url_path, input_header, os, true ) == null ){ return ( false ); } int enc_pos = lowercase_input_header.indexOf( "accept-encoding:"); if ( enc_pos != -1 ){ int e_pos = input_header.indexOf( NL, enc_pos ); if ( e_pos != -1 ){ // check we've not found X-Accept-Encoding (for example) if ( enc_pos > 0 ){ char c = lowercase_input_header.charAt(enc_pos-1); if ( c != FF && c != ' ' ){ enc_pos = -1; } } if ( enc_pos != -1 ){ String accept_encoding = lowercase_input_header.substring(enc_pos+16,e_pos); gzip_reply = HTTPUtils.canGZIP( accept_encoding ); } } } setTaskState( "decoding announce/scrape" ); int pos = 0; byte[] hash = null; List hash_list = null; String link = null; HashWrapper peer_id = null; int tcp_port = 0; String event = null; long uploaded = 0; long downloaded = 0; long left = 0; int num_want = -1; boolean no_peer_id = false; byte compact_mode = TRTrackerServerTorrentImpl.COMPACT_MODE_NONE; String key = null; byte crypto_level = TRTrackerServerPeer.CRYPTO_NONE; int crypto_port = 0; int udp_port = 0; int http_port = 0; int az_ver = 0; boolean stop_to_queue = false; String scrape_flags = null; int up_speed = 0; boolean hide = false; DHTNetworkPosition network_position = null; String real_ip_address = remote_address.getAddress().getHostAddress(); String client_ip_address = real_ip_address; while(pos < str.length()){ int p1 = str.indexOf( '&', pos ); String token; if ( p1 == -1 ){ token = str.substring( pos ); }else{ token = str.substring( pos, p1 ); pos = p1+1; } int p2 = token.indexOf('='); if ( p2 == -1 ){ throw( new Exception( "format invalid" )); } String lhs = token.substring( 0, p2 ).toLowerCase(); String rhs = URLDecoder.decode(token.substring( p2+1 ), Constants.BYTE_ENCODING ); // System.out.println( "param:" + lhs + " = " + rhs ); if ( lhs.equals( "info_hash" )){ byte[] b = rhs.getBytes(Constants.BYTE_ENCODING); if ( hash == null ){ hash = b; }else{ if ( hash_list == null ){ hash_list = new ArrayList(); hash_list.add( hash ); } hash_list.add( b ); } }else if ( lhs.equals( "peer_id" )){ peer_id = new HashWrapper(rhs.getBytes(Constants.BYTE_ENCODING)); }else if ( lhs.equals( "no_peer_id" )){ no_peer_id = rhs.equals("1"); }else if ( lhs.equals( "compact" )){ if ( compact_enabled ){ if ( rhs.equals("1") && compact_mode == TRTrackerServerTorrentImpl.COMPACT_MODE_NONE ){ compact_mode = TRTrackerServerTorrentImpl.COMPACT_MODE_NORMAL; } } }else if ( lhs.equals( "key" )){ if ( server.isKeyEnabled()){ key = rhs; } }else if ( lhs.equals( "port" )){ tcp_port = Integer.parseInt( rhs ); }else if ( lhs.equals( "event" )){ event = rhs; }else if ( lhs.equals( "ip" )){ // System.out.println( "override: " + real_ip_address + " -> " + rhs + " [" + input_header + "]" ); if ( !HostNameToIPResolver.isNonDNSName( rhs )){ for (int i=0;i<rhs.length();i++){ char c = rhs.charAt(i); if ( c != '.' && c != ':' && !Character.isDigit( c )){ throw( new Exception( "IP override address must be resolved by the client" )); } } try{ rhs = HostNameToIPResolver.syncResolve( rhs ).getHostAddress(); }catch( UnknownHostException e ){ throw( new Exception( "IP override address must be resolved by the client" )); } } client_ip_address = rhs; }else if ( lhs.equals( "uploaded" )){ uploaded = Long.parseLong( rhs ); }else if ( lhs.equals( "downloaded" )){ downloaded = Long.parseLong( rhs ); }else if ( lhs.equals( "left" )){ left = Long.parseLong( rhs ); }else if ( lhs.equals( "numwant" )){ num_want = Integer.parseInt( rhs ); }else if ( lhs.equals( "azudp" )){ udp_port = Integer.parseInt( rhs ); // implicit compact mode for 2500 indicated by presence of udp port if ( compact_enabled ){ compact_mode = TRTrackerServerTorrentImpl.COMPACT_MODE_AZ; } }else if ( lhs.equals( "azhttp" )){ http_port = Integer.parseInt( rhs ); }else if ( lhs.equals( "azver" )){ az_ver = Integer.parseInt( rhs ); }else if ( lhs.equals( "supportcrypto" )){ if ( crypto_level == TRTrackerServerPeer.CRYPTO_NONE ){ crypto_level = TRTrackerServerPeer.CRYPTO_SUPPORTED; } }else if ( lhs.equals( "requirecrypto" )){ crypto_level = TRTrackerServerPeer.CRYPTO_REQUIRED; }else if ( lhs.equals( "cryptoport" )){ crypto_port = Integer.parseInt( rhs ); }else if ( lhs.equals( "azq" )){ stop_to_queue = true; }else if ( lhs.equals( "azsf" )){ scrape_flags = rhs; }else if ( lhs.equals( "link" )){ link = rhs; }else if ( lhs.equals( "outform" )){ if ( rhs.equals( "xml" )){ xml_output = true; } }else if ( lhs.equals( "hide" )){ hide = Integer.parseInt( rhs ) == 1; }else if ( TRTrackerServerImpl.supportsExtensions()){ if ( lhs.equals( "aznp" )){ try{ network_position = DHTNetworkPositionManager.deserialisePosition( remote_address.getAddress(), Base32.decode( rhs )); }catch( Throwable e ){ } }else if ( lhs.equals( "azup" )){ up_speed = Integer.parseInt( rhs ); } } if ( p1 == -1 ){ break; } } // let them hide! // this is also useful if an az client wants to just hide themselves on // particular torrents (to prevent inward connections) as they can just // add a tracker-extension to append this option if ( hide ){ tcp_port = 0; crypto_port = 0; http_port = 0; udp_port = 0; } if ( crypto_level == TRTrackerServerPeer.CRYPTO_REQUIRED ){ if ( crypto_port != 0 ){ tcp_port = crypto_port; } } byte[][] hashes = null; if ( hash_list != null ){ hashes = new byte[hash_list.size()][]; hash_list.toArray( hashes ); }else if ( hash != null ){ hashes = new byte[][]{ hash }; } if ( compact_enabled ){ // >= so that if this tracker is "old" and sees a version 3+ it replies with the // best it can - version 2 if ( xml_output ){ compact_mode = TRTrackerServerTorrentImpl.COMPACT_MODE_XML; }else if ( az_ver >= 2 ){ compact_mode = TRTrackerServerTorrentImpl.COMPACT_MODE_AZ_2; } } Map[] root_out = new Map[1]; TRTrackerServerPeerImpl[] peer_out = new TRTrackerServerPeerImpl[1]; specific_torrent = processTrackerRequest( server, str, root_out, peer_out, request_type, hashes, link, scrape_flags, peer_id, no_peer_id, compact_mode, key, event, stop_to_queue, tcp_port&0xffff, udp_port&0xffff, http_port&0xffff, real_ip_address, client_ip_address, downloaded, uploaded, left, num_want, crypto_level, (byte)az_ver, up_speed, network_position ); root = root_out[0]; if ( request_type == TRTrackerServerRequest.RT_SCRAPE ){ // add in tracker type for az clients so they know this is an AZ tracker if ( lowercase_input_header.indexOf( lc_azureus_name ) != -1 ){ root.put( "aztracker", new Long(1)); } } // only post-process if this isn't a cached entry if ( root.get( "_data" ) == null ){ TRTrackerServerPeer post_process_peer = peer_out[0]; if ( post_process_peer == null ){ post_process_peer = new lightweightPeer( client_ip_address, tcp_port, peer_id ); } server.postProcess( post_process_peer, specific_torrent, request_type, str, root ); } }catch( Exception e ){ String warning_message = null; Map error_entries = null; if ( e instanceof TRTrackerServerException ){ TRTrackerServerException tr_excep = (TRTrackerServerException)e; int reason = tr_excep.getResponseCode(); error_entries = tr_excep.getErrorEntries(); if ( reason != -1 ){ String resp = "HTTP/1.1 " + reason + " " + tr_excep.getResponseText() + NL; Map headers = tr_excep.getResponseHeaders(); Iterator it = headers.entrySet().iterator(); while( it.hasNext()){ Map.Entry entry = (Map.Entry)it.next(); String key = (String)entry.getKey(); String value = (String)entry.getValue(); if ( key.equalsIgnoreCase( "connection" )){ if ( !value.equalsIgnoreCase( "close" )){ Debug.out( "Ignoring 'Connection' header" ); continue; } } resp += key + ": " + value + NL; } resp += "Connection: close" + NL; byte[] payload = null; if ( error_entries != null ){ payload = BEncoder.encode( error_entries ); resp += "Content-Length: " + payload.length + NL; }else{ resp += "Content-Length: 0" + NL; } resp += NL; os.write( resp.getBytes()); if ( payload != null ){ os.write( payload ); } os.flush(); return( false ); } if ( tr_excep.isUserMessage()){ warning_message = tr_excep.getMessage(); } }else if ( e instanceof NullPointerException ){ e.printStackTrace(); } String message = e.getMessage(); // e.printStackTrace(); if ( message == null || message.length() == 0 ){ // e.printStackTrace(); message = e.toString(); } root = new HashMap(); root.put( "failure reason", message ); if ( warning_message != null ){ root.put( "warning message", warning_message ); } if ( error_entries != null ){ root.putAll( error_entries ); } } setTaskState( "writing response" ); byte[] data; byte[] header_start; if ( xml_output ){ StringBuffer xml = new StringBuffer( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" ); xml.append( "<RESULT>" ); if ( specific_torrent != null ){ xml.append( "<BTIH>" ); xml.append( ByteFormatter.encodeString( specific_torrent.getHash().getBytes())); xml.append( "</BTIH>" ); xml.append( BEncoder.encodeToXML( root, true )); } xml.append( "</RESULT>" ); data = xml.toString().getBytes("UTF-8" ); header_start = HTTP_RESPONSE_XML_START; }else{ // cache both plain and gzip encoded data for possible reuse data = (byte[])root.get( "_data" ); if ( data == null ){ data = BEncoder.encode( root ); if ( data.length > 1000000 ){ File dump = new File( "bdecoder.dump" ); synchronized( TRTrackerServerProcessorTCP.class ){ try{ Debug.out( "Output is too large, saving diagnostics to " + dump.toString()); PrintWriter pw = new PrintWriter( new FileWriter( dump )); BDecoder.print( pw, root ); pw.close(); }catch( Throwable e ){ } } } root.put( "_data", data ); } header_start = HTTP_RESPONSE_START; } if ( gzip_reply ){ byte[] gzip_data = (byte[])root.get( "_gzipdata"); if ( gzip_data == null ){ ByteArrayOutputStream tos = new ByteArrayOutputStream(data.length); GZIPOutputStream gos = new GZIPOutputStream( tos ); gos.write( data ); gos.close(); gzip_data = tos.toByteArray(); root.put( "_gzipdata", gzip_data ); } data = gzip_data; } // System.out.println( "TRTrackerServerProcessor::reply: sending " + new String(data)); // write the response setTaskState( "writing header" ); os.write( header_start ); byte[] length_bytes = String.valueOf(data.length).getBytes(); os.write( length_bytes ); int header_len = header_start.length + length_bytes.length; setTaskState( "writing content" ); if ( gzip_reply ){ os.write( HTTP_RESPONSE_END_GZIP ); header_len += HTTP_RESPONSE_END_GZIP.length; }else{ os.write( HTTP_RESPONSE_END_NOGZIP ); header_len += HTTP_RESPONSE_END_NOGZIP.length; } os.write( data ); server.updateStats( request_type, specific_torrent, input_header.length(), header_len+data.length ); }finally{ setTaskState( "final os flush" ); os.flush(); } return( false ); } protected String doAuthentication( InetSocketAddress remote_ip, String url_path, String header, OutputStream os, boolean tracker ) throws IOException { // System.out.println( "doAuth: " + server.isTrackerPasswordEnabled() + "/" + server.isWebPasswordEnabled()); boolean apply_web_password = (!tracker) && server.isWebPasswordEnabled(); boolean apply_torrent_password = tracker && server.isTrackerPasswordEnabled(); if ( apply_web_password && server.isWebPasswordHTTPSOnly() && !server.isSSL()){ os.write( ("HTTP/1.1 403 BAD\r\n\r\nAccess Denied\r\n").getBytes() ); os.flush(); return( null ); }else if ( apply_torrent_password || apply_web_password ){ int x = header.indexOf( "Authorization:" ); if ( x == -1 ){ // auth missing. however, if we have external auth defined // and external auth is happy with junk then allow it through if ( server.hasExternalAuthorisation()){ try{ String resource_str = ( server.isSSL()?"https":"http" ) + "://" + UrlUtils.convertIPV6Host(server.getHost()) + ":" + server.getPort() + url_path; URL resource = new URL( resource_str ); if ( server.performExternalAuthorisation( remote_ip, header, resource, "", "" )){ return( "" ); } }catch( MalformedURLException e ){ Debug.printStackTrace( e ); } } }else{ // Authorization: Basic dG9tY2F0OnRvbWNhdA== int p1 = header.indexOf(' ', x ); int p2 = header.indexOf(' ', p1+1 ); String body = header.substring( p2, header.indexOf( '\r', p2 )).trim(); String decoded=new String( Base64.decode(body)); // username:password int cp = decoded.indexOf(':'); String user = decoded.substring(0,cp); String pw = decoded.substring(cp+1); boolean auth_failed = false; if ( server.hasExternalAuthorisation()){ try{ String resource_str = ( server.isSSL()?"https":"http" ) + "://" + UrlUtils.convertIPV6Host(server.getHost()) + ":" + server.getPort() + url_path; URL resource = new URL( resource_str ); if ( server.performExternalAuthorisation( remote_ip, header, resource, user, pw )){ return( user ); } }catch( MalformedURLException e ){ Debug.printStackTrace( e ); } auth_failed = true; } if ( server.hasInternalAuthorisation() && !auth_failed ){ try{ SHA1Hasher hasher = new SHA1Hasher(); byte[] password = pw.getBytes(); byte[] encoded; if( password.length > 0){ encoded = hasher.calculateHash(password); }else{ encoded = new byte[0]; } if ( user.equals( "<internal>")){ byte[] internal_pw = Base64.decode(pw); if ( Arrays.equals( internal_pw, server.getPassword())){ return( user ); } }else if ( user.equalsIgnoreCase(server.getUsername()) && Arrays.equals(encoded, server.getPassword())){ return( user ); } }catch( Exception e ){ Debug.printStackTrace( e ); } } } os.write( ( "HTTP/1.1 401 Not Authorized" + NL + "WWW-Authenticate: Basic realm=\"" + server.getName() + "\"" + NL + "Content-Length: 15" + NL + NL + "Access Denied" + NL).getBytes() ); os.flush(); return( null ); }else{ return( "" ); } } protected boolean handleExternalRequest( InetSocketAddress local_address, InetSocketAddress remote_address, String user, String url, String header, InputStream is, OutputStream os, AsyncController async, boolean[] keep_alive ) throws IOException { URL absolute_url = new URL( server_url + (url.startsWith("/")?url:("/"+url))); return( server.handleExternalRequest( local_address, remote_address, user,url,absolute_url,header, is, os, async, keep_alive )); } }