1 /*
2  * Created on Mar 21, 2006 3:09:00 PM
3  * Copyright (C) Azureus Software, Inc, All Rights Reserved.
4  *
5  * This program is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU General Public License
7  * as published by the Free Software Foundation; either version 2
8  * of the License, or (at your option) any later version.
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  * You should have received a copy of the GNU General Public License
14  * along with this program; if not, write to the Free Software
15  * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
16  */
17 package org.gudy.azureus2.core3.util;
18 
19 import java.io.File;
20 import java.io.IOException;
21 import java.io.UnsupportedEncodingException;
22 import java.net.*;
23 import java.util.ArrayList;
24 import java.util.HashMap;
25 import java.util.HashSet;
26 import java.util.List;
27 import java.util.Locale;
28 import java.util.Map;
29 import java.util.Set;
30 import java.util.regex.Matcher;
31 import java.util.regex.Pattern;
32 
33 import javax.net.ssl.HttpsURLConnection;
34 import javax.net.ssl.SSLContext;
35 import javax.net.ssl.SSLException;
36 import javax.net.ssl.SSLSocket;
37 import javax.net.ssl.SSLSocketFactory;
38 import javax.net.ssl.TrustManager;
39 
40 import org.gudy.azureus2.core3.config.COConfigurationManager;
41 import org.gudy.azureus2.core3.download.DownloadManager;
42 import org.gudy.azureus2.core3.security.SESecurityManager;
43 import org.gudy.azureus2.core3.torrent.TOTorrent;
44 import org.gudy.azureus2.plugins.download.Download;
45 import org.gudy.azureus2.plugins.torrent.Torrent;
46 import org.gudy.azureus2.plugins.torrent.TorrentAnnounceURLList;
47 import org.gudy.azureus2.plugins.torrent.TorrentAnnounceURLListSet;
48 import org.gudy.azureus2.plugins.utils.resourcedownloader.ResourceDownloader;
49 import org.gudy.azureus2.plugins.utils.resourceuploader.ResourceUploader;
50 import org.gudy.azureus2.pluginsimpl.local.PluginCoreUtils;
51 import org.gudy.bouncycastle.util.encoders.Base64;
52 
53 import com.aelitis.azureus.core.networkmanager.admin.NetworkAdmin;
54 
55 
56 /**
57  * @author TuxPaper
58  * @created Mar 21, 2006
59  *
60  */
61 public class UrlUtils
62 {
63 	private static final ThreadPool	connect_pool = new ThreadPool( "URLConnectWithTimeout", 8, true );
64 
65 	static{
connect_pool.setWarnWhenFull()66 		connect_pool.setWarnWhenFull();
67 	}
68 
69 	private static final String[] prefixes = new String[] {
70 			"http://",
71 			"https://",
72 			"ftp://",
73 			"dht://",
74 			"magnet:?",
75 			"magnet://?",
76 			"maggot://" };
77 
78 	private static int MAGNETURL_STARTS_AT = 3;	// dht:// is a form of magnet URL
79 
80 	private static final Object[] XMLescapes = new Object[] {
81 		new String[] { "&", "&" },
82 		new String[] { ">", ">" },
83 		new String[] { "<", "&lt;" },
84 		new String[] { "\"", "&quot;" },
85 		new String[] { "'", "&apos;" },
86 	};
87 
88 	public static Map<String,String>
decodeArgs( String args )89 	decodeArgs(
90 		String	args )
91 	{
92 		Map<String,String>	result = new HashMap<String, String>();
93 
94 		String[] bits = (args.startsWith("?")?args.substring(1):args).split( "&" );
95 
96 		for ( String bit: bits ){
97 
98 			String[] temp = bit.split( "=", 2 );
99 
100 			if ( temp.length == 2 ){
101 
102 				String	lhs = temp[0].toLowerCase( Locale.US );
103 
104 				String	rhs = decode( temp[1] );
105 
106 				result.put( lhs, rhs );
107 
108 			}else{
109 
110 				result.put( "", decode( temp[0] ));
111 			}
112 		}
113 
114 		return( result );
115 	}
116 
117 	public static String
getMagnetURI( byte[] hash )118 	getMagnetURI(
119 		byte[]		hash )
120 	{
121 		return( "magnet:?xt=urn:btih:" + Base32.encode( hash ));
122 	}
123 
124 	public static String
getMagnetURI( byte[] hash, String name, String[] networks )125 	getMagnetURI(
126 		byte[]		hash,
127 		String		name,
128 		String[]	networks )
129 	{
130 		String magnet_uri = getMagnetURI( hash );
131 
132 		magnet_uri += encodeName( name );
133 
134 		magnet_uri += encodeNetworks( networks );
135 
136 		return( magnet_uri );
137 	}
138 
139 	private static String
encodeName( String name )140 	encodeName(
141 		String	name )
142 	{
143 		if ( name == null ){
144 
145 			return( "" );
146 
147 		}else{
148 
149 			return( "&dn=" + UrlUtils.encode(name));
150 		}
151 	}
152 
153 	private static String
encodeNetworks( String[] networks )154 	encodeNetworks(
155 		String[]	networks )
156 	{
157 		String	net_str = "";
158 
159 		if ( networks != null && networks.length > 0 ){
160 
161 			for ( String net: networks ){
162 
163 				if ( net == AENetworkClassifier.AT_PUBLIC && networks.length == 1 ){
164 
165 					break;
166 				}
167 
168 				net_str += "&net=" + net;
169 			}
170 		}
171 
172 		return( net_str );
173 	}
174 
175 	public static byte[]
extractHash( String magnet_uri )176 	extractHash(
177 		String			magnet_uri )
178 	{
179 		magnet_uri = magnet_uri.toLowerCase( Locale.US );
180 
181 		int pos = magnet_uri.indexOf( "btih:" );
182 
183 		if ( pos > 0 ){
184 
185 			magnet_uri = magnet_uri.substring( pos+5 );
186 
187 			pos = magnet_uri.indexOf( '&' );
188 
189 			if ( pos != -1 ){
190 
191 				magnet_uri = magnet_uri.substring( 0, pos );
192 			}
193 
194 			return( decodeSHA1Hash( magnet_uri ));
195 		}
196 
197 		return( null );
198 	}
199 
200 	public static Set<String>
extractNetworks( String[] magnet_uri )201 	extractNetworks(
202 		String[]		magnet_uri )
203 	{
204 		String magnet_uri_in = magnet_uri[0];
205 
206 		Set<String>	result = new HashSet<String>();
207 
208 		int	pos = magnet_uri_in.indexOf( '?' );
209 
210 		if ( pos != -1 ){
211 
212 			String magnet_uri_out = magnet_uri_in.substring( 0, pos+1 );
213 
214 			String[] bits = magnet_uri_in.substring( pos+1 ).split( "&" );
215 
216 			for ( String bit: bits ){
217 
218 				String[] temp = bit.split( "=", 2 );
219 
220 				boolean	remove = false;
221 
222 				if ( temp.length == 2 ){
223 
224 					String	lhs = temp[0];
225 
226 					if ( lhs.equalsIgnoreCase( "net" )){
227 
228 						String	rhs = decode( temp[1] );
229 
230 						result.add( AENetworkClassifier.internalise( rhs ));
231 
232 						remove = true;
233 					}
234 				}
235 
236 				if ( !remove ){
237 
238 					if ( !magnet_uri_out.endsWith( "?" )){
239 
240 						magnet_uri_out += "&";
241 					}
242 
243 					magnet_uri_out += bit;
244 				}
245 			}
246 
247 			if ( result.size() > 0 ){
248 
249 				magnet_uri[0] = magnet_uri_out;
250 			}
251 		}
252 
253 		return( result );
254 	}
255 
256 	public static String
getMagnetURI( Download download )257 	getMagnetURI(
258 		Download		download )
259 	{
260 		return( getMagnetURI( PluginCoreUtils.unwrap(download)));
261 	}
262 
263 	public static String
getMagnetURI( Download download, int max_name_len )264 	getMagnetURI(
265 		Download		download,
266 		int				max_name_len )
267 	{
268 		return( getMagnetURI( PluginCoreUtils.unwrap(download), max_name_len ));
269 	}
270 
271 	public static String
getMagnetURI( DownloadManager dm )272 	getMagnetURI(
273 		DownloadManager		dm )
274 	{
275 		return( getMagnetURI( dm, Integer.MAX_VALUE ));
276 	}
277 
278 	public static String
getMagnetURI( DownloadManager dm, int max_name_len )279 	getMagnetURI(
280 		DownloadManager		dm,
281 		int					max_name_len )
282 	{
283 		if ( dm == null ){
284 
285 			return( null );
286 		}
287 
288 		TOTorrent to_torrent = dm.getTorrent();
289 
290 		if ( to_torrent == null ){
291 
292 			return( null );
293 		}
294 
295 		String name = dm.getDisplayName();
296 
297 		if ( name.length() > max_name_len ){
298 
299 			name = name.substring( 0, max_name_len-3) + "...";
300 		}
301 
302 		String magnet_uri = getMagnetURI( name, PluginCoreUtils.wrap( to_torrent ));
303 
304 		String[]	networks = dm.getDownloadState().getNetworks();
305 
306 		magnet_uri += encodeNetworks( networks );
307 
308 		return( magnet_uri );
309 	}
310 
311 	public static String
getMagnetURI( String name, Torrent torrent )312 	getMagnetURI(
313 		String		name,
314 		Torrent		torrent )
315 	{
316 		String	magnet_str = getMagnetURI( torrent.getHash());
317 
318 		magnet_str += encodeName( name);
319 
320 		List<String>	tracker_urls = new ArrayList<String>();
321 
322 		URL announce_url = torrent.getAnnounceURL();
323 
324 		if ( announce_url != null ){
325 
326 			if ( !TorrentUtils.isDecentralised( announce_url )){
327 
328 				tracker_urls.add( announce_url.toExternalForm());
329 			}
330 		}
331 
332 		TorrentAnnounceURLList list = torrent.getAnnounceURLList();
333 
334 		TorrentAnnounceURLListSet[] sets = list.getSets();
335 
336 		for ( TorrentAnnounceURLListSet set: sets ){
337 
338 			URL[] set_urls = set.getURLs();
339 
340 			if ( set_urls.length > 0 ){
341 
342 				URL set_url = set_urls[0];
343 
344 				if ( !TorrentUtils.isDecentralised( set_url )){
345 
346 					String str = set_url.toExternalForm();
347 
348 					if ( !tracker_urls.contains( str )){
349 
350 						tracker_urls.add( str );
351 					}
352 				}
353 			}
354 		}
355 
356 		for ( String str: tracker_urls ){
357 
358 			magnet_str += "&tr=" + UrlUtils.encode( str );
359 		}
360 
361 		List<String>	ws_urls = new ArrayList<String>();
362 
363 		Object obj = torrent.getAdditionalProperty( "url-list" );
364 
365 		if ( obj instanceof byte[] ){
366 
367 			try{
368 				ws_urls.add( new URL( new String((byte[])obj, "UTF-8" )).toExternalForm());
369 
370 			}catch( Throwable e ){
371 			}
372 		}else if ( obj instanceof List ){
373 
374 			for ( Object o: (List)obj ){
375 
376 				try{
377 					if (o instanceof byte[]) {
378 						ws_urls.add( new URL( new String((byte[])o, "UTF-8" )).toExternalForm());
379 					} else if (o instanceof String) {
380 						ws_urls.add( new URL((String) o).toExternalForm());
381 					}
382 
383 				}catch( Throwable e ){
384 				}
385 			}
386 		} else if ( obj instanceof String ) {
387 			try{
388 				ws_urls.add(new URL((String) obj).toExternalForm());
389 			}catch( Throwable e ){
390 			}
391 		}
392 
393 		for ( String str: ws_urls ){
394 
395 			magnet_str += "&ws=" + UrlUtils.encode( str );
396 		}
397 
398 		return( magnet_str );
399 	}
400 		/**
401 		 * returns magnet uri if input is base 32 or base 16 encoded sha1 hash, null otherwise
402 		 * @param base_hash
403 		 * @return
404 		 */
405 
406 	public static String
normaliseMagnetURI( String base_hash )407 	normaliseMagnetURI(
408 		String		base_hash )
409 	{
410 		byte[]	hash = decodeSHA1Hash( base_hash );
411 
412 		if ( hash != null ){
413 
414 			return( getMagnetURI( hash ));
415 		}
416 
417 		return( null );
418 	}
419 
420 	public static byte[]
decodeSHA1Hash( String str )421 	decodeSHA1Hash(
422 		String	str )
423 	{
424 		if ( str == null ){
425 
426 			return( null );
427 		}
428 
429 		str = str.trim();
430 
431 		byte[] hash = null;
432 
433 		try{
434 			if ( str.length() == 40 ){
435 
436 				hash = ByteFormatter.decodeString( str );
437 
438 			}else if ( str.length() == 32 ){
439 
440 				hash = Base32.decode( str );
441 			}
442 		}catch( Throwable e ){
443 		}
444 
445 		if ( hash != null ){
446 
447 			if ( hash.length != 20 ){
448 
449 				hash = null;
450 			}
451 		}
452 
453 		return( hash );
454 	}
455 
456 	/**
457 	 * test string for possibility that it's an URL.  Considers 40 byte hex
458 	 * strings as URLs
459 	 *
460 	 * @param sURL
461 	 * @return
462 	 */
isURL(String sURL)463 	public static boolean isURL(String sURL) {
464 		return parseTextForURL(sURL, true) != null;
465 	}
466 
isURL(String sURL, boolean bGuess)467 	public static boolean isURL(String sURL, boolean bGuess) {
468 		return parseTextForURL(sURL, true, bGuess) != null;
469 	}
470 
parseTextForURL(String text, boolean accept_magnets)471 	public static String parseTextForURL(String text, boolean accept_magnets) {
472 		return parseTextForURL(text, accept_magnets, true);
473 	}
474 
475 	public static String
getURL( String text )476 	getURL(
477 		String	text )
478 	{
479 		return( parseTextForURL(text, false, false ));
480 	}
481 
parseTextForURL(String text, boolean accept_magnets, boolean guess)482 	public static String parseTextForURL(String text, boolean accept_magnets,
483 			boolean guess) {
484 
485 		if (text == null || text.length() < 5) {
486 			return null;
487 		}
488 
489 		text = text.trim();
490 
491 		if ( text.startsWith( "azplug:" )){
492 
493 			return( text );
494 		}
495 
496 		if ( text.startsWith( "chat:" )){
497 
498 			return( "azplug:?id=azbuddy&arg=" + UrlUtils.encode( text ));
499 		}
500 
501 		if ( text.startsWith( "tor:" )){
502 
503 			String href = parseTextForURL(text.substring(4), false, false );
504 			if (href != null) {
505 				return( "tor:" + href );
506 			}
507 		}
508 
509 		String href = parseHTMLforURL(text);
510 		if (href != null) {
511 			return href;
512 		}
513 
514 		try {
515 			text = text.trim();
516 			text = decodeIfNeeded(text);
517 		} catch (Exception e) {
518 			// sometimes fires a IllegalArgumentException
519 			// catch everything and ignore.
520 		}
521 
522 		String textLower;
523 		try {
524 			textLower = text.toLowerCase();
525 		} catch (Throwable e) {
526 			textLower = text;
527 		}
528 		int max = accept_magnets ? prefixes.length : MAGNETURL_STARTS_AT;
529 		int end = -1;
530 		int start = textLower.length();
531 		String strURL = null;
532 		for (int i = 0; i < max; i++) {
533 			final int testBegin = textLower.indexOf(prefixes[i]);
534 			if (testBegin >= 0 && testBegin < start) {
535 				end = text.indexOf("\n", testBegin + prefixes[i].length());
536 				String strURLTest = (end >= 0) ? text.substring(testBegin, end - 1)
537 						: text.substring(testBegin);
538 				try {
539 					URL parsedURL = new URL(strURLTest);
540 					strURL = parsedURL.toExternalForm();
541 				} catch (MalformedURLException e1) {
542 					e1.printStackTrace();
543 					if (i >= MAGNETURL_STARTS_AT) {
544 						strURL = strURLTest;
545 					}
546 				}
547 			}
548 		}
549 		if (strURL != null) {
550 			return strURL;
551 		}
552 
553 		if (new File(text).exists()) {
554 			return null;
555 		}
556 
557 			// be lenient for raw anon addresses
558 
559 		try{
560 			URL u = new URL( "http://" + text );
561 
562 			String host = u.getHost();
563 
564 			if ( host != null && AENetworkClassifier.categoriseAddress( host ) != AENetworkClassifier.AT_PUBLIC ){
565 
566 				return( u.toExternalForm());
567 			}
568 		}catch( Throwable e ){
569 		}
570 
571 		if (accept_magnets
572 				&& (text.startsWith("bc://") || text.startsWith("bctp://"))) {
573 			return parseTextForMagnets(text);
574 		}
575 
576 			// hack to support appending args to raw hashes
577 
578 		String text_prefix = text;
579 		String text_suffix = "";
580 
581 		int a_pos = text_prefix.indexOf( '?' );
582 		if ( a_pos == -1 ){
583 			a_pos = text_prefix.indexOf( '&' );
584 		}
585 		if ( a_pos != -1 ){
586 			String args = text_prefix.substring( a_pos+1 ).trim();
587 			if ( args.contains( "=" )){
588 				int s_pos = args.indexOf(' ');
589 				if ( s_pos != -1 ){
590 					args = args.substring( 0, s_pos );
591 				}
592 				text_prefix = text_prefix.substring( 0, a_pos );
593 				text_suffix = "&" + args;
594 			}
595 		}
596 
597 		// accept raw hash of 40 hex chars
598 		if (accept_magnets ){
599 
600 			if ( text_prefix.matches("^[a-fA-F0-9]{40}$")) {
601 
602 				// convert from HEX to raw bytes
603 				byte[] infohash = ByteFormatter.decodeString(text_prefix.toUpperCase());
604 				// convert to BASE32
605 				return "magnet:?xt=urn:btih:" + Base32.encode(infohash) + text_suffix;
606 			}
607 
608 			String temp_text = text_prefix.replaceAll( "\\s+", "" );
609 
610 			if ( temp_text.matches("^[a-fA-F0-9]{40}$")) {
611 
612 				// convert from HEX to raw bytes
613 				byte[] infohash = ByteFormatter.decodeString(temp_text.toUpperCase());
614 				// convert to BASE32
615 				return "magnet:?xt=urn:btih:" + Base32.encode(infohash) + text_suffix;
616 			}
617 		}
618 
619 		// accept raw hash of 32 base-32 chars
620 		if (accept_magnets && text_prefix.matches("^[a-zA-Z2-7]{32}$")) {
621 			return "magnet:?xt=urn:btih:" + text_prefix + text_suffix;
622 		}
623 
624 		// javascript:loadOrAlert('WVOPRHRPFSCLAW7UWHCXCH7QNQIU6TWG')
625 
626 		// accept raw hash of 32 base-32 chars, with garbage around it
627 		if (accept_magnets && guess) {
628 			Pattern pattern = Pattern.compile("[^a-zA-Z2-7][a-zA-Z2-7]{32}[^a-zA-Z2-7]");
629 			Matcher matcher = pattern.matcher(text);
630 			if (matcher.find()) {
631 				String hash = text.substring(matcher.start() + 1, matcher.start() + 33);
632 				return "magnet:?xt=urn:btih:" + hash;
633 			}
634 
635 			pattern = Pattern.compile("[^a-fA-F0-9][a-fA-F0-9]{40}[^a-fA-F0-9]");
636 			matcher = pattern.matcher(text);
637 			if (matcher.find()) {
638 				String hash = text.substring(matcher.start() + 1, matcher.start() + 41);
639 				// convert from HEX to raw bytes
640 				byte[] infohash = ByteFormatter.decodeString(hash.toUpperCase());
641 				// convert to BASE32
642 				return "magnet:?xt=urn:btih:" + Base32.encode(infohash);
643 			}
644 		}
645 
646 		return null;
647 	}
648 
649 	public static String
parseTextForMagnets( String text )650 	parseTextForMagnets(
651 		String		text )
652 	{
653 		if (text.startsWith("magnet:") || text.startsWith( "maggot:" )){
654 			return text;
655 		}
656 
657 		// accept raw hash of 40 hex chars
658 		if (text.matches("^[a-fA-F0-9]{40}$")) {
659 			// convert from HEX to raw bytes
660 			byte[] infohash = ByteFormatter.decodeString(text.toUpperCase());
661 			// convert to BASE32
662 			return "magnet:?xt=urn:btih:" + Base32.encode(infohash);
663 		}
664 
665 		String temp_text = text.replaceAll( "\\s+", "" );
666 		if (temp_text.matches("^[a-fA-F0-9]{40}$")) {
667 			// convert from HEX to raw bytes
668 			byte[] infohash = ByteFormatter.decodeString(temp_text.toUpperCase());
669 			// convert to BASE32
670 			return "magnet:?xt=urn:btih:" + Base32.encode(infohash);
671 		}
672 
673 		// accept raw hash of 32 base-32 chars
674 		if (text.matches("^[a-zA-Z2-7]{32}$")) {
675 			return "magnet:?xt=urn:btih:" + text;
676 		}
677 
678 		Pattern pattern;
679 		Matcher matcher;
680 
681 		pattern = Pattern.compile("magnet:\\?[a-z%0-9=_:&.]+", Pattern.CASE_INSENSITIVE);
682 		matcher = pattern.matcher(text);
683 		if (matcher.find()) {
684 			return matcher.group();
685 		}
686 
687 		pattern = Pattern.compile("maggot://[a-z0-9]+:[a-z0-9]", Pattern.CASE_INSENSITIVE);
688 		matcher = pattern.matcher(text);
689 		if (matcher.find()) {
690 			return matcher.group();
691 		}
692 
693 		pattern = Pattern.compile("bc://bt/([a-z0-9=\\+/]+)", Pattern.CASE_INSENSITIVE);
694 		matcher = pattern.matcher(text.replaceAll(" ", "+"));
695 		if (matcher.find()) {
696 			String base64 = matcher.group(1);
697 			byte[] decode = Base64.decode(base64);
698 			if (decode != null && decode.length > 0) {
699 				// Format is AA/<name>/<size>/<hash>/ZZ
700 				try {
701 					String decodeString = new String(decode, "utf8");
702 					pattern = Pattern.compile("AA.*/(.*)/ZZ", Pattern.CASE_INSENSITIVE);
703 					matcher = pattern.matcher(decodeString);
704 					if (matcher.find()) {
705 						String hash = matcher.group(1);
706 						String magnet = parseTextForMagnets(hash);
707 						if (magnet != null) {
708 							pattern = Pattern.compile("AA/(.*)/[0-9]+", Pattern.CASE_INSENSITIVE);
709 							matcher = pattern.matcher(decodeString);
710 							if (matcher.find()) {
711 								String name = matcher.group(1);
712 								return magnet + "&dn=" + encode(name);
713 							}
714 							return magnet;
715 						}
716 					}
717 				} catch (UnsupportedEncodingException e) {
718 				}
719 			}
720 		}
721 
722 		pattern = Pattern.compile("bctp://task/(.*)", Pattern.CASE_INSENSITIVE);
723 		matcher = pattern.matcher(text);
724 		if (matcher.find()) {
725 			// Format is <name>/<size>/<hash>
726 			String decodeString = matcher.group(1);
727 			String magnet = parseTextForMagnets(decodeString);
728 			if (magnet != null) {
729 				pattern = Pattern.compile("(.*)/[0-9]+", Pattern.CASE_INSENSITIVE);
730 				matcher = pattern.matcher(decodeString);
731 				if (matcher.find()) {
732 					String name = matcher.group(1);
733 					return magnet + "&dn=" + encode(name);
734 				}
735 				return magnet;
736 			}
737 		}
738 
739 		// accept raw hash of 32 base-32 chars, with garbage around it
740 		if (true) {
741 			text = "!" + text + "!";
742 			pattern = Pattern.compile("[^a-zA-Z2-7][a-zA-Z2-7]{32}[^a-zA-Z2-7]");
743 			matcher = pattern.matcher(text);
744 			if (matcher.find()) {
745 				String hash = text.substring(matcher.start() + 1, matcher.start() + 33);
746 				return "magnet:?xt=urn:btih:" + hash;
747 			}
748 
749 			pattern = Pattern.compile("[^a-fA-F0-9][a-fA-F0-9]{40}[^a-fA-F0-9]");
750 			matcher = pattern.matcher(text);
751 			if (matcher.find()) {
752 				String hash = text.substring(matcher.start() + 1, matcher.start() + 41);
753 				// convert from HEX to raw bytes
754 				byte[] infohash = ByteFormatter.decodeString(hash.toUpperCase());
755 				// convert to BASE32
756 				return "magnet:?xt=urn:btih:" + Base32.encode(infohash);
757 			}
758 		}
759 
760 		return( null );
761 	}
762 
parseHTMLforURL(String text)763 	private static String parseHTMLforURL(String text) {
764 		if (text == null) {
765 			return null;
766 		}
767 
768 		// examples:
769 		// <A HREF=http://abc.om/moo>test</a>
770 		// <A style=cow HREF="http://abc.om/moo">test</a>
771 		// <a href="http://www.gnu.org/licenses/fdl.html" target="_top">moo</a>
772 
773 		Pattern pat = Pattern.compile("<.*a\\s++.*href=\"?([^\\'\"\\s>]++).*",
774 				Pattern.CASE_INSENSITIVE);
775 		Matcher m = pat.matcher(text);
776 		if (m.find()) {
777 			String sURL = m.group(1);
778 			try {
779 				sURL = decodeIfNeeded(sURL);
780 			} catch (Exception e) {
781 				// sometimes fires a IllegalArgumentException
782 				// catch everything and ignore.
783 			}
784 			return sURL;
785 		}
786 
787 		return null;
788 	}
789 
790 
791 
792 	/**
793 	 * Like URLEncoder.encode, except translates spaces into %20 instead of +
794 	 * @param s
795 	 * @return
796 	 */
encode(String s)797 	public static String encode(String s) {
798 		if (s == null) {
799 			return "";
800 		}
801 		try {
802 			return URLEncoder.encode(s, "UTF-8").replaceAll("\\+", "%20");
803 		} catch (UnsupportedEncodingException e) {
804 			return URLEncoder.encode(s).replaceAll("\\+", "%20");
805 		}
806 	}
807 
decode(String s)808 	public static String decode(String s) {
809 		if (s == null) {
810 			return "";
811 		}
812 		try {
813 			try{
814 				return( URLDecoder.decode(s, "UTF-8"));
815 
816 			}catch( IllegalArgumentException e ){
817 
818 					// handle truncated encodings somewhat gracefully
819 
820 				int pos = s.lastIndexOf( "%" );
821 
822 				if ( pos >= s.length()-2 ){
823 
824 					return( URLDecoder.decode(s.substring( 0, pos ), "UTF-8"));
825 				}
826 
827 				throw( e );
828 			}
829 		} catch (UnsupportedEncodingException e) {
830 
831 			return( URLDecoder.decode(s));
832 		}
833 	}
834 
835 	/**
836 	 * Unfortunately we have code that mindlessly decoded URLs (FileDownloadWindow) which borked (in the case I discovered) magnet uris with encoded
837 	 * parameters (e.g. the &tr= parameter) - doing so screws stuff up later if, for example, the parameter included an encoded '&'
838 	 * @param s
839 	 * @return
840 	 */
841 
842 	public static String
decodeIfNeeded( String s )843 	decodeIfNeeded(
844 		String s )
845 	{
846 		if ( s == null ){
847 
848 			return( "" );
849 		}
850 
851 		try{
852 				// of course someone prolly added the blind URLDecode for a reason so try and just deal with the borkage
853 				// which means looking for &a=b elements and ensure we don't URLDecode the 'b'
854 
855 				// only have issues if there's a naked '?' there
856 
857 			int	q_pos = s.indexOf( '?' );
858 			int a_pos = s.indexOf( '&' );
859 
860 			if ( q_pos == -1 && a_pos == -1 ){
861 
862 				return( decode( s ));
863 			}
864 
865 			int start = Math.min( q_pos, a_pos );
866 
867 			return( decode( s.substring( 0, start )) + s.substring( start ));
868 
869 		}catch( Throwable e ){
870 
871 			return( s );
872 		}
873 	}
874 
escapeXML(String s)875 	public static String escapeXML(String s) {
876 		if (s == null) {
877 			return "";
878 		}
879 		String ret = s;
880 		for (int i = 0; i < XMLescapes.length; i++) {
881 			String[] escapeEntry = (String[])XMLescapes[i];
882 			ret = ret.replaceAll(escapeEntry[0], escapeEntry[1]);
883 		}
884 		return ret;
885 	}
886 
unescapeXML(String s)887 	public static String unescapeXML(String s) {
888 		if (s == null) {
889 			return "";
890 		}
891 		String ret = s;
892 		for (int i = 0; i < XMLescapes.length; i++) {
893 			String[] escapeEntry = (String[])XMLescapes[i];
894 			ret = ret.replaceAll(escapeEntry[1], escapeEntry[0]);
895 		}
896 		return ret;
897 	}
898 
899 	public static String
convertIPV6Host( String host )900 	convertIPV6Host(
901 		String	host )
902 	{
903 		if ( host.indexOf(':') != -1 ){
904 
905 			int	zone_index = host.indexOf( '%' );
906 
907 			if ( zone_index != -1 ){
908 
909 				host = host.substring( 0, zone_index ) + encode( host.substring( zone_index ));
910 			}
911 
912 			return( "[" + host + "]" );
913 		}
914 
915 		return( host );
916 	}
917 
918 	public static String
expandIPV6Host( String host )919 	expandIPV6Host(
920 		String	host )
921 	{
922 		if ( host.indexOf(':') != -1 ){
923 
924 			try{
925 				return( InetAddress.getByAddress(InetAddress.getByName( host ).getAddress()).getHostAddress());
926 
927 			}catch( Throwable e ){
928 
929 				Debug.printStackTrace(e);
930 			}
931 		}
932 
933 		return( host );
934 	}
935 
936 	public static void
connectWithTimeout( final URLConnection connection, long connect_timeout )937 	connectWithTimeout(
938 		final URLConnection		connection,
939 		long					connect_timeout )
940 
941 		throws IOException
942 	{
943 		connectWithTimeouts( connection, connect_timeout, -1 );
944 	}
945 
946 	public static void
connectWithTimeouts( final URLConnection connection, long connect_timeout, long read_timeout )947 	connectWithTimeouts(
948 		final URLConnection		connection,
949 		long					connect_timeout,
950 		long					read_timeout )
951 
952 		throws IOException
953 	{
954 		if ( connect_timeout != -1 ){
955 
956 			connection.setConnectTimeout( (int)connect_timeout );
957 		}
958 
959 		if ( read_timeout != -1 ){
960 
961 			connection.setReadTimeout( (int)read_timeout );
962 		}
963 
964 		connection.connect();
965 	}
966 
967 	private static String	last_headers = COConfigurationManager.getStringParameter( "metasearch.web.last.headers", null );
968 
969 	// private static final String default_headers = "SG9zdDogbG9jYWxob3N0OjQ1MTAwClVzZXItQWdlbnQ6IE1vemlsbGEvNS4wIChXaW5kb3dzOyBVOyBXaW5kb3dzIE5UIDUuMTsgZW4tVVM7IHJ2OjEuOC4xLjE0KSBHZWNrby8yMDA4MDQwNCBGaXJlZm94LzIuMC4wLjE0CkFjY2VwdDogdGV4dC94bWwsYXBwbGljYXRpb24veG1sLGFwcGxpY2F0aW9uL3hodG1sK3htbCx0ZXh0L2h0bWw7cT0wLjksdGV4dC9wbGFpbjtxPTAuOCxpbWFnZS9wbmcsKi8qO3E9MC41CkFjY2VwdC1MYW5ndWFnZTogZW4tdXMsZW47cT0wLjUKQWNjZXB0LUVuY29kaW5nOiBnemlwLGRlZmxhdGUKQWNjZXB0LUNoYXJzZXQ6IElTTy04ODU5LTEsdXRmLTg7cT0wLjcsKjtxPTAuNwpLZWVwLUFsaXZlOiAzMDAKQ29ubmVjdGlvbjoga2VlcC1hbGl2ZQ==";
970 	private static final String default_headers = "QWNjZXB0OiB0ZXh0L2h0bWwsYXBwbGljYXRpb24veGh0bWwreG1sLGFwcGxpY2F0aW9uL3htbDtxPTAuOSwqLyo7cT0wLjgKQWNjZXB0LUNoYXJzZXQ6IElTTy04ODU5LTEsdXRmLTg7cT0wLjcsKjtxPTAuMwpBY2NlcHQtRW5jb2Rpbmc6IGd6aXAsZGVmbGF0ZQpBY2NlcHQtTGFuZ3VhZ2U6IGVuLVVTLGVuO3E9MC44CkNhY2hlLUNvbnRyb2w6IG1heC1hZ2U9MApDb25uZWN0aW9uOiBrZWVwLWFsaXZlClVzZXItQWdlbnQ6IE1vemlsbGEvNS4wIChXaW5kb3dzIE5UIDYuMTsgV09XNjQpIEFwcGxlV2ViS2l0LzUzNi4xMSAoS0hUTUwsIGxpa2UgR2Vja28pIENocm9tZS8yMC4wLjExMzIuNDcgU2FmYXJpLzUzNi4xMQ==";
971 
972 	public static void
setBrowserHeaders( ResourceDownloader rd, String referer )973 	setBrowserHeaders(
974 		ResourceDownloader		rd,
975 		String					referer )
976 	{
977 		setBrowserHeaders( rd, null, referer );
978 	}
979 
980 	public static void
setBrowserHeaders( ResourceDownloader rd, String encoded_headers, String referer )981 	setBrowserHeaders(
982 		ResourceDownloader		rd,
983 		String					encoded_headers,
984 		String					referer )
985 	{
986 		String	headers_to_use = getBrowserHeadersToUse( encoded_headers );
987 
988 		try{
989 			String header_string = new String( Base64.decode( headers_to_use ), "UTF-8" );
990 
991 			String[]	headers = header_string.split( "\n" );
992 
993 			for (int i=0;i<headers.length;i++ ){
994 
995 				String	header = headers[i];
996 
997 				int	pos = header.indexOf( ':' );
998 
999 				if ( pos != -1 ){
1000 
1001 					String	lhs = header.substring(0,pos).trim();
1002 					String	rhs	= header.substring(pos+1).trim();
1003 
1004 					if ( !( lhs.equalsIgnoreCase( "Host") ||
1005 							lhs.equalsIgnoreCase( "Referer" ))){
1006 
1007 						rd.setProperty( "URL_" + lhs, rhs );
1008 					}
1009 				}
1010 			}
1011 
1012 			if ( referer != null && referer.length() > 0 ){
1013 
1014 				rd.setProperty( "URL_Referer", referer );
1015 			}
1016 		}catch( Throwable e ){
1017 		}
1018 	}
1019 
1020 	public static void
setBrowserHeaders( ResourceUploader ru, String encoded_headers, String referer )1021 	setBrowserHeaders(
1022 		ResourceUploader		ru,
1023 		String					encoded_headers,
1024 		String					referer )
1025 	{
1026 		String	headers_to_use = getBrowserHeadersToUse( encoded_headers );
1027 
1028 		try{
1029 			String header_string = new String( Base64.decode( headers_to_use ), "UTF-8" );
1030 
1031 			String[]	headers = header_string.split( "\n" );
1032 
1033 			for (int i=0;i<headers.length;i++ ){
1034 
1035 				String	header = headers[i];
1036 
1037 				int	pos = header.indexOf( ':' );
1038 
1039 				if ( pos != -1 ){
1040 
1041 					String	lhs = header.substring(0,pos).trim();
1042 					String	rhs	= header.substring(pos+1).trim();
1043 
1044 					if ( !( lhs.equalsIgnoreCase( "Host") ||
1045 							lhs.equalsIgnoreCase( "Referer" ))){
1046 
1047 						ru.setProperty( "URL_" + lhs, rhs );
1048 					}
1049 				}
1050 			}
1051 
1052 			if ( referer != null && referer.length() > 0 ){
1053 
1054 				ru.setProperty( "URL_Referer", referer );
1055 			}
1056 		}catch( Throwable e ){
1057 		}
1058 	}
1059 
1060 	public static void
setBrowserHeaders( URLConnection connection, String referer )1061 	setBrowserHeaders(
1062 		URLConnection			connection,
1063 		String					referer )
1064 	{
1065 		setBrowserHeaders( connection, null, referer );
1066 	}
1067 
1068 	public static void
setBrowserHeaders( URLConnection connection, String encoded_headers, String referer )1069 	setBrowserHeaders(
1070 		URLConnection			connection,
1071 		String					encoded_headers,
1072 		String					referer )
1073 	{
1074 		String	headers_to_use = getBrowserHeadersToUse( encoded_headers );
1075 
1076 		try{
1077 
1078 			String header_string = new String( Base64.decode( headers_to_use ), "UTF-8" );
1079 
1080 			String[]	headers = header_string.split( "\n" );
1081 
1082 			for (int i=0;i<headers.length;i++ ){
1083 
1084 				String	header = headers[i];
1085 
1086 				int	pos = header.indexOf( ':' );
1087 
1088 				if ( pos != -1 ){
1089 
1090 					String	lhs = header.substring(0,pos).trim();
1091 					String	rhs	= header.substring(pos+1).trim();
1092 
1093 					if ( !( lhs.equalsIgnoreCase( "Host") ||
1094 							lhs.equalsIgnoreCase( "Referer" ))){
1095 
1096 						connection.setRequestProperty( lhs, rhs );
1097 					}
1098 				}
1099 			}
1100 
1101 			if ( referer != null && referer.length() > 0 ){
1102 
1103 				connection.setRequestProperty( "Referer", referer );
1104 			}
1105 		}catch( Throwable e ){
1106 		}
1107 	}
1108 
1109 	public static Map
getBrowserHeaders( String referer )1110 	getBrowserHeaders(
1111 		String					referer )
1112 	{
1113 		String	headers_to_use = getBrowserHeadersToUse( null );
1114 
1115 		Map	result = new HashMap();
1116 
1117 		try{
1118 
1119 			String header_string = new String( Base64.decode( headers_to_use ), "UTF-8" );
1120 
1121 			String[]	headers = header_string.split( "\n" );
1122 
1123 			for (int i=0;i<headers.length;i++ ){
1124 
1125 				String	header = headers[i];
1126 
1127 				int	pos = header.indexOf( ':' );
1128 
1129 				if ( pos != -1 ){
1130 
1131 					String	lhs = header.substring(0,pos).trim();
1132 					String	rhs	= header.substring(pos+1).trim();
1133 
1134 					if ( !( lhs.equalsIgnoreCase( "Host") ||
1135 							lhs.equalsIgnoreCase( "Referer" ))){
1136 
1137 						result.put( lhs, rhs );
1138 					}
1139 				}
1140 			}
1141 
1142 			if ( referer != null && referer.length() > 0){
1143 
1144 				result.put( "Referer", referer );
1145 			}
1146 		}catch( Throwable e ){
1147 		}
1148 
1149 		return( result );
1150 	}
1151 
1152 	private static String
getBrowserHeadersToUse( String encoded_headers )1153 	getBrowserHeadersToUse(
1154 		String		encoded_headers )
1155 	{
1156 		String	headers_to_use = encoded_headers;
1157 
1158 		synchronized( UrlUtils.class ){
1159 
1160 			if ( headers_to_use == null ){
1161 
1162 				if ( last_headers != null ){
1163 
1164 					headers_to_use = last_headers;
1165 
1166 				}else{
1167 
1168 					headers_to_use = default_headers;
1169 				}
1170 			}else{
1171 
1172 				if ( last_headers == null || !headers_to_use.equals( last_headers )){
1173 
1174 					COConfigurationManager.setParameter( "metasearch.web.last.headers", headers_to_use );
1175 				}
1176 
1177 				last_headers = headers_to_use;
1178 			}
1179 		}
1180 
1181 		return( headers_to_use );
1182 	}
1183 
queryHasParameter(String query_string, String param_name, boolean case_sensitive)1184 	public static boolean queryHasParameter(String query_string, String param_name, boolean case_sensitive) {
1185 		if (!case_sensitive) {
1186 			query_string = query_string.toLowerCase();
1187 			param_name = param_name.toLowerCase();
1188 		}
1189 		if (query_string.charAt(0) == '?') {
1190 			query_string = '&' + query_string.substring(1);
1191 		}
1192 		else if (query_string.charAt(0) != '&') {
1193 			query_string = '&' + query_string;
1194 		}
1195 
1196 		return query_string.indexOf("&" + param_name + "=") != -1;
1197 	}
1198 
1199 	public static boolean
containsPasskey( URL url )1200 	containsPasskey(
1201 		URL		url )
1202 	{
1203 		if ( url == null ){
1204 
1205 			return( false );
1206 		}
1207 
1208 		String url_str = url.toExternalForm();
1209 
1210 		return( url_str.matches(".*[0-9a-z]{20,40}.*"));
1211 	}
1212 
1213 	public static URL
setPort( URL u, int port )1214 	setPort(
1215 		URL		u,
1216 		int		port )
1217 	{
1218 		if ( port == -1 ){
1219 			port = u.getDefaultPort();
1220 		}
1221 		StringBuffer result = new StringBuffer();
1222 		result.append(u.getProtocol());
1223 		result.append(":");
1224 		String authority=u.getAuthority();
1225 		if (authority != null && authority.length() > 0) {
1226 			result.append("//");
1227 			int pos = authority.indexOf( '@' );
1228 			if ( pos != -1 ){
1229 				result.append(authority.substring(0,pos+1));
1230 				authority = authority.substring(pos+1);
1231 			}
1232 			pos = authority.lastIndexOf(':');
1233 			if ( pos == -1 ){
1234 				if ( port > 0 ){
1235 					result.append(authority + ":" + port );
1236 				}else{
1237 					result.append(authority);
1238 				}
1239 			}else{
1240 				if ( port > 0 ){
1241 					result.append(authority.substring(0,pos+1) + port );
1242 				}else{
1243 					result.append(authority.substring(0,pos));
1244 				}
1245 			}
1246 		}
1247 		if (u.getPath() != null) {
1248 			result.append(u.getPath());
1249 		}
1250 		if (u.getQuery() != null) {
1251 			result.append('?');
1252 			result.append(u.getQuery());
1253 		}
1254 		if (u.getRef() != null) {
1255 			result.append("#");
1256 			result.append(u.getRef());
1257 		}
1258 		try{
1259 			return( new URL( result.toString()));
1260 		}catch( Throwable e ){
1261 			Debug.out(e);
1262 			return(u);
1263 		}
1264 	}
1265 
1266 	public static URL
setHost( URL u, String host )1267 	setHost(
1268 		URL			u,
1269 		String		host )
1270 	{
1271 		StringBuffer result = new StringBuffer();
1272 		result.append(u.getProtocol());
1273 		result.append(":");
1274 		String authority=u.getAuthority();
1275 		if (authority != null && authority.length() > 0) {
1276 			result.append("//");
1277 			int pos = authority.indexOf( '@' );
1278 			if ( pos != -1 ){
1279 				result.append(authority.substring(0,pos+1));
1280 				authority = authority.substring(pos+1);
1281 			}
1282 			pos = authority.lastIndexOf(':');
1283 			if ( pos == -1 ){
1284 				result.append(host );
1285 			}else{
1286 				result.append(host + authority.substring(pos));
1287 			}
1288 		}
1289 		if (u.getPath() != null) {
1290 			result.append(u.getPath());
1291 		}
1292 		if (u.getQuery() != null) {
1293 			result.append('?');
1294 			result.append(u.getQuery());
1295 		}
1296 		if (u.getRef() != null) {
1297 			result.append("#");
1298 			result.append(u.getRef());
1299 		}
1300 		try{
1301 			return( new URL( result.toString()));
1302 		}catch( Throwable e ){
1303 			Debug.out(e);
1304 			return(u);
1305 		}
1306 	}
1307 
1308 	public static URL
setProtocol( URL u, String protocol )1309 	setProtocol(
1310 		URL			u,
1311 		String		protocol )
1312 	{
1313 		String str = u.toExternalForm();
1314 
1315 		int pos = str.indexOf( ":" );
1316 
1317 		try{
1318 			return( new URL( protocol + str.substring( pos )));
1319 
1320 		}catch( Throwable e ){
1321 
1322 			Debug.out( e );
1323 
1324 			return( u );
1325 		}
1326 	}
1327 
1328 	public static URL
getBaseURL( URL u )1329 	getBaseURL(
1330 		URL		u )
1331 	{
1332 		StringBuffer result = new StringBuffer();
1333 		result.append(u.getProtocol());
1334 		result.append(":");
1335 		String authority=u.getAuthority();
1336 		if (authority != null && authority.length() > 0) {
1337 			result.append("//");
1338 			int pos = authority.indexOf( '@' );
1339 			if ( pos != -1 ){
1340 				result.append(authority.substring(0,pos+1));
1341 				authority = authority.substring(pos+1);
1342 			}
1343 			pos = authority.lastIndexOf(':');
1344 			int	port = u.getPort();
1345 			if ( port == -1 ){
1346 				port = u.getDefaultPort();
1347 			}
1348 			if ( pos == -1 ){
1349 				result.append(authority + ":" + port );
1350 			}else{
1351 				result.append(authority.substring(0,pos+1) + port );
1352 			}
1353 		}
1354 
1355 		try{
1356 			return( new URL( result.toString()));
1357 		}catch( Throwable e ){
1358 			Debug.out(e);
1359 			return(u);
1360 		}
1361 	}
1362 
1363 	public static String
getCanonicalString( URL url )1364 	getCanonicalString(
1365 		URL		url )
1366 	{
1367 		String protocol = url.getProtocol();
1368 
1369 		if ( !protocol.equals( protocol.toLowerCase( Locale.US ))){
1370 
1371 			protocol = protocol.toLowerCase( Locale.US );
1372 
1373 			url = UrlUtils.setProtocol( url, protocol );
1374 		}
1375 
1376 		int	port = url.getPort();
1377 
1378 		if ( protocol.equals( "http" ) || protocol.equals( "https" )){
1379 
1380 			if ( port == url.getDefaultPort()){
1381 
1382 				url = UrlUtils.setPort( url, 0 );
1383 			}
1384 		}else{
1385 
1386 			if ( port == -1 ){
1387 
1388 				url = UrlUtils.setPort( url, url.getDefaultPort());
1389 			}
1390 		}
1391 
1392 		return( url.toString());
1393 	}
1394 
1395 		/**
1396 		 * Returns an explicit IPv4 url if the supplied one has both IPv6 and IPv4 addresses
1397 		 * @param url
1398 		 * @return
1399 		 */
1400 
1401 	public static URL
getIPV4Fallback( URL url )1402 	getIPV4Fallback(
1403 		URL	url )
1404 	{
1405 		try{
1406 			InetAddress[] addresses = AddressUtils.getAllByName( url.getHost());
1407 
1408 			if ( addresses.length > 0 ){
1409 
1410 				InetAddress	ipv4	= null;
1411 				InetAddress	ipv6	= null;
1412 
1413 				for ( InetAddress a: addresses ){
1414 
1415 					if ( a instanceof Inet4Address ){
1416 
1417 						ipv4 = a;
1418 
1419 					}else{
1420 
1421 						ipv6 = a;
1422 					}
1423 				}
1424 
1425 				if ( ipv4 != null && ipv6 != null ){
1426 
1427 					url = UrlUtils.setHost( url, ipv4.getHostAddress());
1428 
1429 					return( url );
1430 				}
1431 			}
1432 		}catch( Throwable f ){
1433 		}
1434 
1435 		return( null );
1436 	}
1437 
1438 	public static long
getContentLength( URLConnection con )1439 	getContentLength(
1440 		URLConnection	con )
1441 	{
1442 		long res = con.getContentLength();
1443 
1444 		if ( res == -1 ){
1445 
1446 			try{
1447 				String	str = con.getHeaderField( "content-length" );
1448 
1449 				if ( str != null ){
1450 
1451 					res = Long.parseLong( str );
1452 				}
1453 			}catch( Throwable e ){
1454 
1455 			}
1456 		}
1457 
1458 		return( res );
1459 	}
1460 
1461 	public static boolean
SSLSocketSNIHack( String host_name, SSLSocket socket )1462 	SSLSocketSNIHack(
1463 		String		host_name,
1464 		SSLSocket	socket )
1465 	{
1466 			// http://stackoverflow.com/questions/30817934/extended-server-name-sni-extension-not-sent-with-jdk1-8-0-but-send-with-jdk1-7
1467 			// also https://bugs.openjdk.java.net/browse/JDK-8144566 kinda
1468 
1469 
1470 		try{
1471 			Object sni_host_name = Class.forName( "javax.net.ssl.SNIHostName").getConstructor( String.class ).newInstance( host_name );
1472 
1473 			List<Object> sni_host_names = new ArrayList<Object>(1);
1474 
1475 			sni_host_names.add( sni_host_name );
1476 
1477 			Object ssl_parameters = SSLSocket.class.getMethod( "getSSLParameters" ).invoke( socket );
1478 
1479 			Class ssl_parameters_class = Class.forName( "javax.net.ssl.SSLParameters" );
1480 
1481 			ssl_parameters_class.getMethod( "setServerNames", List.class ).invoke( ssl_parameters, sni_host_names );
1482 
1483 			socket.getClass().getMethod( "setSSLParameters" , ssl_parameters_class ).invoke( socket, ssl_parameters );
1484 
1485 			/*
1486 			SNIHostName serverName = new SNIHostName("whatever.com");
1487 			List<SNIServerName> serverNames = new ArrayList<>(1);
1488 			serverNames.add(serverName);
1489 
1490 			SSLParameters params = socket.getSSLParameters();
1491 			params.setServerNames(serverNames);
1492 			socket.setSSLParameters(params);
1493 			*/
1494 
1495 			return( true );
1496 
1497 		}catch( Throwable e ){
1498 
1499 			return( false );
1500 		}
1501 	}
1502 
1503 	public static SSLSocketFactory
DHHackIt( final SSLSocketFactory factory )1504 	DHHackIt(
1505 		final SSLSocketFactory	factory )
1506 	{
1507 		SSLSocketFactory hack = new
1508 			SSLSocketFactory()
1509 			{
1510 				@Override
1511 				public Socket createSocket() throws IOException {
1512 					Socket result = factory.createSocket();
1513 
1514 					hack( result );
1515 
1516 					return( result );
1517 				}
1518 				@Override
1519 					public Socket createSocket(
1520 						InetAddress address,
1521 						int port,
1522 						InetAddress localAddress,
1523 						int localPort)
1524 						throws IOException {
1525 					Socket result = factory.createSocket( address, port, localAddress, localPort );
1526 
1527 					hack( result );
1528 
1529 					return( result );
1530 				}
1531 				@Override
1532 				public Socket createSocket(
1533 						InetAddress host,
1534 						int port)
1535 						throws IOException {
1536 					Socket result = factory.createSocket( host, port );
1537 
1538 					hack( result );
1539 
1540 					return( result );
1541 				}
1542 				@Override
1543 				public Socket createSocket(
1544 						Socket s,
1545 						String host,
1546 						int port,
1547 						boolean autoClose)
1548 						throws IOException {
1549 					Socket result = factory.createSocket( s, host, port, autoClose );
1550 
1551 					hack( result );
1552 
1553 					return( result );
1554 				}
1555 				@Override
1556 				public Socket createSocket(
1557 						String host,
1558 						int port)
1559 						throws IOException,
1560 						UnknownHostException {
1561 					Socket result = factory.createSocket( host, port );
1562 
1563 					hack( result );
1564 
1565 					return( result );
1566 				}
1567 				@Override
1568 				public Socket createSocket(
1569 						String host,
1570 						int port,
1571 						InetAddress localHost,
1572 						int localPort)
1573 						throws IOException,
1574 						UnknownHostException {
1575 					Socket result = factory.createSocket( host, port, localHost, localPort );
1576 
1577 					hack( result );
1578 
1579 					return( result );
1580 				}
1581 				@Override
1582 				public String[] getDefaultCipherSuites() {
1583 					String[] result = factory.getDefaultCipherSuites();
1584 
1585 					result = hack( result );
1586 
1587 					return( result );
1588 				}
1589 				@Override
1590 				public String[] getSupportedCipherSuites() {
1591 					String[] result = factory.getSupportedCipherSuites();
1592 
1593 					result = hack( result );
1594 
1595 					return( result );
1596 				}
1597 
1598 				private void
1599 				hack(
1600 					Socket	socket )
1601 				{
1602 					SSLSocket ssl_socket = (SSLSocket)socket;
1603 
1604 					ssl_socket.setEnabledCipherSuites( hack( ssl_socket.getEnabledCipherSuites()));
1605 				}
1606 
1607 				private String[]
1608 				hack(
1609 					String[]	cs  )
1610 				{
1611 					List<String> new_cs = new ArrayList<String>();
1612 
1613 					for ( String x: cs ){
1614 
1615 						if ( x.contains( "_DH_" ) || x.contains( "_DHE_" )){
1616 
1617 						}else{
1618 
1619 							new_cs.add( x );
1620 						}
1621 					}
1622 
1623 					return( new_cs.toArray(new String[new_cs.size()]));
1624 				}
1625 			};
1626 
1627 		return( hack );
1628 	}
1629 
1630 	public static void
DHHackIt( HttpsURLConnection ssl_con )1631 	DHHackIt(
1632 		HttpsURLConnection	ssl_con )
1633 	{
1634 		final SSLSocketFactory factory = ssl_con.getSSLSocketFactory();
1635 
1636 		SSLSocketFactory hack = DHHackIt( factory );
1637 
1638 		ssl_con.setSSLSocketFactory( hack );
1639 	}
1640 
1641 	public static Socket
connectSocketAndWrite( boolean is_ssl, String target_host, int target_port, byte[] bytes, int connect_timeout, int read_timeout )1642 	connectSocketAndWrite(
1643 		boolean		is_ssl,
1644 		String		target_host,
1645 		int			target_port,
1646 		byte[]		bytes,
1647 		int			connect_timeout,
1648 		int			read_timeout )
1649 
1650 		throws Exception
1651 	{
1652 			// some versions of java 6 don't support the creation of unconnected sockets
1653 			// which is required to allow connect timeout to be set before connecting
1654 			// think it only actually affected SSL because the SSL Factory had a bug
1655 
1656 		boolean is_java_17_plus = Constants.isJava7OrHigher;
1657 
1658 			// try without regard for broken versions
1659 
1660 		try{
1661 			return( connectSocketAndWrite( is_ssl, target_host, target_port, bytes, connect_timeout, read_timeout, false ));
1662 
1663 		}catch( Exception e ){
1664 
1665 			if ( is_java_17_plus ){
1666 
1667 				throw( e );
1668 			}
1669 
1670 			return( connectSocketAndWrite( is_ssl, target_host, target_port, bytes, connect_timeout, read_timeout, true ));
1671 		}
1672 	}
1673 
1674 	public static Socket
connectSocketAndWrite( boolean is_ssl, String target_host, int target_port, byte[] bytes, int connect_timeout, int read_timeout, boolean unconnected_socket_hack )1675 	connectSocketAndWrite(
1676 		boolean		is_ssl,
1677 		String		target_host,
1678 		int			target_port,
1679 		byte[]		bytes,
1680 		int			connect_timeout,
1681 		int			read_timeout,
1682 		boolean		unconnected_socket_hack )
1683 
1684 		throws Exception
1685 	{
1686 		boolean	cert_hack			= false;
1687 		boolean	dh_hack 			= false;
1688 		boolean	internal_error_hack	= false;
1689 
1690 		boolean		hacks_to_do = true;
1691 		Exception	last_error 	= null;
1692 
1693 		while( hacks_to_do ){
1694 
1695 			hacks_to_do = false;
1696 
1697 			Socket	target = null;
1698 
1699 			boolean	ok = false;
1700 
1701 			try{
1702 
1703 				InetSocketAddress targetSockAddress = new InetSocketAddress(  InetAddress.getByName(target_host) , target_port  );
1704 
1705 			    InetAddress bindIP = NetworkAdmin.getSingleton().getSingleHomedServiceBindAddress(targetSockAddress.getAddress() instanceof Inet6Address ? NetworkAdmin.IP_PROTOCOL_VERSION_REQUIRE_V6 : NetworkAdmin.IP_PROTOCOL_VERSION_REQUIRE_V4);
1706 
1707 				if ( is_ssl ){
1708 
1709 					TrustManager[] tms_delegate = SESecurityManager.getAllTrustingTrustManager();
1710 
1711 					SSLContext sc = SSLContext.getInstance("SSL");
1712 
1713 					sc.init( null, tms_delegate, RandomUtils.SECURE_RANDOM );
1714 
1715 					SSLSocketFactory factory = sc.getSocketFactory();
1716 
1717 					if ( dh_hack ){
1718 
1719 						factory = DHHackIt( factory );
1720 					}else{
1721 
1722 						factory = DHHackIt( factory );
1723 
1724 					}
1725 
1726 					if ( unconnected_socket_hack ){
1727 
1728 						if ( bindIP == null ){
1729 
1730 							target = factory.createSocket(targetSockAddress.getAddress(), targetSockAddress.getPort());
1731 
1732 						}else{
1733 
1734 							target = factory.createSocket(targetSockAddress.getAddress(), targetSockAddress.getPort(), bindIP, 0 );
1735 						}
1736 					}else{
1737 
1738 						target = factory.createSocket();
1739 					}
1740 				}else{
1741 
1742 					if ( unconnected_socket_hack ){
1743 
1744 						if ( bindIP == null ){
1745 
1746 							target = new Socket(targetSockAddress.getAddress(), targetSockAddress.getPort());
1747 
1748 						}else{
1749 
1750 							target = new Socket(targetSockAddress.getAddress(), targetSockAddress.getPort(), bindIP, 0 );
1751 						}
1752 					}else{
1753 
1754 						target = new Socket();
1755 					}
1756 				}
1757 
1758 				if ( internal_error_hack ){
1759 
1760 					SSLSocketSNIHack( target_host, (SSLSocket)target );
1761 				}
1762 
1763 				target.setSoTimeout( read_timeout );
1764 
1765 				if ( !unconnected_socket_hack ){
1766 
1767 			        if ( bindIP != null ){
1768 
1769 			        	target.bind( new InetSocketAddress( bindIP, 0 ) );
1770 			        }
1771 
1772 			        target.connect( targetSockAddress, connect_timeout );
1773 				}
1774 
1775 				target.getOutputStream().write( bytes );
1776 
1777 				ok = true;
1778 
1779 				return( target );
1780 
1781 			}catch( Exception e ){
1782 
1783 				last_error = e;
1784 
1785 				if ( e instanceof SSLException ){
1786 
1787 					String msg = Debug.getNestedExceptionMessage( e );
1788 
1789 					if ( msg.contains( "DH keypair" )){
1790 
1791 						if ( !dh_hack ){
1792 
1793 							dh_hack = true;
1794 
1795 							hacks_to_do = true;
1796 						}
1797 					}else if ( msg.contains( "internal_error" )){
1798 
1799 						if ( !internal_error_hack ){
1800 
1801 							internal_error_hack = true;
1802 
1803 							hacks_to_do = true;
1804 						}
1805 					}
1806 
1807 					if ( !cert_hack ){
1808 
1809 						cert_hack = true;
1810 
1811 						SESecurityManager.installServerCertificates( new URL( "https://" + target_host +  ":" + target_port + "/" ));
1812 
1813 						hacks_to_do = true;
1814 					}
1815 				}
1816 
1817 				if ( !hacks_to_do ){
1818 
1819 					throw( e );
1820 				}
1821 			}finally{
1822 
1823 				if ( !ok ){
1824 
1825 					if ( target != null ){
1826 
1827 						target.close();
1828 					}
1829 				}
1830 			}
1831 		}
1832 
1833 		throw( last_error );
1834 	}
1835 
1836 
main(String[] args)1837 	public static void main(String[] args) {
1838 
1839 		//MagnetURIHandler.getSingleton();
1840 
1841 		System.out.println( URLEncoder.encode( "http://a.b.c/fred?a=10&b=20"));
1842 
1843 		byte[] infohash = ByteFormatter.decodeString("1234567890123456789012345678901234567890");
1844 		String[] test = {
1845 				"http://moo.com",
1846 				"http%3A%2F/moo%2Ecom",
1847 				"magnet:?moo",
1848 				"magnet%3A%3Fxt=urn:btih:26",
1849 				"magnet%3A//%3Fmooo",
1850 				"magnet:?xt=urn:btih:" + Base32.encode(infohash),
1851 				"aaaaaaaaaabbbbbbbbbbccccccccccdddddddddd",
1852 				"magnet:?dn=OpenOffice.org_2.0.3_Win32Intel_install.exe&xt=urn:sha1:PEMIGLKMNFI4HZ4CCHZNPKZJNMAAORKN&xt=urn:tree:tiger:JMIJVWHCQUX47YYH7O4XIBCORNU2KYKHBBC6DHA&xt=urn:ed2k:1c0804541f34b6583a383bb8f2cec682&xl=96793015&xs=http://mirror.switch.ch/ftp/mirror/OpenOffice/stable/2.0.3/OOo_2.0.3_Win32Intel_install.exe%3Fa%3D10%26b%3D20",
1853 				};
1854 		for (int i = 0; i < test.length; i++) {
1855 			System.out.println( test[i] );
1856 			System.out.println("URLDecoder.decode: -> " + URLDecoder.decode(test[i]));
1857 			System.out.println("decode:            -> " + decode(test[i]));
1858 			System.out.println("decodeIf:          -> " + decodeIfNeeded(test[i]));
1859 			System.out.println("isURL:             -> " + isURL(test[i]));
1860 			System.out.println("parse:             -> " + parseTextForURL(test[i], true));
1861 		}
1862 
1863 		String[] testEncode = {
1864 			"a b"
1865 		};
1866 		for (int i = 0; i < testEncode.length; i++) {
1867 			String txt = testEncode[i];
1868 			try {
1869 				System.out.println("URLEncoder.encode: " + txt + " -> "
1870 						+ URLEncoder.encode(txt, "UTF8"));
1871 			} catch (UnsupportedEncodingException e) {
1872 			}
1873 			System.out.println("URLEncoder.encode: " + txt + " -> "
1874 					+ URLEncoder.encode(txt));
1875 			System.out.println("encode: " + txt + " -> " + encode(txt));
1876 		}
1877 
1878 	}
1879 }
1880