diff --git a/EventEmitter.js b/EventEmitter.js new file mode 100644 index 0000000..a56831c --- /dev/null +++ b/EventEmitter.js @@ -0,0 +1,17 @@ +'use strict'; + +var EventEmitter = function () { + this.ecb = {}; +}; +EventEmitter.prototype.emit = function (id, data) { + (this.ecb[id] || []).forEach(c => c(data)); + chrome.runtime.sendMessage({ + cmd: 'event', + id, + data + }); +}; +EventEmitter.prototype.on = function (id, callback) { + this.ecb[id] = this.ecb[id] || []; + this.ecb[id].push(callback); +}; diff --git a/common.js b/common.js new file mode 100644 index 0000000..885f29f --- /dev/null +++ b/common.js @@ -0,0 +1,95 @@ +/* globals Tor, proxy, privacy, ui */ +'use strict'; + +var prefs = { + webrtc: 2, + policy: { + 'proxy': 0, // 0: turn on when tor is active and turn off when tor is disabled; 1: turn on when browser starts and do not turn off when tor is disabled + 'webrtc': 0, // 0: turn on when tor is active and turn off when tor is disabled; 1: turn on when browser starts and do not turn off when tor is disabled + }, + 'auto-run': false, + 'directory': null +}; +chrome.storage.onChanged.addListener(ps => { + Object.keys(ps).forEach(p => prefs[p] = ps[p].newValue); +}); + +var tor = new Tor({ + directory: '' +}); + +// get external IP address +tor.on('status', status => { + ui.emit('title', {status}); + if (status === 'connected') { + tor.getIP(); + } +}); + +// Set proxy +tor.on('status', s => { + if (s === 'connected') { + proxy.set(tor.info); + privacy.set(prefs.webrtc); + } + else if (s === 'disconnected') { + if (prefs.policy.proxy === 0) { + proxy.reset(); + } + if (prefs.policy.webrtc === 0) { + privacy.reset(); + } + } +}); +// ip changes +tor.on('ip', ip => ui.emit('title', {ip})); +// +proxy.addListener('change', bol => ui.emit('title', { + proxy: bol ? 'SOCKS' : 'default' +})); +chrome.storage.local.get(prefs, p => { + prefs = p; + // directory + tor.directory = p.directory; + // auto run? + if (prefs['auto-run']) { + tor.refresh(); + } + if (prefs.policy.proxy === 1) { + privacy.set(prefs.proxy); + } + if (prefs.policy.webrtc === 1) { + privacy.set(prefs.webrtc); + } +}); +// logs +proxy.addListener('change', s => { + tor.emit('stdout', `Proxy status is "${s}"`); +}); +privacy.addListener('change', (type, state) => { + tor.emit('stdout', `Protection: module -> ${type}, status -> ${state}`); +}); + +chrome.runtime.onMessage.addListener(request => { + if (request.method === 'popup-command') { + if (request.cmd) { + tor.command(request.cmd); + } + } + else if (request.method === 'popup-action') { + if (request.cmd === 'connection') { + if (request.action === 'disconnect') { + tor.disconnect(); + } + else { + if (prefs.directory) { + tor.refresh(); + } + else { + ui.notification('Tor Bundle path is not set in the options page'); + chrome.runtime.openOptionsPage(); + } + } + } + } +}); diff --git a/data/helper b/data/helper new file mode 120000 index 0000000..2b9b3a4 --- /dev/null +++ b/data/helper @@ -0,0 +1 @@ +../../external-application-button/data/helper/ \ No newline at end of file diff --git a/data/icons/128.png b/data/icons/128.png new file mode 100644 index 0000000..0e8725f Binary files /dev/null and b/data/icons/128.png differ diff --git a/data/icons/16.png b/data/icons/16.png new file mode 100644 index 0000000..3653191 Binary files /dev/null and b/data/icons/16.png differ diff --git a/data/icons/256.png b/data/icons/256.png new file mode 100644 index 0000000..87c826d Binary files /dev/null and b/data/icons/256.png differ diff --git a/data/icons/32.png b/data/icons/32.png new file mode 100644 index 0000000..cc30e4b Binary files /dev/null and b/data/icons/32.png differ diff --git a/data/icons/48.png b/data/icons/48.png new file mode 100644 index 0000000..c024f34 Binary files /dev/null and b/data/icons/48.png differ diff --git a/data/icons/512.png b/data/icons/512.png new file mode 100644 index 0000000..1c29954 Binary files /dev/null and b/data/icons/512.png differ diff --git a/data/icons/64.png b/data/icons/64.png new file mode 100644 index 0000000..c3eeac2 Binary files /dev/null and b/data/icons/64.png differ diff --git a/data/icons/enabled/128.png b/data/icons/enabled/128.png new file mode 100644 index 0000000..8a874c7 Binary files /dev/null and b/data/icons/enabled/128.png differ diff --git a/data/icons/enabled/16.png b/data/icons/enabled/16.png new file mode 100644 index 0000000..6160a87 Binary files /dev/null and b/data/icons/enabled/16.png differ diff --git a/data/icons/enabled/256.png b/data/icons/enabled/256.png new file mode 100644 index 0000000..5a0884d Binary files /dev/null and b/data/icons/enabled/256.png differ diff --git a/data/icons/enabled/32.png b/data/icons/enabled/32.png new file mode 100644 index 0000000..d5271cc Binary files /dev/null and b/data/icons/enabled/32.png differ diff --git a/data/icons/enabled/48.png b/data/icons/enabled/48.png new file mode 100644 index 0000000..fd92408 Binary files /dev/null and b/data/icons/enabled/48.png differ diff --git a/data/icons/enabled/512.png b/data/icons/enabled/512.png new file mode 100644 index 0000000..c8b5b76 Binary files /dev/null and b/data/icons/enabled/512.png differ diff --git a/data/icons/enabled/64.png b/data/icons/enabled/64.png new file mode 100644 index 0000000..9b44d71 Binary files /dev/null and b/data/icons/enabled/64.png differ diff --git a/data/options/index.html b/data/options/index.html new file mode 100644 index 0000000..59efb72 --- /dev/null +++ b/data/options/index.html @@ -0,0 +1,29 @@ + + + + My Test Extension Options + + + + +

+ Tor bundle path: + +

First download the latest "Tor Bundle" pack from github.com, and extract it in a local directory. Then place the root's absolute path here.
+

+ +
+ + +
+ + + + diff --git a/data/options/index.js b/data/options/index.js new file mode 100644 index 0000000..4031597 --- /dev/null +++ b/data/options/index.js @@ -0,0 +1,22 @@ +'use strict'; + +function save () { + let directory = document.getElementById('directory').value; + chrome.storage.local.set({ + directory + }, () => { + let status = document.getElementById('status'); + status.textContent = 'Options saved.'; + setTimeout(() => status.textContent = '', 750); + }); +} + +function restore () { + chrome.storage.local.get({ + directory: '', + }, (prefs) => { + document.getElementById('directory').value = prefs.directory; + }); +} +document.addEventListener('DOMContentLoaded', restore); +document.getElementById('save').addEventListener('click', save); diff --git a/data/popup/index.css b/data/popup/index.css new file mode 100644 index 0000000..0f94cea --- /dev/null +++ b/data/popup/index.css @@ -0,0 +1,74 @@ +body { + width: 500px; + height: 300px; +} +img { + cursor: pointer; +} +input[type=button] { + border: solid 1px #eee; + background-color: #fff; + width: 120px; + margin: 2px 0; + outline: none; + cursor: pointer; +} +input { + outline: none; +} +input[type=button]:active { + border-color: #00a; +} + +[hbox] { + display: flex; + flex-direction: row; +} +[vbox] { + display: flex; + flex-direction: column; +} +[flex="1"] { + flex: 1; +} +[flex="2"] { + flex: 2; +} +[pack=center] { + justify-content: center; +} +[align=center] { + align-items: center; +} +body[data-status=connecting] img[data-cmd=connection] { + opacity: 0.3; +} + +#log { + padding: 10px; + overflow-x: hidden; + overflow-y: auto; + width: calc(47vw - 20px); + background-color: rgba(0, 0, 0, 0.01); + border: dashed 1px rgba(0, 0, 0, 0.05); +} +#log>* { + margin-bottom: 10px; +} +#toolbar { + margin-top: 8px; +} + +.msg span:nth-child(1) { + font-size: 80%; +} +.msg span:nth-child(2) { + padding: 0 5px; + background-color: rgba(0, 0, 0, 0.05); +} + +.log { + background-color: rgba(0, 0, 0, 0.05); + border: dotted 1px rgba(0, 0, 0, 0.05); + padding: 0 5px; +} diff --git a/data/popup/index.html b/data/popup/index.html new file mode 100644 index 0000000..987fd23 --- /dev/null +++ b/data/popup/index.html @@ -0,0 +1,32 @@ + + + + + + + +
+
+ +
+ +
+ +
+
+
+ + + + +
+ + + + diff --git a/data/popup/index.js b/data/popup/index.js new file mode 100644 index 0000000..07f6810 --- /dev/null +++ b/data/popup/index.js @@ -0,0 +1,73 @@ +'use strict'; + +var elements = { + log: document.getElementById('log'), + template: document.querySelector('#log template'), + webrtc: document.getElementById('prefs.webrtc') +}; + +function log (msg) { + function single (msg) { + let node = document.importNode(elements.template.content, true); + let parts = /(.*)\[(err|warn|notice)\] (.*)/.exec(msg); + if (parts) { + node.querySelector('span:nth-child(1)').textContent = parts[1]; + node.querySelector('span:nth-child(2)').textContent = parts[2]; + node.querySelector('span:nth-child(3)').textContent = parts[3]; + } + else { + node = document.createElement('span'); + node.classList.add('log'); + node.textContent = msg; + } + + elements.log.appendChild(node); + elements.log.scrollTop = elements.log.scrollHeight; + } + msg.split('\n').filter(m => m.trim()).forEach(single); +} + +function status (s) { + document.body.dataset.status = s; + document.querySelector('[data-cmd="connection"]').src = + s === 'disconnected' ? 'off.png' : 'on.png'; +} + +window.addEventListener('load', () => { + chrome.runtime.getBackgroundPage(b => { + log(b.tor.info.stdout); + status(b.tor.info.status); + }); +}); + +chrome.runtime.onMessage.addListener(request => { + if (request.cmd === 'event' && request.id === 'stdout') { + log(request.data); + } + else if (request.cmd === 'event' && request.id === 'status') { + status(request.data); + } +}); + +document.addEventListener('click', e => { + let cmd = e.target.dataset.rcmd; + if (cmd) { + chrome.runtime.sendMessage({ + method: 'popup-command', + cmd + }); + } + cmd = e.target.dataset.cmd; + if (cmd === 'verify') { + chrome.tabs.create({ + url: 'https://check.torproject.org/' + }); + } + else if (cmd === 'connection') { + chrome.runtime.sendMessage({ + method: 'popup-action', + cmd, + action: document.body.dataset.status === 'disconnected' ? 'connect' : 'disconnect' + }); + } +}); diff --git a/data/popup/off.png b/data/popup/off.png new file mode 100644 index 0000000..66019bb Binary files /dev/null and b/data/popup/off.png differ diff --git a/data/popup/on.png b/data/popup/on.png new file mode 100644 index 0000000..79d342a Binary files /dev/null and b/data/popup/on.png differ diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..38b5614 --- /dev/null +++ b/manifest.json @@ -0,0 +1,47 @@ +{ + "name": "Tor Browser", + "short_name": "itbrowser", + "description": "Enables Tor network and modify few settings to protect user privacy", + "author": "Jeremy Schomery", + "version": "0.1.0", + "manifest_version": 2, + "permissions": [ + "storage", + "tabs", + "proxy", + "privacy", + "webRequest", + "downloads", + "management", + "notifications", + "nativeMessaging", + "https://api.github.com/repos/andy-portmen/native-client/releases/latest" + ], + "background": { + "scripts": [ + "EventEmitter.js", + "tor.js", + "proxy.js", + "privacy.js", + "ui.js", + "common.js" + ] + }, + "browser_action": { + "default_icon": { + "16": "data/icons/16.png", + "32": "data/icons/32.png" + }, + "default_popup": "data/popup/index.html" + }, + "homepage_url": "http://add0n.com/media-tools.html", + "icons": { + "16": "data/icons/16.png", + "48": "data/icons/48.png", + "128": "data/icons/128.png" + }, + "options_ui": { + "page": "data/options/index.html", + "chrome_style": true + } +} diff --git a/privacy.js b/privacy.js new file mode 100644 index 0000000..00f0602 --- /dev/null +++ b/privacy.js @@ -0,0 +1,38 @@ +'use strict'; + +var privacy = { + onchanges: [], + modes: { + 0: 'default_public_and_private_interfaces', + 1: 'default_public_interface_only', + 2: 'disable_non_proxied_udp', + }, + current: { + value: 'default' + }, + set: (mode = 2, callback = function () {}) => { + chrome.privacy.network.webRTCIPHandlingPolicy.get({}, o => { + privacy.current = { + value: o.value + }; + + chrome.privacy.network.webRTCIPHandlingPolicy.set({ + value: privacy.modes[mode] + }, () => { + privacy.onchanges.forEach(c => c('webrtc', true)); + callback(); + }); + }); + }, + reset: (callback = function () {}) => { + chrome.privacy.network.webRTCIPHandlingPolicy.set(privacy.current, () => { + privacy.onchanges.forEach(c => c('webrtc', false)); + callback(); + }); + }, + addListener: (method, callback) => { + if (method === 'change') { + privacy.onchanges.push(callback); + } + } +}; diff --git a/proxy.js b/proxy.js new file mode 100644 index 0000000..28a2d29 --- /dev/null +++ b/proxy.js @@ -0,0 +1,49 @@ +'use strict'; + +var proxy = { + onchanges: [], + current: { + value: { + mode: 'system' + } + }, + set: (info, callback = function () {}) => { + let rule = { + host: info['socks-host'], + port: info['socks-port'], + scheme: 'socks5' + }; + chrome.proxy.settings.get({}, o => { + proxy.current = { + value: o.value + }; + console.log(proxy.current); + + chrome.proxy.settings.set({ + value: { + mode: 'fixed_servers', + rules: { + proxyForHttp: rule, + proxyForHttps: rule, + proxyForFtp: rule, + fallbackProxy: rule + } + } + }, () => { + proxy.onchanges.forEach(c => c(true)); + callback(); + }); + }); + }, + reset: (callback = function () {}) => { + chrome.proxy.settings.set(proxy.current, () => { + proxy.onchanges.forEach(c => c(false)); + callback(); + }); + }, + addListener: (method, callback) => { + if (method === 'change') { + proxy.onchanges.push(callback); + } + } +}; diff --git a/tor.js b/tor.js new file mode 100644 index 0000000..01a3de5 --- /dev/null +++ b/tor.js @@ -0,0 +1,195 @@ +/* globals EventEmitter */ +'use strict'; + +var Native = function () { + this.callback = null; + this.channel = chrome.runtime.connectNative('com.add0n.node'); + + function onDisconnect () { + chrome.tabs.create({ + url: '/data/helper/index.html' + }); + } + + this.channel.onDisconnect.addListener(onDisconnect); + this.channel.onMessage.addListener(res => { + if (res && res.stdout && res.stdout.type === 'Buffer') { + res.stdout = { + data: String.fromCharCode.apply(String, res.stdout.data), + type: 'String' + }; + } + + if (!res) { + chrome.tabs.create({ + url: '/data/helper/index.html' + }); + } + else if (this.callback) { + this.callback(res); + } + }); +}; +Native.prototype.exec = function (command, args, callback = function () {}) { + this.callback = function (res) { + callback(res); + }; + this.channel.postMessage({ + cmd: 'exec', + command, + arguments: args + }); +}; + +var Tor = function (options) { + this.callback = this.response; + EventEmitter.call(this); + this.directory = options.directory; + this.callbacks = []; + + this.info = { + status: 'disconnected', + password: options.password || 'tor-browser', + stdout: 'Press the switch button to get started', + stderr: '', + progress: 0, + ip: '0.0.0.0', + 'socks-host': 'localhost', + 'socks-port': 22050, + 'control-port': 22051 + }; + + this.on('stdout', m => { + this.info.stdout += m; + }); + this.on('status', s => { + this.info.status = s; + }); + this.on('progress', o => { + this.info.progress = o.value; + if (o.value === 100) { + this.emit('status', 'connected'); + } + }); +}; +Tor.prototype = Object.create(Native.prototype); +Tor.prototype = Object.create(EventEmitter.prototype); + +Tor.prototype.response = function (res) { + this.callbacks.forEach(c => c(res)); + if (res.code) { + this.emit('status', 'disconnected'); + + if (res.code !== 0 && (res.code !== 1 || res.stderr !== '')) { + window.alert(`Something went wrong! + +----- +Code: ${res.code} +Output: ${res.stdout} +Error: ${res.stderr}` + ); + } + } + + if (res.stdout) { + this.emit('stdout', res.stdout.data); + } + if (res.stderr) { + this.emit('stderr', res.stdout.data); + } + + if (res.stdout) { + res.stdout.data.split('\n').forEach(data => { + let err = /\[(err|warn|notice)\] (.*)/.exec(data); + if (err) { + this.emit('console', { + type: err[1], + msg: err[2] + }); + } + + let progress = /Bootstrapped (\d+)%\: (.*)/.exec(data); + if (progress) { + this.emit('progress', { + value: +progress[1], + msg: progress[2] + }); + } + }); + } +}; + +Tor.prototype.connect = function () { + this.emit('status', 'connecting'); + this.channel.postMessage({ + cmd: 'spawn', + command: [this.directory, 'tor'], + arguments: ['-f', 'torrc'], + properties: { + detached: false, + cwd: this.directory + }, + kill: true + }); +}; + +Tor.prototype.refresh = function () { + Native.call(this); + this.callback = this.response; + this.stdout = 'Press the switch button to get started'; + this.stderr = ''; + this.connect(); +}; + +Tor.prototype.command = function (command, callback = function () {}) { + let commands = [ + `AUTHENTICATE "${this.info.password}"\r\n`, // Chapter 3.5 + command + '\r\n', + 'QUIT\r\n' + ]; + this.emit('stdout', 'Command: ' + command + '\n\r'); + + chrome.runtime.sendNativeMessage('com.add0n.node', { + cmd: 'net', + commands, + host: this.info['control-host'], + port: this.info['control-port'], + password: '' + }, res => { + callback(res); + this.emit('stdout', res.replace(/250[ \-\+]/g, '').replace(/\n\r?/g, '↵')); + }); +}; + +Tor.prototype.disconnect = function () { + this.channel.disconnect(); + this.emit('status', 'disconnected'); + this.emit('stdout', 'Kill Tor instance\n\r'); +}; +var aaa; +Tor.prototype.getIP = function (callback = function () {}) { + this.command('GETINFO circuit-status', res => { + aaa = res; + let id = (res || '') + .split('\n') + .filter(s => /^\d+\sBUILT/.test(s)).map(s => /\$([^\s\$\~]+)[\s\~][^\$]*$/.exec(s)) + .filter(a => a) + .map(a => a[1]).shift(); + if (id) { + this.command('GETINFO ns/id/' + id, res => { + let ip = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/.exec(res || ''); + if (ip) { + callback(ip[0]); + this.info.ip = ip[0]; + this.emit('ip', ip[0]); + } + else { + callback(); + } + }); + } + else { + callback(); + } + }); +}; diff --git a/ui.js b/ui.js new file mode 100644 index 0000000..37d81c2 --- /dev/null +++ b/ui.js @@ -0,0 +1,43 @@ +/* globals EventEmitter */ +'use strict'; + +var ui = new EventEmitter(); +ui.cache = { + status: 'disconnected', + ip: '0.0.0.0', + country: '-', + proxy: 'default' +}; + +ui.on('title', obj => { + ui.cache = Object.assign(ui.cache, obj); + + chrome.browserAction.setTitle({ + title: `Tor Browser (${ui.cache.status}) + +IP: ${ui.cache.ip} +Country: ${ui.cache.country} +Proxy: ${ui.cache.proxy}` + }); +}); +ui.on('title', obj => { + if (obj.status) { + let active = obj.status === 'connected'; + chrome.browserAction.setIcon({ + path: { + 16: '/data/icons/' + (active ? 'enabled/' : '') + '16.png', + 32: '/data/icons/' + (active ? 'enabled/' : '') + '32.png', + 64: '/data/icons/' + (active ? 'enabled/' : '') + '64.png', + } + }); + } +}); + +ui.notification = function (message) { + chrome.notifications.create({ + type: 'basic', + title: 'Tor Protector', + message, + iconUrl: '/data/icons/48.png' + }); +};