forked from mirrors/gecko-dev
- Use nsIProtocolProxyService to look up proxy - Pass the found proxy to CreateTransport Differential Revision: https://phabricator.services.mozilla.com/D104357
660 lines
16 KiB
JavaScript
660 lines
16 KiB
JavaScript
"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 = event => {
|
|
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(
|
|
details => {
|
|
// 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();
|
|
});
|