Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Warnings

"use strict";
/* globals TCPServerSocket */
const CC = Components.Constructor;
const BinaryInputStream = CC(
"@mozilla.org/binaryinputstream;1",
"nsIBinaryInputStream",
"setInputStream"
);
const currentThread =
Cc["@mozilla.org/thread-manager;1"].getService().currentThread;
// Most of the socks logic here is copied and upgraded to support authentication
// for socks5. The original test is from netwerk/test/unit/test_socks.js
// Socks 4 support was left in place for future tests.
const STATE_WAIT_GREETING = 1;
const STATE_WAIT_SOCKS4_REQUEST = 2;
const STATE_WAIT_SOCKS4_USERNAME = 3;
const STATE_WAIT_SOCKS4_HOSTNAME = 4;
const STATE_WAIT_SOCKS5_GREETING = 5;
const STATE_WAIT_SOCKS5_REQUEST = 6;
const STATE_WAIT_SOCKS5_AUTH = 7;
const STATE_WAIT_INPUT = 8;
const STATE_FINISHED = 9;
/**
* A basic socks proxy setup that handles a single http response page. This
* is used for testing socks auth with webrequest. We don't bother making
* sure we buffer ondata, etc., we'll never get anything but tiny chunks here.
*/
class SocksClient {
constructor(server, socket) {
this.server = server;
this.type = "";
this.username = "";
this.dest_name = "";
this.dest_addr = [];
this.dest_port = [];
this.inbuf = [];
this.state = STATE_WAIT_GREETING;
this.socket = socket;
socket.onclose = () => {
this.server.requestCompleted(this);
};
socket.ondata = event => {
let len = event.data.byteLength;
if (len == 0 && this.state == STATE_FINISHED) {
this.close();
this.server.requestCompleted(this);
return;
}
this.inbuf = new Uint8Array(event.data);
Promise.resolve().then(() => {
this.callState();
});
};
}
callState() {
switch (this.state) {
case STATE_WAIT_GREETING:
this.checkSocksGreeting();
break;
case STATE_WAIT_SOCKS4_REQUEST:
this.checkSocks4Request();
break;
case STATE_WAIT_SOCKS4_USERNAME:
this.checkSocks4Username();
break;
case STATE_WAIT_SOCKS4_HOSTNAME:
this.checkSocks4Hostname();
break;
case STATE_WAIT_SOCKS5_GREETING:
this.checkSocks5Greeting();
break;
case STATE_WAIT_SOCKS5_REQUEST:
this.checkSocks5Request();
break;
case STATE_WAIT_SOCKS5_AUTH:
this.checkSocks5Auth();
break;
case STATE_WAIT_INPUT:
this.checkRequest();
break;
default:
do_throw("server: read in invalid state!");
}
}
write(buf) {
this.socket.send(new Uint8Array(buf).buffer);
}
checkSocksGreeting() {
if (!this.inbuf.length) {
return;
}
if (this.inbuf[0] == 4) {
this.type = "socks4";
this.state = STATE_WAIT_SOCKS4_REQUEST;
this.checkSocks4Request();
} else if (this.inbuf[0] == 5) {
this.type = "socks";
this.state = STATE_WAIT_SOCKS5_GREETING;
this.checkSocks5Greeting();
} else {
do_throw("Unknown socks protocol!");
}
}
checkSocks4Request() {
if (this.inbuf.length < 8) {
return;
}
this.dest_port = this.inbuf.slice(2, 4);
this.dest_addr = this.inbuf.slice(4, 8);
this.inbuf = this.inbuf.slice(8);
this.state = STATE_WAIT_SOCKS4_USERNAME;
this.checkSocks4Username();
}
readString() {
let i = this.inbuf.indexOf(0);
let str = null;
if (i >= 0) {
let decoder = new TextDecoder();
str = decoder.decode(this.inbuf.slice(0, i));
this.inbuf = this.inbuf.slice(i + 1);
}
return str;
}
checkSocks4Username() {
let str = this.readString();
if (str == null) {
return;
}
this.username = str;
if (
this.dest_addr[0] == 0 &&
this.dest_addr[1] == 0 &&
this.dest_addr[2] == 0 &&
this.dest_addr[3] != 0
) {
this.state = STATE_WAIT_SOCKS4_HOSTNAME;
this.checkSocks4Hostname();
} else {
this.sendSocks4Response();
}
}
checkSocks4Hostname() {
let str = this.readString();
if (str == null) {
return;
}
this.dest_name = str;
this.sendSocks4Response();
}
sendSocks4Response() {
this.state = STATE_WAIT_INPUT;
this.inbuf = [];
this.write([0, 0x5a, 0, 0, 0, 0, 0, 0]);
}
/**
* checks authentication information.
*
* buf[0] socks version
* buf[1] number of auth methods supported
* buf[2+nmethods] value for each auth method
*
* Response is
* byte[0] socks version
* byte[1] desired auth method
*
* For whatever reason, Firefox does not present auth method 0x02 however
* responding with that does cause Firefox to send authentication if
* the nsIProxyInfo instance has the data. IUUC Firefox should send
* supported methods, but I'm no socks expert.
*/
checkSocks5Greeting() {
if (this.inbuf.length < 2) {
return;
}
let nmethods = this.inbuf[1];
if (this.inbuf.length < 2 + nmethods) {
return;
}
// See comment above, keeping for future update.
// let methods = this.inbuf.slice(2, 2 + nmethods);
this.inbuf = [];
if (this.server.password || this.server.username) {
this.state = STATE_WAIT_SOCKS5_AUTH;
this.write([5, 2]);
} else {
this.state = STATE_WAIT_SOCKS5_REQUEST;
this.write([5, 0]);
}
}
checkSocks5Auth() {
equal(this.inbuf[0], 0x01, "subnegotiation version");
let uname_len = this.inbuf[1];
let pass_len = this.inbuf[2 + uname_len];
let unnamebuf = this.inbuf.slice(2, 2 + uname_len);
let pass_start = 2 + uname_len + 1;
let pwordbuf = this.inbuf.slice(pass_start, pass_start + pass_len);
let decoder = new TextDecoder();
let username = decoder.decode(unnamebuf);
let password = decoder.decode(pwordbuf);
this.inbuf = [];
equal(username, this.server.username, "socks auth username");
equal(password, this.server.password, "socks auth password");
if (username == this.server.username && password == this.server.password) {
this.state = STATE_WAIT_SOCKS5_REQUEST;
// x00 is success, any other value closes the connection
this.write([1, 0]);
return;
}
this.state = STATE_FINISHED;
this.write([1, 1]);
}
checkSocks5Request() {
if (this.inbuf.length < 4) {
return;
}
let atype = this.inbuf[3];
let len;
let name = false;
switch (atype) {
case 0x01:
len = 4;
break;
case 0x03:
len = this.inbuf[4];
name = true;
break;
case 0x04:
len = 16;
break;
default:
do_throw("Unknown address type " + atype);
}
if (name) {
if (this.inbuf.length < 4 + len + 1 + 2) {
return;
}
let buf = this.inbuf.slice(5, 5 + len);
let decoder = new TextDecoder();
this.dest_name = decoder.decode(buf);
len += 1;
} else {
if (this.inbuf.length < 4 + len + 2) {
return;
}
this.dest_addr = this.inbuf.slice(4, 4 + len);
}
len += 4;
this.dest_port = this.inbuf.slice(len, len + 2);
this.inbuf = this.inbuf.slice(len + 2);
this.sendSocks5Response();
}
sendSocks5Response() {
let buf;
if (this.dest_addr.length == 16) {
// send a successful response with the address, [::1]:80
buf = [5, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 80];
} else {
// send a successful response with the address, 127.0.0.1:80
buf = [5, 0, 0, 1, 127, 0, 0, 1, 0, 80];
}
this.state = STATE_WAIT_INPUT;
this.inbuf = [];
this.write(buf);
}
checkRequest() {
let decoder = new TextDecoder();
let request = decoder.decode(this.inbuf);
if (request == "PING!") {
this.state = STATE_FINISHED;
this.socket.send("PONG!");
} else if (request.startsWith("GET / HTTP/1.1")) {
this.socket.send(
"HTTP/1.1 200 OK\r\n" +
"Content-Length: 2\r\n" +
"Content-Type: text/html\r\n" +
"\r\nOK"
);
this.state = STATE_FINISHED;
}
}
close() {
this.socket.close();
}
}
class SocksTestServer {
constructor() {
this.client_connections = new Set();
this.listener = new TCPServerSocket(-1, { binaryType: "arraybuffer" }, -1);
this.listener.onconnect = event => {
let client = new SocksClient(this, event.socket);
this.client_connections.add(client);
};
}
requestCompleted(client) {
this.client_connections.delete(client);
}
close() {
for (let client of this.client_connections) {
client.close();
}
this.client_connections = new Set();
if (this.listener) {
this.listener.close();
this.listener = null;
}
}
setUserPass(username, password) {
this.username = username;
this.password = password;
}
}
/**
* Tests the basic socks logic using a simple socket connection and the
* protocol proxy service. Before 902346, TCPSocket has no way to tie proxy
* data to it, so we go old school here.
*/
class SocksTestClient {
constructor(socks, dest, resolve, reject) {
let pps = Cc["@mozilla.org/network/protocol-proxy-service;1"].getService(
Ci.nsIProtocolProxyService
);
let sts = Cc["@mozilla.org/network/socket-transport-service;1"].getService(
Ci.nsISocketTransportService
);
let pi_flags = 0;
if (socks.dns == "remote") {
pi_flags = Ci.nsIProxyInfo.TRANSPARENT_PROXY_RESOLVES_HOST;
}
let pi = pps.newProxyInfoWithAuth(
socks.version,
socks.host,
socks.port,
socks.username,
socks.password,
"",
"",
pi_flags,
-1,
null
);
this.trans = sts.createTransport([], dest.host, dest.port, pi, null);
this.input = this.trans.openInputStream(
Ci.nsITransport.OPEN_BLOCKING,
0,
0
);
this.output = this.trans.openOutputStream(
Ci.nsITransport.OPEN_BLOCKING,
0,
0
);
this.outbuf = String();
this.resolve = resolve;
this.reject = reject;
this.write("PING!");
this.input.asyncWait(this, 0, 0, currentThread);
}
onInputStreamReady(stream) {
let len = 0;
try {
len = stream.available();
} catch (e) {
// This will happen on auth failure.
this.reject(e);
return;
}
let bin = new BinaryInputStream(stream);
let data = bin.readByteArray(len);
let decoder = new TextDecoder();
let result = decoder.decode(data);
if (result == "PONG!") {
this.resolve(result);
} else {
this.reject();
}
}
write(buf) {
this.outbuf += buf;
this.output.asyncWait(this, 0, 0, currentThread);
}
onOutputStreamReady(stream) {
let len = stream.write(this.outbuf, this.outbuf.length);
if (len != this.outbuf.length) {
this.outbuf = this.outbuf.substring(len);
stream.asyncWait(this, 0, 0, currentThread);
} else {
this.outbuf = String();
}
}
close() {
this.output.close();
}
}
const socksServer = new SocksTestServer();
socksServer.setUserPass("foo", "bar");
registerCleanupFunction(() => {
socksServer.close();
});
// A simple ping/pong to test the socks server.
add_task(async function test_socks_server() {
let socks = {
version: "socks",
host: "127.0.0.1",
port: socksServer.listener.localPort,
username: "foo",
password: "bar",
dns: false,
};
let dest = {
host: "localhost",
port: 8888,
};
new Promise((resolve, reject) => {
new SocksTestClient(socks, dest, resolve, reject);
})
.then(result => {
equal("PONG!", result, "socks test ok");
})
.catch(result => {
ok(false, `socks test failed ${result}`);
});
});
// Register a proxy to be used by TCPSocket connections later.
function registerProxy(socks) {
let pps = Cc["@mozilla.org/network/protocol-proxy-service;1"].getService(
Ci.nsIProtocolProxyService
);
let filter = {
QueryInterface: ChromeUtils.generateQI(["nsIProtocolProxyFilter"]),
applyFilter(uri, proxyInfo, callback) {
callback.onProxyFilterResult(
pps.newProxyInfoWithAuth(
socks.version,
socks.host,
socks.port,
socks.username,
socks.password,
"",
"",
socks.dns == "remote"
? Ci.nsIProxyInfo.TRANSPARENT_PROXY_RESOLVES_HOST
: 0,
-1,
null
)
);
},
};
pps.registerFilter(filter, 0);
registerCleanupFunction(() => {
pps.unregisterFilter(filter);
});
}
// A simple ping/pong to test the socks server with TCPSocket.
add_task(async function test_tcpsocket_proxy() {
let socks = {
version: "socks",
host: "127.0.0.1",
port: socksServer.listener.localPort,
username: "foo",
password: "bar",
dns: false,
};
let dest = {
host: "localhost",
port: 8888,
};
registerProxy(socks);
await new Promise((resolve, reject) => {
let client = new TCPSocket(dest.host, dest.port);
client.onopen = () => {
client.send("PING!");
};
client.ondata = e => {
equal("PONG!", e.data, "socks test ok");
resolve();
};
client.onerror = () => reject();
});
});
add_task(async function test_webRequest_socks_proxy() {
async function background(port) {
function checkProxyData(details) {
browser.test.assertEq("127.0.0.1", details.proxyInfo.host, "proxy host");
browser.test.assertEq(port, details.proxyInfo.port, "proxy port");
browser.test.assertEq("socks", details.proxyInfo.type, "proxy type");
browser.test.assertEq(
"foo",
details.proxyInfo.username,
"proxy username not set"
);
browser.test.assertEq(
undefined,
details.proxyInfo.password,
"no proxy password passed to webrequest"
);
}
browser.webRequest.onBeforeRequest.addListener(
details => {
checkProxyData(details);
},
{ urls: ["<all_urls>"] }
);
browser.webRequest.onAuthRequired.addListener(
() => {
// We should never get onAuthRequired for socks proxy
browser.test.fail("onAuthRequired");
},
{ urls: ["<all_urls>"] },
["blocking"]
);
browser.webRequest.onCompleted.addListener(
details => {
checkProxyData(details);
browser.test.sendMessage("done");
},
{ urls: ["<all_urls>"] }
);
browser.proxy.onRequest.addListener(
() => {
return [
{
type: "socks",
host: "127.0.0.1",
port,
username: "foo",
password: "bar",
},
];
},
{ urls: ["<all_urls>"] }
);
}
let handlingExt = ExtensionTestUtils.loadExtension({
manifest: {
permissions: ["proxy", "webRequest", "webRequestBlocking", "<all_urls>"],
},
background: `(${background})(${socksServer.listener.localPort})`,
});
// proxy.register is deprecated - bug 1443259.
ExtensionTestUtils.failOnSchemaWarnings(false);
await handlingExt.startup();
ExtensionTestUtils.failOnSchemaWarnings(true);
let contentPage =
await ExtensionTestUtils.loadContentPage(`http://localhost/`);
await handlingExt.awaitMessage("done");
await contentPage.close();
await handlingExt.unload();
});
add_task(async function test_onRequest_tcpsocket_proxy() {
async function background(port) {
browser.proxy.onRequest.addListener(
() => {
return [
{
type: "socks",
host: "127.0.0.1",
port,
username: "foo",
password: "bar",
},
];
},
{ urls: ["<all_urls>"] }
);
}
let handlingExt = ExtensionTestUtils.loadExtension({
manifest: {
permissions: ["proxy", "webRequest", "webRequestBlocking", "<all_urls>"],
},
background: `(${background})(${socksServer.listener.localPort})`,
});
await handlingExt.startup();
await new Promise((resolve, reject) => {
let client = new TCPSocket("localhost", 8888);
client.onopen = () => {
client.send("PING!");
};
client.ondata = e => {
equal("PONG!", e.data, "socks test ok");
resolve();
};
client.onerror = () => reject();
});
await handlingExt.unload();
});