Интернет на магнитах 4 — Делим магнит на части

В своей статьях «интернет на магнитах» я предлагал публиковать универсальные магниты с помощью которых можно скачать файл из любой p2p сети (Gnutella, Gnutella2, Edonkey2000, DirectConnect, BitTorrent). Их можно получить, либо с помощью программы rhash, либо смешав разные магниты и ссылки на файл на странице сервиса Magnet Converter. Но есть проблема в том что не многие клиенты для p2p сетей лояльно относятся к произвольному порядку параметров в магните. Я решил написать скрипт который исправляет эту ситуацию на всех страницах интернета.

9e586137c5954456904e538332ed9ef2.jpg

Под катом вас ждёт JavaScript код с комментариями и тестовая ссылка.

Итак. Создаём папку «magnet-converter» в которой будут файлы:

Файл «manifest.json»:


Чтоб скрипт можно было подключить как расширение Chrome.
{ 
  "manifest_version": 2,
  "name": "Magnet Splitter",
  "description": "Splits universal magnet links to suitable for different p2p clients",
  "version": "1.0",
  "content_scripts": [
    {
      "matches": [ "http://*/*", "https://*/*" ],
      "js": [ "insert.js" ],
	  "run_at": "document_end"
    }
  ],
  "web_accessible_resources": [
    "magnet-converter.user.js"
  ]
} 

Файл «insert.js»:


Вставляем скрипт на страницу.
var script   = document.createElement('script');
script.type  = 'text/javascript';
script.src   = chrome.extension.getURL("magnet-converter.user.js");
script.async = 1;
document.head.appendChild(script);

Файл «magnet-converter.user.js»


Сам скрипт. Его можно подключить как пользовательский скрипт Greasemonkey и Tampermonkey. Также владелец сайта который публикует универсальные магниты может подключить его к своим страницам.
// ==UserScript==
// @name Magnet Splitter
// @description Splits universal magnet links to suitable for different p2p clients.
// @author ivan386
// @license MIT
// @version 1.0
// @run-at document-end
// @include http://*/*
// @include https://*/*
// ==/UserScript==

