gecko-dev/netwerk/test/unit/test_proxyconnect.js

361 lines
9.6 KiB
JavaScript

/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
// test_connectonly tests happy path of proxy connect
// 1. CONNECT to localhost:socketserver_port
// 2. Write 200 Connection established
// 3. Write data to the tunnel (and read server-side)
// 4. Read data from the tunnel (and write server-side)
// 5. done
// test_connectonly_noproxy tests an http channel with only connect set but
// no proxy configured.
// 1. OnTransportAvailable callback NOT called (checked in step 2)
// 2. StopRequest callback called
// 3. done
// test_connectonly_nonhttp tests an http channel with only connect set with a
// non-http proxy.
// 1. OnTransportAvailable callback NOT called (checked in step 2)
// 2. StopRequest callback called
// 3. done
// -1 then initialized with an actual port from the serversocket
"use strict";
var socketserver_port = -1;
const CC = Components.Constructor;
const ServerSocket = CC(
"@mozilla.org/network/server-socket;1",
"nsIServerSocket",
"init"
);
const BinaryInputStream = CC(
"@mozilla.org/binaryinputstream;1",
"nsIBinaryInputStream",
"setInputStream"
);
const BinaryOutputStream = CC(
"@mozilla.org/binaryoutputstream;1",
"nsIBinaryOutputStream",
"setOutputStream"
);
const STATE_NONE = 0;
const STATE_READ_CONNECT_REQUEST = 1;
const STATE_WRITE_CONNECTION_ESTABLISHED = 2;
const STATE_CHECK_WRITE = 3; // write to the tunnel
const STATE_CHECK_WRITE_READ = 4; // wrote to the tunnel, check connection data
const STATE_CHECK_READ = 5; // read from the tunnel
const STATE_CHECK_READ_WROTE = 6; // wrote to connection, check tunnel data
const STATE_COMPLETED = 100;
const CONNECT_RESPONSE_STRING = "HTTP/1.1 200 Connection established\r\n\r\n";
const CHECK_WRITE_STRING = "hello";
const CHECK_READ_STRING = "world";
const ALPN = "webrtc";
var connectRequest = "";
var checkWriteData = "";
var checkReadData = "";
var threadManager;
var socket;
var streamIn;
var streamOut;
var accepted = false;
var acceptedSocket;
var state = STATE_NONE;
var transportAvailable = false;
var proxiedChannel;
var listener = {
expectedCode: -1, // uninitialized
onStartRequest: function test_onStartR() {},
onDataAvailable: function test_ODA() {
do_throw("Should not get any data!");
},
onStopRequest: function test_onStopR(request, status) {
if (state === STATE_COMPLETED) {
Assert.equal(transportAvailable, false, "transport available not called");
Assert.equal(status, 0x80004005, "error code matches");
Assert.equal(proxiedChannel.httpProxyConnectResponseCode, 200);
nextTest();
return;
}
Assert.equal(accepted, true, "socket accepted");
accepted = false;
},
};
var upgradeListener = {
onTransportAvailable: (transport, socketIn, socketOut) => {
if (!transport || !socketIn || !socketOut) {
do_throw("on transport available failed");
}
if (state !== STATE_CHECK_WRITE) {
do_throw("bad state");
}
transportAvailable = true;
socketIn.asyncWait(connectHandler, 0, 0, threadManager.mainThread);
socketOut.asyncWait(connectHandler, 0, 0, threadManager.mainThread);
},
QueryInterface: ChromeUtils.generateQI(["nsIHttpUpgradeListener"]),
};
var connectHandler = {
onInputStreamReady: input => {
try {
const bis = new BinaryInputStream(input);
var data = bis.readByteArray(input.available());
dataAvailable(data);
if (state !== STATE_COMPLETED) {
input.asyncWait(connectHandler, 0, 0, threadManager.mainThread);
}
} catch (e) {
do_throw(e);
}
},
onOutputStreamReady: output => {
writeData(output);
},
QueryInterface: iid => {
if (
iid.equals(Ci.nsISupports) ||
iid.equals(Ci.nsIInputStreamCallback) ||
iid.equals(Ci.nsIOutputStreamCallback)
) {
return this;
}
throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
},
};
function dataAvailable(data) {
switch (state) {
case STATE_READ_CONNECT_REQUEST:
connectRequest += String.fromCharCode.apply(String, data);
const headerEnding = connectRequest.indexOf("\r\n\r\n");
const alpnHeaderIndex = connectRequest.indexOf(`ALPN: ${ALPN}`);
if (headerEnding != -1) {
const requestLine = `CONNECT localhost:${socketserver_port} HTTP/1.1`;
Assert.equal(connectRequest.indexOf(requestLine), 0, "connect request");
Assert.equal(headerEnding, connectRequest.length - 4, "req head only");
Assert.notEqual(alpnHeaderIndex, -1, "alpn header found");
state = STATE_WRITE_CONNECTION_ESTABLISHED;
streamOut.asyncWait(connectHandler, 0, 0, threadManager.mainThread);
}
break;
case STATE_CHECK_WRITE_READ:
checkWriteData += String.fromCharCode.apply(String, data);
if (checkWriteData.length >= CHECK_WRITE_STRING.length) {
Assert.equal(checkWriteData, CHECK_WRITE_STRING, "correct write data");
state = STATE_CHECK_READ;
streamOut.asyncWait(connectHandler, 0, 0, threadManager.mainThread);
}
break;
case STATE_CHECK_READ_WROTE:
checkReadData += String.fromCharCode.apply(String, data);
if (checkReadData.length >= CHECK_READ_STRING.length) {
Assert.equal(checkReadData, CHECK_READ_STRING, "correct read data");
state = STATE_COMPLETED;
streamIn.asyncWait(null, 0, 0, null);
acceptedSocket.close(0);
nextTest();
}
break;
default:
do_throw("bad state: " + state);
}
}
function writeData(output) {
let bos = new BinaryOutputStream(output);
switch (state) {
case STATE_WRITE_CONNECTION_ESTABLISHED:
bos.write(CONNECT_RESPONSE_STRING, CONNECT_RESPONSE_STRING.length);
state = STATE_CHECK_WRITE;
break;
case STATE_CHECK_READ:
bos.write(CHECK_READ_STRING, CHECK_READ_STRING.length);
state = STATE_CHECK_READ_WROTE;
break;
case STATE_CHECK_WRITE:
bos.write(CHECK_WRITE_STRING, CHECK_WRITE_STRING.length);
state = STATE_CHECK_WRITE_READ;
break;
default:
do_throw("bad state: " + state);
}
}
function makeChan(url) {
if (!url) {
url = "https://localhost:" + socketserver_port + "/";
}
var flags =
Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL |
Ci.nsILoadInfo.SEC_DONT_FOLLOW_REDIRECTS |
Ci.nsILoadInfo.SEC_COOKIES_OMIT;
var chan = NetUtil.newChannel({
uri: url,
loadUsingSystemPrincipal: true,
securityFlags: flags,
});
chan = chan.QueryInterface(Ci.nsIHttpChannel);
var internal = chan.QueryInterface(Ci.nsIHttpChannelInternal);
internal.HTTPUpgrade(ALPN, upgradeListener);
internal.setConnectOnly(false);
return chan;
}
function socketAccepted(socket1, transport) {
accepted = true;
// copied from httpd.js
const SEGMENT_SIZE = 8192;
const SEGMENT_COUNT = 1024;
switch (state) {
case STATE_NONE:
state = STATE_READ_CONNECT_REQUEST;
break;
default:
return;
}
acceptedSocket = transport;
try {
streamIn = transport
.openInputStream(0, SEGMENT_SIZE, SEGMENT_COUNT)
.QueryInterface(Ci.nsIAsyncInputStream);
streamOut = transport
.openOutputStream(0, 0, 0)
.QueryInterface(Ci.nsIAsyncOutputStream);
streamIn.asyncWait(connectHandler, 0, 0, threadManager.mainThread);
} catch (e) {
transport.close(Cr.NS_BINDING_ABORTED);
do_throw(e);
}
}
function stopListening() {
if (tests && tests.length !== 0 && do_throw) {
do_throw("should never stop");
}
}
function createProxy() {
try {
threadManager = Cc["@mozilla.org/thread-manager;1"].getService();
socket = new ServerSocket(-1, true, 1);
socketserver_port = socket.port;
socket.asyncListen({
onSocketAccepted: socketAccepted,
onStopListening: stopListening,
});
} catch (e) {
do_throw(e);
}
}
function test_connectonly() {
Services.prefs.setCharPref("network.proxy.ssl", "localhost");
Services.prefs.setIntPref("network.proxy.ssl_port", socketserver_port);
Services.prefs.setBoolPref("network.proxy.allow_hijacking_localhost", true);
Services.prefs.setIntPref("network.proxy.type", 1);
var chan = makeChan();
proxiedChannel = chan.QueryInterface(Ci.nsIProxiedChannel);
chan.asyncOpen(listener);
do_test_pending();
}
function test_connectonly_noproxy() {
clearPrefs();
var chan = makeChan();
chan.asyncOpen(listener);
do_test_pending();
}
function test_connectonly_nonhttp() {
clearPrefs();
Services.prefs.setCharPref("network.proxy.socks", "localhost");
Services.prefs.setIntPref("network.proxy.socks_port", socketserver_port);
Services.prefs.setBoolPref("network.proxy.allow_hijacking_localhost", true);
Services.prefs.setIntPref("network.proxy.type", 1);
var chan = makeChan();
chan.asyncOpen(listener);
do_test_pending();
}
function nextTest() {
transportAvailable = false;
if (!tests.length) {
do_test_finished();
return;
}
tests.shift()();
do_test_finished();
}
var tests = [
test_connectonly,
test_connectonly_noproxy,
test_connectonly_nonhttp,
];
function clearPrefs() {
Services.prefs.clearUserPref("network.proxy.ssl");
Services.prefs.clearUserPref("network.proxy.ssl_port");
Services.prefs.clearUserPref("network.proxy.socks");
Services.prefs.clearUserPref("network.proxy.socks_port");
Services.prefs.clearUserPref("network.proxy.allow_hijacking_localhost");
Services.prefs.clearUserPref("network.proxy.type");
}
function run_test() {
createProxy();
registerCleanupFunction(clearPrefs);
nextTest();
do_test_pending();
}