summaryrefslogtreecommitdiffstats
path: root/comm/mailnews/test/resources/NetworkTestUtils.jsm
blob: 131eb9c9eb0251f5bd47d3cf1a0c132acc5ed26b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
/* 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/. */

/**
 * This file provides utilities useful in testing more advanced networking
 * scenarios, such as proxies and SSL connections.
 */

const EXPORTED_SYMBOLS = ["NetworkTestUtils"];

var CC = Components.Constructor;

const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm");

const ServerSocket = CC(
  "@mozilla.org/network/server-socket;1",
  "nsIServerSocket",
  "init"
);
const BinaryInputStream = CC(
  "@mozilla.org/binaryinputstream;1",
  "nsIBinaryInputStream",
  "setInputStream"
);

// The following code is adapted from network/test/unit/test_socks.js, in order
// to provide a SOCKS proxy server for our testing code.
//
// For more details on how SOCKSv5 works, please read RFC 1928.
var currentThread = Services.tm.currentThread;

const STATE_WAIT_GREETING = 1;
const STATE_WAIT_SOCKS5_REQUEST = 2;

/**
 * A client of a SOCKS connection.
 *
 * This doesn't implement all of SOCKSv5, just enough to get a simple proxy
 * working for the test code.
 *
 * @param {nsIInputStream} client_in - The nsIInputStream of the socket.
 * @param {nsIOutputStream} client_out - The nsIOutputStream of the socket.
 */
function SocksClient(client_in, client_out) {
  this.client_in = client_in;
  this.client_out = client_out;
  this.inbuf = [];
  this.state = STATE_WAIT_GREETING;
  this.waitRead(this.client_in);
}
SocksClient.prototype = {
  // ... implement nsIInputStreamCallback ...
  QueryInterface: ChromeUtils.generateQI(["nsIInputStreamCallback"]),
  onInputStreamReady(input) {
    var len = input.available();
    var bin = new BinaryInputStream(input);
    var data = bin.readByteArray(len);
    this.inbuf = this.inbuf.concat(data);

    switch (this.state) {
      case STATE_WAIT_GREETING:
        this.handleGreeting();
        break;
      case STATE_WAIT_SOCKS5_REQUEST:
        this.handleSocks5Request();
        break;
    }

    if (!this.sub_transport) {
      this.waitRead(input);
    }
  },

  // Listen on the input for the next packet
  waitRead(input) {
    input.asyncWait(this, 0, 0, currentThread);
  },

  // Simple handler to write out a binary string (because xpidl sucks here)
  write(buf) {
    this.client_out.write(buf, buf.length);
  },

  // Handle the first SOCKSv5 client message
  handleGreeting() {
    if (this.inbuf.length == 0) {
      return;
    }

    if (this.inbuf[0] != 5) {
      dump("Unknown protocol version: " + this.inbuf[0] + "\n");
      this.close();
      return;
    }

    // Some quality checks to make sure we've read the entire greeting.
    if (this.inbuf.length < 2) {
      return;
    }
    var nmethods = this.inbuf[1];
    if (this.inbuf.length < 2 + nmethods) {
      return;
    }
    this.inbuf = [];

    // Tell them that we don't log into this SOCKS server.
    this.state = STATE_WAIT_SOCKS5_REQUEST;
    this.write("\x05\x00");
  },

  // Handle the second SOCKSv5 message
  handleSocks5Request() {
    if (this.inbuf.length < 4) {
      return;
    }

    // Find the address:port requested.
    var atype = this.inbuf[3];
    var len, addr;
    if (atype == 0x01) {
      // IPv4 Address
      len = 4;
      addr = this.inbuf.slice(4, 8).join(".");
    } else if (atype == 0x03) {
      // Domain name
      len = this.inbuf[4];
      addr = String.fromCharCode.apply(null, this.inbuf.slice(5, 5 + len));
      len = len + 1;
    } else if (atype == 0x04) {
      // IPv6 address
      len = 16;
      addr = this.inbuf
        .slice(4, 20)
        .map(i => i.toString(16))
        .join(":");
    }
    var port = (this.inbuf[4 + len] << 8) | this.inbuf[5 + len];
    dump("Requesting " + addr + ":" + port + "\n");

    // Map that data to the port we report.
    var foundPort = gPortMap.get(addr + ":" + port);
    dump("This was mapped to " + foundPort + "\n");

    if (foundPort !== undefined) {
      this.write(
        "\x05\x00\x00" + // Header for response
          "\x04" +
          "\x00".repeat(15) +
          "\x01" + // IPv6 address ::1
          String.fromCharCode(foundPort >> 8) +
          String.fromCharCode(foundPort & 0xff) // Port number
      );
    } else {
      this.write(
        "\x05\x05\x00" + // Header for failed response
          "\x04" +
          "\x00".repeat(15) +
          "\x01" + // IPv6 address ::1
          "\x00\x00"
      );
      this.close();
      return;
    }

    // At this point, we contact the local server on that port and then we feed
    // the data back and forth. Easiest way to do that is to open the connection
    // and use the async copy to do it in a background thread.
    let sts = Cc["@mozilla.org/network/socket-transport-service;1"].getService(
      Ci.nsISocketTransportService
    );
    let trans = sts.createTransport([], "localhost", foundPort, null, null);
    let tunnelInput = trans.openInputStream(0, 1024, 1024);
    let tunnelOutput = trans.openOutputStream(0, 1024, 1024);
    this.sub_transport = trans;
    NetUtil.asyncCopy(tunnelInput, this.client_out);
    NetUtil.asyncCopy(this.client_in, tunnelOutput);
  },

  close() {
    this.client_in.close();
    this.client_out.close();
    if (this.sub_transport) {
      this.sub_transport.close(Cr.NS_OK);
    }
  },
};