(function() {
    'use strict';

	function try_decode(data){
		try{
			data = decodeURIComponent(data);
		}catch(e){console.error(e)}
		return data;
	}

/*Разбираем магнит на части*/
	function parse_magnet(params, file){
		if (!file) file = {}
		params.replace(/([a-z0-9\.]+)=((([a-z0-9\.]+:)*)([^&]+))&?/gmi,
		function(all, name, data, urn, _, urn_data){
			data = try_decode(data)
			switch (name){
				case "dn": // (Display Name) — имя файла.
					file.name = data;
					break;
				case "xl": // (eXact Length) — размер файла в байтах.
					file.size = data;
					break;
				case "br": // Битрейт - скорость последовательной загрузки файла для его воспроизведения.
					file.bitrate = data;
					break;
				case "tr": // (TRacker) — адрес трекера для BitTorrent клиентов.
					if (!file.trackers) file.trackers = [];
					file.trackers.push(data);
					break;
				case "x.do": // Ссылка на страницу описания файла.
					if (!file.description_url) file.description_url = [];
					file.description_url.push(data);
					break;
				case "fl": // Ссылка на торрент файл.
					if (!file.torrent_file_url) file.torrent_file_url = [];
					file.torrent_file_url.push(data);
					break;
				case "as": // (Acceptable Source) — веб-ссылка на файл в Интернете.
				case "ws": // Web Seed - прямая ссылка на файл или каталог для загрузки.
					if (!file.url) file.url = [];
					file.url.push(data);
					break;
				case "xs": // (eXact Source) — ссылка на источник файла в P2P сети.
					if (!file.xurl) file.xurl = [];
					file.xurl.push(data);
					break;
				case "xt": // URN, содержащий хеш
					if (!file.hash) file.hash = {};
					switch (urn){
						case "urn:sha1:": // sha1 хеш  файла (Base32)
							file.hash.sha1 = urn_data;
						break;
						case "urn:ed2k:": // ed2k хеш  файла (Hex)
						case "urn:ed2khash:":
							file.hash.ed2k = urn_data;
						break;
						case "urn:aich:": // Advanced Intelligent Corruption Handler хеш  файла (Base32)
							file.hash.aich = urn_data;
						break;
						case "urn:btih:": // BitTorrent Info Hash (Hex, Base32)
							if (urn_data.length < 40)
								file.hash.btih = base32_to_hex(urn_data);
							else
								file.hash.btih = urn_data;
						break;
						case "urn:tree:tiger:": // Tiger Tree Hash (TTH) файла (Base32)
							file.hash.tree_tiger = urn_data;
						break;
						case "urn:bitprint:": // [SHA1].[TTH] (Base32)
							var sha1_tth = urn_data.split(".");
							if (sha1_tth && sha1_tth.length == 2){
								file.hash.sha1 = sha1_tth[0];
								file.hash.tree_tiger = sha1_tth[1];
								file.hash.bitprint = urn_data;
							}
						break;
					}
				break;
			}
		})
		return file;
	}


/*
Здесь происходит сборка магнита для торрента
Торрент клиенту нужен прежде всего bittorrent info hash (btih).
Он следует первым и обязателен для этого типа магнита.

Пример магнита:

magnet:?xt=urn:btih:16253d9beb0df49fe30bacc62ea10ba63939f0f8&dn=ruwiki-20141114-pages-meta-current.xml.bz2&tr=http%3A%2F%2Furl2torrent.net%3A6970%2Fannounce&ws=http%3A%2F%2Fdumps.wikimedia.org%2Fruwiki%2F20141114%2Fruwiki-20141114-pages-meta-current.xml.bz2&fl=http://torcache.net/torrent/16253D9BEB0DF49FE30BACC62EA10BA63939F0F8.torrent
*/

	function torrent_magnet(file){
		var magnet = ["magnet:?"]
		if (file && file.hash && file.hash.btih) {
			magnet.push("xt=urn:btih:")
			magnet.push(encodeURIComponent(file.hash.btih))
		}else return undefined;
		if (file.name) {
			magnet.push("&dn=")
			magnet.push(encodeURIComponent(file.name))
		}
		if (file.trackers) {
			for (var i=0; i < file.trackers.length; i++){
				magnet.push("&tr=")
				magnet.push(encodeURIComponent(file.trackers[i]))
			}
		}
		if (file.url){
			for (var i=0; i < file.url.length; i++){
				magnet.push("&ws=")
				magnet.push(encodeURIComponent(file.url[i]))
			}
		}
		if (file.torrent_file_url){
			for (var i=0; i < file.torrent_file_url.length; i++){
				magnet.push("&fl=")
				magnet.push(file.torrent_file_url[i])
			}
		}
		return magnet.join("")
	}

/*
Эта функция собирает магнит для DirectConnect (DC++) клиентов.
Для них обязателен tree tiger hash (TTH). Некоторые клиенты понимают и urn: bitprint в котором также содержится TTH, но urn: tree: tiger: должны понимать все.
Если не будет остальных параметров DC++ откроет окно поиска файла по TTH.

Пример магнита:

magnet:?xt=urn:tree:tiger:JNOANHGPELY63I2OMSPQ3J73AS2P4AWB5MTBJCQ&xl=2981763794&dn=ruwiki-20141114-pages-meta-current.xml.bz2&xs=http://cache.freebase.be/u24iyrp3wtlry5kjh7sacblft2lam62u&xs=dchub://allavtovo.ru&x.do=https%3A%2F%2Fdumps.wikimedia.org%2Fruwiki%2F20141114%2F
*/

	function dc_magnet(file){
		var magnet = ["magnet:?"]
		if (file && file.hash && file.hash.tree_tiger) {
			magnet.push("xt=urn:tree:tiger:")
			magnet.push(file.hash.tree_tiger)
		}else return;
		if (file.size) {
			magnet.push("&xl=")
			magnet.push(encodeURIComponent(file.size))
		}
		if (file.name) {
			magnet.push("&dn=")
			magnet.push(encodeURIComponent(file.name))
		}
		if (file.xurl){
			for (var i=0; i < file.xurl.length; i++){
				magnet.push("&xs=")
				magnet.push(file.xurl[i])
			}
		}
		if (file.description_url){
			for (var i=0; i < file.description_url.length; i++){
				magnet.push("&x.do=")
				magnet.push(encodeURIComponent(file.description_url[i]))
			}
		}
		return magnet.join("")
	}

/*
Здесь собирается ed2k ссылка.
На данный момент магниты из Edonkey2000 клиентов понимал только aMule. Поэтому мы собираем стандартную для этой сети ed2k ссылку.

Пример ссылки:

ed2k://|file|ruwiki-20141114-pages-meta-current.xml.bz2|2981763794|0218392e98873112284de6913efee0df|s=http%3A%2F%2Fdumps.wikimedia.org%2Fruwiki%2F20141114%2Fruwiki-20141114-pages-meta-current.xml.bz2|/
*/

	function ed2k_link(file){
		var link = ["ed2k://|file"]
		if (file && file.hash && file.name && file.size && file.hash.ed2k) {
			link.push(encodeURIComponent(file.name))
			link.push(file.size)
			link.push(file.hash.ed2k)
			if (file.hash.aich){
				link.push("h=" + file.hash.aich)
			}
			if (file.url){
				for (var i=0; i < file.url.length; i++){
					link.push("s=" + encodeURIComponent(file.url[i]))
				}
			}
			link.push("/")
			return link.join("|")
		}
	}


/*
Полный магнит
Этот магнит может использовать любой p2p клиент если в нём содержатся нужные ему хеши. Но справляется с этой задачей пока только Shareaza.

Пример магнита:

magnet:?xt=urn:ed2k:0218392e98873112284de6913efee0df&xl=2981763794&dn=ruwiki-20141114-pages-meta-current.xml.bz2&xt=urn:bitprint:U24IYRP3WTLRY5KJH7SACBLFT2LAM62U.JNOANHGPELY63I2OMSPQ3J73AS2P4AWB5MTBJCQ&xt=urn:btih:16253d9beb0df49fe30bacc62ea10ba63939f0f8&tr=http%3A%2F%2Furl2torrent.net%3A6970%2Fannounce&ws=http%3A%2F%2Fdumps.wikimedia.org%2Fruwiki%2F20141114%2Fruwiki-20141114-pages-meta-current.xml.bz2&x.do=https%3A%2F%2Fdumps.wikimedia.org%2Fruwiki%2F20141114%2F&fl=http%3A%2F%2Ftorcache.net%2Ftorrent%2F16253D9BEB0DF49FE30BACC62EA10BA63939F0F8.torrent&xs=http://cache.freebase.be/u24iyrp3wtlry5kjh7sacblft2lam62u&xs=dchub://allavtovo.ru
*/

	function full_magnet(file, as){
		var magnet = ["magnet:?"]
		var amp = false
		if (!file) return;
		if (file.hash && file.hash.ed2k){
			magnet.push("xt=urn:ed2k:")
			magnet.push(file.hash.ed2k)
			amp = true;
		}
		if (file.size) {
			if (amp) magnet.push("&"); else amp = true;
			magnet.push("xl=")
			magnet.push(encodeURIComponent(file.size))
		}
		if (file.name) {
			if (amp) magnet.push("&"); else amp = true;
			magnet.push("dn=")
			magnet.push(encodeURIComponent(file.name))
		}
		if (file.hash){
			if (file.hash.aich){
				if (amp) magnet.push("&"); else amp = true;
				magnet.push("xt=urn:aich:")
				magnet.push(file.hash.aich)
			}
			if (file.hash.sha1 && file.hash.tree_tiger){
				if (amp) magnet.push("&"); else amp = true;
				magnet.push("xt=urn:bitprint:")
				magnet.push(file.hash.sha1)
				magnet.push(".")
				magnet.push(file.hash.tree_tiger)
			}else if(file.hash.sha1){
				if (amp) magnet.push("&"); else amp = true;
				magnet.push("xt=urn:sha1:")
				magnet.push(file.hash.sha1)
			}else if(file.hash.tree_tiger){
				if (amp) magnet.push("&"); else amp = true;
				magnet.push("xt=urn:tree:tiger:")
				magnet.push(file.hash.tree_tiger)
			}
			if (file.hash.btih){
				if (amp) magnet.push("&"); else amp = true;
				magnet.push("xt=urn:btih:")
				magnet.push(file.hash.btih)
			}
		}
		if (file.trackers){
			for (var i=0; i < file.trackers.length; i++){
				if (amp) magnet.push("&"); else amp = true;
				magnet.push("tr=")
				magnet.push(encodeURIComponent(file.trackers[i]))
			}
		}
		if (file.url){
			for (var i=0; i < file.url.length; i++){
				if (amp) magnet.push("&"); else amp = true;
				if (as || !(file.hash && file.hash.btih))
					magnet.push("as=");
				else
					magnet.push("ws=");
				magnet.push(encodeURIComponent(file.url[i]))
			}
		}
		if (file.bitrate){
			if (amp) magnet.push("&"); else amp = true;
			magnet.push("br=")
			magnet.push(encodeURIComponent(file.bitrate))
		}
		if (file.description_url){
			for (var i=0; i < file.description_url.length; i++){
				if (amp) magnet.push("&"); else amp = true;
				magnet.push("x.do=")
				magnet.push(encodeURIComponent(file.description_url[i]))
			}
		}
		if (file.torrent_file_url){
			for (var i=0; i < file.torrent_file_url.length; i++){
				if (amp) magnet.push("&"); else amp = true;
				magnet.push("fl=")
				magnet.push(encodeURIComponent(file.torrent_file_url[i]))
			}
		}
		if (file.xurl){
			for (var i=0; i < file.xurl.length; i++){
				if (amp) magnet.push("&"); else amp = true;
				magnet.push("xs=")
				magnet.push(file.xurl[i])
			}
		}
		return magnet.join("")
	}


/*
Раньше в магнитах для торрента использовалось base32 кодирование btih. Сейчас используется HEX. Shareaza по старинке использует Base32 и эта функция конвертирует btih обратно в HEX.
*/
	function base32_to_hex(base32){
		// http://tools.ietf.org/html/rfc4648
		if (!base32) return "";
		var number = 0
		var bit_pos = 0
		var detector = /[2-7a-z]/gmi
		var hex_str = []
		base32.replace(detector, function(letter){
			var val = parseInt(letter, 36);

			if (val <= 7)
				val += 24;
			else
				val -= 10;
			
			number = ((number << 5) | val) & 255
			bit_pos += 5
			
			for (; bit_pos >= 4;){
				bit_pos -= 4
				var hex_num = (number >> bit_pos) & 15
				hex_str.push(hex_num.toString(16))
			}
		})
		
		return hex_str.join("");
	}

/*
Бонус: Микро торрент
Если в универсальном магните есть sha1 хеш используя его мы можем сгенерировать микро торрент файл. Это торрент в котором только одна чась которая включает весь файл. Sha1 хеш этой части совпадает в Sha1 хешем самого файла. Этот торрент можно получить из магнита что позволяет шарить в сети BitTorrent небольшие файлы без необходимости создания полноценного торрент файла. Также микро торрентом можно передать через сеть BitTorrent большой торрент файл полностью, а не только info часть.
*/
	
// Функция подсчёта длины строки
	function raw_length(encodedURI){
		var count = 0;
		var index = 0;
		for (index = encodedURI.indexOf("%"); index>=0 && (++count) ; index = encodedURI.indexOf("%", index+1));
		return encodedURI.length-count*2
	}
	
// Добавляем bencode кодированную строку
	function push_string(string, to_list){
		to_list.push(raw_length(string))
		to_list.push(":")
		to_list.push(string)
	}

/*Функция сборки микро торрент файла.
Для того чтобы собрать его нам нужены: sha1 хеш файла, размер файла, имя файла. Параметры в info части микро торрента записываются в алфавитном порядке по имени параметра. Это уменьшает вариативность сборки торрент файла и увеличивает вероятность получения того же btih из того же магнита. У uTorrent есть ограничение на минимальный размер части поэтому если файл меньше 16384 байт размер части устанавливается равным этому числу.

Пример:

data:application/x-bittorrent;,d4:infod6:lengthi10826029e4:name23:mediawiki-1.15.1.tar.gz12:piece%20lengthi10826029e6:pieces20:%bc%6f%a7%90%b7%73%88%92%c6%b4%15%fc%76%65%8a%97%67%63%71%5de8:url-listl68:http%3A%2F%2Fdownload.wikimedia.org%2Fmediawiki%2F1.15%2Fmediawiki-1.15.1.tar.gzee
*/

	function make_micro_torrent(file){
		if (file.name && file.size && file.hash && file.hash.sha1){
			var torrent = ["data:application/x-bittorrent;,d"]
			if (file.trackers && file.trackers.length>0){
				torrent.push("8:announce")
				push_string(file.trackers[0], torrent)
				if (file.trackers.length>1){
					torrent.push("13:announce-listl")
					for (var i = 1; file.trackers.length>i; i++){
						var tracker = encodeURIComponent(decodeURIComponent(file.trackers[i]))
						push_string(tracker, torrent)
					}
					torrent.push("e")
				}
			}
			torrent.push("4:infod")
			torrent.push("6:length")
			torrent.push("i")
			torrent.push(file.size)
			torrent.push("e")
			torrent.push("4:name")
			var name = encodeURIComponent(file.name)
			push_string(name, torrent)
			torrent.push("12:piece%20length")
			torrent.push("i")
			torrent.push(file.size < 16384 ? 16384 : file.size)
			torrent.push("e")
			torrent.push("6:pieces")
			var sha1 = file.hash.sha1
			if (sha1.length < 40)
				sha1 = base32_to_hex(sha1);
			sha1 = sha1.replace(/[0-9A-Fa-f]{2}/g,"%$&")
			push_string(sha1, torrent)
			torrent.push("e")
			if (file.url && file.url.length>0){
				torrent.push("8:url-listl")
				for (var i = 0; file.url.length>i; i++){
					var url = encodeURIComponent(decodeURIComponent(file.url[i]))
					push_string(url, torrent)
				}
				torrent.push("e")
			}
			torrent.push("e")
			return torrent.join("")
		}
	}

/* Функция для вставки новых ссылок на страницу. */
	function insert_link_after(sibling, href, class_name){
		var new_link = document.createElement("A");
		if (href) new_link.href = href;
		if (class_name) new_link.className = class_name;
		return sibling.parentNode.insertBefore(new_link, sibling.nextSibling)
	}

/*Поехали
Перебираем ссылки и ищем в них магниты. Ссылки перебираются в обратном порядке так как мы добавляем новые и массив ссылок автоматом увеличивается. Сейчас не проверял, но раньше я на этом в бесконечный цикл попадал.
*/
	var links = document.getElementsByTagName("A");
	for (var i = links.length - 1; i >= 0; i--){
		var link = links[i]
		var magnet_index = link.href.indexOf("magnet:?")
		var magnet_link
		
		if (magnet_index < 0){
			magnet_index = link.href.indexOf("magnet%3A%3F")
			if( magnet_index > -1 )
				magnet_link = try_decode(link.href.substr(magnet_index));
		}
		else{
			magnet_link = link.href.substr(magnet_index)
		}
		
		if (magnet_index > -1){
			var file = parse_magnet(magnet_link);
			var spliter = "/" + file.name + "#magnet";
			if ( link.href.indexOf( spliter ) ){
				/*
				URL + magnet
				Добавляем прямую ссылку на файл.
				*/
				
				if (!file.url) file.url = [];
				file.url.push(link.href.split( spliter )[0] + "/" + file.name);
			}
			
			var length = 0
			for(var key in file.hash){
				length ++
			}
			
			if (length > 1)
				console.log("universal magnet")
			else
				console.log("simple magnet")
			
			var link_functions = {"micro-torrent": make_micro_torrent, "torrent-magnet":torrent_magnet, "dc-magnet": dc_magnet, "ed2k-link": ed2k_link, "full-magnet": full_magnet}
			for (var name in link_functions){
				var href = link_functions[name](file)
				if (href){
					var a = insert_link_after(link, href, name);
					if ( name == "micro-torrent" ){
						// Используем атрибут download для того чтоб задать имя торрент файла.
						a.setAttribute("download", (file.name) + ".micro.torrent");
					}
						
				}
					
			}
		}
	}
	
/* Текст ссылкам зададим через стили. Так же можно и картинки вписать в виде Data URI. */

	document.head.appendChild(document.createElement('style')).appendChild(document.createTextNode(`
[class*='-magnet'], .ed2k-link, .micro-torrent{
	margin-left: 1em
}
a[class*='-magnet']:before, .ed2k-link:before, .micro-torrent:before{
	content: attr(class)
}`))
})();

Установка

  1. Копируем папку «magnet-converter» с содержимым на локальный диск.
  2. В браузере Chrome открываем «chrome://extensions/» или «Главное Меню > Дополнительные инструменты > Расширения»
  3. Ставим галочку «Режим разработчика»
  4. Нажимаем «Загрузить распакованное расширение…»
  5. Находим и выбираем папку «magnet-converter» со скриптами.
  6. Нажимаем ОК.

Тест

Ссылка для теста скрипта:

Тест

Результат должен выглядеть так:

Тест full-magnet ed2k-link dc-magnet torrent-magnet micro-torrent
Появятся 5 дополнительных ссылок для разных p2p клиентов.

Заключение


Надеюсь что разработчики p2p клиентов допишут код так что бы универсальный магнит распознавался клиентом корректно и отпадёт необходимость его делить.

Ссылки:


Код на GitHub
Интернет на магнитах 1 — Магнит
Интернет на магнитах 2 — Гипертекст
Интернет на магнитах 3 — P2P Сайт и Форум
Интернет на магнитах 4 — Делим магнит на части

Комментарии (0)

© Habrahabr.ru