--- -- A smallish SOCKS version 5 proxy protocol implementation -- -- @author "Patrik Karlsson " -- module(... or "socks", package.seeall) -- SOCKS Authentication methods AuthMethod = { NONE = 0, GSSAPI = 1, USERPASS = 2, } Request = { -- Class that handles the connection request to the server Connect = { -- Creates a new instance of the class -- @param auth_method table of requested authentication methods -- @return o instance on success, nil on failure new = function(self, auth_method) local o = { version = 5, auth_method = ( "table" ~= type(auth_method) and { auth_method } or auth_method ) } setmetatable(o, self) self.__index = self return o end, -- Converts the instance to string, so that it can be sent to the -- server. -- @return string containing the raw request __tostring = function(self) local methods = "" for _, m in ipairs(self.auth_method) do methods = methods .. string.char(m) end return bin.pack("Cp", self.version, methods) end, }, -- Class that handles the authentication request to the server Authenticate = { -- Creates a new instance of the class -- @param auth_method number with the requested authentication method -- @param creds method specific table of credentials -- currently only user and pass authentication is supported -- this method requires two fields to be present -- username and password -- @return o instance on success, nil on failure new = function(self, auth_method, creds) local o = { auth_method = auth_method, creds = creds } setmetatable(o, self) self.__index = self if ( auth_method == 2 ) then return o end end, -- Converts the instance to string, so that it can be sent to the -- server. -- @return string containing the raw request __tostring = function(self) -- we really don't support anything but 2, but let's pretend that -- we actually do if ( 2 == self.auth_method ) then local version = 1 local username= self.creds.username or "" local password= self.creds.password or "" username = (username == "") and "\0" or username password = (password == "") and "\0" or password return bin.pack("Cpp", version, username, password) end end, } } Response = { -- Class that handles the connection response Connect = { -- Creates a new instance of the class -- @param data string containing the data as received over the socket -- @return o instance on success, nil on failure new = function(self, data) local o = { data = data } setmetatable(o, self) self.__index = self if ( o:parse() ) then return o end end, -- Parses the received data and populates member variables -- @return true on success, false on failure parse = function(self) if ( #self.data ~= 2 ) then return end local pos pos, self.version, self.method = bin.unpack("CC", self.data) return true end }, -- Class that handles the authentication response Authenticate = { Status = { SUCCESS = 0, -- could be anything but zero FAIL = 1, }, -- Creates a new instance of the class -- @param data string containing the data as received over the socket -- @return o instance on success, nil on failure new = function(self, data) local o = { data = data } setmetatable(o, self) self.__index = self if ( o:parse() ) then return o end end, -- Parses the received data and populates member variables -- @return true on success, false on failure parse = function(self) if ( #self.data ~= 2 ) then return end local pos pos, self.version, self.status = bin.unpack("CC", self.data) return true end, -- checks if the authentication was successful or not -- @return true on success, false on failure isSuccess = function(self) return ( self.status == self.Status.SUCCESS ) end, } } -- A buffered socket implementation Socket = { retries = 3, -- Creates a new socket instance -- @param host table containing the host table -- @param port table containing the port table -- @param options table containing options, currenlty supports: -- timeout - socket timeout in ms -- @return o new instance of Socket new = function(self, host, port, options) local o = { host = host, port = port, options = options or {} } setmetatable(o, self) self.__index = self o.Socket = nmap.new_socket() o.Buffer = nil return o end, -- Connects the socket to the server -- @return status true on success false on failure -- @return err string containing error message on failure connect = function( self ) self.Socket:set_timeout(self.options.timeout or 10000) return self.Socket:connect( self.host, self.port ) end, -- Closes an open connection. -- -- @return Status (true or false). -- @return Error code (if status is false). close = function( self ) return self.Socket:close() end, -- Opposed to the socket:receive_bytes function, that returns -- at least x bytes, this function returns the amount of bytes requested. -- -- @param count of bytes to read -- @return true on success, false on failure -- @return data containing bytes read from the socket -- err containing error message if status is false recv = function( self, count ) local status, data self.Buffer = self.Buffer or "" if ( #self.Buffer < count ) then status, data = self.Socket:receive_bytes( count - #self.Buffer ) if ( not(status) or #data < count - #self.Buffer ) then return false, data end self.Buffer = self.Buffer .. data end data = self.Buffer:sub( 1, count ) self.Buffer = self.Buffer:sub( count + 1) return true, data end, -- Sends data over the socket -- -- @return Status (true or false). -- @return Error code (if status is false). send = function( self, data ) return self.Socket:send( data ) end, } -- The main script interface Helper = { -- Create a new instance of the class -- @param host table containing the host table -- @param port table containing the port table -- @param options table containing library options, currenlty: -- timeout - socket timeout in ms -- @return o instance of Helper new = function(self, host, port, options) local o = { host = host, port = port, options = options } setmetatable(o, self) self.__index = self return o end, -- Get the authentication method name by number -- @param method number containing the authentication method -- @return string containing the method name or Unknown authNameByNumber = function(self, method) local methods = { [0] = "No authentication", [1] = "GSSAPI", [2] = "Username and password", } return methods[method] or ("Unknown method (%d)"):format(method) end, -- Connects to the SOCKS server -- @param auth_method table containing the auth. methods to request -- @return status true on success, false on failure -- @return response table containing the respons or err string on failure connect = function(self, auth_method) self.socket = Socket:new(self.host, self.port, self.options) local status, err = self.socket:connect() if ( not(status) ) then return status, err end auth_method = auth_method or {AuthMethod.NONE, AuthMethod.GSSAPI, AuthMethod.USERPASS} status = self.socket:send( tostring(Request.Connect:new(auth_method)) ) if ( not(status) ) then self.socket:close() return false, "Failed to send connection request to server" end local status, data = self.socket:recv(2) if ( not(status) ) then self.socket:close() return false, "Failed to receive connection response from server" end local response = Response.Connect:new(data) if ( not(response) ) then return false, "Failed to parse response from server" end if ( response.version ~= 5 ) then return false, ("Unsupported SOCKS version (%d)"):format(response.version) end if ( response.method == 0xFF ) then return false, "No acceptable authentication methods" end -- store the method so authenticate knows what to use self.auth_method = response.method return true, response end, -- Authenticates to the SOCKS server -- @param creds table containing authentication method specific fields -- currently only authentication method 2 (username and pass) is -- implemented. That method requires the following fields: -- username - containing the username -- password - containing the password -- @return status true on success, false on failure -- @return err string containing the error message authenticate = function(self, creds) if ( self.auth_method ~= 2 ) then return false, "Authentication method not supported" end local req = Request.Authenticate:new(self.auth_method, creds) if ( not(req) ) then return false, "Failed to create authentication request" end local status = self.socket:send(tostring(req)) if ( not(status) ) then return false, "Failed to send authentication request" end if ( 2 == self.auth_method ) then local status, data = self.socket:recv(2) local auth = Response.Authenticate:new(data) if ( not(auth) ) then return false, "Failed to parse authentication response" end if ( auth:isSuccess() ) then return true, "Authentication was successfull" else return false, "Authentication failed" end end return false, "Unsupported authentication method" end, -- closes the connection to the server close = function(self) return self.socket:close() end, }