Last active
January 27, 2016 10:09
-
-
Save adammw/f53312848235aa7de00f to your computer and use it in GitHub Desktop.
DEPRECATED; See https://github.com/adammw/snitunnel
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env node | |
// snitunnel.js | |
// Listens on port 443 and forwards the connection over a HTTP proxy | |
// Using the SNI extension of the TLS handshake to work out the server name | |
var net = require('net'); | |
var http = require('http'); | |
var debug = require('debug')('snitunnel'); | |
const TUNNEL_HOST = 'my.tunnel.host'; | |
const TUNNEL_PORT = 8080; | |
// TLS Parsing Code ported from sniproxy | |
// https://github.com/dlundquist/sniproxy/blob/master/src/tls.c | |
// Under BSD License | |
const TLS_HEADER_LEN = 5; | |
const TLS_HANDSHAKE_CONTENT_TYPE = 0x16; | |
const TLS_HANDSHAKE_TYPE_CLIENT_HELLO = 0x01; | |
function parse_tls_header(data) { | |
if (data.length < TLS_HEADER_LEN) | |
return -1; | |
if (data[0] & 0x80 && data[2] == 1) { | |
debug("Received SSL 2.0 Client Hello which can not support SNI."); | |
return -2; | |
} | |
var tls_content_type = data[0]; | |
if (tls_content_type != TLS_HANDSHAKE_CONTENT_TYPE) { | |
debug("Request did not begin with TLS handshake."); | |
return -5; | |
} | |
var tls_version_major = data[1]; | |
var tls_version_minor = data[2]; | |
if (tls_version_major < 3) { | |
debug("Received SSL %d.%d handshake which which can not support SNI.", | |
tls_version_major, tls_version_minor); | |
return -2; | |
} | |
/* return TLS record length */ | |
return data.readUInt16BE(3); | |
}; | |
function parse_tls_record(data) { | |
if (data[0] != TLS_HANDSHAKE_TYPE_CLIENT_HELLO) { | |
debug("Not a client hello"); | |
return -5; | |
} | |
/* Skip fixed length records: | |
1 Handshake Type | |
3 Length | |
2 Version (again) | |
32 Random | |
*/ | |
var pos = 38; | |
/* Session ID */ | |
var sessionIDLength = data[pos]; | |
pos += 1 + sessionIDLength; | |
/* Cipher Suites */ | |
var ciperSuiteLength = data.readUInt16BE(pos); | |
pos += 2 + ciperSuiteLength; | |
/* Compression Methods */ | |
var compressionMethodsLength = data[pos]; | |
pos += 1 + compressionMethodsLength; | |
// if (pos == data.length && tls_version_major == 3 && tls_version_minor == 0) { | |
// debug("Received SSL 3.0 handshake without extensions"); | |
// return -2; | |
// } | |
/* Extensions */ | |
var extensionsLength = data.readUInt16BE(pos); | |
return parse_extensions(data.slice(pos + 2)); | |
} | |
function parse_extensions(data) { | |
var pos = 0; | |
while((pos + 4) < data.length) { | |
var extensionHeader = data.readUInt16BE(pos); pos += 2; | |
var extensionLength = data.readUInt16BE(pos); pos += 2; | |
if (extensionHeader == 0x0000) { | |
return parse_server_name_extension(data.slice(pos, pos + extensionLength)); | |
} | |
pos += extensionLength; | |
} | |
return -2; | |
} | |
function parse_server_name_extension(data) { | |
var pos = 2; | |
while((pos + 3) < data.length) { | |
var nameType = data[pos]; pos += 1; | |
var nameLength = data.readUInt16BE(pos); pos += 2; | |
switch(nameType) { | |
case 0x00: /* host_name */ | |
return data.slice(pos, pos + nameLength).toString(); | |
default: | |
debug('Unknown server name extension name type: %d', nameType); | |
} | |
pos += nameLength; | |
} | |
return -2; | |
} | |
var server = net.createServer(function(serverSocket) { | |
var headerLength = null; | |
var serverName = null; | |
serverSocket.on('readable', function headerListener() { | |
if (headerLength === null) { | |
var header = serverSocket.read(TLS_HEADER_LEN); | |
if (header === null) return; | |
headerLength = parse_tls_header(header); | |
if (headerLength <= 0) { | |
serverSocket.end(); | |
return; | |
}; | |
} | |
if (serverName === null) { | |
var tlsRecordData = serverSocket.read(headerLength); | |
if (tlsRecordData === null) return; | |
serverName = parse_tls_record(tlsRecordData); | |
if (typeof serverName != 'string') { | |
socket.end(); | |
return; | |
} | |
debug('found server name extension: %s', serverName); | |
serverSocket.removeListener('readable', headerListener); | |
// TODO: validate host, reject connections | |
// For direct connection, replace with: | |
// var clientSocket = net.connect(443, serverName, function() { ... }); | |
var req = http.request({ | |
host: TUNNEL_HOST, | |
port: TUNNEL_PORT, | |
method: 'CONNECT', | |
path: `${serverName}:443` | |
}); | |
req.end(); | |
req.on('connect', function(res, clientSocket, head) { | |
clientSocket.write(header); | |
clientSocket.write(tlsRecordData); | |
serverSocket.pipe(clientSocket).pipe(serverSocket); | |
}); | |
} else { | |
console.log('listener fired twice'); | |
} | |
}); | |
}); | |
server.listen(443, function() { | |
var addr = server.address(); | |
console.log('Server listening on %s %s:%d', addr.family, addr.address, addr.port); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment