commit 41267cf007565d3fae1b0a5e2244dde55414dfae Author: Chang Lan changlan9@gmail.com Date: Thu Mar 27 17:59:50 2014 -0700
Add a Chrome packaged app and extension.
We use Chrome `webRequest` API to modify the `Host` header. However, this API is for extensions only. Chrome extensions cannot listen to sockets, which is supposed to be done by packaged apps. Therefore, the communication channel is a little bit hacky:
meek-client --(Socket)-- app --(Message)-- extension --(HTTPS)-- www.google.com
Caveat: the `webRequest` does not handle SPDY connection properly. We need to disable SPDY in Chrome. --- chrome/app/background.js | 124 ++++++++++++++++++++++++++++++++++++++++ chrome/app/manifest.json | 26 +++++++++ chrome/extension/background.js | 71 +++++++++++++++++++++++ chrome/extension/manifest.json | 19 ++++++ 4 files changed, 240 insertions(+)
diff --git a/chrome/app/background.js b/chrome/app/background.js new file mode 100644 index 0000000..818ba9e --- /dev/null +++ b/chrome/app/background.js @@ -0,0 +1,124 @@ +// attempt to keep app from going inactive + +chrome.alarms.create("ping", {when: 5000, periodInMinutes: 1 }); +chrome.alarms.onAlarm.addListener(function(alarm) { console.info("alarm name = " + alarm.name); }); + +const IP = "127.0.0.1"; +const PORT = 7000; +const EXTENSION_ID = "epmfkpbifhkdhcedgfppmeeoonjenkee"; + +const STATE_READING_LENGTH = 1; +const STATE_READING_OBJECT = 2; +const STATE_DONE = 3; + +var serverSocketId; +var state = STATE_READING_LENGTH; +var buf = new Uint8Array(4); +var bytesToRead = buf.length; + +chrome.sockets.tcpServer.create({}, function(createInfo) { + listenAndAccept(createInfo.socketId); +}); + +function listenAndAccept(socketId) { + chrome.sockets.tcpServer.listen(socketId, + IP, PORT, function(resultCode) { + onListenCallback(socketId, resultCode) + }); +} + +function onListenCallback(socketId, resultCode) { + if (resultCode < 0) { + console.log("Error listening:" + + chrome.runtime.lastError.message); + return; + } + serverSocketId = socketId; + chrome.sockets.tcpServer.onAccept.addListener(onAccept); +} + +function onAccept(info) { + if (info.socketId != serverSocketId) + return; + console.log("Client connected."); + chrome.sockets.tcp.onReceive.addListener(onReceive); + chrome.sockets.tcp.setPaused(info.clientSocketId, false); +} + +function readIntoBuf(data) { + var n = Math.min(data.byteLength, bytesToRead); + buf.subarray(buf.length - bytesToRead, n).set(new Uint8Array(data.slice(0, n))); + bytesToRead -= n; + return data.slice(n); +} + +function onReceive(info) { + console.log("Data received."); + var data = info.data; + switch (state) { + case STATE_READING_LENGTH: + data = readIntoBuf(data); + if (bytesToRead > 0) + return; + + var b = buf; + bytesToRead = (b[0] << 24) | (b[1] << 16) | (b[2] << 8) | b[3]; + console.log(bytesToRead); + buf = new Uint8Array(bytesToRead); + state = STATE_READING_OBJECT; + + case STATE_READING_OBJECT: + data = readIntoBuf(data); + if (bytesToRead > 0) + return; + + var str = ab2str(buf); + console.log(str); + var request = JSON.parse(str); + makeRequest(request, info.socketId); + + state = STATE_READING_LENGTH; + buf = new Uint8Array(4); + bytesToRead = buf.length; + } +} + +function makeRequest(request, client_socket) { + chrome.runtime.sendMessage(EXTENSION_ID, request, function(response) { + returnResponse(response, client_socket); + }); +} + +function returnResponse(response, client_socket) { + var str = JSON.stringify(response); + var b = str2ab(str); + + var buf = new Uint8Array(4 + b.byteLength); + var len = b.byteLength; + buf[0] = (len >> 24) & 0xff; + buf[1] = (len >> 16) & 0xff; + buf[2] = (len >> 8) & 0xff; + buf[3] = len & 0xff; + buf.set(new Uint8Array(b), 4); + + chrome.sockets.tcp.send(client_socket, buf.buffer, function(info) { + if (info.resultCode != 0) + console.log("Send failed"); + }); +} + +function ab2str(buffer) { + var encodedString = String.fromCharCode.apply(null, buffer), + decodedString = decodeURIComponent(escape(encodedString)); + return decodedString; +} + +function str2ab(string) { + var string = unescape(encodeURIComponent(string)), + charList = string.split(''), + buf = []; + for (var i = 0; i < charList.length; i++) { + buf.push(charList[i].charCodeAt(0)); + } + return (new Uint8Array(buf)).buffer; +} diff --git a/chrome/app/manifest.json b/chrome/app/manifest.json new file mode 100644 index 0000000..6c288d2 --- /dev/null +++ b/chrome/app/manifest.json @@ -0,0 +1,26 @@ +{ + "manifest_version": 2, + "name": "meek-browser-app", + "minimum_chrome_version": "24", + "version": "0.1", + + "permissions": [ + "alarms" + ], + + "sockets": { + "tcp": { + "connect": "*" + }, + "tcpServer": { + "listen": "127.0.0.1:7000" + } + }, + + "app": { + "background": { + "scripts": ["background.js"], + "persistent": true + } + } +} diff --git a/chrome/extension/background.js b/chrome/extension/background.js new file mode 100644 index 0000000..2e48212 --- /dev/null +++ b/chrome/extension/background.js @@ -0,0 +1,71 @@ +// attempt to keep app from going inactive + +chrome.alarms.create("ping", {when: 5000, periodInMinutes: 1 }); +chrome.alarms.onAlarm.addListener(function(alarm) { console.info("alarm name = " + alarm.name); }); + +var host = 'meek-reflect.appspot.com'; + +function onBeforeSendHeadersCallback(details) { + var did_set = false; + for (var i = 0; i < details.requestHeaders.length; ++i) { + if (details.requestHeaders[i].name === 'Host') { + details.requestHeaders[i].value = host; + did_set = true; + } + } + if (!did_set) { + details.requestHeaders.push({ + name: 'Host', + value: host + }); + } + return { requestHeaders: details.requestHeaders }; +} + +chrome.runtime.onMessageExternal.addListener(function(request, header, sendResponse) { + var timeout = 2000; + var xhr = new XMLHttpRequest(); + xhr.ontimeout = function() { + console.error(url + "timed out."); + chrome.webRequest.onBeforeSendHeaders.removeListener(onBeforeSendHeadersCallback); + }; + xhr.onerror = function() { + chrome.webRequest.onBeforeSendHeaders.removeListener(onBeforeSendHeadersCallback); + var response = { error: xhr.statusText }; + sendResponse(response); + }; + xhr.onreadystatechange = function() { + if (xhr.readyState == 4) { + chrome.webRequest.onBeforeSendHeaders.removeListener(onBeforeSendHeadersCallback); + var response = {status: xhr.status, body: xhr.responseText }; + sendResponse(response); + } + }; + var requestMethod = request.method; + var url = request.url; + xhr.open(requestMethod, url); + if (request.header != undefined) { + for (var key in request.header) { + if (key != "Host") { // TODO: Add more restricted header fields + xhr.setRequestHeader(key, request.header[key]); + } else { + host = request.header[key]; + } + } + } + var body = null; + if (request.body != undefined) { + body = request.body; + } + + chrome.webRequest.onBeforeSendHeaders.addListener(onBeforeSendHeadersCallback, { + urls: [url], + types: ['xmlhttprequest'] + }, ['requestHeaders', 'blocking']); + + xhr.send(body); +}); + +function onReceiveXHR(xhr) { + console.log(xhr.responseText); +} diff --git a/chrome/extension/manifest.json b/chrome/extension/manifest.json new file mode 100644 index 0000000..0e245db --- /dev/null +++ b/chrome/extension/manifest.json @@ -0,0 +1,19 @@ +{ + "manifest_version": 2, + "name": "meek-browser-extension", + "minimum_chrome_version": "24", + "version": "0.1", + + "permissions": [ + "notifications", + "alarms", + "webRequest", + "webRequestBlocking", + "<all_urls>" + ], + + "background": { + "scripts": ["background.js"], + "persistent": true + } +}
tor-commits@lists.torproject.org