/*!
 * 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 )
}