Skip to content

Commit

Permalink
✨ feat: Client connect support added
Browse files Browse the repository at this point in the history
  • Loading branch information
MoIzadloo committed Mar 7, 2023
1 parent 034fb86 commit 99a5def
Show file tree
Hide file tree
Showing 13 changed files with 288 additions and 121 deletions.
62 changes: 41 additions & 21 deletions src/client/client.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
/**
* The Client class is responsible for creating a TCP socket connection,
* and communicate with a standard socks server
*/
import net from 'net'
import Event from '../helper/event'
import Connection, { EventTypes } from '../helper/connection'
Expand All @@ -10,21 +6,48 @@ import { Handlers } from '../helper/handlers'
import Authenticator from './auth/authenticator'
import Address from '../helper/address'

/**
* The Client class is responsible for creating a TCP socket connection,
* and communicate with a standard socks server
*/
export class Client {
/**
* Socks server host
*/
private readonly host: string

/**
* Socks server port
*/
private readonly port: number

/**
* Triggers whenever the socket initialized
*/
private readonly connectionListener: (() => void) | undefined

/**
* The object which contains all default handlers
*/
private readonly handlers: Handlers

/**
* The main event object
*/
private readonly event: Event<EventTypes>

constructor(
port: number,
host: string,
connectionListener: (() => void) | undefined
) {
this.host = host
this.port = port
this.handlers = new Handlers({
connect: handlers.connect,
associate: handlers.associate,
bind: handlers.bind,
})
this.event = new Event<EventTypes>()
this.connectionListener = connectionListener
}
Expand All @@ -37,23 +60,20 @@ export class Client {
* @returns void
*/
connect(port: number, host: string, version: 4 | 5) {
const socket = net.connect(this.port, this.host, this.connectionListener)
const connection = new Connection(
this.event,
socket,
new Handlers({
connect: handlers.connect,
associate: handlers.associate,
bind: handlers.bind,
})
)
connection.version = version
connection.address = new Address(port, host)
if (version === 5) {
const authenticator = new Authenticator(connection)
authenticator.authenticate()
}
return socket
return new Promise<net.Socket>((resolve, reject) => {
const socket = net.connect(this.port, this.host, this.connectionListener)
const connection = new Connection(this.event, socket, this.handlers)
connection.version = version
connection.address = new Address(port, host)
connection.resolve = resolve
connection.reject = reject
if (version === 5) {
const authenticator = new Authenticator(connection)
authenticator.authenticate()
} else if (version === 4) {
connection.handlers.req.connect(connection)
}
})
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/client/handlers/associate.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { handler } from '../../helper/handler'

/**
* Default implementation of connect
* Handle associate request
* @returns void
*/
export const associate = handler((info) => {
Expand Down
2 changes: 1 addition & 1 deletion src/client/handlers/bind.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { handler } from '../../helper/handler'

/**
* Default implementation of connect
* Handle bind request
* @returns void
*/
export const bind = handler((info) => {
Expand Down
42 changes: 34 additions & 8 deletions src/client/handlers/connect.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { handler } from '../../helper/handler'
import Writable from '../../helper/writable'
import { COMMANDS } from '../../helper/constants'
import { Readable } from '../../helper/readable'
import { COMMANDS, SOCKS4REPLY, SOCKS5REPLY } from '../../helper/constants'

/**
* Default implementation of connect
* Handle connect request
* @returns void
*/
export const connect = handler((info, socket) => {
export const connect = handler((info, socket, resolve, reject) => {
const writeable = new Writable()
writeable.push(info.version)
if (info.version === 5) {
Expand All @@ -29,10 +30,35 @@ export const connect = handler((info, socket) => {
addressBuff.port
)
}
socket.write(writeable.toBuffer())
socket.on('data', (data) => {
console.log(data)
socket.removeAllListeners('data')
})
} else if (info.version === 4) {
const addressBuff = info.address.toBuffer()
writeable.push(
COMMANDS.connect,
Buffer.concat([addressBuff.port, addressBuff.host, Buffer.from([0x00])])
)
}
socket.write(writeable.toBuffer())
socket.on('data', (data) => {
const readable = new Readable(data)
const version = readable.read(1)
const reply = readable.read(1)
if (reject) {
if (
reply.readInt8() !== SOCKS5REPLY.succeeded.code &&
reply.readInt8() !== SOCKS4REPLY.granted.code
) {
let msg = ''
msg += Object.values(
version.readInt8() === 5 ? SOCKS5REPLY : SOCKS4REPLY
).find((rep) => {
return rep.code === reply.readInt8()
})?.msg
reject(msg)
}
}
if (resolve) {
resolve(socket)
}
socket.removeAllListeners('data')
})
})
Empty file removed src/client/state/socks4.ts
Empty file.
30 changes: 16 additions & 14 deletions src/helper/address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,24 @@ class Address {
public type: string
public port: number

constructor(port: number, host: string, type?: string) {
constructor(port: number, host: string) {
/**
* Regular expression for ipv4
*/
const ipv4Regex =
'^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$'

/**
* Regular expression for ipv6
*/
const ipv6Regex =
'(?!^(?:(?:.*(?:::.*::|:::).*)|::|[0:]+[01]|.*[^:]:|[0-9a-fA-F](?:.*:.*){8}[0-9a-fA-F]|(?:[0-9a-fA-F]:){1,6}[0-9a-fA-F])$)^(?:(::|[0-9a-fA-F]{1,4}:{1,2})([0-9a-fA-F]{1,4}:{1,2}){0,6}([0-9a-fA-F]{1,4}|::)?)$'

/**
* Regular expression for domain
*/
const domainRegex = '^[a-zA-Z0-9-\\_]+\\.[a-zA-Z]+?$'

/**
* Port
*/
Expand All @@ -40,16 +47,14 @@ class Address {
/**
* Type (ipv4 | ipv6 | domain)
*/
if (type) {
this.type = type.toLowerCase()
if (this.host.match(ipv4Regex)) {
this.type = 'ipv4'
} else if (this.host.match(ipv6Regex)) {
this.type = 'ipv6'
} else if (this.host.match(domainRegex)) {
this.type = 'domain'
} else {
if (this.host.match(ipv4Regex)) {
this.type = 'ipv4'
} else if (this.host.match(ipv6Regex)) {
this.type = 'ipv6'
} else {
this.type = 'domain'
}
throw new Error('Invalid host address type')
}
}

Expand All @@ -66,16 +71,13 @@ class Address {
type: number
): Address {
let addr
let atype: 'ipv4' | 'ipv6' | 'domain'
if (type === ADDRESSTYPES.ipv6 || type === ADDRESSTYPES.ipv4) {
addr = ipaddr.fromByteArray(bufToArray(host))
atype = addr.kind()
addr = addr.toString()
} else {
addr = host.toString()
atype = 'domain'
}
return new Address(port.readInt16BE(), addr, atype)
return new Address(port.readInt16BE(), addr)
}

/**
Expand Down
10 changes: 10 additions & 0 deletions src/helper/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@ export type Options = {
* a new instance get constructed pre each connection
*/
class Connection {
/**
* Resolve function for client only
*/
public resolve?: (value: net.Socket | PromiseLike<net.Socket>) => void

/**
* Reject function for client only
*/
public reject?: (reason?: any) => void

/**
* Current state
*/
Expand Down
86 changes: 70 additions & 16 deletions src/helper/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,30 +54,84 @@ export const COMMANDS: Commands = Object.freeze({
associate: 0x03,
})

interface Socks5reply {
succeeded: number
serveFailure: number
connRefused: number
interface Reply {
code: number
msg: string
}

type Socks5ReplyName =
| 'succeeded'
| 'generalFailure'
| 'notAllowed'
| 'netUnreachable'
| 'hostUnreachable'
| 'connRefused'
| 'ttlExpired'
| 'cmdNotSupported'
| 'atypeNotSupported'

/**
* Socks5's specific available reply codes
*/
export const SOCKS5REPLY: Socks5reply = Object.freeze({
succeeded: 0x00,
serveFailure: 0x01,
connRefused: 0x05,
export const SOCKS5REPLY: Record<Socks5ReplyName, Reply> = Object.freeze({
succeeded: {
code: 0x00,
msg: 'Succeeded',
},
generalFailure: {
code: 0x01,
msg: 'General SOCKS server failure',
},
notAllowed: {
code: 0x02,
msg: 'Connection not allowed by ruleset',
},
netUnreachable: {
code: 0x03,
msg: 'Network unreachable',
},
hostUnreachable: {
code: 0x03,
msg: 'Host unreachable',
},
connRefused: {
code: 0x05,
msg: 'Connection refused',
},
ttlExpired: {
code: 0x06,
msg: 'TTL expired',
},
cmdNotSupported: {
code: 0x07,
msg: 'Command not supported',
},
atypeNotSupported: {
code: 0x08,
msg: 'Address type not supported',
},
})

interface Socks4reply {
succeeded: number
connRefused: number
}
type Socks4ReplyName = 'granted' | 'rejected' | 'identFail' | 'diffUserId'

/**
* Socks5'4 specific available reply codes
* Socks4's specific available reply codes
*/
export const SOCKS4REPLY: Socks4reply = Object.freeze({
succeeded: 0x5a,
connRefused: 0x5b,
export const SOCKS4REPLY: Record<Socks4ReplyName, Reply> = Object.freeze({
granted: {
code: 0x5a,
msg: 'Request granted',
},
rejected: {
code: 0x5b,
msg: 'Request rejected or failed',
},
identFail: {
code: 0x5c,
msg: 'request rejected because SOCKS server cannot connect to identity on the client',
},
diffUserId: {
code: 0x5d,
msg: 'request rejected because the client program and identity report different user-ids',
},
})
13 changes: 10 additions & 3 deletions src/helper/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,30 @@ export interface Info {
address: Address
}

export type Handler = (info: Info, socket: net.Socket) => void
export type Handler = (
info: Info,
socket: net.Socket,
resolve?: (value: net.Socket | PromiseLike<net.Socket>) => void,
reject?: (reason?: any) => void
) => void

/**
* Inputs a callback function and return a function that accept connection and,
* executes callback with function its properties
* @returns function
*/
export const handler =
(callback: (info: Info, socket: net.Socket) => void) =>
(callback: Handler) =>
(connection: Connection): void => {
if (connection.version && connection.address) {
callback(
{
version: connection.version,
address: connection.address,
},
connection.socket
connection.socket,
connection.resolve,
connection.reject
)
}
}

0 comments on commit 99a5def

Please sign in to comment.