// A SOCKS server that runs on a random port.
function SocksTestServer() {
  this.listener = ServerSocket(-1, true, -1);
  dump("Starting SOCKS server on " + this.listener.port + "\n");
  this.port = this.listener.port;
  this.listener.asyncListen(this);
  this.client_connections = [];
}
SocksTestServer.prototype = {
  QueryInterface: ChromeUtils.generateQI(["nsIServerSocketListener"]),

  onSocketAccepted(socket, trans) {
    var input = trans.openInputStream(0, 0, 0);
    var output = trans.openOutputStream(0, 0, 0);
    var client = new SocksClient(input, output);
    this.client_connections.push(client);
  },

  onStopListening(socket) {},

  close() {
    for (let client of this.client_connections) {
      client.close();
    }
    this.client_connections = [];
    if (this.listener) {
      this.listener.close();
      this.listener = null;
    }
  },
};

var gSocksServer = null;
// hostname:port -> the port on localhost that the server really runs on.
var gPortMap = new Map();

var NetworkTestUtils = {
  /**
   * Set up a proxy entry such that requesting a connection to hostName:port
   * will instead cause a connection to localRemappedPort. This will use a SOCKS
   * proxy (because any other mechanism is too complicated). Since this is
   * starting up a server, it does behoove you to call shutdownServers when you
   * no longer need to use the proxy server.
   *
   * @param {string} hostName - The DNS name to use for the client.
   * @param {integer} hostPort - The port number to use for the client.
   * @param {integer} localRemappedPort - The port number on which the real server sits.
   */
  configureProxy(hostName, hostPort, localRemappedPort) {
    if (gSocksServer == null) {
      gSocksServer = new SocksTestServer();
      // Using PAC makes much more sense here. However, it turns out that PAC
      // appears to be broken with synchronous proxy resolve, so enabling the
      // PAC mode requires bug 791645 to be fixed first.
      /*
      let pac = 'data:text/plain,function FindProxyForURL(url, host) {' +
        "if (host == 'localhost' || host == '127.0.0.1') {" +
          'return "DIRECT";' +
        '}' +
        'return "SOCKS5 127.0.0.1:' + gSocksServer.port + '";' +
      '}';
      dump(pac + '\n');
      Services.prefs.setIntPref("network.proxy.type", 2);
      Services.prefs.setCharPref("network.proxy.autoconfig_url", pac);
      */

      // Until then, we'll serve the actual proxy via a proxy filter.
      let pps = Cc["@mozilla.org/network/protocol-proxy-service;1"].getService(
        Ci.nsIProtocolProxyService
      );
      let filter = {
        QueryInterface: ChromeUtils.generateQI(["nsIProtocolProxyFilter"]),
        applyFilter(aURI, aProxyInfo, aCallback) {
          if (aURI.host != "localhost" && aURI.host != "127.0.0.1") {
            aCallback.onProxyFilterResult(
              pps.newProxyInfo(
                "socks",
                "localhost",
                gSocksServer.port,
                "",
                "",
                Ci.nsIProxyInfo.TRANSPARENT_PROXY_RESOLVES_HOST,
                0,
                null
              )
            );
            return;
          }
          aCallback.onProxyFilterResult(aProxyInfo);
        },
      };
      pps.registerFilter(filter, 0);
    }
    dump("Requesting to map " + hostName + ":" + hostPort + "\n");
    gPortMap.set(hostName + ":" + hostPort, localRemappedPort);
  },

  /**
   * Turn off any servers started by this file (e.g., the SOCKS proxy server).
   */
  shutdownServers() {
    if (gSocksServer) {
      gSocksServer.close();
    }
  },
};