vpn.email.server.gfw
Version:
Vpn.Email over firewall mode
348 lines (275 loc) • 10.4 kB
text/typescript
/*!
* Copyright 2017 Vpn.Email network security technology Canada Inc. All Rights Reserved.
*
* Vpn.Email network technolog Canada Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as Net from 'net'
import * as Http from 'http'
import * as Dns from 'dns'
import * as fs from 'fs'
import * as Async from 'async'
import * as Stream from 'stream'
import * as ShortId from 'shortid'
import * as cluster from 'cluster'
import * as HttpProxy from './proxy/httpProxy'
import * as IptablesAdd from './util/iptablesAdd'
import * as Compress from './proxy/compress'
import * as StreamFun from './proxy/streamFunction'
const MaxAllowedTimeOut = 1000 * 60 * 5
const saveLog = ( _log: string, fileName: string ) => {
const log = new Date ().toString () + `: ${ _log }\n`
fs.appendFile ( fileName, log, { encoding: 'utf8' }, err => {
console.log ( log )
})
}
const otherRespon = ( body: string| Buffer, _status: number ) => {
const Ranges = ( _status === 200 ) ? 'Accept-Ranges: bytes\r\n' : ''
const Content = ( _status === 200 ) ? `Content-Type: text/plain; charset=utf-8\r\n` : 'Content-Type: text/html\r\n'
const headers = `Server: nginx/1.6.2\r\n`
+ `Date: ${ new Date ().toUTCString()}\r\n`
+ Content
+ `Content-Length: ${ body.length }\r\n`
+ `Connection: keep-alive\r\n`
+ `Vary: Accept-Encoding\r\n`
//+ `Transfer-Encoding: chunked\r\n`
+ '\r\n'
const status = _status === 200 ? 'HTTP/1.1 200 OK\r\n' : 'HTTP/1.1 404 Not Found\r\n'
return status + headers + body
}
const return404 = () => {
const kkk = '<html>\r\n<head><title>404 Not Found</title></head>\r\n<body bgcolor="white">\r\n<center><h1>404 Not Found</h1></center>\r\n<hr><center>nginx/1.6.2</center>\r\n</body>\r\n</html>\r\n'
return otherRespon ( Buffer.from ( kkk, 'utf8' ), 404 )
}
const dnsLookup = ( hostName: string, CallBack ) => {
return Dns.lookup ( hostName, { all: true }, ( err, data ) => {
if ( err )
return CallBack ( err )
const _buf = Buffer.from ( JSON.stringify ( data ), 'utf8' )
return CallBack ( null, _buf )
})
}
class listen extends Stream.Transform {
constructor ( private headString: string ) { super ()}
public _transform ( chunk: Buffer, encode, cb ) {
console.log ( this.headString )
console.log ( chunk.toString ('hex'))
console.log ( this.headString )
return cb ( null, chunk )
}
}
class ssModeV1 {
private logFileName = logDir + this.clientIp
private serverNet: Net.Server = null
private saveLog ( log: string ) {
saveLog ( log, this.logFileName )
}
private nslookupRequest ( hostName: string, socket: Net.Socket ) {
return Async.waterfall ([
next => dnsLookup ( hostName, next ),
( data1, next ) => Compress.encrypt ( Buffer.from( JSON.stringify ( data1 ), 'utf8' ), this.password, next )
], ( err, result: Buffer ) => {
if ( err ) {
saveLog ( 'nslookup error!' + err.message, this.logFileName )
return socket.end ( return404 ())
}
const lll = result.toString( 'base64' )
const time = new Date ().getTime ()
const _buf = otherRespon ( lll, 200 )
return socket.end ( _buf, () => {
const _time = ( new Date().getTime () - time ) / 1000
return saveLog ( `nslookup to client [${ lll.length }] byte speed:[${ lll.length / _time }] byte/sec`, this.logFileName )
})
})
}
constructor ( private clientIp: string, private port: number, private password: string ) {
IptablesAdd.appPort ( port, ( err, ret ) => {
if ( err ) {
saveLog ( `IptablesAdd.appPort ERROR: ${ err.message }`, this.logFileName )
return process.exit ( 1 )
}
this.serverNet = Net.createServer ( socket => {
const _remoteAddress = socket.remoteAddress
const remoteAddress = _remoteAddress.split ( ':' ).length > 2 ? _remoteAddress.split ( ':' )[3] : _remoteAddress
const id = `[${ remoteAddress }]:[${ socket.remotePort }]`
const isAllowIP = remoteAddress === this.clientIp
const streamFunBlock = new StreamFun.blockRequestData ( isAllowIP, MaxAllowedTimeOut )
const streamDecrypt = new Compress.decryptStream ( this.password )
const streamEncrypt = new Compress.encryptStream ( this.password, 500, null, () => {
const firstConnect = new FirstConnect ( socket, streamEncrypt )
firstConnect.on ( 'error', err => {
console.log ( `firstConnect.on ERROR:`, err.message )
return socket.end ( return404 ())
})
socket.pipe ( streamFunBlock ).pipe ( streamDecrypt ).pipe ( firstConnect )
})
streamFunBlock.on ( 'error', err => {
console.log ( `streamFunBlock.on ERROR:`, err.message )
if ( /404/.test ( err.message))
return socket.end ( return404 ())
return socket.end ()
})
socket.on ( 'end', () => {
return this.serverNet.getConnections (( err, count ) => {
console.log ( id, 'socket.on END! connected = ', count )
})
})
socket.on ( 'unpipe', src => {
return socket.end ()
})
socket.on ( 'error', err => {
return saveLog ( 'HTTP on ERROR:' + err.message, this.logFileName )
})
})
this.serverNet.on ( 'error', err => {
return saveLog ( 'SS mode Net on error:' + err.message, this.logFileName )
})
this.serverNet.listen ( port, null, 512, () => {
const log = 'SS mode start up listening ' + `[${ clientIp }:${ port }]`
console.log ( log )
return saveLog ( log, this.logFileName )
})
})
IptablesAdd.appPort ( port + 1, ( err, ret ) => {
if ( err ) {
saveLog ( `IptablesAdd.appPort ERROR: ${ err.message }`, this.logFileName )
return process.exit ( 1 )
}
const http1 = Net.createServer ( socket => {
const _remoteAddress = socket.remoteAddress
const remoteAddress = _remoteAddress.split (':').length > 2 ? _remoteAddress.split (':')[3] : _remoteAddress
const id = `[${ remoteAddress }]:[${ socket.remotePort }]`
socket.once ( 'data', _buf => {
const keepRead = ( data: Buffer ) => {
const header = new HttpProxy.httpProxy( data )
if ( header._parts.length < 2 ){
return socket.once ( 'data', _Buf => {
const _data = Buffer.concat ([ data, _Buf ])
return keepRead ( _data )
})
}
if ( ! header.isHttpRequest ) {
return socket.end ()
}
if ( remoteAddress !== clientIp || ! header.isGet ) {
return socket.end ( return404 ())
}
const url = header.Url.path.substr ( 1 )
if ( ! url || ! url.length ) {
saveLog ( 'HTTP.on data GET url null', this.logFileName )
return socket.end ( return404 ())
}
const Data = Buffer.from ( url, 'base64' )
try {
const request: VE_IPptpStream = JSON.parse ( Data.toString ( 'utf8' ))
return this.testConnect ( id, request, socket )
} catch ( e ) {
return console.log ( 'test data JSON.parse catch ERROR:', e.message )
}
}
return keepRead ( _buf )
})
socket.on ( 'error', err => {
return saveLog ( 'SS test mode Net on error:' + err.message, this.logFileName )
})
})
http1.on ( 'error', err => {
return saveLog ( 'SS test mode Net on error:' + err.message, this.logFileName )
})
http1.listen ( port + 1, null, 512, () => {
const log = 'SS test mode start up listening ' + `[${ clientIp }:${ port + 1 }]`
console.log ( log )
return saveLog ( log, this.logFileName )
})
})
}
private testConnect ( id: string, data: VE_IPptpStream, socket: Net.Socket ) {
const conn = Net.createConnection ( data.port, data.host, () => {
conn.pipe ( socket ).pipe ( conn )
conn.write ( Buffer.from ( data.buffer, 'base64' ))
})
conn.on ( 'end', () => {
return console.log ( 'test getWayRequest conn.on END' )
})
return conn.on ( 'error', err => {
return console.log ( 'test getWayRequest conn.on error', err.message )
})
}
}
class FirstConnect extends Stream.Writable {
private socket: Net.Socket = null
constructor ( private clientSocket: Net.Socket, private encrypt: Compress.encryptStream ) { super ()}
public _write ( chunk: Buffer, encode, cb ) {
if ( ! this.socket ) {
const _data = chunk.toString ( 'utf8' )
try {
const data = JSON.parse ( _data )
if ( data.hostName && data.hostName.length ) {
return dnsLookup ( data.hostName, ( err, data ) => {
if ( err ) {
return cb ( err )
}
this.encrypt.pipe ( this.clientSocket )
this.encrypt.end ( data )
})
}
if ( data.uuid ) {
return this.socket = Net.connect ({ port: data.port, host: data.host }, () => {
this.socket.on ( 'error', err => {
console.log ( 'FirstConnect socket on error!', err.message )
this.end ()
})
this.socket.pipe ( this.encrypt ).pipe ( this.clientSocket )
this.socket.write ( Buffer.from ( data.buffer, 'base64' ))
return cb ()
})
}
return cb ( new Error ( 'unknow connect!' ))
} catch ( e ) {
return cb ( e )
}
}
if ( this.socket.writable ) {
this.socket.write ( chunk )
return cb ()
}
return cb ( new Error ( 'FirstConnect socket.writable=false' ))
}
}
const clientIp = process.argv [2]
const clientPort = process.argv [3]
const password = process.argv [4]
const logDir = '/var/log/vpn.email/'
const logSystem = logDir + 'syslog'
if ( !clientIp || !clientPort || ! password ) {
console.log( `Usage: node server clientIP clientPort password!` )
process.exit ( 1 )
}
fs.access ( logDir, fs.constants.R_OK | fs.constants.W_OK, err => {
if ( err ) {
fs.mkdir ( logDir, err1 => {
if ( err1 ) {
console.log ( `${ new Date ().toString ()} vpn.email.server.gfw can't mkdir log path!`)
process.exit (1)
}
})
}
})
if ( cluster.isMaster) {
let worker = cluster.fork();
worker.on ( 'exit', () => {
worker = cluster.fork ();
})
} else {
new ssModeV1( clientIp, parseInt( clientPort ), password )
}