ref: 2ebee720a8af84e6f1a4d4e390f723526553b5e0
parent: a7fd3774d4706aeee559afbf04cc016b085002a1
author: kitzman <[email protected]>
date: Tue Nov 21 10:23:46 EST 2023
added lua libs
--- a/README
+++ b/README
@@ -10,6 +10,19 @@
bin/rc/txcheck - fd statistics for aux/statusbar
bin/rc/vtb - green vt
+lib/lua/net.lua - p9 wrappers for /net and /mnt/web
+lib/lua/r2pipe.lua - r2pipe for lua
+lib/lua/nseport/afp.lua - (stolen) from nmap
+lib/lua/nseport/datetime.lua - (stolen) from nmap
+lib/lua/nseport/ipOps.lua - (stolen) from nmap
+lib/lua/nseport/json.lua - (stolen) from nmap
+lib/lua/nseport/nmap.lua - (stolen) from nmap
+lib/lua/nseport/stdnse.lua - (stolen) from nmap
+lib/lua/nseport/strict.lua - (stolen) from nmap
+lib/lua/nseport/stringaux.lua - (stolen) from nmap
+lib/lua/nseport/tableaux.lua - (stolen) from nmap
+lib/lua/nseport/unicode.lua - (stolen) from nmap
+
themes:
lib/pico/gruvboxl.pico - pico-scripted wallpaper
--- /dev/null
+++ b/lib/lua/net.lua
@@ -1,0 +1,275 @@
+---
+-- plan9 network utilities
+---
+
+---
+-- dial(2)
+---
+DialAux = {
+ new = function(self, proto)
+ local o = {}
+ setmetatable(o, self)
+ self.__index = self
+ if proto == nil then
+ o.proto = "tcp"
+ else
+ o.proto = proto
+ end
+ return o
+ end,
+
+ -- connect (compat method)
+ connect = function(self, host, port)
+ raddr = self.proto.."!"..host.."!"..port
+
+ end,
+
+ -- dial (needs more checks)
+ dial = function(self, raddr)
+ local cs_fd, clone_fd, data_fd, ctl_fd, proto
+ local data_fid, conn_param, clone_path, addr
+
+ if not(self.clone_path) and not(self.addr) then
+ cs_fd = io.open("/net/cs", "r+")
+
+ if not(cs_fd) then
+ return false, "unable to open cs file"
+ end
+
+ cs_fd:write(raddr)
+ cs_fd:seek("set")
+ conn_param = cs_fd:read("a")
+ cs_fd:close()
+
+ self.clone_path = string.sub(conn_param, 0, string.find(conn_param, " ") - 1)
+ self.addr = string.sub(conn_param, string.find(conn_param, " ") + 1)
+ end
+
+ clone_path = self.clone_path
+ addr = self.addr
+
+ clone_fd = io.open(clone_path, "r+")
+ if not(cs_fd) then
+ return false, "unable to open clone file"
+ end
+
+ data_fid = clone_fd:read()
+ clone_fd:seek("set")
+ local n = clone_fd:write("connect "..addr.."\n")
+ clone_fd:flush()
+
+ data_fdin = io.open("/net/"..proto.."/"..data_fid.."/data", "r+")
+ data_fdout = io.open("/net/"..proto.."/"..data_fid.."/data", "a+")
+
+ if not(data_fdin) then
+ return false, "unable to open data"
+ end
+
+ self.data_fdin = data_fdin
+ self.data_fdout = data_fdout
+ self.clone_fd = clone_fd
+
+ return true, nil
+ end,
+
+ -- close connection and file descriptors
+ close = function(self)
+ self.data_fdin:close()
+ self.data_fdout:close()
+ self.clone_fd:write("close\n")
+ self.clone_fd:close()
+
+ self.data_fdin = nil
+ self.data_fdout = nil
+ self.clone_fd = nil
+ end,
+
+ -- dummy compat function
+ set_timeout = function(self, t)
+ self.timeout = t
+ end,
+
+ -- send data
+ send = function(self, d)
+-- self.data_fdout:seek("set")
+ local fd = self.data_fdout:write(d)
+ self.data_fdout:flush()
+ if fd == nil then
+ return false, "connection dropped"
+ end
+ return true, d
+ end,
+
+ -- receive data
+ receive_bytes = function(self, sz)
+ if sz == nil then
+ sz = SOCKET_MAXSZ
+ end
+ local data = self.data_fdin:read(sz)
+ if not(data) then
+ return false, "eof"
+ end
+ return true, data
+ end,
+
+ -- receive all
+ receive_all = function(self)
+ local data = self.data_fdin:read("a")
+ if not(data) then
+ return false, "eof"
+ end
+ return true, data
+ end,
+}
+
+---
+-- webfs(4) client
+---
+WebfsRequest = {
+ new = function(self, mtpt)
+ local o = {}
+ setmetatable(o, self)
+ self.__index = self
+ if mtpt == nil then
+ o.mtpt = "/mnt/web"
+ else
+ o.mtpt = mtpt
+ end
+ return o
+ end,
+
+ open = function(self)
+ local clone_fd
+ local web_fid, clone_path
+
+ clone_path=self.mtpt.."/clone"
+ clone_fd = io.open(clone_path, "r+")
+ if not(clone_fd) then
+ return false, "unable to clone"
+ end
+
+ web_fid = clone_fd:read()
+ clone_fd:seek("set")
+
+ self.clone_fd = clone_fd
+ self.web_fid = web_fid
+
+ return true, nil
+ end,
+
+ set_param = function(self, paramname, paramval)
+-- self.clone_fd:seek("set")
+ self.clone_fd:write(paramname.." "..paramval.."\n")
+ self.clone_fd:flush()
+ end,
+
+ read_body = function(self)
+ local body_fd, status, buf
+
+ body_fd = io.open(self.mtpt.."/"..self.web_fid.."/body", "r")
+ if not(body_fd) then
+ return false, "body cannot be opened"
+ end
+
+ status, buf = body_fd:read("a")
+ body_fd:close()
+
+ return status, buf
+ end,
+
+ post_body = function(self, body)
+ local body_fd, status, buf
+
+ body_fd = io.open(self.mtpt.."/"..self.web_fid.."/postbody", "w")
+ if not(body_fd) then
+ return false, "body cannot be opened"
+ end
+
+ status, buf = body_fd:write("a")
+ body_fd:close()
+
+ return status, buf
+ end,
+
+ close = function(self)
+ self.clone_fd:close()
+ end,
+
+}
+
+Webfs = {
+ new = function(self, mtpt)
+ local o = {}
+ setmetatable(o, self)
+ self.__index = self
+ if mtpt == nil then
+ o.mtpt = "/mnt/web"
+ else
+ o.mtpt = mtpt
+ end
+ return o
+ end,
+
+ get = function(self, url, headers)
+ local request, status, data
+ if not(headers) then
+ headers = {}
+ end
+
+ request = WebfsRequest:new(self.mtpt)
+ status, data = request:open()
+ if not(status) then
+ return status, data
+ end
+
+ request:set_param("url", url)
+ for hdrname,hdrval in pairs(headers) do
+ request:set_param("headers", string.format("%s: %s", hdrname, hdrval))
+ end
+
+ status, data = request:read_body()
+ request:close()
+ return status, data
+ end,
+
+ post = function(self, url, body, contenttype, headers)
+ local request, status, data
+ if not(headers) then
+ headers = {}
+ end
+
+ request = WebfsRequest:new(self.mtpt)
+ status, data = request:open()
+ if not(status) then
+ return status, data
+ end
+
+ request:set_param("url", url)
+ request:set_param("request", "POST")
+ for k,header in pairs(headers) do
+ request:set_param("headers", header)
+ end
+ if contenttype then
+ request:set_param("contenttype", contenttype)
+ end
+
+ status, data = request:post_body(body)
+ if not(status) then
+ request:close()
+ return status, data
+ end
+
+ status, data = request:read_body()
+ request:close()
+ return status, data
+ end,
+}
+
+local net = {
+ DialAux = DialAux,
+ Socket = DialAux, -- nmap/unix compat (someday)
+ WebfsRequest = WebfsRequest,
+ Webfs = Webfs,
+}
+
+return net
--- /dev/null
+++ b/lib/lua/nseport/afp.lua
@@ -1,0 +1,2011 @@
+local stdnse = require "stdnse"
+local datetime = require "datetime"
+local ipOps = require "ipOps"
+local os = require "os"
+local string = require "string"
+local stringaux = require "stringaux"
+local table = require "table"
+local net = require "net"
+
+_ENV = stdnse.module("afp", stdnse.seeall);
+
+local HAVE_SSL, openssl = pcall(require,'openssl')
+
+-- Table of valid REQUESTs
+local REQUEST = {
+ CloseSession = 0x01,
+ OpenSession = 0x04,
+ Command = 0x02,
+ GetStatus = 0x03,
+ Write = 0x06,
+}
+
+-- Table of headers flags to be set accordingly in requests and responses
+local FLAGS = {
+ Request = 0,
+ Response = 1
+}
+
+-- Table of possible AFP_COMMANDs
+COMMAND = {
+ FPCloseVol = 0x02,
+ FPCloseFork = 0x04,
+ FPCopyFile = 0x05,
+ FPCreateDir = 0x06,
+ FPCreateFile = 0x07,
+ FPGetSrvrInfo = 0x0f,
+ FPGetSrvParms = 0x10,
+ FPLogin = 0x12,
+ FPLoginCont = 0x13,
+ FPLogout = 0x14,
+ FPMapId = 0x15,
+ FPMapName = 0x16,
+ FPGetUserInfo = 0x25,
+ FPOpenVol = 0x18,
+ FPOpenFork = 0x1a,
+ FPGetFileDirParams = 0x22,
+ FPChangePassword = 0x24,
+ FPReadExt = 0x3c,
+ FPWriteExt = 0x3d,
+ FPGetAuthMethods = 0x3e,
+ FPLoginExt = 0x3f,
+ FPEnumerateExt2 = 0x44,
+}
+
+USER_BITMAP = {
+ UserId = 0x01,
+ PrimaryGroupId = 0x2,
+ UUID = 0x4
+}
+
+VOL_BITMAP = {
+ Attributes = 0x1,
+ Signature = 0x2,
+ CreationDate = 0x4,
+ ModificationDate = 0x8,
+ BackupDate = 0x10,
+ ID = 0x20,
+ BytesFree = 0x40,
+ BytesTotal = 0x80,
+ Name = 0x100,
+ ExtendedBytesFree = 0x200,
+ ExtendedBytesTotal = 0x400,
+ BlockSize = 0x800
+}
+
+FILE_BITMAP = {
+ Attributes = 0x1,
+ ParentDirId = 0x2,
+ CreationDate = 0x4,
+ ModificationDate = 0x8,
+ BackupDate = 0x10,
+ FinderInfo = 0x20,
+ LongName = 0x40,
+ ShortName = 0x80,
+ NodeId = 0x100,
+ DataForkSize = 0x200,
+ ResourceForkSize = 0x400,
+ ExtendedDataForkSize = 0x800,
+ LaunchLimit = 0x1000,
+ UTF8Name = 0x2000,
+ ExtendedResourceForkSize = 0x4000,
+ UnixPrivileges = 0x8000,
+ ALL = 0xFFFF
+}
+
+DIR_BITMAP = {
+ Attributes = 0x1,
+ ParentDirId = 0x2,
+ CreationDate = 0x4,
+ ModificationDate = 0x8,
+ BackupDate = 0x10,
+ FinderInfo = 0x20,
+ LongName = 0x40,
+ ShortName = 0x80,
+ NodeId = 0x100,
+ OffspringCount = 0x200,
+ OwnerId = 0x400,
+ GroupId = 0x800,
+ AccessRights = 0x1000,
+ UTF8Name = 0x2000,
+ UnixPrivileges = 0x8000,
+ ALL = 0xBFFF,
+}
+
+PATH_TYPE = {
+ ShortName = 1,
+ LongName = 2,
+ UTF8Name = 3,
+}
+
+ACCESS_MODE = {
+ Read = 0x1,
+ Write = 0x2,
+ DenyRead = 0x10,
+ DenyWrite = 0x20
+}
+
+-- Access controls
+ACLS = {
+ OwnerSearch = 0x1,
+ OwnerRead = 0x2,
+ OwnerWrite = 0x4,
+
+ GroupSearch = 0x100,
+ GroupRead = 0x200,
+ GroupWrite = 0x400,
+
+ EveryoneSearch = 0x10000,
+ EveryoneRead = 0x20000,
+ EveryoneWrite = 0x40000,
+
+ UserSearch = 0x100000,
+ UserRead = 0x200000,
+ UserWrite = 0x400000,
+
+ BlankAccess = 0x10000000,
+ UserIsOwner = 0x80000000
+}
+
+-- User authentication modules
+UAM =
+{
+ NoUserAuth = "No User Authent",
+ ClearText = "Cleartxt Passwrd",
+ RandNum = "Randnum Exchange",
+ TwoWayRandNum = "2-Way Randnum",
+ DHCAST128 = "DHCAST128",
+ DHX2 = "DHX2",
+ Kerberos = "Client Krb v2",
+ Reconnect = "Recon1",
+}
+
+ERROR =
+{
+ SocketError = 1000,
+ CustomError = 0xdeadbeef,
+
+ FPNoErr = 0,
+ FPAccessDenied = -5000,
+ FPAuthContinue = -5001,
+ FPBadUAM = -5002,
+ FPBadVersNum = -5003,
+ FPBitmapErr = - 5004,
+ FPCantMove = - 5005,
+ FPEOFErr = -5009,
+ FPItemNotFound = -5012,
+ FPLockErr = -5013,
+ FPMiscErr = -5014,
+ FPObjectExists = -5017,
+ FPObjectNotFound = -5018,
+ FPParamErr = -5019,
+ FPUserNotAuth = -5023,
+ FPCallNotSupported = -5024,
+}
+
+MAP_ID =
+{
+ UserIDToName = 1,
+ GroupIDToName = 2,
+ UserIDToUTF8Name = 3,
+ GroupIDToUTF8Name = 4,
+ UserUUIDToUTF8Name = 5,
+ GroupUUIDToUTF8Name = 6
+}
+
+MAP_NAME =
+{
+ NameToUserID = 1,
+ NameToGroupID = 2,
+ UTF8NameToUserID = 3,
+ UTF8NameToGroupID = 4,
+ UTF8NameToUserUUID = 5,
+ UTF8NameToGroupUUID = 6
+}
+
+
+SERVERFLAGS =
+{
+ CopyFile = 0x01,
+ ChangeablePasswords = 0x02,
+ NoPasswordSaving = 0x04,
+ ServerMessages = 0x08,
+ ServerSignature = 0x10,
+ TCPoverIP = 0x20,
+ ServerNotifications = 0x40,
+ Reconnect = 0x80,
+ OpenDirectory = 0x100,
+ UTF8ServerName = 0x200,
+ UUIDs = 0x400,
+ SuperClient = 0x8000
+}
+
+local ERROR_MSG = {
+ [ERROR.FPAccessDenied]="Access Denied",
+ [ERROR.FPAuthContinue]="Authentication is not yet complete",
+ [ERROR.FPBadUAM]="Specified UAM is unknown",
+ [ERROR.FPBadVersNum]="Server does not support the specified AFP version",
+ [ERROR.FPBitmapErr]="Attempt was made to get or set a parameter that cannot be obtained or set with this command, or a required bitmap is null",
+ [ERROR.FPCantMove]="Attempt was made to move a directory into one of its descendant directories.",
+ [ERROR.FPEOFErr]="No more matches or end of fork reached.",
+ [ERROR.FPLockErr]="Some or all of the requested range is locked by another user; a lock range conflict exists.",
+ [ERROR.FPMiscErr]="Non-AFP error occurred.",
+ [ERROR.FPObjectNotFound]="Input parameters do not point to an existing directory, file, or volume.",
+ [ERROR.FPParamErr]="Parameter error.",
+ [ERROR.FPObjectExists] = "File or directory already exists.",
+ [ERROR.FPUserNotAuth] = "UAM failed (the specified old password doesn't match); no user is logged in yet for the specified session; authentication failed; password is incorrect.",
+ [ERROR.FPItemNotFound] = "Specified APPL mapping, comment, or icon was not found in the Desktop database; specified ID is unknown.",
+ [ERROR.FPCallNotSupported] = "Server does not support this command.",
+}
+
+-- Dates are shifted forward one day to avoid referencing 12/31/1969 UTC
+-- when specifying 1/1/1970 (local) in a timezone that is ahead of UTC
+local TIME_OFFSET = os.time({year=2000, month=1, day=2, hour=0}) - os.time({year=1970, month=1, day=2, hour=0})
+
+-- Check if all the bits in flag are set in bitmap.
+local function flag_is_set(bitmap, flag)
+ return (bitmap & flag) == flag
+end
+
+-- Serialize path of a given type
+-- NB: For now the actual UTF-8 encoding is ignored
+local function encode_path (path)
+ if path.type == PATH_TYPE.ShortName or path.type == PATH_TYPE.LongName then
+ return string.pack("Bs1", path.type, path.name)
+ elseif path.type == PATH_TYPE.UTF8Name then
+ return string.pack(">BI4s2", path.type, 0x08000103, path.name)
+ end
+ assert(false, ("Unrecognized path type '%s'"):format(tostring(path.type)))
+end
+
+-- Response class returned by all functions in Proto
+Response = {
+
+ new = function(self,o)
+ o = o or {}
+ setmetatable(o, self)
+ self.__index = self
+ return o
+ end,
+
+ --- Sets the error code
+ --
+ -- @param code number containing the error code
+ setErrorCode = function( self, code )
+ self.error_code = code
+ end,
+
+ --- Gets the error code
+ --
+ -- @return code number containing the error code
+ getErrorCode = function( self )
+ return self.error_code
+ end,
+
+ --- Gets the error message
+ --
+ -- @return msg string containing the error
+ getErrorMessage = function(self)
+ if self.error_msg then
+ return self.error_msg
+ else
+ return ERROR_MSG[self.error_code] or ("Unknown error (%d) occurred"):format(self.error_code)
+ end
+ end,
+
+ --- Sets the error message
+ --
+ -- @param msg string containing the error message
+ setErrorMessage = function(self, msg)
+ self.error_code = ERROR.CustomError
+ self.error_msg = msg
+ end,
+
+ --- Sets the result
+ --
+ -- @param result result to set
+ setResult = function(self, result)
+ self.result = result
+ end,
+
+ --- Get the result
+ --
+ -- @return result
+ getResult = function(self)
+ return self.result
+ end,
+
+ --- Sets the packet
+ setPacket = function( self, packet )
+ self.packet = packet
+ end,
+
+ getPacket = function( self )
+ return self.packet
+ end,
+
+ --- Gets the packet data
+ getPacketData = function(self)
+ return self.packet.data
+ end,
+
+ --- Gets the packet header
+ getPacketHeader = function(self)
+ return self.packet.header
+ end,
+}
+
+--- Proto class containing all AFP specific code
+--
+-- For more details consult:
+-- http://developer.apple.com/mac/library/documentation/Networking/Reference/AFP_Reference/Reference/reference.html
+Proto = {
+
+ RequestId = 1,
+
+ new = function(self,o)
+ o = o or {}
+ setmetatable(o, self)
+ self.__index = self
+ return o
+ end,
+
+ setSocket = function(self, socket)
+ self.socket = socket
+ end,
+
+ --- Creates an AFP packet
+ --
+ -- @param command number should be one of the commands in the COMMAND table
+ -- @param data_offset number holding the offset to the data
+ -- @param data the actual data of the request
+ create_fp_packet = function( self, command, data_offset, data )
+ local reserved = 0
+ local data = data or ""
+ local data_len = data:len()
+ local header = string.pack(">BBI2I4I4I4", FLAGS.Request, command, self.RequestId, data_offset, data_len, reserved)
+
+ self.RequestId = self.RequestId + 1
+ return header .. data
+ end,
+
+ --- Parses the FP header (first 16-bytes of packet)
+ --
+ -- @param packet string containing the raw packet
+ -- @return table with header data containing <code>flags</code>, <code>command</code>,
+ -- <code>request_id</code>, <code>error_code</code>, <code>length</code> and <code>reserved</code> fields
+ parse_fp_header = function( self, packet )
+ local header = {}
+ local pos
+
+ header.flags, header.command, header.request_id, pos = string.unpack( ">BBI2", packet )
+ header.error_code, header.length, header.reserved, pos = string.unpack( ">i4I4I4", packet, pos )
+
+ if header.error_code ~= 0 then
+ header.error_msg = ERROR_MSG[header.error_code] or ("Unknown error: %d"):format(header.error_code)
+ header.error_msg = "ERROR: " .. header.error_msg
+ end
+ header.raw = packet:sub(1,16)
+ return header
+ end,
+
+ --- Reads a AFP packet of the socket
+ --
+ -- @return Response object
+ read_fp_packet = function( self )
+
+ local packet = {}
+ local buf = ""
+ local status, response
+
+ status, buf = self.socket:receive_bytes(16)
+ if ( not status ) then
+ response = Response:new()
+ response:setErrorCode(ERROR.SocketError)
+ response:setErrorMessage(buf)
+ return response
+ end
+
+ packet.header = self:parse_fp_header( buf )
+ while buf:len() < packet.header.length + packet.header.raw:len() do
+ local tmp
+ status, tmp = self.socket:receive_bytes( packet.header.length + 16 - buf:len() )
+ if not status then
+ response = Response:new()
+ response:setErrorCode(ERROR.SocketError)
+ response:setErrorMessage(buf)
+ return response
+ end
+ buf = buf .. tmp
+ end
+
+ packet.data = buf:len() > 16 and buf:sub( 17 ) or ""
+ response = Response:new()
+ response:setErrorCode(packet.header.error_code)
+ response:setPacket(packet)
+
+ return response
+ end,
+
+ --- Sends the raw packet over the socket
+ --
+ -- @param packet containing the raw data
+ -- @return Response object
+ send_fp_packet = function( self, packet )
+ return self.socket:send(packet)
+ end,
+
+ --- Sends an DSIOpenSession request to the server and handles the response
+ --
+ -- @return Response object
+ dsi_open_session = function( self, host, port )
+ local data_offset = 0
+ local option = 0x01 -- Attention Quantum
+ local option_len = 4
+ local quantum = 1024
+ local data, packet, status
+
+ data = string.pack( ">BBI4", option, option_len, quantum )
+ packet = self:create_fp_packet( REQUEST.OpenSession, data_offset, data )
+
+ self:send_fp_packet( packet )
+ return self:read_fp_packet()
+ end,
+
+ --- Sends an DSICloseSession request to the server and handles the response
+ dsi_close_session = function( self )
+ local data_offset = 0
+ local option = 0x01 -- Attention Quantum
+ local option_len = 4
+ local quantum = 1024
+ local data, packet, status
+
+ data = ""
+ packet = self:create_fp_packet( REQUEST.CloseSession, data_offset, data )
+
+ self:send_fp_packet( packet )
+ end,
+
+ -- Sends an FPCopyFile request to the server
+ --
+ -- @param src_vol number containing the ID of the src file volume
+ -- @param srd_did number containing the directory id of the src file
+ -- @param src_path string containing the file path/name of the src file
+ -- @param dst_vol number containing the ID of the dst file volume
+ -- @param dst_did number containing the id of the dest. directory
+ -- @param dst_path string containing the dest path (can be nil or "")
+ -- @param new_name string containing the new name of the destination
+ -- @return Response object
+ fp_copy_file = function(self, src_vol, src_did, src_path, dst_vol, dst_did, dst_path, new_name )
+ local data_offset = 0
+ local unicode_names, unicode_hint = 0x03, 0x08000103
+ local data, packet, response
+
+ -- make sure we have empty names rather than nil values
+ local dst_path = dst_path or ""
+ local src_path = src_path or ""
+ local new_name = new_name or ""
+
+ data = string.pack(">BxI2I4I2I4", COMMAND.FPCopyFile, src_vol, src_did, dst_vol, dst_did )
+ .. encode_path({type=PATH_TYPE.UTF8Name, name=src_path})
+ .. encode_path({type=PATH_TYPE.UTF8Name, name=dst_path})
+ .. encode_path({type=PATH_TYPE.UTF8Name, name=new_name})
+
+ packet = self:create_fp_packet( REQUEST.Command, data_offset, data )
+ self:send_fp_packet( packet )
+ return self:read_fp_packet()
+ end,
+
+ --- Sends an GetStatus DSI request (which is basically a FPGetSrvrInfo
+ -- AFP request) to the server and handles the response
+ --
+ -- @return status (true or false)
+ -- @return table with server information (if status is true) or error string
+ -- (if status is false)
+ fp_get_server_info = function(self)
+ local packet
+ local data_offset = 0
+ local response, result = {}, {}
+ local offsets = {}
+ local pos
+ local status
+
+ local data = string.pack("Bx", COMMAND.FPGetSrvrInfo)
+ packet = self:create_fp_packet(REQUEST.GetStatus, data_offset, data)
+ self:send_fp_packet(packet)
+ response = self:read_fp_packet()
+
+ if response:getErrorCode() ~= ERROR.FPNoErr then
+ return response
+ end
+
+ packet = response.packet
+
+ -- parse and store the offsets in the 'header'
+ offsets.machine_type, offsets.afp_version_count,
+ offsets.uam_count, offsets.volume_icon_and_mask, pos
+ = string.unpack(">I2I2I2I2", packet.data)
+
+ -- the flags are directly in the 'header'
+ result.flags = {}
+ result.flags.raw, pos = string.unpack(">I2", packet.data, pos)
+
+ -- the short server name is stored directly in the 'header' as
+ -- well
+ result.server_name, pos = string.unpack("s1", packet.data, pos)
+
+ -- Server offset should begin at an even boundary see link below
+ -- http://developer.apple.com/mac/library/documentation/Networking/Reference/AFP_Reference/Reference/reference.html#//apple_ref/doc/uid/TP40003548-CH3-CHDIEGED
+ if (pos + 1) % 2 ~= 0 then
+ pos = pos + 1
+ end
+
+ -- and some more offsets
+ offsets.server_signature, offsets.network_addresses_count,
+ offsets.directory_names_count, offsets.utf8_server_name, pos
+ = string.unpack(">I2I2I2I2", packet.data, pos)
+
+ -- this sets up all the server flags in the response table as booleans
+ result.flags.SuperClient = flag_is_set(result.flags.raw, SERVERFLAGS.SuperClient)
+ result.flags.UUIDs = flag_is_set(result.flags.raw, SERVERFLAGS.UUIDs)
+ result.flags.UTF8ServerName = flag_is_set(result.flags.raw, SERVERFLAGS.UTF8ServerName)
+ result.flags.OpenDirectory = flag_is_set(result.flags.raw, SERVERFLAGS.OpenDirectory)
+ result.flags.Reconnect = flag_is_set(result.flags.raw, SERVERFLAGS.Reconnect)
+ result.flags.ServerNotifications = flag_is_set(result.flags.raw, SERVERFLAGS.ServerNotifications)
+ result.flags.TCPoverIP = flag_is_set(result.flags.raw, SERVERFLAGS.TCPoverIP)
+ result.flags.ServerSignature = flag_is_set(result.flags.raw, SERVERFLAGS.ServerSignature)
+ result.flags.ServerMessages = flag_is_set(result.flags.raw, SERVERFLAGS.ServerMessages)
+ result.flags.NoPasswordSaving = flag_is_set(result.flags.raw, SERVERFLAGS.NoPasswordSaving)
+ result.flags.ChangeablePasswords = flag_is_set(result.flags.raw, SERVERFLAGS.ChangeablePasswords)
+ result.flags.CopyFile = flag_is_set(result.flags.raw, SERVERFLAGS.CopyFile)
+
+ -- store the machine type
+ result.machine_type = string.unpack("s1", packet.data, offsets.machine_type + 1)
+
+ -- this tells us the number of afp versions supported
+ result.afp_version_count, pos = string.unpack("B", packet.data, offsets.afp_version_count + 1)
+
+ -- now we loop through them all, storing for the response
+ result.afp_versions = {}
+ for i = 1,result.afp_version_count do
+ local v
+ v, pos = string.unpack("s1", packet.data, pos)
+ table.insert(result.afp_versions, v)
+ end
+
+ -- same idea as the afp versions here
+ result.uam_count, pos = string.unpack("B", packet.data, offsets.uam_count + 1)
+
+ result.uams = {}
+ for i = 1,result.uam_count do
+ local uam
+ uam, pos = string.unpack("s1", packet.data, pos)
+ table.insert(result.uams, uam)
+ end
+
+ -- volume_icon_and_mask would normally be parsed out here,
+ -- however the apple docs say it is deprecated in Mac OS X, so
+ -- we don't bother with it
+
+ -- server signature is 16 bytes
+ result.server_signature = string.sub(packet.data, offsets.server_signature + 1, offsets.server_signature + 16)
+
+ -- this is the same idea as afp_version and uam above
+ result.network_addresses_count, pos = string.unpack("B", packet.data, offsets.network_addresses_count + 1)
+
+ result.network_addresses = {}
+
+ -- gets a little complicated in here, basically each entry has
+ -- a length byte, a tag byte, and then the data. We parse
+ -- differently based on the tag
+ for i = 1, result.network_addresses_count do
+ local length
+ local tag
+
+ length, tag, pos = string.unpack("BB", packet.data, pos)
+
+ if tag == 0x00 then
+ -- reserved, shouldn't ever come up, maybe this should
+ -- return an error? maybe not, lets just ignore this
+ elseif tag == 0x01 then
+ -- four byte ip
+ local ip
+ ip, pos = string.unpack("c4", packet.data, pos)
+ table.insert(result.network_addresses, ipOps.str_to_ip(ip))
+ elseif tag == 0x02 then
+ -- four byte ip and two byte port
+ local ip, port
+ ip, port, pos = string.unpack("c4 >I2", packet.data, pos)
+ table.insert(result.network_addresses, string.format("%s:%d", ipOps.str_to_ip(ip), port))
+ elseif tag == 0x03 then
+ -- ddp address (two byte network, one byte
+ -- node, one byte socket) not tested, anyone
+ -- use ddp anymore?
+ local network, node, socket
+ network, node, socket, pos = string.unpack(">I2BB", packet.data, pos)
+ table.insert(result.network_addresses, string.format("ddp %d.%d:%d", network, node, socket))
+ elseif tag == 0x04 then
+ -- dns name (string)
+ local temp
+ temp, pos = string.unpack("z", packet.data:sub(1,pos+length-3), pos)
+ table.insert(result.network_addresses, temp)
+ elseif tag == 0x05 then
+ -- four byte ip and two byte port, client
+ -- should use ssh. not tested, should work as it
+ -- is the same as tag 0x02
+ local ip, port
+ ip, port, pos = string.unpack("c4 >I2", packet.data, pos)
+ table.insert(result.network_addresses, string.format("ssh://%s:%d", ipOps.str_to_ip(ip), port))
+ elseif tag == 0x06 then
+ -- 16 byte ipv6
+ -- not tested, but should work (next tag is
+ -- tested)
+ local ip
+ ip, pos = string.unpack("c16", packet.data, pos)
+
+ table.insert(result.network_addresses, ipOps.str_to_ip(ip))
+ elseif tag == 0x07 then
+ -- 16 byte ipv6 and two byte port
+ local ip, port
+ ip, port, pos = string.unpack(">c16 I2", packet.data, pos)
+
+ table.insert(result.network_addresses,
+ string.format("[%s]:%d", ipOps.str_to_ip(ip), port))
+ end
+ end
+
+ -- same idea as the others here
+ result.directory_names_count, pos = string.unpack("B", packet.data, offsets.directory_names_count + 1)
+
+ result.directory_names = {}
+ for i = 1, result.directory_names_count do
+ local dirname
+ dirname, pos = string.unpack("s1", packet.data, pos)
+ table.insert(result.directory_names, dirname)
+ end
+
+ -- only one utf8 server name. note this string has a two-byte length.
+ result.utf8_server_name = string.unpack(">s2", packet.data, offsets.utf8_server_name + 1)
+ response.result = result
+
+ return response
+ end,
+
+
+ --- Sends an FPGetUserInfo AFP request to the server and handles the response
+ --
+ -- @return response object with the following result <code>user_bitmap</code> and
+ -- <code>uid</code> fields
+ fp_get_user_info = function( self )
+
+ local packet, pos, status, response
+ local data_offset = 0
+ local flags = 1 -- Default User
+ local uid = 0
+ local bitmap = USER_BITMAP.UserId
+ local result = {}
+
+ local data = string.pack( ">BBI4I2", COMMAND.FPGetUserInfo, flags, uid, bitmap )
+ packet = self:create_fp_packet( REQUEST.Command, data_offset, data )
+
+ self:send_fp_packet( packet )
+ response = self:read_fp_packet()
+ if response:getErrorCode() ~= ERROR.FPNoErr then
+ return response
+ end
+
+ response.result.user_bitmap, response.result.uid, pos = string.unpack(">I2I4", packet.data)
+
+ return response
+ end,
+
+ --- Sends an FPGetSrvrParms AFP request to the server and handles the response
+ --
+ -- @return response object with the following result <code>server_time</code>,
+ -- <code>vol_count</code>, <code>volumes</code> fields
+ fp_get_srvr_parms = function(self)
+ local packet, status, data
+ local data_offset = 0
+ local response = {}
+ local pos = 0
+ local parms = {}
+
+ data = string.pack("Bx", COMMAND.FPGetSrvParms)
+ packet = self:create_fp_packet( REQUEST.Command, data_offset, data )
+ self:send_fp_packet( packet )
+ response = self:read_fp_packet()
+
+ if response:getErrorCode() ~= ERROR.FPNoErr then
+ return response
+ end
+
+ data = response:getPacketData()
+ parms.server_time, parms.vol_count, pos = string.unpack(">I4B", data)
+
+ parms.volumes = {}
+
+ for i=1, parms.vol_count do
+ local volume_name
+ -- pos+1 to skip over the volume bitmap
+ volume_name, pos = string.unpack("s1", data, pos + 1)
+ table.insert(parms.volumes, string.format("%s", volume_name) )
+ end
+
+ response:setResult(parms)
+
+ return response
+ end,
+
+
+ --- Sends an FPLogin request to the server and handles the response
+ --
+ -- This function currently only supports the 3.1 through 3.3 protocol versions
+ -- It currently supports the following authentication methods:
+ -- o No User Authent
+ -- o DHCAST128
+ --
+ -- The DHCAST128 UAM should work against most servers even though it's
+ -- superceded by the DHX2 UAM.
+ --
+ -- @param afp_version string (AFP3.3|AFP3.2|AFP3.1)
+ -- @param uam string containing authentication information
+ -- @return Response object
+ fp_login = function( self, afp_version, uam, username, password, options )
+ local packet, status, data
+ local data_offset = 0
+ local status, response
+
+ if not HAVE_SSL then
+ response = Response:new()
+ response:setErrorMessage("OpenSSL not available, aborting ...")
+ return response
+ end
+
+ -- currently we only support AFP3.3
+ if afp_version == nil or ( afp_version ~= "AFP3.3" and afp_version ~= "AFP3.2" and afp_version ~= "AFP3.1" ) then
+ response = Response:new()
+ response:setErrorMessage("Incorrect AFP version")
+ return response
+ end
+
+ if ( uam == "No User Authent" ) then
+ data = string.pack( "Bs1s1", COMMAND.FPLogin, afp_version, uam )
+ packet = self:create_fp_packet( REQUEST.Command, data_offset, data )
+ self:send_fp_packet( packet )
+ return self:read_fp_packet( )
+ elseif( uam == "DHCAST128" ) then
+ local dhx_s2civ, dhx_c2civ = 'CJalbert', 'LWallace'
+ local p, g, Ra, Ma, Mb, K, nonce
+ local EncData, PlainText, K_bin, auth_response
+ local Id
+ local username = username or ""
+ local password = password or ""
+
+ username = username .. string.rep('\0', (#username + 1) % 2)
+
+ p = openssl.bignum_hex2bn("BA2873DFB06057D43F2024744CEEE75B")
+ g = openssl.bignum_dec2bn("7")
+ Ra = openssl.bignum_hex2bn("86F6D3C0B0D63E4B11F113A2F9F19E3BBBF803F28D30087A1450536BE979FD42")
+ Ma = openssl.bignum_mod_exp(g, Ra, p)
+
+ data = string.pack( "Bs1s1s1", COMMAND.FPLogin, afp_version, uam, username) .. openssl.bignum_bn2bin(Ma)
+ packet = self:create_fp_packet( REQUEST.Command, data_offset, data )
+ self:send_fp_packet( packet )
+ response = self:read_fp_packet( )
+ if ( response:getErrorCode() ~= ERROR.FPAuthContinue ) then
+ return response
+ end
+
+ if ( response.packet.header.length ~= 50 ) then
+ response:setErrorMessage("LoginContinue packet contained invalid data")
+ return response
+ end
+
+ Id, Mb, EncData = string.unpack(">I2c16c32", response.packet.data )
+
+ Mb = openssl.bignum_bin2bn( Mb )
+ K = openssl.bignum_mod_exp (Mb, Ra, p)
+ K_bin = openssl.bignum_bn2bin(K)
+ nonce = openssl.decrypt("cast5-cbc", K_bin, dhx_s2civ, EncData, false ):sub(1,16)
+ nonce = openssl.bignum_add( openssl.bignum_bin2bn(nonce), openssl.bignum_dec2bn("1") )
+ PlainText = openssl.bignum_bn2bin(nonce) .. Util.ZeroPad(password, 64)
+ auth_response = openssl.encrypt( "cast5-cbc", K_bin, dhx_c2civ, PlainText, true)
+
+ data = string.pack( ">BBI2", COMMAND.FPLoginCont, 0, Id) .. auth_response
+ packet = self:create_fp_packet( REQUEST.Command, data_offset, data )
+ self:send_fp_packet( packet )
+ response = self:read_fp_packet( )
+ if ( response:getErrorCode() ~= ERROR.FPNoErr ) then
+ return response
+ end
+ return response
+ end
+ response:setErrorMessage("Unsupported uam: " .. uam or "nil")
+ return response
+ end,
+
+ -- Terminates sessions and frees server resources established by FPLoginand FPLoginExt.
+ --
+ -- @return response object
+ fp_logout = function( self )
+ local packet, data, response
+ local data_offset = 0
+
+ data = string.pack("Bx", COMMAND.FPLogout)
+ packet = self:create_fp_packet( REQUEST.Command, data_offset, data )
+ self:send_fp_packet( packet )
+ return self:read_fp_packet( )
+ end,
+
+ --- Sends an FPOpenVol request to the server and handles the response
+ --
+ -- @param bitmap number bitmask of volume information to request
+ -- @param volume_name string containing the volume name to query
+ -- @return response object with the following result <code>bitmap</code> and
+ -- <code>volume_id</code> fields
+ fp_open_vol = function( self, bitmap, volume_name )
+ local packet, status, pos, data
+ local data_offset = 0
+ local response, volume = {}, {}
+
+ data = string.pack(">BxI2s1", COMMAND.FPOpenVol, bitmap, volume_name)
+ packet = self:create_fp_packet( REQUEST.Command, data_offset, data )
+ self:send_fp_packet( packet )
+ response = self:read_fp_packet()
+ if response:getErrorCode() ~= ERROR.FPNoErr then
+ return response
+ end
+
+ volume.bitmap, volume.volume_id, pos = string.unpack(">I2I2", response.packet.data)
+ response:setResult(volume)
+ return response
+ end,
+
+
+ --- Sends an FPGetFileDirParms request to the server and handles the response
+ --
+ -- @param volume_id number containing the id of the volume to query
+ -- @param did number containing the id of the directory to query
+ -- @param file_bitmap number bitmask of file information to query
+ -- @param dir_bitmap number bitmask of directory information to query
+ -- @param path table containing the name and the name encoding type of the directory to query
+ -- @return response object with the following result <code>file_bitmap</code>, <code>dir_bitmap</code>,
+ -- <code>file_type</code> and (<code>dir<code> or <code>file</code> tables) depending on whether
+ -- <code>did</code> is a file or directory
+ fp_get_file_dir_parms = function( self, volume_id, did, file_bitmap, dir_bitmap, path )
+
+ local packet, status, data
+ local data_offset = 0
+ local response, parms = {}, {}
+ local pos
+
+ if ( did == nil ) then
+ response = Response:new()
+ response:setErrorMessage("No Directory Id supplied")
+ return response
+ end
+
+ if ( volume_id == nil ) then
+ response = Response:new()
+ response:setErrorMessage("No Volume Id supplied")
+ return response
+ end
+
+ data = string.pack(">BxI2I4I2I2", COMMAND.FPGetFileDirParams, volume_id, did, file_bitmap, dir_bitmap)
+ .. encode_path(path)
+ packet = self:create_fp_packet( REQUEST.Command, data_offset, data )
+ self:send_fp_packet( packet )
+ response = self:read_fp_packet()
+
+ if response:getErrorCode() ~= ERROR.FPNoErr then
+ return response
+ end
+
+ parms.file_bitmap, parms.dir_bitmap, parms.file_type, pos = string.unpack( ">I2I2Bx", response.packet.data )
+
+ -- file or dir?
+ if ( parms.file_type == 0x80 ) then
+ pos, parms.dir = Util.decode_dir_bitmap( parms.dir_bitmap, response.packet.data, pos )
+ else
+ -- file
+ pos, parms.file = Util.decode_file_bitmap( parms.file_bitmap, response.packet.data, pos )
+ end
+
+ response:setResult(parms)
+ return response
+ end,
+
+ --- Sends an FPEnumerateExt2 request to the server and handles the response
+ --
+ -- @param volume_id number containing the id of the volume to query
+ -- @param did number containing the id of the directory to query
+ -- @param file_bitmap number bitmask of file information to query
+ -- @param dir_bitmap number bitmask of directory information to query
+ -- @param req_count number
+ -- @param start_index number
+ -- @param reply_size number
+ -- @param path table containing the name and the name encoding type of the directory to query
+ -- @return response object with the following result set to a table of tables containing
+ -- <code>file_bitmap</code>, <code>dir_bitmap</code>, <code>req_count</code> fields
+ fp_enumerate_ext2 = function( self, volume_id, did, file_bitmap, dir_bitmap, req_count, start_index, reply_size, path )
+
+ local packet, pos, status
+ local data_offset = 0
+ local response,records = {}, {}
+
+ local data = string.pack( ">BxI2I4I2I2", COMMAND.FPEnumerateExt2, volume_id, did, file_bitmap, dir_bitmap )
+ .. string.pack( ">I2I4I4", req_count, start_index, reply_size)
+ .. encode_path(path)
+ packet = self:create_fp_packet( REQUEST.Command, data_offset, data )
+
+ self:send_fp_packet( packet )
+ response = self:read_fp_packet( )
+
+ if response:getErrorCode() ~= ERROR.FPNoErr then
+ return response
+ end
+
+ file_bitmap, dir_bitmap, req_count, pos = string.unpack(">I2I2I2", response.packet.data)
+
+ records = {}
+
+ for i=1, req_count do
+ local record = {}
+ local len, _, ftype
+
+ len, ftype, pos = string.unpack(">I2Bx", response.packet.data, pos)
+
+ if ( ftype == 0x80 ) then
+ _, record = Util.decode_dir_bitmap( dir_bitmap, response.packet.data, pos )
+ else
+ -- file
+ _, record = Util.decode_file_bitmap( file_bitmap, response.packet.data, pos )
+ end
+
+ if ( len % 2 ) ~= 0 then
+ len = len + 1
+ end
+
+ pos = pos + ( len - 4 )
+
+ record.type = ftype
+ table.insert(records, record)
+ end
+
+ response:setResult(records)
+ return response
+ end,
+
+ --- Sends an FPOpenFork request to the server and handles the response
+ --
+ -- @param flag number
+ -- @param volume_id number containing the id of the volume to query
+ -- @param did number containing the id of the directory to query
+ -- @param file_bitmap number bitmask of file information to query
+ -- @param access_mode number containing bitmask of options from <code>ACCESS_MODE</code>
+ -- @param path string containing the name of the directory to query
+ -- @return response object with the following result contents <code>file_bitmap</code> and <code>fork_id</code>
+ fp_open_fork = function( self, flag, volume_id, did, file_bitmap, access_mode, path )
+
+ local packet
+ local data_offset = 0
+ local response, fork = {}, {}
+
+ local data = string.pack( ">BBI2I4I2I2", COMMAND.FPOpenFork, flag, volume_id, did, file_bitmap, access_mode )
+ .. encode_path(path)
+
+ packet = self:create_fp_packet( REQUEST.Command, data_offset, data )
+ self:send_fp_packet( packet )
+ response = self:read_fp_packet()
+
+ if response:getErrorCode() ~= ERROR.FPNoErr then
+ return response
+ end
+
+ fork.file_bitmap, fork.fork_id = string.unpack(">I2I2", response.packet.data)
+ response:setResult(fork)
+ return response
+ end,
+
+ --- FPCloseFork
+ --
+ -- @param fork number containing the fork to close
+ -- @return response object
+ fp_close_fork = function( self, fork )
+ local packet
+ local data_offset = 0
+ local response = {}
+
+ local data = string.pack( ">BxI2", COMMAND.FPCloseFork, fork )
+
+ packet = self:create_fp_packet( REQUEST.Command, data_offset, data )
+ self:send_fp_packet( packet )
+ return self:read_fp_packet( )
+ end,
+
+ --- FPCreateDir
+ --
+ -- @param vol_id number containing the volume id
+ -- @param dir_id number containing the directory id
+ -- @param path table containing the name and name encoding type of the directory to query
+ -- @return response object
+ fp_create_dir = function( self, vol_id, dir_id, path )
+ local packet
+ local data_offset = 0
+ local response = {}
+
+ local data = string.pack( ">BxI2I4", COMMAND.FPCreateDir, vol_id, dir_id)
+ .. encode_path(path)
+
+ packet = self:create_fp_packet( REQUEST.Command, data_offset, data )
+ self:send_fp_packet( packet )
+ return self:read_fp_packet( )
+ end,
+
+ --- Sends an FPCloseVol request to the server and handles the response
+ --
+ -- @param volume_id number containing the id of the volume to close
+ -- @return response object
+ fp_close_vol = function( self, volume_id )
+ local packet
+ local data_offset = 0
+ local response = {}
+
+ local data = string.pack( ">BxI2", COMMAND.FPCloseVol, volume_id )
+
+ packet = self:create_fp_packet( REQUEST.Command, data_offset, data )
+ self:send_fp_packet( packet )
+ return self:read_fp_packet( )
+ end,
+
+ --- FPReadExt
+ --
+ -- @param fork number containing the open fork
+ -- @param offset number containing the offset from where writing should start. Negative value indicates offset from the end of the fork
+ -- @param count number containing the number of bytes to be written
+ -- @return response object
+ fp_read_ext = function( self, fork, offset, count )
+ local packet, response
+ local data_offset = 0
+ local block_size = 1024
+ local data = string.pack( ">BxI2I8I8", COMMAND.FPReadExt, fork, offset, count )
+
+ packet = self:create_fp_packet( REQUEST.Command, data_offset, data )
+ self:send_fp_packet( packet )
+ response = self:read_fp_packet( )
+
+ if ( response:getErrorCode() == ERROR.FPEOFErr and response.packet.header.length > 0 ) then
+ response:setErrorCode( ERROR.FPNoErr )
+ end
+
+ response:setResult( response.packet.data )
+ return response
+ end,
+
+ --- FPWriteExt
+ --
+ -- @param flag number indicates whether Offset is relative to the beginning or end of the fork.
+ -- @param fork number containing the open fork
+ -- @param offset number containing the offset from where writing should start. Negative value indicates offset from the end of the fork
+ -- @param count number containing the number of bytes to be written
+ -- @param fdata string containing the data to be written
+ -- @return response object
+ fp_write_ext = function( self, flag, fork, offset, count, fdata )
+ local packet
+ local data_offset = 20
+ local data
+
+ if count > fdata:len() then
+ local err = Response:new()
+ err:setErrorMessage("fp_write_ext: Count is greater than the amount of data")
+ return err
+ end
+ if count < 0 then
+ local err = Response:new()
+ err:setErrorMessage("fp_write_ext: Count must exceed zero")
+ return err
+ end
+
+ data = string.pack( ">BBI2I8I8", COMMAND.FPWriteExt, flag, fork, offset, count) .. fdata
+ packet = self:create_fp_packet( REQUEST.Write, data_offset, data )
+ self:send_fp_packet( packet )
+ return self:read_fp_packet( )
+ end,
+
+ --- FPCreateFile
+ --
+ -- @param flag number where 0 indicates a soft create and 1 indicates a hard create.
+ -- @param vol_id number containing the volume id
+ -- @param did number containing the ancestor directory id
+ -- @param path string containing the path, including the volume, path and file name
+ -- @return response object
+ fp_create_file = function(self, flag, vol_id, did, path )
+ local packet
+ local data_offset = 0
+ local data = string.pack(">BBI2I4", COMMAND.FPCreateFile, flag, vol_id, did)
+ .. encode_path(path)
+
+ packet = self:create_fp_packet( REQUEST.Command, data_offset, data )
+ self:send_fp_packet( packet )
+ return self:read_fp_packet()
+ end,
+
+ --- FPMapId
+ --
+ -- @param subfunc number containing the subfunction to call
+ -- @param id number containing th id to translate
+ -- @return response object with the id in the <code>result</code> field
+ fp_map_id = function( self, subfunc, id )
+ local packet, response
+ local data_offset = 0
+ local data = string.pack( "BB", COMMAND.FPMapId, subfunc )
+
+ if ( subfunc == MAP_ID.UserUUIDToUTF8Name or subfunc == MAP_ID.GroupUUIDToUTF8Name ) then
+ data = data .. string.pack(">I8", id)
+ else
+ data = data .. string.pack(">I4", id)
+ end
+
+ packet = self:create_fp_packet( REQUEST.Command, data_offset, data )
+ self:send_fp_packet( packet )
+ response = self:read_fp_packet( )
+
+ if response:getErrorCode() ~= ERROR.FPNoErr then
+ return response
+ end
+
+ -- Netatalk returns the name with 1-byte length prefix,
+ -- Mac OS has a 2-byte (UTF-8) length prefix
+ local len = string.unpack("B", response.packet.data)
+
+ -- if length is zero assume 2-byte length (UTF-8 name)
+ if len == 0 then
+ response:setResult(string.unpack(">s2", response.packet.data))
+ else
+ response:setResult(string.unpack("s1", response.packet.data ))
+ end
+ return response
+ end,
+
+ --- FPMapName
+ --
+ -- @param subfunc number containing the subfunction to call
+ -- @param name string containing name to map
+ -- @return response object with the mapped name in the <code>result</code> field
+ fp_map_name = function( self, subfunc, name )
+ local packet
+ local data_offset = 0
+ local data = string.pack(">BBs2", COMMAND.FPMapName, subfunc, name )
+ local response
+
+ packet = self:create_fp_packet( REQUEST.Command, data_offset, data )
+ self:send_fp_packet( packet )
+ response = self:read_fp_packet( )
+
+ if response:getErrorCode() ~= ERROR.FPNoErr then
+ return response
+ end
+
+ response:setResult(string.unpack(">I4", response.packet.data))
+ return response
+ end,
+}
+
+--- The helper class wraps the protocol class and their functions. It contains
+-- high-level functions with descriptive names, facilitating the use and
+-- minimizing the need to fully understand the AFP low-level protocol details.
+Helper = {
+
+ --- Creates a new helper object
+ new = function(self,o,username,password)
+ local o = {}
+ setmetatable(o, self)
+ self.__index = self
+ o.username = username
+ o.password = password
+ return o
+ end,
+
+ --- Connects to the remote server and establishes a new AFP session
+ --
+ -- @param host table as received by the action function of the script
+ -- @param port table as received by the action function of the script
+ -- @return status boolean
+ -- @return string containing error message (if status is false)
+ OpenSession = function( self, host, port )
+ local status, response
+
+ self.socket = net.Socket:new_socket()
+ status = self.socket:connect(host, port)
+ if not status then
+ return false, "Socket connection failed"
+ end
+
+ self.proto = Proto:new( { socket=self.socket} )
+ response = self.proto:dsi_open_session(self.socket)
+
+ if response:getErrorCode() ~= ERROR.FPNoErr then
+ self.socket:close()
+ return false, response:getErrorMessage()
+ end
+
+ return true
+ end,
+
+ --- Closes the AFP session and then the socket
+ --
+ -- @return status boolean
+ -- @return string containing error message (if status is false)
+ CloseSession = function( self )
+ local status, packet = self.proto:dsi_close_session( )
+ self.socket:close()
+
+ return status, packet
+ end,
+
+ --- Terminates the connection, without closing the AFP session
+ --
+ -- @return status (always true)
+ -- @return string (always "")
+ Terminate = function( self )
+ self.socket:close()
+ return true,""
+ end,
+
+ --- Logs in to an AFP service
+ --
+ -- @param username (optional) string containing the username
+ -- @param password (optional) string containing the user password
+ -- @param options table containing additional options <code>uam</code>
+ Login = function( self, username, password, options )
+ local uam = ( options and options.UAM ) and options.UAM or "DHCAST128"
+ local response
+
+ -- username and password arguments override the ones supplied using the
+ -- script arguments afp.username and afp.password
+ local username = username or self.username
+ local password = password or self.password
+
+ if ( username and uam == "DHCAST128" ) then
+ response = self.proto:fp_login( "AFP3.1", "DHCAST128", username, password )
+ elseif( username ) then
+ return false, ("Unsupported UAM: %s"):format(uam)
+ else
+ response = self.proto:fp_login( "AFP3.1", "No User Authent" )
+ end
+
+ if response:getErrorCode() ~= ERROR.FPNoErr then
+ return false, response:getErrorMessage()
+ end
+
+ return true, "Success"
+ end,
+
+ --- Logs out from the AFP service
+ Logout = function(self)
+ return self.proto:fp_logout()
+ end,
+
+ --- Walks the directory tree specified by <code>str_path</code> and returns the node information
+ --
+ -- @param str_path string containing the directory
+ -- @return status boolean true on success, otherwise false
+ -- @return item table containing node information <code>DirectoryId</code> and <code>DirectoryName</code>
+ WalkDirTree = function( self, str_path )
+ local status, response
+ local elements = stringaux.strsplit( "/", str_path )
+ local f_bm = FILE_BITMAP.NodeId + FILE_BITMAP.ParentDirId + FILE_BITMAP.LongName
+ local d_bm = DIR_BITMAP.NodeId + DIR_BITMAP.ParentDirId + DIR_BITMAP.LongName
+ local item = { DirectoryId = 2 }
+
+ response = self.proto:fp_open_vol( VOL_BITMAP.ID, elements[1] )
+ if response:getErrorCode() ~= ERROR.FPNoErr then
+ return false, response:getErrorMessage()
+ end
+
+ item.VolumeId = response.result.volume_id
+ item.DirectoryName = str_path
+
+ for i=2, #elements do
+ local path = { type=PATH_TYPE.LongName, name=elements[i] }
+ response = self.proto:fp_get_file_dir_parms( item.VolumeId, item.DirectoryId, f_bm, d_bm, path )
+ if response:getErrorCode() ~= ERROR.FPNoErr then
+ return false, response:getErrorMessage()
+ end
+ item.DirectoryId = response.result.dir.NodeId
+ item.DirectoryName = response.result.dir.LongName
+ end
+
+ return true, item
+ end,
+
+ --- Reads a file on the AFP server
+ --
+ -- @param str_path string containing the AFP sharepoint, path and filename eg. HR/Documents/File.doc
+ -- @return status boolean true on success, false on failure
+ -- @return content string containing the file contents
+ ReadFile = function( self, str_path )
+ local status, response, fork, content, vol_name
+ local offset, count, did = 0, 1024, 2
+ local status, path, vol_id
+ local p = Util.SplitPath( str_path )
+
+ status, response = self:WalkDirTree( p.dir )
+ if ( not status ) then
+ return false, response
+ end
+
+ vol_id = response.VolumeId
+ did = response.DirectoryId
+
+ path = { type=PATH_TYPE.LongName, name=p.file }
+
+ response = self.proto:fp_open_fork(0, vol_id, did, 0, ACCESS_MODE.Read, path )
+ if response:getErrorCode() ~= ERROR.FPNoErr then
+ return false, response:getErrorMessage()
+ end
+
+ fork = response.result.fork_id
+ content = ""
+
+ while true do
+ response = self.proto:fp_read_ext( fork, offset, count )
+ if response:getErrorCode() ~= ERROR.FPNoErr then
+ break
+ end
+ content = content .. response.result
+ offset = offset + count
+ end
+
+ response = self.proto:fp_close_fork( fork )
+ if response:getErrorCode() ~= ERROR.FPNoErr then
+ return false, response:getErrorMessage()
+ end
+
+ return true, content
+ end,
+
+ --- Writes a file to the AFP server
+ --
+ -- @param str_path string containing the AFP sharepoint, path and filename eg. HR/Documents/File.doc
+ -- @param fdata string containing the data to write to the file
+ -- @return status boolean true on success, false on failure
+ -- @return error string containing error message if status is false
+ WriteFile = function( self, str_path, fdata )
+ local status, response, fork, content
+ local offset, count = 1, 1024
+ local status, vol_id, did, path
+ local p = Util.SplitPath( str_path )
+
+ status, response = self:WalkDirTree( p.dir )
+ vol_id = response.VolumeId
+ did = response.DirectoryId
+
+ if ( not status ) then
+ return false, response
+ end
+
+ path = { type=PATH_TYPE.LongName, name=p.file }
+
+ status, response = self.proto:fp_create_file( 0, vol_id, did, path )
+ if not status then
+ if ( response.header.error_code ~= ERROR.FPObjectExists ) then
+ return false, response.header.error_msg
+ end
+ end
+
+ response = self.proto:fp_open_fork( 0, vol_id, did, 0, ACCESS_MODE.Write, path )
+ if response:getErrorCode() ~= ERROR.FPNoErr then
+ return false, response:getErrorMessage()
+ end
+
+ fork = response.result.fork_id
+
+ response = self.proto:fp_write_ext( 0, fork, 0, fdata:len(), fdata )
+
+ return true, nil
+ end,
+
+ --- Maps a user id (uid) to a user name
+ --
+ -- @param uid number containing the uid to resolve
+ -- @return status boolean true on success, false on failure
+ -- @return username string on success
+ -- error string on failure
+ UIDToName = function( self, uid )
+ local response = self.proto:fp_map_id( MAP_ID.UserIDToName, uid )
+ if response:getErrorCode() ~= ERROR.FPNoErr then
+ return false, response:getErrorMessage()
+ end
+ return true, response.result
+ end,
+
+ --- Maps a group id (gid) to group name
+ --
+ -- @param gid number containing the gid to lookup
+ -- @return status boolean true on success, false on failure
+ -- @return groupname string on success
+ -- error string on failure
+ GIDToName = function( self, gid )
+ local response = self.proto:fp_map_id( MAP_ID.GroupIDToName, gid )
+ if response:getErrorCode() ~= ERROR.FPNoErr then
+ return false, response:getErrorMessage()
+ end
+ return true, response.result
+ end,
+
+ --- Maps a username to a UID
+ --
+ -- @param name string containing the username to map to an UID
+ -- @return status boolean true on success, false on failure
+ -- @return UID number on success
+ -- error string on failure
+ NameToUID = function( self, name )
+ local response = self.proto:fp_map_name( MAP_NAME.NameToUserID, name )
+ if response:getErrorCode() ~= ERROR.FPNoErr then
+ return false, response:getErrorMessage()
+ end
+ return true, response.result
+ end,
+
+ --- List the contents of a directory
+ --
+ -- @param str_path string containing the sharepoint and directory names
+ -- @param options table options containing zero or more of the options
+ -- <code>max_depth</code> and <code>dironly</code>
+ -- @param depth number containing the current depth (used when called recursively)
+ -- @param parent table containing information about the parent object (used when called recursively)
+ -- @return status boolean true on success, false on failure
+ -- @return dir table containing a table for each directory item with the following:
+ -- <code>type</code>, <code>name</code>, <code>id</code>,
+ -- <code>fsize</code>, <code>uid</code>, <code>gid</code>,
+ -- <code>privs</code>, <code>create</code>, <code>modify</code>
+ Dir = function( self, str_path, options, depth, parent )
+ local status, result
+ local depth = depth or 1
+ local options = options or { max_depth = 1 }
+ local response, records
+ local f_bm = FILE_BITMAP.NodeId | FILE_BITMAP.ParentDirId
+ | FILE_BITMAP.LongName | FILE_BITMAP.UnixPrivileges
+ | FILE_BITMAP.CreationDate | FILE_BITMAP.ModificationDate
+ | FILE_BITMAP.ExtendedDataForkSize
+ local d_bm = DIR_BITMAP.NodeId | DIR_BITMAP.ParentDirId
+ | DIR_BITMAP.LongName | DIR_BITMAP.UnixPrivileges
+ | DIR_BITMAP.CreationDate | DIR_BITMAP.ModificationDate
+
+ local TYPE_DIR = 0x80
+
+ if ( parent == nil ) then
+ status, response = self:WalkDirTree( str_path )
+ if ( not status ) then
+ return false, response
+ end
+
+ parent = {}
+ parent.vol_id = response.VolumeId
+ parent.did = response.DirectoryId
+ parent.dir_name = response.DirectoryName or ""
+ parent.out_tbl = {}
+ end
+
+ if ( options and options.max_depth and options.max_depth > 0 and options.max_depth < depth ) then
+ return false, "Max Depth Reached"
+ end
+
+ local path = { type=PATH_TYPE.LongName, name="" }
+ response = self.proto:fp_enumerate_ext2( parent.vol_id, parent.did, f_bm, d_bm, 1000, 1, 1000 * 300, path)
+
+ if response:getErrorCode() ~= ERROR.FPNoErr then
+ return false, response:getErrorMessage()
+ end
+
+ records = response.result or {}
+ local dir_items = {}
+
+ for _, record in ipairs( records ) do
+ local isdir = record.type == TYPE_DIR
+ -- Skip non-directories if option "dironly" is set
+ if isdir or not options.dironly then
+ local item = {type = record.type,
+ name = record.LongName,
+ id = record.NodeId,
+ fsize = record.ExtendedDataForkSize or 0}
+ local privs = (record.UnixPrivileges or {}).ua_permissions
+ if privs then
+ item.uid = record.UnixPrivileges.uid
+ item.gid = record.UnixPrivileges.gid
+ item.privs = (isdir and "d" or "-") .. Util.decode_unix_privs(privs)
+ end
+ item.create = Util.time_to_string(record.CreationDate)
+ item.modify = Util.time_to_string(record.ModificationDate)
+ table.insert( dir_items, item )
+ end
+ if isdir then
+ self:Dir("", options, depth + 1, { vol_id = parent.vol_id, did=record.NodeId, dir_name=record.LongName, out_tbl=dir_items} )
+ end
+ end
+
+ table.insert( parent.out_tbl, dir_items )
+
+ return true, parent.out_tbl
+ end,
+
+ --- Displays a directory tree
+ --
+ -- @param str_path string containing the sharepoint and the directory
+ -- @param options table options containing zero or more of the options
+ -- <code>max_depth</code> and <code>dironly</code>
+ -- @return dirtree table containing the directories
+ DirTree = function( self, str_path, options )
+ local options = options or {}
+ options.dironly = true
+ return self:Dir( str_path, options )
+ end,
+
+ --- List the AFP sharepoints
+ --
+ -- @return volumes table containing the sharepoints
+ ListShares = function( self )
+ local response
+ response = self.proto:fp_get_srvr_parms( )
+
+ if response:getErrorCode() ~= ERROR.FPNoErr then
+ return false, response:getErrorMessage()
+ end
+
+ return true, response.result.volumes
+ end,
+
+ --- Determine the sharepoint permissions
+ --
+ -- @param vol_name string containing the name of the volume
+ -- @return status boolean true on success, false on failure
+ -- @return acls table containing the volume acls as returned by <code>acls_to_long_string</code>
+ GetSharePermissions = function( self, vol_name )
+ local status, response, acls
+
+ response = self.proto:fp_open_vol( VOL_BITMAP.ID, vol_name )
+
+ if response:getErrorCode() == ERROR.FPNoErr then
+ local vol_id = response.result.volume_id
+ local path = { type = PATH_TYPE.LongName, name = "" }
+
+ response = self.proto:fp_get_file_dir_parms( vol_id, 2, FILE_BITMAP.ALL, DIR_BITMAP.ALL, path )
+ if response:getErrorCode() == ERROR.FPNoErr then
+ if ( response.result.dir and response.result.dir.AccessRights ) then
+ acls = Util.acls_to_long_string(response.result.dir.AccessRights)
+ acls.name = nil
+ end
+ end
+ self.proto:fp_close_vol( vol_id )
+ end
+
+ return true, acls
+ end,
+
+ --- Gets the Unix permissions of a file
+ -- @param vol_name string containing the name of the volume
+ -- @param str_path string containing the name of the file
+ -- @return status true on success, false on failure
+ -- @return acls table (on success) containing the following fields
+ -- <code>uid</code> - a numeric user identifier
+ -- <code>gid</code> - a numeric group identifier
+ -- <code>privs</code> - a string value representing the permissions
+ -- eg: drwx------
+ -- @return err string (on failure) containing the error message
+ GetFileUnixPermissions = function(self, vol_name, str_path)
+ local response = self.proto:fp_open_vol( VOL_BITMAP.ID, vol_name )
+
+ if ( response:getErrorCode() ~= ERROR.FPNoErr ) then
+ return false, response:getErrorMessage()
+ end
+
+ local vol_id = response.result.volume_id
+ local path = { type = PATH_TYPE.LongName, name = str_path }
+ response = self.proto:fp_get_file_dir_parms( vol_id, 2, FILE_BITMAP.UnixPrivileges, DIR_BITMAP.UnixPrivileges, path )
+ if ( response:getErrorCode() ~= ERROR.FPNoErr ) then
+ return false, response:getErrorMessage()
+ end
+
+ local item = response.result.file or response.result.dir
+ local item_type = ( response.result.file ) and "-" or "d"
+ local privs = item.UnixPrivileges and item.UnixPrivileges.ua_permissions
+ if ( privs ) then
+ local uid = item.UnixPrivileges.uid
+ local gid = item.UnixPrivileges.gid
+ local str_privs = item_type .. Util.decode_unix_privs(privs)
+ return true, { uid = uid, gid = gid, privs = str_privs }
+ end
+ end,
+
+ --- Gets the Unix permissions of a file
+ -- @param vol_name string containing the name of the volume
+ -- @param str_path string containing the name of the file
+ -- @return status true on success, false on failure
+ -- @return size containing the size of the file in bytes
+ -- @return err string (on failure) containing the error message
+ GetFileSize = function( self, vol_name, str_path )
+ local response = self.proto:fp_open_vol( VOL_BITMAP.ID, vol_name )
+
+ if ( response:getErrorCode() ~= ERROR.FPNoErr ) then
+ return false, response:getErrorMessage()
+ end
+
+ local vol_id = response.result.volume_id
+ local path = { type = PATH_TYPE.LongName, name = str_path }
+ response = self.proto:fp_get_file_dir_parms( vol_id, 2, FILE_BITMAP.ExtendedDataForkSize, 0, path )
+ if ( response:getErrorCode() ~= ERROR.FPNoErr ) then
+ return false, response:getErrorMessage()
+ end
+
+ return true, response.result.file and response.result.file.ExtendedDataForkSize or 0
+ end,
+
+
+ --- Returns the creation, modification and backup dates of a file
+ -- @param vol_name string containing the name of the volume
+ -- @param str_path string containing the name of the file
+ -- @return status true on success, false on failure
+ -- @return dates table containing the following fields:
+ -- <code>create</code> - Creation date of the file
+ -- <code>modify</code> - Modification date of the file
+ -- <code>backup</code> - Date of last backup
+ -- @return err string (on failure) containing the error message
+ GetFileDates = function( self, vol_name, str_path )
+ local response = self.proto:fp_open_vol( VOL_BITMAP.ID, vol_name )
+
+ if ( response:getErrorCode() ~= ERROR.FPNoErr ) then
+ return false, response:getErrorMessage()
+ end
+
+ local vol_id = response.result.volume_id
+ local path = { type = PATH_TYPE.LongName, name = str_path }
+ local f_bm = FILE_BITMAP.CreationDate + FILE_BITMAP.ModificationDate + FILE_BITMAP.BackupDate
+ local d_bm = DIR_BITMAP.CreationDate + DIR_BITMAP.ModificationDate + DIR_BITMAP.BackupDate
+ response = self.proto:fp_get_file_dir_parms( vol_id, 2, f_bm, d_bm, path )
+ if ( response:getErrorCode() ~= ERROR.FPNoErr ) then
+ return false, response:getErrorMessage()
+ end
+
+ local item = response.result.file or response.result.dir
+
+ local create = Util.time_to_string(item.CreationDate)
+ local backup = Util.time_to_string(item.BackupDate)
+ local modify = Util.time_to_string(item.ModificationDate)
+
+ return true, { create = create, backup = backup, modify = modify }
+ end,
+
+ --- Creates a new directory on the AFP sharepoint
+ --
+ -- @param str_path containing the sharepoint and the directory
+ -- @return status boolean true on success, false on failure
+ -- @return dirId number containing the new directory id
+ CreateDir = function( self, str_path )
+ local status, response, vol_id, did
+ local p = Util.SplitPath( str_path )
+ local path = { type=PATH_TYPE.LongName, name=p.file }
+
+
+ status, response = self:WalkDirTree( p.dir )
+ if not status then
+ return false, response
+ end
+
+ response = self.proto:fp_create_dir( response.VolumeId, response.DirectoryId, path )
+ if response:getErrorCode() ~= ERROR.FPNoErr then
+ return false, response:getErrorMessage()
+ end
+
+ return true, response
+ end,
+
+}
+
+--- Util class, containing some static functions used by Helper and Proto
+Util =
+{
+ --- Pads a string with zeroes
+ --
+ -- @param str string containing the string to be padded
+ -- @param len number containing the length of the new string
+ -- @return str string containing the new string
+ ZeroPad = function( str, len )
+ return str .. string.rep('\0', len - str:len())
+ end,
+
+ --- Splits a path into two pieces, directory and file
+ --
+ -- @param str_path string containing the path to split
+ -- @return dir table containing <code>dir</code> and <code>file</code>
+ SplitPath = function( str_path )
+ local elements = stringaux.strsplit("/", str_path)
+ local dir, file = "", ""
+
+ if #elements < 2 then
+ return nil
+ end
+
+ file = elements[#elements]
+
+ table.remove( elements, #elements )
+ dir = table.concat( elements, "/" )
+
+ return { ['dir']=dir, ['file']=file }
+
+ end,
+
+ --- Converts a group bitmask of Search, Read and Write to table
+ --
+ -- @param acls number containing bitmasked acls
+ -- @return table of ACLs
+ acl_group_to_long_string = function(acls)
+
+ local acl_table = {}
+
+ if ( acls & ACLS.OwnerSearch ) == ACLS.OwnerSearch then
+ table.insert( acl_table, "Search")
+ end
+
+ if ( acls & ACLS.OwnerRead ) == ACLS.OwnerRead then
+ table.insert( acl_table, "Read")
+ end
+
+ if ( acls & ACLS.OwnerWrite ) == ACLS.OwnerWrite then
+ table.insert( acl_table, "Write")
+ end
+
+ return acl_table
+ end,
+
+
+ --- Converts a numeric acl to string
+ --
+ -- @param acls number containing acls as received from <code>fp_get_file_dir_parms</code>
+ -- @return table of long ACLs
+ acls_to_long_string = function( acls )
+
+ local owner = Util.acl_group_to_long_string( ( acls & 255 ) )
+ local group = Util.acl_group_to_long_string( ( (acls >> 8) & 255 ) )
+ local everyone = Util.acl_group_to_long_string( ( (acls >> 16) & 255 ) )
+ local user = Util.acl_group_to_long_string( ( (acls >> 24) & 255 ) )
+
+ local blank = ( acls & ACLS.BlankAccess ) == ACLS.BlankAccess and "Blank" or nil
+ local isowner = ( acls & ACLS.UserIsOwner ) == ACLS.UserIsOwner and "IsOwner" or nil
+
+ local options = {}
+
+ if blank then
+ table.insert(options, "Blank")
+ end
+
+ if isowner then
+ table.insert(options, "IsOwner")
+ end
+
+ local acls_tbl = {}
+
+ table.insert( acls_tbl, string.format( "Owner: %s", table.concat(owner, ",") ) )
+ table.insert( acls_tbl, string.format( "Group: %s", table.concat(group, ",") ) )
+ table.insert( acls_tbl, string.format( "Everyone: %s", table.concat(everyone, ",") ) )
+ table.insert( acls_tbl, string.format( "User: %s", table.concat(user, ",") ) )
+
+ if #options > 0 then
+ table.insert( acls_tbl, string.format( "Options: %s", table.concat(options, ",") ) )
+ end
+
+ return acls_tbl
+
+ end,
+
+
+ --- Converts AFP file timestamp to a standard text format
+ --
+ -- @param timestamp value returned by FPEnumerateExt2 or FPGetFileDirParms
+ -- @return string representing the timestamp
+ time_to_string = function (timestamp)
+ return timestamp and datetime.format_timestamp(timestamp + TIME_OFFSET) or nil
+ end,
+
+
+ --- Decodes the UnixPrivileges.ua_permissions value
+ --
+ -- @param privs number containing the UnixPrivileges.ua_permissions value
+ -- @return string containing the ACL characters
+ decode_unix_privs = function( privs )
+ local owner = ( ( privs & ACLS.OwnerRead ) == ACLS.OwnerRead ) and "r" or "-"
+ owner = owner .. (( ( privs & ACLS.OwnerWrite ) == ACLS.OwnerWrite ) and "w" or "-")
+ owner = owner .. (( ( privs & ACLS.OwnerSearch ) == ACLS.OwnerSearch ) and "x" or "-")
+
+ local group = ( ( privs & ACLS.GroupRead ) == ACLS.GroupRead ) and "r" or "-"
+ group = group .. (( ( privs & ACLS.GroupWrite ) == ACLS.GroupWrite ) and "w" or "-")
+ group = group .. (( ( privs & ACLS.GroupSearch ) == ACLS.GroupSearch ) and "x" or "-")
+
+ local other = ( ( privs & ACLS.EveryoneRead ) == ACLS.EveryoneRead ) and "r" or "-"
+ other = other .. (( ( privs & ACLS.EveryoneWrite ) == ACLS.EveryoneWrite ) and "w" or "-")
+ other = other .. (( ( privs & ACLS.EveryoneSearch ) == ACLS.EveryoneSearch ) and "x" or "-")
+
+ return owner .. group .. other
+ end,
+
+
+ --- Decodes a file bitmap
+ --
+ -- @param bitmap number containing the bitmap
+ -- @param data string containing the data to be decoded
+ -- @param pos number containing the offset into data
+ -- @return pos number containing the new offset after decoding
+ -- @return file table containing the decoded values
+ decode_file_bitmap = function( bitmap, data, pos )
+ local origpos = pos
+ local file = {}
+
+ if ( ( bitmap & FILE_BITMAP.Attributes ) == FILE_BITMAP.Attributes ) then
+ file.Attributes, pos = string.unpack(">I2", data, pos )
+ end
+ if ( ( bitmap & FILE_BITMAP.ParentDirId ) == FILE_BITMAP.ParentDirId ) then
+ file.ParentDirId, pos = string.unpack(">I4", data, pos )
+ end
+ if ( ( bitmap & FILE_BITMAP.CreationDate ) == FILE_BITMAP.CreationDate ) then
+ file.CreationDate, pos = string.unpack(">I4", data, pos )
+ end
+ if ( ( bitmap & FILE_BITMAP.ModificationDate ) == FILE_BITMAP.ModificationDate ) then
+ file.ModificationDate, pos = string.unpack(">I4", data, pos )
+ end
+ if ( ( bitmap & FILE_BITMAP.BackupDate ) == FILE_BITMAP.BackupDate ) then
+ file.BackupDate, pos = string.unpack(">I4", data, pos )
+ end
+ if ( ( bitmap & FILE_BITMAP.FinderInfo ) == FILE_BITMAP.FinderInfo ) then
+ file.FinderInfo, pos = string.unpack("c32", data, pos )
+ end
+ if ( ( bitmap & FILE_BITMAP.LongName ) == FILE_BITMAP.LongName ) then
+ local offset
+ offset, pos = string.unpack(">I2", data, pos)
+ if offset > 0 then
+ file.LongName = string.unpack("s1", data, origpos + offset)
+ end
+ end
+ if ( ( bitmap & FILE_BITMAP.ShortName ) == FILE_BITMAP.ShortName ) then
+ local offset
+ offset, pos = string.unpack(">I2", data, pos)
+ if offset > 0 then
+ file.ShortName = string.unpack("s1", data, origpos + offset)
+ end
+ end
+ if ( ( bitmap & FILE_BITMAP.NodeId ) == FILE_BITMAP.NodeId ) then
+ file.NodeId, pos = string.unpack(">I4", data, pos )
+ end
+ if ( ( bitmap & FILE_BITMAP.DataForkSize ) == FILE_BITMAP.DataForkSize ) then
+ file.DataForkSize, pos = string.unpack(">I4", data, pos )
+ end
+ if ( ( bitmap & FILE_BITMAP.ResourceForkSize ) == FILE_BITMAP.ResourceForkSize ) then
+ file.ResourceForkSize, pos = string.unpack(">I4", data, pos )
+ end
+ if ( ( bitmap & FILE_BITMAP.ExtendedDataForkSize ) == FILE_BITMAP.ExtendedDataForkSize ) then
+ file.ExtendedDataForkSize, pos = string.unpack(">I8", data, pos )
+ end
+ if ( ( bitmap & FILE_BITMAP.LaunchLimit ) == FILE_BITMAP.LaunchLimit ) then
+ -- should not be set as it's deprecated according to:
+ -- http://developer.apple.com/mac/library/documentation/Networking/Reference/AFP_Reference/Reference/reference.html#//apple_ref/doc/c_ref/kFPLaunchLimitBit
+ end
+ if ( ( bitmap & FILE_BITMAP.UTF8Name ) == FILE_BITMAP.UTF8Name ) then
+ local offset
+ offset, pos = string.unpack(">I2", data, pos)
+ if offset > 0 then
+ -- +4 to skip over the encoding hint
+ file.UTF8Name = string.unpack(">s2", data, origpos + offset + 4)
+ end
+ -- Skip over the trailing pad
+ pos = pos + 4
+ end
+ if ( ( bitmap & FILE_BITMAP.ExtendedResourceForkSize ) == FILE_BITMAP.ExtendedResourceForkSize ) then
+ file.ExtendedResourceForkSize, pos = string.unpack(">I8", data, pos )
+ end
+ if ( ( bitmap & FILE_BITMAP.UnixPrivileges ) == FILE_BITMAP.UnixPrivileges ) then
+ local unixprivs = {}
+ unixprivs.uid, unixprivs.gid, unixprivs.permissions, unixprivs.ua_permissions, pos = string.unpack(">I4I4I4I4", data, pos)
+ file.UnixPrivileges = unixprivs
+ end
+ return pos, file
+ end,
+
+ --- Decodes a directory bitmap
+ --
+ -- @param bitmap number containing the bitmap
+ -- @param data string containing the data to be decoded
+ -- @param pos number containing the offset into data
+ -- @return pos number containing the new offset after decoding
+ -- @return dir table containing the decoded values
+ decode_dir_bitmap = function( bitmap, data, pos )
+ local origpos = pos
+ local dir = {}
+
+ if ( ( bitmap & DIR_BITMAP.Attributes ) == DIR_BITMAP.Attributes ) then
+ dir.Attributes, pos = string.unpack(">I2", data, pos)
+ end
+ if ( ( bitmap & DIR_BITMAP.ParentDirId ) == DIR_BITMAP.ParentDirId ) then
+ dir.ParentDirId, pos = string.unpack(">I4", data, pos)
+ end
+ if ( ( bitmap & DIR_BITMAP.CreationDate ) == DIR_BITMAP.CreationDate ) then
+ dir.CreationDate, pos = string.unpack(">I4", data, pos)
+ end
+ if ( ( bitmap & DIR_BITMAP.ModificationDate ) == DIR_BITMAP.ModificationDate ) then
+ dir.ModificationDate, pos = string.unpack(">I4", data, pos)
+ end
+ if ( ( bitmap & DIR_BITMAP.BackupDate ) == DIR_BITMAP.BackupDate ) then
+ dir.BackupDate, pos = string.unpack(">I4", data, pos)
+ end
+ if ( ( bitmap & DIR_BITMAP.FinderInfo ) == DIR_BITMAP.FinderInfo ) then
+ dir.FinderInfo, pos = string.unpack("c32", data, pos)
+ end
+ if ( ( bitmap & DIR_BITMAP.LongName ) == DIR_BITMAP.LongName ) then
+ local offset
+ offset, pos = string.unpack(">I2", data, pos)
+
+ -- TODO: This really needs to be addressed someway
+ -- Barely, never, ever happens, which makes it difficult to pin down
+ -- http://developer.apple.com/mac/library/documentation/Networking/Reference/AFP_Reference/Reference/reference.html#//apple_ref/doc/uid/TP40003548-CH3-CHDBEHBG
+
+ -- [nnposter, 8/1/2020] URL above not available. Offset below (pos+4)
+ -- seems illogical, as it partially covers two separate fields: bottom
+ -- half of the file ID and the entire offspring count.
+ -- Disabled the hack, as it interfered with valid cases
+
+ --[[
+ local justkidding = string.unpack(">I4", data, pos + 4)
+ if ( justkidding ~= 0 ) then
+ offset = 5
+ end
+ ]]
+
+ if offset > 0 then
+ dir.LongName = string.unpack("s1", data, origpos + offset)
+ end
+ end
+ if ( ( bitmap & DIR_BITMAP.ShortName ) == DIR_BITMAP.ShortName ) then
+ local offset
+ offset, pos = string.unpack(">I2", data, pos)
+ if offset > 0 then
+ dir.ShortName = string.unpack("s1", data, origpos + offset)
+ end
+ end
+ if ( ( bitmap & DIR_BITMAP.NodeId ) == DIR_BITMAP.NodeId ) then
+ dir.NodeId, pos = string.unpack(">I4", data, pos )
+ end
+ if ( ( bitmap & DIR_BITMAP.OffspringCount ) == DIR_BITMAP.OffspringCount ) then
+ dir.OffspringCount, pos = string.unpack(">I2", data, pos )
+ end
+ if ( ( bitmap & DIR_BITMAP.OwnerId ) == DIR_BITMAP.OwnerId ) then
+ dir.OwnerId, pos = string.unpack(">I4", data, pos )
+ end
+ if ( ( bitmap & DIR_BITMAP.GroupId ) == DIR_BITMAP.GroupId ) then
+ dir.GroupId, pos = string.unpack(">I4", data, pos )
+ end
+ if ( ( bitmap & DIR_BITMAP.AccessRights ) == DIR_BITMAP.AccessRights ) then
+ dir.AccessRights, pos = string.unpack(">I4", data, pos )
+ end
+ if ( ( bitmap & DIR_BITMAP.UTF8Name ) == DIR_BITMAP.UTF8Name ) then
+ local offset
+ offset, pos = string.unpack(">I2", data, pos)
+ if offset > 0 then
+ -- +4 to skip over the encoding hint
+ dir.UTF8Name = string.unpack(">s2", data, origpos + offset + 4)
+ end
+ -- Skip over the trailing pad
+ pos = pos + 4
+ end
+ if ( ( bitmap & DIR_BITMAP.UnixPrivileges ) == DIR_BITMAP.UnixPrivileges ) then
+ local unixprivs = {}
+
+ unixprivs.uid, unixprivs.gid, unixprivs.permissions, unixprivs.ua_permissions, pos = string.unpack(">I4I4I4I4", data, pos)
+ dir.UnixPrivileges = unixprivs
+ end
+ return pos, dir
+ end,
+
+}
+
+
+
+
+return _ENV;
--- /dev/null
+++ b/lib/lua/nseport/datetime.lua
@@ -1,0 +1,252 @@
+--- Functions for dealing with dates and timestamps
+--
+-- @copyright Same as Nmap--See https://nmap.org/book/man-legal.html
+-- @class module
+-- @name datetime
+-- @author Daniel Miller
+
+local stdnse = require "stdnse"
+local os = require "os"
+local math = require "math"
+local string = require "string"
+_ENV = stdnse.module("datetime", stdnse.seeall)
+
+local difftime = os.difftime
+local time = os.time
+local date = os.date
+
+local floor = math.floor
+local fmod = math.fmod
+
+local format = string.format
+local match = string.match
+
+--- Record a time difference between the scanner and the target
+--
+-- The skew will be recorded in the host's registry for later retrieval and
+-- analysis. Adjusts for network distance by subtracting half the smoothed
+-- round-trip time.
+--
+--@param host The host being scanned
+--@param timestamp The target timestamp, in seconds.
+--@param received The local time the stamp was received, in seconds.
+function record_skew(host, timestamp, received)
+ local skew_tab = host.registry.datetime_skew
+ skew_tab = skew_tab or {}
+ -- No srtt? I suppose we'll ignore it, but this could cause problems
+ local srtt = host.times and host.times.srtt or 0
+ local adjusted = difftime(floor(timestamp), floor(received)) - srtt / 2.0
+ skew_tab[#skew_tab + 1] = adjusted
+ stdnse.debug2("record_skew: %s", adjusted)
+ host.registry.datetime_skew = skew_tab
+end
+
+-- Work around Windows error formatting time zones where 1970/1/1 UTC was 1969/12/31
+local utc_offset_seconds
+do
+ -- What does the calendar say locally?
+ local localtime = date("*t", 86400)
+ -- What does the calendar say in UTC?
+ local gmtime = date("!*t", 86400)
+ -- Interpret both as local calendar dates and find the difference.
+ utc_offset_seconds = difftime(time(localtime), time(gmtime))
+end
+
+-- The offset in seconds between local time and UTC.
+--
+-- That is, if we interpret a UTC date table as a local date table by passing
+-- it to os.time, how much must be added to the resulting integer timestamp to
+-- make it correct?
+--
+-- In other words, subtract this value from a timestamp if you intend to use it
+-- in os.date.
+function utc_offset() return utc_offset_seconds end
+
+--- Convert a date table into an integer timestamp.
+--
+-- Unlike os.time, this does not assume that the date table represents a local
+-- time. Rather, it takes an optional offset number of seconds representing the
+-- time zone, and returns the timestamp that would result using that time zone
+-- as local time. If the offset is omitted or 0, the date table is interpreted
+-- as a UTC date. For example, 4:00 UTC is the same as 5:00 UTC+1:
+-- <code>
+-- date_to_timestamp({year=1970,month=1,day=1,hour=4,min=0,sec=0}) --> 14400
+-- date_to_timestamp({year=1970,month=1,day=1,hour=4,min=0,sec=0}, 0) --> 14400
+-- date_to_timestamp({year=1970,month=1,day=1,hour=5,min=0,sec=0}, 1*60*60) --> 14400
+-- </code>
+-- And 4:00 UTC+1 is an earlier time:
+-- <code>
+-- date_to_timestamp({year=1970,month=1,day=1,hour=4,min=0,sec=0}, 1*60*60) --> 10800
+-- </code>
+function date_to_timestamp(date_t, offset)
+ local status, tm = pcall(time, date_t)
+ if not status then
+ stdnse.debug1("Invalid date for this platform: %s", tm)
+ return nil
+ end
+ offset = offset or 0
+ return tm + utc_offset() - offset
+end
+
+local function format_tz(offset)
+ local sign, hh, mm
+
+ if not offset then
+ return ""
+ end
+ if offset < 0 then
+ sign = "-"
+ offset = -offset
+ else
+ sign = "+"
+ end
+ -- Truncate to minutes.
+ offset = floor(offset / 60)
+ hh = floor(offset / 60)
+ mm = floor(fmod(offset, 60))
+
+ return format("%s%02d:%02d", sign, hh, mm)
+end
+--- Format a date and time (and optional time zone) for structured output.
+--
+-- Formatting is done according to RFC 3339 (a profile of ISO 8601), except
+-- that a time zone may be omitted to signify an unspecified local time zone.
+-- Time zones are given as an integer number of seconds from UTC. Use
+-- <code>0</code> to mark UTC itself. Formatted strings with a time zone look
+-- like this:
+-- <code>
+-- format_timestamp(os.time(), 0) --> "2012-09-07T23:37:42+00:00"
+-- format_timestamp(os.time(), 2*60*60) --> "2012-09-07T23:37:42+02:00"
+-- </code>
+-- Without a time zone they look like this:
+-- <code>
+-- format_timestamp(os.time()) --> "2012-09-07T23:37:42"
+-- </code>
+--
+-- This function should be used for all dates emitted as part of NSE structured
+-- output.
+function format_timestamp(t, offset)
+ if type(t) == "table" then
+ return format(
+ "%d-%02d-%02dT%02d:%02d:%02d",
+ t.year, t.month, t.day, t.hour, t.min, t.sec
+ )
+ else
+ local tz_string = format_tz(offset)
+ offset = offset or 0
+ local status, result = pcall(date, "!%Y-%m-%dT%H:%M:%S", floor(t + offset))
+ if not status then
+ local tmp = floor(t + offset)
+ local extra_years
+ local seconds_in_year = 31556926
+ if tmp > 0xffffffff then
+ -- Maybe too far in the future?
+ extra_years = (tmp - 0xffffffff) // seconds_in_year + 1
+ elseif tmp < -utc_offset() then
+ -- Windows can't display times before the epoch
+ extra_years = tmp // seconds_in_year
+ end
+ if extra_years then
+ tmp = tmp - extra_years * seconds_in_year
+ status, result = pcall(date, "!*t", tmp)
+ if status then
+ -- seconds_in_year is imprecise, so we truncate to date only
+ result = format("%d-%02d-%02d?", result.year + extra_years, result.month, result.day)
+ end
+ end
+ end
+ if not status then
+ return ("Invalid timestamp: %s"):format(t)
+ end
+ return result .. tz_string
+ end
+end
+
+--- Format a time interval into a string
+--
+-- String is in the same format as format_difftime
+-- @param interval A time interval
+-- @param unit The time unit division as a number. If <code>interval</code> is
+-- in milliseconds, this is 1000 for instance. Default: 1 (seconds)
+-- @return The time interval in string format
+function format_time(interval, unit)
+ local sign = ""
+ if interval < 0 then
+ sign = "-"
+ interval = math.abs(interval)
+ end
+ unit = unit or 1
+ local precision = floor(math.log(unit, 10))
+
+ local sec = (interval % (60 * unit)) / unit
+ interval = interval // (60 * unit)
+ local min = interval % 60
+ interval = interval // 60
+ local hr = interval % 24
+ interval = interval // 24
+
+ local s = format("%.0fd%02.0fh%02.0fm%02.".. precision .."fs",
+ interval, hr, min, sec)
+ -- trim off leading 0 and "empty" units
+ return sign .. (match(s, "([1-9].*)") or format("%0.".. precision .."fs", 0))
+end
+
+--- Format the difference between times <code>t2</code> and <code>t1</code>
+-- into a string
+--
+-- String is in one of the forms (signs may vary):
+-- * 0s
+-- * -4s
+-- * +2m38s
+-- * -9h12m34s
+-- * +5d17h05m06s
+-- * -2y177d10h13m20s
+-- The string shows <code>t2</code> relative to <code>t1</code>; i.e., the
+-- calculation is <code>t2</code> minus <code>t1</code>.
+function format_difftime(t2, t1)
+ local d, s, sign, yeardiff
+
+ d = difftime(time(t2), time(t1))
+ if d > 0 then
+ sign = "+"
+ elseif d < 0 then
+ sign = "-"
+ t2, t1 = t1, t2
+ d = -d
+ else
+ sign = ""
+ end
+ -- t2 is always later than or equal to t1 here.
+
+ -- The year is a tricky case because it's not a fixed number of days
+ -- the way a day is a fixed number of hours or an hour is a fixed
+ -- number of minutes. For example, the difference between 2008-02-10
+ -- and 2009-02-10 is 366 days because 2008 was a leap year, but it
+ -- should be printed as 1y0d0h0m0s, not 1y1d0h0m0s. We advance t1 to be
+ -- the latest year such that it is still before t2, which means that its
+ -- year will be equal to or one less than t2's. The number of years
+ -- skipped is stored in yeardiff.
+ if t2.year > t1.year then
+ local tmpyear = t1.year
+ -- Put t1 in the same year as t2.
+ t1.year = t2.year
+ d = difftime(time(t2), time(t1))
+ if d < 0 then
+ -- Too far. Back off one year.
+ t1.year = t2.year - 1
+ d = difftime(time(t2), time(t1))
+ end
+ yeardiff = t1.year - tmpyear
+ t1.year = tmpyear
+ else
+ yeardiff = 0
+ end
+
+ local s = format_time(d)
+ if yeardiff == 0 then return sign .. s end
+ -- Years.
+ s = format("%dy", yeardiff) .. s
+ return sign .. s
+end
+
+return _ENV
--- /dev/null
+++ b/lib/lua/nseport/ipOps.lua
@@ -1,0 +1,753 @@
+---
+-- Utility functions for manipulating and comparing IP addresses.
+--
+-- @copyright Same as Nmap--See https://nmap.org/book/man-legal.html
+
+local math = require "math"
+local stdnse = require "stdnse"
+local string = require "string"
+local stringaux = require "stringaux"
+local table = require "table"
+local type = type
+local ipairs = ipairs
+local tonumber = tonumber
+
+_ENV = stdnse.module("ipOps", stdnse.seeall)
+
+local pack = string.pack
+local unpack = string.unpack
+
+---
+-- Checks to see if the supplied IP address is part of a non-routable
+-- address space.
+--
+-- The non-Internet-routable address spaces known to this function are
+-- * IPv4 Loopback (RFC3330)
+-- * IPv4 Private Use (RFC1918)
+-- * IPv4 Link Local (RFC3330)
+-- * IPv4 IETF Protocol Assignments (RFC 5736)
+-- * IPv4 TEST-NET-1, TEST-NET-2, TEST-NET-3 (RFC 5737)
+-- * IPv4 Network Interconnect Device Benchmark Testing (RFC 2544)
+-- * IPv4 Reserved for Future Use (RFC 1112, Section 4)
+-- * IPv4 Multicast Local Network Control Block (RFC 3171, Section 3)
+-- * IPv6 Unspecified and Loopback (RFC3513)
+-- * IPv6 Site-Local (RFC3513, deprecated in RFC3879)
+-- * IPv6 Unique Local Unicast (RFC4193)
+-- * IPv6 Link Local Unicast (RFC4291)
+-- @param ip String representing an IPv4 or IPv6 address. Shortened notation
+-- is permitted.
+-- @usage
+-- local is_private = ipOps.isPrivate( "192.168.1.1" )
+-- @return True or false (or <code>nil</code> in case of an error).
+-- @return String error message in case of an error or
+-- String non-routable address containing the supplied IP address.
+isPrivate = function( ip )
+ local err
+
+ ip, err = expand_ip( ip )
+ if err then return nil, err end
+
+ if ip:match( ":" ) then
+
+ local is_private
+ local ipv6_private = { "::/127", "FC00::/7", "FE80::/10", "FEC0::/10" }
+
+ for _, range in ipairs( ipv6_private ) do
+ is_private, err = ip_in_range( ip, range )
+ if is_private == true then
+ return true, range
+ end
+ if err then
+ return nil, err
+ end
+ end
+
+ elseif ip:sub(1,3) == '10.' then
+
+ return true, '10/8'
+
+ elseif ip:sub(1,4) == '127.' then
+
+ return true, '127/8'
+
+ elseif ip:sub(1,8) == '169.254.' then
+
+ return true, '169.254/16'
+
+ elseif ip:sub(1,4) == '172.' then
+
+ local p, e = ip_in_range(ip, '172.16/12')
+ if p == true then
+ return true, '172.16/12'
+ else
+ return p, e
+ end
+
+ elseif ip:sub(1,4) == '192.' then
+
+ if ip:sub(5,8) == '168.' then
+ return true, '192.168/16'
+ elseif ip:match('^192%.[0][0]?[0]?%.[0][0]?[0]?%.') then
+ return true, '192.0.0/24'
+ elseif ip:match('^192%.[0][0]?[0]?%.[0]?[0]?2') then
+ return true, '192.0.2/24'
+ end
+
+ elseif ip:sub(1,4) == '198.' then
+
+ if ip:match('^198%.[0]?18%.') or ip:match('^198%.[0]?19%.') then
+ return true, '198.18/15'
+ elseif ip:match('^198%.[0]?51%.100%.') then
+ return true, '198.51.100/24'
+ end
+
+ elseif ip:match('^203%.[0][0]?[0]?%.113%.') then
+
+ return true, '203.0.113/24'
+
+ elseif ip:match('^224%.[0][0]?[0]?%.[0][0]?[0]?%.') then
+
+ return true, '224.0.0/24'
+
+ elseif ip:match('^24[0-9]%.') or ip:match('^25[0-5]%.') then
+
+ return true, '240.0.0/4'
+
+ end
+
+ return false, nil
+
+end
+
+
+
+---
+-- Converts the supplied IPv4 address into a DWORD value.
+--
+-- For example, the address a.b.c.d becomes (((a*256+b)*256+c)*256+d).
+--
+-- Note: IPv6 addresses are not supported. Currently, numbers in NSE are
+-- limited to 10^14, and consequently not all IPv6 addresses can be
+-- represented. Consider using <code>ip_to_str</code> for IPv6 addresses.
+-- @param ip String representing an IPv4 address. Shortened notation is
+-- permitted.
+-- @usage
+-- local dword = ipOps.todword( "73.150.2.210" )
+-- @return Number corresponding to the supplied IP address (or <code>nil</code>
+-- in case of an error).
+-- @return String error message in case of an error.
+todword = function( ip )
+
+ if type( ip ) ~= "string" or ip:match( ":" ) then
+ return nil, "Error in ipOps.todword: Expected IPv4 address."
+ end
+
+ local n, ret, err = {}
+ n, err = get_parts_as_number( ip )
+ if err then return nil, err end
+
+ ret = (((n[1]*256+n[2]))*256+n[3])*256+n[4]
+
+ return ret
+
+end
+
+---
+-- Converts the supplied IPv4 address from a DWORD value into a dotted string.
+--
+-- For example, the address (((a*256+b)*256+c)*256+d) becomes a.b.c.d.
+--
+--@param ip DWORD representing an IPv4 address.
+--@return The string representing the address.
+fromdword = function( ip )
+ if type( ip ) ~= "number" then
+ stdnse.debug1("Error in ipOps.fromdword: Expected 32-bit number.")
+ return nil
+ end
+ return string.format("%d.%d.%d.%d", unpack("BBBB", pack(">I4", ip)))
+end
+
+---
+-- Separates the supplied IP address into its constituent parts and
+-- returns them as a table of numbers.
+--
+-- For example, the address 139.104.32.123 becomes { 139, 104, 32, 123 }.
+-- @usage
+-- local a, b, c, d;
+-- local t, err = ipOps.get_parts_as_number( "139.104.32.123" )
+-- if t then a, b, c, d = table.unpack( t ) end
+-- @param ip String representing an IPv4 or IPv6 address. Shortened notation
+-- is permitted.
+-- @return Array of numbers for each part of the supplied IP address (or
+-- <code>nil</code> in case of an error).
+-- @return String error message in case of an error.
+get_parts_as_number = function( ip )
+ local err
+
+ ip, err = expand_ip( ip )
+ if err then return nil, err end
+
+ local pattern, base
+ if ip:match( ":" ) then
+ pattern = "%x+"
+ base = 16
+ else
+ pattern = "%d+"
+ base = 10
+ end
+ local t = {}
+ for part in string.gmatch(ip, pattern) do
+ t[#t+1] = tonumber( part, base )
+ end
+
+ return t
+
+end
+
+
+
+---
+-- Compares two IP addresses.
+--
+-- When comparing addresses from different families,
+-- IPv4 addresses will sort before IPv6 addresses.
+-- @param left String representing an IPv4 or IPv6 address. Shortened
+-- notation is permitted.
+-- @param op A comparison operator which may be one of the following strings:
+-- <code>"eq"</code>, <code>"ge"</code>, <code>"le"</code>,
+-- <code>"gt"</code> or <code>"lt"</code> (respectively ==, >=, <=,
+-- >, <).
+-- @param right String representing an IPv4 or IPv6 address. Shortened
+-- notation is permitted.
+-- @usage
+-- if ipOps.compare_ip( "2001::DEAD:0:0:0", "eq", "2001:0:0:0:DEAD::" ) then
+-- ...
+-- end
+-- @return True or false (or <code>nil</code> in case of an error).
+-- @return String error message in case of an error.
+compare_ip = function( left, op, right )
+
+ if type( left ) ~= "string" or type( right ) ~= "string" then
+ return nil, "Error in ipOps.compare_ip: Expected IP address as a string."
+ end
+
+ local err ={}
+ left, err[#err+1] = ip_to_str( left )
+ right, err[#err+1] = ip_to_str( right )
+ if #err > 0 then
+ return nil, table.concat( err, " " )
+ end
+
+ -- by prepending the length, IPv4 (length 4) sorts before IPv6 (length 16)
+ left = pack("s1", left)
+ right = pack("s1", right)
+
+ if ( op == "eq" ) then
+ return ( left == right )
+ elseif ( op == "ne" ) then
+ return ( left ~= right )
+ elseif ( op == "le" ) then
+ return ( left <= right )
+ elseif ( op == "ge" ) then
+ return ( left >= right )
+ elseif ( op == "lt" ) then
+ return ( left < right )
+ elseif ( op == "gt" ) then
+ return ( left > right )
+ end
+
+ return nil, "Error in ipOps.compare_ip: Invalid Operator."
+end
+
+--- Sorts a table of IP addresses
+--
+-- An in-place sort using <code>table.sort</code> to sort by IP address.
+-- Sorting non-IP addresses is likely to result in a bad sort and possibly an infinite loop.
+--
+-- @param ips The table of IP addresses to sort
+-- @param op The comparison operation to use. Default: "lt" (ascending)
+ip_sort = function (ips, op)
+ op = op or "lt"
+ return table.sort(ips, function(a, b) return compare_ip(a, op, b) end)
+end
+
+---
+-- Checks whether the supplied IP address is within the supplied range of IP
+-- addresses.
+--
+-- The address and the range must both belong to the same address family.
+-- @param ip String representing an IPv4 or IPv6 address. Shortened
+-- notation is permitted.
+-- @param range String representing a range of IPv4 or IPv6 addresses in
+-- first-last or CIDR notation (e.g.
+-- <code>"192.168.1.1 - 192.168.255.255"</code> or
+-- <code>"2001:0A00::/23"</code>).
+-- @usage
+-- if ipOps.ip_in_range( "192.168.1.1", "192/8" ) then ... end
+-- @return True or false (or <code>nil</code> in case of an error).
+-- @return String error message in case of an error.
+ip_in_range = function( ip, range )
+
+ local first, last, err = get_ips_from_range( range )
+ if err then return nil, err end
+ ip, err = expand_ip( ip )
+ if err then return nil, err end
+ if ( ip:match( ":" ) and not first:match( ":" ) ) or ( not ip:match( ":" ) and first:match( ":" ) ) then
+ return nil, "Error in ipOps.ip_in_range: IP address is of a different address family to Range."
+ end
+
+ err = {}
+ local ip_ge_first, ip_le_last
+ ip_ge_first, err[#err+1] = compare_ip( ip, "ge", first )
+ ip_le_last, err[#err+1] = compare_ip( ip, "le", last )
+ if #err > 0 then
+ return nil, table.concat( err, " " )
+ end
+
+ if ip_ge_first and ip_le_last then
+ return true
+ else
+ return false
+ end
+
+end
+
+
+
+---
+-- Expands an IP address supplied in shortened notation.
+-- Serves also to check the well-formedness of an IP address.
+--
+-- Note: IPv4in6 notated addresses will be returned in pure IPv6 notation unless
+-- the IPv4 portion is shortened and does not contain a dot, in which case the
+-- address will be treated as IPv6.
+-- @param ip String representing an IPv4 or IPv6 address in shortened or full notation.
+-- @param family String representing the address family to expand to. Only
+-- affects IPv4 addresses when "inet6" is provided, causing the function to
+-- return an IPv4-mapped IPv6 address.
+-- @usage
+-- local ip = ipOps.expand_ip( "2001::" )
+-- @return String representing a fully expanded IPv4 or IPv6 address (or
+-- <code>nil</code> in case of an error).
+-- @return String error message in case of an error.
+expand_ip = function( ip, family )
+ local err
+
+ if type( ip ) ~= "string" or ip == "" then
+ return nil, "Error in ipOps.expand_ip: Expected IP address as a string."
+ end
+
+ local err4 = "Error in ipOps.expand_ip: An address assumed to be IPv4 was malformed."
+
+ if not ip:match( ":" ) then
+ -- ipv4: missing octets should be "0" appended
+ if ip:match( "[^%.0-9]" ) then
+ return nil, err4
+ end
+ local octets = {}
+ for octet in string.gmatch( ip, "%d+" ) do
+ if tonumber( octet, 10 ) > 255 then return nil, err4 end
+ octets[#octets+1] = octet
+ end
+ if #octets > 4 then return nil, err4 end
+ while #octets < 4 do
+ octets[#octets+1] = "0"
+ end
+ if family == "inet6" then
+ return ( table.concat( { 0,0,0,0,0,"ffff",
+ stdnse.tohex( 256*octets[1]+octets[2] ),
+ stdnse.tohex( 256*octets[3]+octets[4] )
+ }, ":" ) )
+ else
+ return ( table.concat( octets, "." ) )
+ end
+ end
+
+ if family ~= nil and family ~= "inet6" then
+ return nil, "Error in ipOps.expand_ip: Cannot convert IPv6 address to IPv4"
+ end
+
+ if ip:match( "[^%.:%x]" ) then
+ return nil, ( err4:gsub( "IPv4", "IPv6" ) )
+ end
+
+ -- preserve ::
+ ip = string.gsub(ip, "::", ":z:")
+
+ -- get a table of each hexadectet
+ local hexadectets = {}
+ for hdt in string.gmatch( ip, "[%.z%x]+" ) do
+ hexadectets[#hexadectets+1] = hdt
+ end
+
+ -- deal with IPv4in6 (last hexadectet only)
+ local t = {}
+ if hexadectets[#hexadectets]:match( "[%.]+" ) then
+ hexadectets[#hexadectets], err = expand_ip( hexadectets[#hexadectets] )
+ if err then return nil, ( err:gsub( "IPv4", "IPv4in6" ) ) end
+ t = stringaux.strsplit( "[%.]+", hexadectets[#hexadectets] )
+ for i, v in ipairs( t ) do
+ t[i] = tonumber( v, 10 )
+ end
+ hexadectets[#hexadectets] = stdnse.tohex( 256*t[1]+t[2] )
+ hexadectets[#hexadectets+1] = stdnse.tohex( 256*t[3]+t[4] )
+ end
+
+ -- deal with :: and check for invalid address
+ local z_done = false
+ for index, value in ipairs( hexadectets ) do
+ if value:match( "[%.]+" ) then
+ -- shouldn't have dots at this point
+ return nil, ( err4:gsub( "IPv4", "IPv6" ) )
+ elseif value == "z" and z_done then
+ -- can't have more than one ::
+ return nil, ( err4:gsub( "IPv4", "IPv6" ) )
+ elseif value == "z" and not z_done then
+ z_done = true
+ hexadectets[index] = "0"
+ local bound = 8 - #hexadectets
+ for i = 1, bound, 1 do
+ table.insert( hexadectets, index+i, "0" )
+ end
+ elseif tonumber( value, 16 ) > 65535 then
+ -- more than FFFF!
+ return nil, ( err4:gsub( "IPv4", "IPv6" ) )
+ end
+ end
+
+ -- make sure we have exactly 8 hexadectets
+ if #hexadectets > 8 then return nil, ( err4:gsub( "IPv4", "IPv6" ) ) end
+ while #hexadectets < 8 do
+ hexadectets[#hexadectets+1] = "0"
+ end
+
+ return ( table.concat( hexadectets, ":" ) )
+
+end
+
+
+
+---
+-- Returns the first and last IP addresses in the supplied range of addresses.
+-- @param range String representing a range of IPv4 or IPv6 addresses in either
+-- CIDR or first-last notation.
+-- @usage
+-- first, last = ipOps.get_ips_from_range( "192.168.0.0/16" )
+-- @return String representing the first address in the supplied range (or
+-- <code>nil</code> in case of an error).
+-- @return String representing the last address in the supplied range (or
+-- <code>nil</code> in case of an error).
+-- @return String error message in case of an error.
+get_ips_from_range = function( range )
+
+ if type( range ) ~= "string" then
+ return nil, nil, "Error in ipOps.get_ips_from_range: Expected a range as a string."
+ end
+
+ local ip, prefix = range:match("^%s*([%x:.]+)/(%d+)%s*$")
+ if ip then
+ return get_first_last_ip(ip, prefix)
+ end
+ local first, last = range:match("^%s*([%x:.]+)%s*%-%s*([%x:.]+)%s*$")
+ if not first then
+ return nil, nil, "Error in ipOps.get_ips_from_range: The range supplied could not be interpreted."
+ end
+
+ local err
+ first, err = expand_ip(first)
+ if not err then
+ last, err = expand_ip(last)
+ end
+ if not err then
+ local af = function (ip) return ip:find(":") and 6 or 4 end
+ if af(first) ~= af(last) then
+ err = "Error in ipOps.get_ips_from_range: First IP address is of a different address family to last IP address."
+ end
+ end
+ if err then
+ return nil, nil, err
+ end
+ return first, last
+end
+
+---
+-- Calculates the first and last IP addresses of a range of addresses,
+-- given an IP address in the range and a prefix length for that range
+-- @param ip String representing an IPv4 or IPv6 address. Shortened notation
+-- is permitted.
+-- @param prefix Number or a string representing a decimal number corresponding
+-- to a prefix length.
+-- @usage
+-- first, last = ipOps.get_first_last_ip( "192.0.0.0", 26)
+-- @return String representing the first IP address of the range denoted by
+-- the supplied parameters (or <code>nil</code> in case of an error).
+-- @return String representing the last IP address of the range denoted by
+-- the supplied parameters (or <code>nil</code> in case of an error).
+-- @return String error message in case of an error.
+get_first_last_ip = function(ip, prefix)
+ local err
+ ip, err = ip_to_bin(ip)
+ if err then return nil, nil, err end
+
+ prefix = tonumber(prefix)
+ if not prefix or prefix ~= math.floor(prefix) or prefix < 0 or prefix > #ip then
+ return nil, nil, "Error in ipOps.get_first_last_ip: Invalid prefix."
+ end
+
+ local net = ip:sub(1, prefix)
+ local first = bin_to_ip(net .. ("0"):rep(#ip - prefix))
+ local last = bin_to_ip(net .. ("1"):rep(#ip - prefix))
+ return first, last
+end
+
+---
+-- Calculates the first IP address of a range of addresses given an IP address in
+-- the range and prefix length for that range.
+-- @param ip String representing an IPv4 or IPv6 address. Shortened notation
+-- is permitted.
+-- @param prefix Number or a string representing a decimal number corresponding
+-- to a prefix length.
+-- @usage
+-- first = ipOps.get_first_ip( "192.0.0.0", 26 )
+-- @return String representing the first IP address of the range denoted by the
+-- supplied parameters (or <code>nil</code> in case of an error).
+-- @return String error message in case of an error.
+get_first_ip = function(ip, prefix)
+ local first, last, err = get_first_last_ip(ip, prefix)
+ return first, err
+end
+
+---
+-- Calculates the last IP address of a range of addresses given an IP address in
+-- the range and prefix length for that range.
+-- @param ip String representing an IPv4 or IPv6 address. Shortened notation
+-- is permitted.
+-- @param prefix Number or a string representing a decimal number corresponding
+-- to a prefix length.
+-- @usage
+-- last = ipOps.get_last_ip( "192.0.0.0", 26 )
+-- @return String representing the last IP address of the range denoted by the
+-- supplied parameters (or <code>nil</code> in case of an error).
+-- @return String error message in case of an error.
+get_last_ip = function(ip, prefix)
+ local first, last, err = get_first_last_ip(ip, prefix)
+ return last, err
+end
+
+---
+-- Converts an IP address into an opaque string (big-endian)
+-- @param ip String representing an IPv4 or IPv6 address.
+-- @param family (optional) Address family to convert to. "ipv6" converts IPv4
+-- addresses to IPv4-mapped IPv6.
+-- @usage
+-- opaque = ipOps.ip_to_str( "192.168.3.4" )
+-- @return 4- or 16-byte string representing IP address (or <code>nil</code>
+-- in case of an error).
+-- @return String error message in case of an error
+ip_to_str = function( ip, family )
+ local err
+
+ ip, err = expand_ip( ip, family )
+ if err then return nil, err end
+
+ local t = {}
+
+ if not ip:match( ":" ) then
+ -- ipv4 string
+ for octet in string.gmatch( ip, "%d+" ) do
+ t[#t+1] = pack("B", octet)
+ end
+ else
+ -- ipv6 string
+ for hdt in string.gmatch( ip, "%x+" ) do
+ t[#t+1] = pack( ">I2", tonumber(hdt, 16) )
+ end
+ end
+
+
+ return table.concat( t )
+end
+
+---
+-- Converts an opaque string (big-endian) into an IP address
+--
+-- @param ip Opaque string representing an IP address. If length 4, then IPv4
+-- is assumed. If length 16, then IPv6 is assumed.
+-- @return IP address in readable notation (or <code>nil</code> in case of an
+-- error)
+-- @return String error message in case of an error
+str_to_ip = function (ip)
+ if #ip == 4 then
+ return ("%d.%d.%d.%d"):format(unpack("BBBB", ip))
+ elseif #ip == 16 then
+ local full = ("%x:%x:%x:%x:%x:%x:%x:%x"):format(unpack((">I2"):rep(8), ip))
+ full = full:gsub(":[:0]+", "::", 1) -- Collapse the first (should be longest?) series of :0:
+ full = full:gsub("^0::", "::", 1) -- handle special case of ::1
+ return full
+ else
+ return nil, "Invalid length"
+ end
+end
+
+---
+-- Converts an IP address into a string representing the address as binary
+-- digits.
+-- @param ip String representing an IPv4 or IPv6 address. Shortened notation
+-- is permitted.
+-- @usage
+-- bit_string = ipOps.ip_to_bin( "2001::" )
+-- @return String representing the supplied IP address as 32 or 128 binary
+-- digits (or <code>nil</code> in case of an error).
+-- @return String error message in case of an error.
+ip_to_bin = function( ip )
+ local err
+
+ ip, err = expand_ip( ip )
+ if err then return nil, err end
+
+ local t, mask = {}
+
+ if not ip:match( ":" ) then
+ -- ipv4 string
+ for octet in string.gmatch( ip, "%d+" ) do
+ t[#t+1] = stdnse.tohex( tonumber(octet) )
+ end
+ mask = "00"
+ else
+ -- ipv6 string
+ for hdt in string.gmatch( ip, "%x+" ) do
+ t[#t+1] = hdt
+ end
+ mask = "0000"
+ end
+
+ -- padding
+ for i, v in ipairs( t ) do
+ t[i] = mask:sub( 1, # mask - # v ) .. v
+ end
+
+ return hex_to_bin( table.concat( t ) )
+
+end
+
+
+
+---
+-- Converts a string of binary digits into an IP address.
+-- @param binstring String representing an IP address as 32 or 128 binary
+-- digits.
+-- @usage
+-- ip = ipOps.bin_to_ip( "01111111000000000000000000000001" )
+-- @return String representing an IP address (or <code>nil</code> in
+-- case of an error).
+-- @return String error message in case of an error.
+bin_to_ip = function( binstring )
+
+ if type( binstring ) ~= "string" or binstring:match( "[^01]+" ) then
+ return nil, "Error in ipOps.bin_to_ip: Expected string of binary digits."
+ end
+
+ local af
+ if # binstring == 32 then
+ af = 4
+ elseif # binstring == 128 then
+ af = 6
+ else
+ return nil, "Error in ipOps.bin_to_ip: Expected exactly 32 or 128 binary digits."
+ end
+
+ local t = {}
+ if af == 6 then
+ local pattern = string.rep( "[01]", 16 )
+ for chunk in string.gmatch( binstring, pattern ) do
+ t[#t+1] = stdnse.tohex( tonumber( chunk, 2 ) )
+ end
+ return table.concat( t, ":" )
+ end
+
+ if af == 4 then
+ local pattern = string.rep( "[01]", 8 )
+ for chunk in string.gmatch( binstring, pattern ) do
+ t[#t+1] = tonumber( chunk, 2 ) .. ""
+ end
+ return table.concat( t, "." )
+ end
+
+end
+
+
+
+local bin_lookup = {
+ ["0"]="0000",
+ ["1"]="0001",
+ ["2"]="0010",
+ ["3"]="0011",
+ ["4"]="0100",
+ ["5"]="0101",
+ ["6"]="0110",
+ ["7"]="0111",
+ ["8"]="1000",
+ ["9"]="1001",
+ ["a"]="1010",
+ ["b"]="1011",
+ ["c"]="1100",
+ ["d"]="1101",
+ ["e"]="1110",
+ ["f"]="1111",
+}
+setmetatable(bin_lookup, {
+ __index = function()
+ error("Error in ipOps.hex_to_bin: Expected string representing a hexadecimal number.")
+ end
+ })
+---
+-- Converts a string of hexadecimal digits into the corresponding string of
+-- binary digits.
+--
+-- Each hex digit results in four bits.
+-- @param hex String representing a hexadecimal number.
+-- @usage
+-- bin_string = ipOps.hex_to_bin( "F00D" )
+-- @return String representing the supplied number in binary digits (or
+-- <code>nil</code> in case of an error).
+-- @return String error message in case of an error.
+hex_to_bin = function( hex )
+ if type( hex ) ~= "string" then
+ return nil, "Error in ipOps.hex_to_bin: Expected string"
+ end
+
+ local status, result = pcall( string.gsub, string.lower(hex), ".", bin_lookup)
+ if status then
+ return result
+ end
+ return status, result
+end
+
+---
+-- Convert a CIDR subnet mask to dotted decimal notation.
+--
+-- @param subnet CIDR string representing the subnet mask.
+-- @usage
+-- local netmask = ipOps.cidr_to_subnet( "/16" )
+-- @return Dotted decimal representation of the suppliet subnet mask (e.g. "255.255.0.0")
+cidr_to_subnet = function( subnet )
+ local bits = subnet:match("/(%d%d)$")
+ if not bits then return nil end
+ return fromdword((0xFFFFFFFF >> tonumber(bits)) ~ 0xFFFFFFFF)
+end
+
+---
+-- Convert a dotted decimal subnet mask to CIDR notation.
+--
+-- @param subnet Dotted decimal string representing the subnet mask.
+-- @usage
+-- local cidr = ipOps.subnet_to_cidr( "255.255.0.0" )
+-- @return CIDR representation of the supplied subnet mask (e.g. "/16").
+subnet_to_cidr = function( subnet )
+ local dword, err = todword(subnet)
+ if not dword then return nil, err end
+ return "/" .. tostring(32 - (math.tointeger(math.log((dword ~ 0xFFFFFFFF) + 1, 2))))
+end
+
+
+return _ENV;
--- /dev/null
+++ b/lib/lua/nseport/json.lua
@@ -1,0 +1,257 @@
+---
+-- Library methods for handling JSON data. It handles JSON encoding and
+-- decoding according to RFC 4627.
+--
+-- There is a straightforward mapping between JSON and Lua data types. One
+-- exception is JSON <code>NULL</code>, which is not the same as Lua
+-- <code>nil</code>. (A better match for Lua <code>nil</code> is JavaScript
+-- <code>undefined</code>.) <code>NULL</code> values in JSON are represented by
+-- the special value <code>json.NULL</code>.
+--
+-- @author Martin Holst Swende
+-- @author David Fifield
+-- @author Patrick Donnelly
+-- @copyright Same as Nmap--See https://nmap.org/book/man-legal.html
+
+-- Version 0.4
+-- Created 01/25/2010 - v0.1 - created by Martin Holst Swende <[email protected]>
+-- Heavily modified 02/22/2010 - v0.3. Rewrote the parser into an OO-form, to not have to handle
+-- all kinds of state with parameters and return values.
+-- Modified 02/27/2010 - v0.4 Added unicode handling (written by David Fifield). Renamed toJson
+-- and fromJson into generate() and parse(), implemented more proper numeric parsing and added some more error checking.
+
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+local unicode = require "unicode"
+--local unittest = require "unittest"
+_ENV = stdnse.module("json", stdnse.seeall)
+
+local lpeg = require "lpeg";
+local locale = lpeg.locale;
+local P = lpeg.P;
+local R = lpeg.R;
+local S = lpeg.S;
+local V = lpeg.V;
+local C = lpeg.C;
+local Cb = lpeg.Cb;
+local Cc = lpeg.Cc;
+local Cf = lpeg.Cf;
+local Cg = lpeg.Cg;
+local Cp = lpeg.Cp;
+local Cs = lpeg.Cs;
+local Ct = lpeg.Ct;
+local Cmt = lpeg.Cmt;
+
+-- case sensitive keyword
+local function K (a)
+ return P(a) * -(locale().alnum + "_");
+end
+
+local NULL = {};
+_M.NULL = NULL;
+
+--- Makes a table be treated as a JSON Array when generating JSON
+--
+-- A table treated as an Array has all non-number indices ignored.
+-- @param t a table to be treated as an array
+function make_array(t)
+ local mt = getmetatable(t) or {}
+ mt["json"] = "array"
+ setmetatable(t, mt)
+ return t
+end
+
+--- Makes a table be treated as a JSON Object when generating JSON
+--
+-- @param t a table to be treated as an object
+function make_object(t)
+ local mt = getmetatable(t) or {}
+ mt["json"] = "object"
+ setmetatable(t, mt)
+ return t
+end
+
+-- Decode a Unicode escape, assuming that self.pos starts just after the
+-- initial \u. May consume an additional escape in the case of a UTF-16
+-- surrogate pair. See RFC 2781 for UTF-16.
+local unicode_esc = P [[\u]] * C(locale().xdigit * locale().xdigit * locale().xdigit * locale().xdigit);
+local function unicode16 (subject, position, hex)
+ local cp = assert(tonumber(hex, 16));
+
+ if cp < 0xD800 or cp > 0xDFFF then
+ return position, unicode.utf8_enc(cp);
+ elseif cp >= 0xDC00 and cp <= 0xDFFF then
+ error(("Not a Unicode character: U+%04X"):format(cp));
+ end
+
+ -- Beginning of a UTF-16 surrogate.
+ local lowhex = unicode_esc:match(subject, position);
+
+ if not lowhex then
+ error(("Bad unicode escape \\u%s (missing low surrogate)"):format(hex))
+ else
+ local cp2 = assert(tonumber(lowhex, 16));
+ if not (cp2 >= 0xDC00 and cp2 <= 0xDFFF) then
+ error(("Bad unicode escape \\u%s\\u%s (bad low surrogate)"):format(hex, lowhex))
+ end
+ position = position+6 -- consume '\uXXXX'
+ cp = 0x10000 + (cp & 0x3FF) * 0x400 + (cp2 & 0x3FF)
+ return position, unicode.utf8_enc(cp);
+ end
+end
+
+-- call lpeg.locale on the grammar to add V "space"
+local json = locale {
+ V "json";
+
+ json = V "space"^0 * V "value" * V "space"^0 * P(-1); -- FIXME should be 'V "object" + V "array"' instead of 'V "value"' ?
+
+ value = V "string" +
+ V "number" +
+ V "object" +
+ V "array" +
+ K "true" * Cc(true)+
+ K "false" * Cc(false)+
+ K "null" * Cc(NULL);
+
+ object = Cf(Ct "" * P "{" * V "space"^0 * (V "members")^-1 * V "space"^0 * P "}", rawset) / make_object;
+ members = V "pair" * (V "space"^0 * P "," * V "space"^0 * V "pair")^0;
+ pair = Cg(V "string" * V "space"^0 * P ":" * V "space"^0 * V "value");
+
+ array = Ct(P "[" * V "space"^0 * (V "elements")^-1 * V "space"^0 * P "]") / make_array;
+ elements = V "value" * V "space"^0 * (P "," * V "space"^0 * V "value")^0;
+
+ string = Ct(P [["]] * (V "char")^0 * P [["]]) / table.concat;
+ char = P [[\"]] * Cc [["]] +
+ P [[\\]] * Cc [[\]] +
+ P [[\b]] * Cc "\b" +
+ P [[\f]] * Cc "\f" +
+ P [[\n]] * Cc "\n" +
+ P [[\r]] * Cc "\r" +
+ P [[\t]] * Cc "\t" +
+ P [[\u]] * Cmt(C(V "xdigit" * V "xdigit" * V "xdigit" * V "xdigit"), unicode16) +
+ P [[\]] * C(1) +
+ (C(1) - P [["]]);
+
+ number = C((P "-")^-1 * V "space"^0 * (V "hexadecimal" + V "floating" + V "integer")) / function (a) return assert(tonumber(a)) end;
+ hexadecimal = P "0x" * V "xdigit"^1;
+ floating = (V "digit"^1 * P "." * V "digit"^0 + V "digit"^0 * P "." * V "digit"^1) * (V "exponent")^-1;
+ integer = V "digit"^1 * (V "exponent")^-1;
+ exponent = S "eE" * (S "-+")^-1 * V "digit"^1;
+};
+json = P(json); -- compile the grammar
+
+
+--- Parses JSON data into a Lua object.
+--
+-- This is the method you probably want to use if you use this library from a
+-- script.
+--@param data a json string
+--@return status true if ok, false if bad
+--@return an object representing the json, or error message
+function parse (data)
+ local status, object = pcall(json.match, json, data);
+
+ if not status then
+ return false, object;
+ elseif object then
+ return true, object;
+ else
+ return false, "syntax error";
+ end
+end
+
+--Some local shortcuts
+local function dbg(str,...)
+-- stdnse.debug1("Json:"..str, ...)
+end
+
+-- See section 2.5 for escapes.
+-- For convenience, ESCAPE_TABLE maps to escape sequences complete with
+-- backslash, and REVERSE_ESCAPE_TABLE maps from single escape characters
+-- (no backslash).
+local ESCAPE_TABLE = {}
+local REVERSE_ESCAPE_TABLE = {}
+do
+ local escapes = {
+ ["\x22"] = "\"",
+ ["\x5C"] = "\\",
+ ["\x2F"] = "/",
+ ["\x08"] = "b",
+ ["\x0C"] = "f",
+ ["\x0A"] = "n",
+ ["\x0D"] = "r",
+ ["\x09"] = "t",
+ }
+ for k, v in pairs(escapes) do
+ ESCAPE_TABLE[k] = "\\" .. v
+ REVERSE_ESCAPE_TABLE[v] = k
+ end
+end
+
+-- Escapes a string
+--@param str the string
+--@return a string where the special chars have been escaped
+local function escape(str)
+ return "\"" .. string.gsub(str, ".", ESCAPE_TABLE) .. "\""
+end
+
+--- Checks what JSON type a variable will be treated as when generating JSON
+-- @param var a variable to inspect
+-- @return a string containing the JSON type. Valid values are "array",
+-- "object", "number", "string", "boolean", and "null"
+function typeof(var)
+ local t = type(var)
+ if var == NULL then
+ return "null"
+ elseif t == "table" then
+ local mtval = rawget(getmetatable(var) or {}, "json")
+ if mtval == "array" or (mtval ~= "object" and #var > 0) then
+ return "array"
+ else
+ return "object"
+ end
+ else
+ return t
+ end
+ error("Unknown data type in typeof")
+end
+
+--- Creates json data from an object
+--@param obj a table containing data
+--@return a string containing valid json
+function generate(obj)
+ -- NULL-check must be performed before
+ -- checking type == table, since the NULL-object
+ -- is a table
+ if obj == NULL then
+ return "null"
+ elseif obj == false then
+ return "false"
+ elseif obj == true then
+ return "true"
+ elseif type(obj) == "number" then
+ return tostring(obj)
+ elseif type(obj) == "string" then
+ return escape(obj)
+ elseif type(obj) == "table" then
+ local k, v, elems, jtype
+ elems = {}
+ jtype = typeof(obj)
+ if jtype == "array" then
+ for _, v in ipairs(obj) do
+ elems[#elems + 1] = generate(v)
+ end
+ return "[" .. table.concat(elems, ", ") .. "]"
+ elseif jtype == "object" then
+ for k, v in pairs(obj) do
+ elems[#elems + 1] = escape(k) .. ": " .. generate(v)
+ end
+ return "{" .. table.concat(elems, ", ") .. "}"
+ end
+ end
+ error("Unknown data type in generate")
+end
+
+return _ENV;
--- /dev/null
+++ b/lib/lua/nseport/nmap.lua
@@ -1,0 +1,191 @@
+---
+-- nmap.lua is a drop-in replacement for the nmap lua api
+--
+
+local p9 = require "p9"
+
+---
+-- nmap functions
+--
+
+-- clock
+local function clock()
+ return os.clock()
+end
+
+-- log function
+local function log_write (where, ...)
+ if where == "stdout" then
+ io.stdout:write(...)
+ else
+ io.stderr:write(...)
+ end
+end
+
+-- debugging
+local function debugging ()
+ local nse_debug_level
+ nse_debug_level = tonumber(os.getenv("NSE_DEBUG"))
+ if nse_debug_level == nil then
+ return 0
+ end
+ return nse_debug_level
+end
+
+-- verbosity
+local function verbosity ()
+ local nse_verbose_level
+ nse_verbose_level = tonumber(os.getenv("NSE_VERBOSE"))
+ if nse_verbose_level == nil then
+ return 0
+ end
+ return nse_verbose_level
+end
+
+---
+-- plan9 "socket" api
+--
+
+SOCKET_MAXSZ = 4096
+
+local function sleep (seconds)
+ p9.sleep(seconds * 1000)
+end
+
+Socket = {
+ new = function(self, proto)
+ local o = {}
+ setmetatable(o, self)
+ self.__index = self
+ if proto ~= nil then
+ self.proto = proto
+ end
+ return o
+ end,
+
+ -- connect to host and port
+ connect = function(self, host, port)
+ local cs_fd, clone_fd, data_fd, ctl_fd, proto
+ local data_fid, conn_param, clone_path, addr
+
+ if self.proto == nil then
+ proto = "tcp"
+ else
+ proto = self.proto
+ end
+
+ cs_fd = io.open("/net/cs", "r+")
+ cs_fd:write(proto.."!"..host.."!"..port)
+ cs_fd:seek("set")
+ conn_param = cs_fd:read("a")
+ cs_fd:close()
+
+ clone_path = string.sub(conn_param, 0, string.find(conn_param, " ") - 1)
+ addr = string.sub(conn_param, string.find(conn_param, " ") + 1)
+
+ clone_fd = io.open(clone_path, "r+")
+ data_fid = clone_fd:read()
+ clone_fd:seek("set")
+ local n = clone_fd:write("connect "..addr.."\n")
+ clone_fd:flush()
+
+ data_fd = io.open("/net/"..proto.."/"..data_fid.."/data", "r+")
+
+ self.data_fd = data_fd
+ self.clone_fd = clone_fd
+
+ return true
+ end,
+
+ -- close connection and file descriptors
+ close = function(self)
+ self.data_fd:close()
+ self.clone_fd:write("close\n")
+ self.clone_fd:close()
+ end,
+
+ -- dummy compat function
+ set_timeout = function(self, t)
+ self.timeout = t
+ end,
+
+ -- send data
+ send = function(self, d)
+ local fd = self.data_fd:write(d)
+ self.data_fd:flush()
+ if fd == nil then
+ return false, nil
+ end
+ return true, d
+ end,
+
+ -- send wrapper (why does this exist?!)
+ sendto = function(self, host, port, d)
+ if self.data_fd == nil then
+ self:connect(host, port)
+ end
+ return self:send(d)
+ end,
+
+ -- receive data
+ receive = function(self, sz)
+ if sz == nil then
+ sz = SOCKET_MAXSZ
+ end
+ local data = self.data_fd:read(sz)
+ return true, data
+ end,
+
+ -- receive blocking
+ receive_buf = function(self, delim, incdelim)
+ local data = ""
+ local fdelim = false
+ while not(fdelim) do
+ local status, msg = self:receive()
+ if not(status) then
+ return status, data
+ end
+ print("one iteration yielded ", msg)
+ fdelim = string.find(msg, "["..delim.."]")
+ if fdelim then
+ local where = fdelim - 1
+ if incdelim then
+ where = where + 1
+ end
+ data = data..string.sub(msg, 0, where)
+ self.data_fd:seek(self.data_fd:seek() - fdelim)
+ return true, data
+ end
+
+ data = data..msg
+ p9.sleep(100)
+ end
+ return true, data
+ end,
+}
+
+local function new_socket ()
+ return Socket:new()
+end
+
+local socket = {
+ sleep = sleep,
+ new_socket = new_socket,
+}
+
+---
+-- exports
+--
+
+local registry
+
+local nmapcompat = {
+ registry = registry,
+ clock = clock,
+ log_write = log_write,
+ debugging = debugging,
+ verbosity = verbosity,
+ socket = socket,
+}
+
+return nmapcompat;
--- /dev/null
+++ b/lib/lua/nseport/stdnse.lua
@@ -1,0 +1,583 @@
+---
+-- Standard Nmap Scripting Engine functions. This module contains various handy
+-- functions that are too small to justify modules of their own.
+--
+-- @copyright Same as Nmap--See https://nmap.org/book/man-legal.html
+-- @class module
+-- @name stdnse
+
+local _G = require "_G"
+local p9 = require "p9"
+local coroutine = require "coroutine"
+local math = require "math"
+local string = require "string"
+local table = require "table"
+local assert = assert;
+local error = error;
+local getmetatable = getmetatable;
+local ipairs = ipairs
+local pairs = pairs
+local next = next
+local rawset = rawset
+local require = require;
+local select = select
+local setmetatable = setmetatable;
+local tonumber = tonumber;
+local tostring = tostring;
+local print = print;
+local type = type
+local pcall = pcall
+
+local ceil = math.ceil
+local max = math.max
+
+local format = string.format;
+local rep = string.rep
+local match = string.match
+local find = string.find
+local sub = string.sub
+local gsub = string.gsub
+local char = string.char
+local byte = string.byte
+local gmatch = string.gmatch
+
+local concat = table.concat;
+local insert = table.insert;
+local remove = table.remove;
+local pack = table.pack;
+local unpack = table.unpack;
+
+local EMPTY = require "strict" {}; -- Empty constant table
+
+_ENV = {};
+
+--- Sleeps for a given amount of time.
+--
+-- This causes the program to yield control and not regain it until the time
+-- period has elapsed. The time may have a fractional part. Internally, the
+-- timer provides millisecond resolution.
+-- @name sleep
+-- @class function
+-- @param t Time to sleep, in seconds.
+-- @usage stdnse.sleep(1.5)
+local function sleep (seconds)
+ p9.sleep(seconds * 1000)
+end
+
+_ENV.sleep = sleep;
+
+-- These stub functions get overwritten by the script run loop in nse_main.lua
+-- These empty stubs will be used if a library calls stdnse.debug while loading
+_ENV.getid = function () return end
+_ENV.getinfo = function () return end
+_ENV.gethostport = function () return end
+
+do
+ local t = {
+ ["0"] = "0000",
+ ["1"] = "0001",
+ ["2"] = "0010",
+ ["3"] = "0011",
+ ["4"] = "0100",
+ ["5"] = "0101",
+ ["6"] = "0110",
+ ["7"] = "0111",
+ ["8"] = "1000",
+ ["9"] = "1001",
+ a = "1010",
+ b = "1011",
+ c = "1100",
+ d = "1101",
+ e = "1110",
+ f = "1111"
+ };
+
+--- Converts the given number, n, to a string in a binary number format (12
+-- becomes "1100"). Leading 0s not stripped.
+-- @param n Number to convert.
+-- @return String in binary format.
+ function tobinary(n)
+ -- enforced by string.format: assert(tonumber(n), "number expected");
+ return gsub(format("%x", n), "%w", t)
+ end
+end
+
+--- Converts the given number, n, to a string in an octal number format (12
+-- becomes "14").
+-- @param n Number to convert.
+-- @return String in octal format.
+function tooctal(n)
+ -- enforced by string.format: assert(tonumber(n), "number expected");
+ return format("%o", n)
+end
+
+local tohex_helper = function(b)
+ return format("%02x", byte(b))
+end
+--- Encode a string or integer in hexadecimal (12 becomes "c", "AB" becomes
+-- "4142").
+--
+-- An optional second argument is a table with formatting options. The possible
+-- fields in this table are
+-- * <code>separator</code>: A string to use to separate groups of digits.
+-- * <code>group</code>: The size of each group of digits between separators. Defaults to 2, but has no effect if <code>separator</code> is not also given.
+-- @usage
+-- stdnse.tohex("abc") --> "616263"
+-- stdnse.tohex("abc", {separator = ":"}) --> "61:62:63"
+-- stdnse.tohex("abc", {separator = ":", group = 4}) --> "61:6263"
+-- stdnse.tohex(123456) --> "1e240"
+-- stdnse.tohex(123456, {separator = ":"}) --> "1:e2:40"
+-- stdnse.tohex(123456, {separator = ":", group = 4}) --> "1:e240"
+-- @param s String or number to be encoded.
+-- @param options Table specifying formatting options.
+-- @return String in hexadecimal format.
+function tohex( s, options )
+ options = options or EMPTY
+ local separator = options.separator
+ local hex
+
+ if type( s ) == "number" then
+ hex = format("%x", s)
+ elseif type( s ) == 'string' then
+ hex = gsub(s, ".", tohex_helper)
+ else
+ error( "Type not supported in tohex(): " .. type(s), 2 )
+ end
+
+ -- format hex if we got a separator
+ if separator then
+ local group = options.group or 2
+ local subs = 0
+ local pat = "(%x)(" .. rep("[^:]", group) .. ")%f[\0:]"
+ repeat
+ hex, subs = gsub(hex, pat, "%1:%2")
+ until subs == 0
+ end
+
+ return hex
+end
+
+
+local fromhex_helper = function (h)
+ return char(tonumber(h, 16))
+end
+---Decode a hexadecimal string to raw bytes
+--
+-- The string can contain any amount of whitespace and capital or lowercase
+-- hexadecimal digits. There must be an even number of hex digits, since it
+-- takes 2 hex digits to make a byte.
+--
+-- @param hex A string in hexadecimal representation
+-- @return A string of bytes or nil if string could not be decoded
+-- @return Error message if string could not be decoded
+function fromhex (hex)
+ local p = find(hex, "[^%x%s]")
+ if p then
+ return nil, "Invalid hexadecimal digits at position " .. p
+ end
+ hex = gsub(hex, "%s+", "")
+ if #hex % 2 ~= 0 then
+ return nil, "Odd number of hexadecimal digits"
+ end
+ return gsub(hex, "..", fromhex_helper)
+end
+
+local colonsep = {separator=":"}
+---Format a MAC address as colon-separated hex bytes.
+--@param mac The MAC address in binary, such as <code>host.mac_addr</code>
+--@return The MAC address in XX:XX:XX:XX:XX:XX format
+function format_mac(mac)
+ return tohex(mac, colonsep)
+end
+
+---Either return the string itself, or return "<blank>" (or the value of the second parameter) if the string
+-- was blank or nil.
+--
+--@param string The base string.
+--@param blank The string to return if <code>string</code> was blank
+--@return Either <code>string</code> or, if it was blank, <code>blank</code>
+function string_or_blank(string, blank)
+ if(string == nil or string == "") then
+ if(blank == nil) then
+ return "<blank>"
+ else
+ return blank
+ end
+ else
+ return string
+ end
+end
+
+local timespec_multipliers = {[""] = 1, s = 1, m = 60, h = 60 * 60, ms = 0.001}
+---
+-- Parses a time duration specification, which is a number followed by a
+-- unit, and returns a number of seconds.
+--
+-- The unit is optional and defaults to seconds. The possible units
+-- (case-insensitive) are
+-- * <code>ms</code>: milliseconds,
+-- * <code>s</code>: seconds,
+-- * <code>m</code>: minutes,
+-- * <code>h</code>: hours.
+-- In case of a parsing error, the function returns <code>nil</code>
+-- followed by an error message.
+--
+-- @usage
+-- parse_timespec("10") --> 10
+-- parse_timespec("10ms") --> 0.01
+-- parse_timespec("10s") --> 10
+-- parse_timespec("10m") --> 600
+-- parse_timespec("10h") --> 36000
+-- parse_timespec("10z") --> nil, "Can't parse time specification \"10z\" (bad unit \"z\")"
+--
+-- @param timespec A time specification string.
+-- @return A number of seconds, or <code>nil</code> followed by an error
+-- message.
+function parse_timespec(timespec)
+ if timespec == nil then return nil, "Can't parse nil timespec" end
+ local n, unit, t, m
+
+ n, unit = match(timespec, "^([%d.]+)(.*)$")
+ if not n then
+ return nil, format("Can't parse time specification \"%s\"", timespec)
+ end
+
+ t = tonumber(n)
+ if not t then
+ return nil, format("Can't parse time specification \"%s\" (bad number \"%s\")", timespec, n)
+ end
+
+ m = timespec_multipliers[unit]
+ if not m then
+ return nil, format("Can't parse time specification \"%s\" (bad unit \"%s\")", timespec, unit)
+ end
+
+ return t * m
+end
+
+--- Returns the current time in milliseconds since the epoch
+-- @return The current time in milliseconds since the epoch
+function clock_ms()
+ return p9.clock() * 1000
+end
+
+--- Returns the current time in microseconds since the epoch
+-- @return The current time in microseconds since the epoch
+function clock_us()
+ return p9.clock() * 1000000
+end
+
+---Get the indentation symbols at a given level.
+local function format_get_indent(indent)
+ return rep(" ", #indent)
+end
+
+-- A helper for format_output (see below).
+local function format_output_sub(status, data, indent)
+ if (#data == 0) then
+ return ""
+ end
+
+ -- Used to put 'ERROR: ' in front of all lines on error messages
+ local prefix = ""
+ -- Initialize the output string to blank (or, if we're at the top, add a newline)
+ local output = {}
+ if(not(indent)) then
+ insert(output, '\n')
+ end
+
+ if(not(status)) then
+ if(nmap.debugging() < 1) then
+ return nil
+ end
+ prefix = "ERROR: "
+ end
+
+ -- If a string was passed, turn it into a table
+ if(type(data) == 'string') then
+ data = {data}
+ end
+
+ -- Make sure we have an indent value
+ indent = indent or {}
+
+ if(data['name']) then
+ if(data['warning'] and nmap.debugging() > 0) then
+ insert(output, format("%s%s%s (WARNING: %s)\n",
+ format_get_indent(indent), prefix,
+ data['name'], data['warning']))
+ else
+ insert(output, format("%s%s%s\n",
+ format_get_indent(indent), prefix,
+ data['name']))
+ end
+ elseif(data['warning'] and nmap.debugging() > 0) then
+ insert(output, format("%s%s(WARNING: %s)\n",
+ format_get_indent(indent), prefix,
+ data['warning']))
+ end
+
+ for i, value in ipairs(data) do
+ if(type(value) == 'table') then
+ -- Do a shallow copy of indent
+ local new_indent = {}
+ for _, v in ipairs(indent) do
+ insert(new_indent, v)
+ end
+
+ if(i ~= #data) then
+ insert(new_indent, false)
+ else
+ insert(new_indent, true)
+ end
+
+ insert(output, format_output_sub(status, value, new_indent))
+
+ elseif(type(value) == 'string') then
+ -- ensure it ends with a newline
+ if sub(value, -1) ~= "\n" then value = value .. "\n" end
+ for line in gmatch(value, "([^\r\n]-)\n") do
+ insert(output, format("%s %s%s\n",
+ format_get_indent(indent),
+ prefix, line))
+ end
+ end
+ end
+
+ return concat(output)
+end
+
+---This function is deprecated.
+--
+-- Please use structured NSE output instead: https://nmap.org/book/nse-api.html#nse-structured-output
+--
+-- Takes a table of output on the commandline and formats it for display to the
+-- user.
+--
+-- This is basically done by converting an array of nested tables into a
+-- string. In addition to numbered array elements, each table can have a 'name'
+-- and a 'warning' value. The 'name' will be displayed above the table, and
+-- 'warning' will be displayed, with a 'WARNING' tag, if and only if debugging
+-- is enabled.
+--
+-- Here's an example of a table:
+-- <code>
+-- local domains = {}
+-- domains['name'] = "DOMAINS"
+-- table.insert(domains, 'Domain 1')
+-- table.insert(domains, 'Domain 2')
+--
+-- local names = {}
+-- names['name'] = "NAMES"
+-- names['warning'] = "Not all names could be determined!"
+-- table.insert(names, "Name 1")
+--
+-- local response = {}
+-- table.insert(response, "Apple pie")
+-- table.insert(response, domains)
+-- table.insert(response, names)
+--
+-- return stdnse.format_output(true, response)
+-- </code>
+--
+-- With debugging enabled, this is the output:
+-- <code>
+-- Host script results:
+-- | smb-enum-domains:
+-- | Apple pie
+-- | DOMAINS
+-- | Domain 1
+-- | Domain 2
+-- | NAMES (WARNING: Not all names could be determined!)
+-- |_ Name 1
+-- </code>
+--
+--@param status A boolean value dictating whether or not the script succeeded.
+-- If status is false, and debugging is enabled, 'ERROR' is prepended
+-- to every line. If status is false and debugging is disabled, no output
+-- occurs.
+--@param data The table of output.
+--@param indent Used for indentation on recursive calls; should generally be set to
+-- nil when calling from a script.
+-- @return <code>nil</code>, if <code>data</code> is empty, otherwise a
+-- multiline string.
+function format_output(status, data, indent)
+ -- If data is nil, die with an error (I keep doing that by accident)
+ assert(data, "No data was passed to format_output()")
+
+ -- Don't bother if we don't have any data
+ if (#data == 0) then
+ return nil
+ end
+
+ local result = format_output_sub(status, data, indent)
+
+ -- Check for an empty result
+ if(result == nil or #result == "" or result == "\n" or result == "\n") then
+ return nil
+ end
+
+ return result
+end
+
+--- Module function that mimics some behavior of Lua 5.1 module function.
+--
+-- This convenience function returns a module environment to set the _ENV
+-- upvalue. The _NAME, _PACKAGE, and _M fields are set as in the Lua 5.1
+-- version of this function. Each option function (e.g. stdnse.seeall)
+-- passed is run with the new environment, in order.
+--
+-- @see stdnse.seeall
+-- @see strict
+-- @usage
+-- _ENV = stdnse.module(name, stdnse.seeall, require "strict");
+-- @param name The module name.
+-- @param ... Option functions which modify the environment of the module.
+function module (name, ...)
+ local env = {};
+ env._NAME = name;
+ env._PACKAGE = match(name, "(.+)%.[^.]+$");
+ env._M = env;
+ local mods = pack(...);
+ for i = 1, mods.n do
+ mods[i](env);
+ end
+ return env;
+end
+
+--- Change environment to load global variables.
+--
+-- Option function for use with stdnse.module. It is the same
+-- as package.seeall from Lua 5.1.
+--
+-- @see stdnse.module
+-- @usage
+-- _ENV = stdnse.module(name, stdnse.seeall);
+-- @param env Environment to change.
+function seeall (env)
+ local m = getmetatable(env) or {};
+ m.__index = _G;
+ setmetatable(env, m);
+end
+
+--- Return a table that keeps elements in order of insertion.
+--
+-- The pairs function, called on a table returned by this function, will yield
+-- elements in the order they were inserted. This function is meant to be used
+-- to construct output tables returned by scripts.
+--
+-- Reinserting a key that is already in the table does not change its position
+-- in the order. However, removing a key by assigning to <code>nil</code> and
+-- then doing another assignment will move the key to the end of the order.
+--
+-- @return An ordered table.
+function output_table ()
+ local t = {}
+ local order = {}
+ local function iterator ()
+ for i, key in ipairs(order) do
+ coroutine.yield(key, t[key])
+ end
+ end
+ local mt = {
+ __newindex = function (_, k, v)
+ if t[k] == nil and v ~= nil then
+ -- New key?
+ insert(order, k)
+ elseif v == nil then
+ -- Deleting an existing key?
+ for i, key in ipairs(order) do
+ if key == k then
+ remove(order, i)
+ break
+ end
+ end
+ end
+ rawset(t, k, v)
+ end,
+ __index = t,
+ __pairs = function (_)
+ return coroutine.wrap(iterator)
+ end,
+ __call = function (_) -- hack to mean "not_empty?"
+ return not not next(order)
+ end,
+ __len = function (_)
+ return #order
+ end
+ }
+ return setmetatable({}, mt)
+end
+
+--- A pretty printer for Lua objects.
+--
+-- Takes an object (usually a table) and prints it using the
+-- printer function. The printer function takes a sole string
+-- argument and will be called repeatedly.
+--
+-- @param obj The object to pretty print.
+-- @param printer The printer function.
+function pretty_printer (obj, printer)
+ if printer == nil then printer = print end
+
+ local function aux (obj, spacing)
+ local t = type(obj)
+ if t == "table" then
+ printer "{\n"
+ for k, v in pairs(obj) do
+ local spacing = spacing.."\t"
+ printer(spacing)
+ printer "["
+ aux(k, spacing)
+ printer "] = "
+ aux(v, spacing)
+ printer ",\n"
+ end
+ printer(spacing.."}")
+ elseif t == "string" then
+ printer(format("%q", obj))
+ else
+ printer(tostring(obj))
+ end
+ end
+
+ return aux(obj, "")
+end
+
+--- Returns a conservative timeout for a host
+--
+-- If the host parameter is a NSE host table with a <code>times.timeout</code>
+-- attribute, then the return value is the host timeout scaled according to the
+-- max_timeout. The scaling factor is defined by a linear formula such that
+-- (max_timeout=8000, scale=2) and (max_timeout=1000, scale=1)
+--
+-- @param host The host object to base the timeout on. If this is anything but
+-- a host table, the max_timeout is returned.
+-- @param max_timeout The maximum timeout in milliseconds. This is the default
+-- timeout used if there is no host.times.timeout. Default: 8000
+-- @param min_timeout The minimum timeout in milliseconds that will be
+-- returned. Default: 1000
+-- @return The timeout in milliseconds, suitable for passing to set_timeout
+-- @usage
+-- assert(host.times.timeout == 1.3)
+-- assert(get_timeout() == 8000)
+-- assert(get_timeout(nil, 5000) == 5000)
+-- assert(get_timeout(host) == 2600)
+-- assert(get_timeout(host, 10000, 3000) == 3000)
+function get_timeout(host, max_timeout, min_timeout)
+ max_timeout = max_timeout or 8000
+ local t = type(host) == "table" and host.times and host.times.timeout
+ if not t then
+ return max_timeout
+ end
+ t = t * (max_timeout + 6000) / 7
+ min_timeout = min_timeout or 1000
+ if t < min_timeout then
+ return min_timeout
+ elseif t > max_timeout then
+ return max_timeout
+ end
+ return t
+end
+
+return _ENV;
--- /dev/null
+++ b/lib/lua/nseport/strict.lua
@@ -1,0 +1,88 @@
+---
+-- Strict declared global library. Checks for undeclared global variables
+-- during runtime execution.
+--
+-- This module places the <code>strict</code> function in the global
+-- environment. The strict function allows a script to add runtime checking so
+-- that undeclared globals cause an error to be raised. This is useful for
+-- finding accidental use of globals when local was intended.
+--
+-- A global variable is considered "declared" if the script makes an assignment
+-- to the global name (even <code>nil</code>) in the file scope.
+--
+-- @class module
+-- @name strict
+-- @copyright Same as Nmap--See https://nmap.org/book/man-legal.html
+
+local debug = require "debug"
+
+local error = error;
+local getmetatable = getmetatable;
+local rawset = rawset;
+local rawget = rawget;
+local setmetatable = setmetatable;
+local type = type;
+
+local getinfo = debug.getinfo;
+
+_ENV = {};
+
+local function what ()
+ local d = getinfo(3, "S");
+ return d and d.what or "C";
+end
+
+--- The strict function.
+--
+-- This function adds runtime checking to the global environment for use of
+-- undeclared globals. A global is 'undeclared' if not assigned in the file
+-- (script) scope previously. An error will be raised on use of an undeclared
+-- global.
+--
+-- This function should be passed last to stdnse.module in order to allow
+-- other environment option functions (e.g. stdnse.seeall) to change the
+-- environment first. This is important for allowing globals outside the
+-- library (in _G) to be indexed.
+--
+-- @see stdnse.module
+-- @usage
+-- _ENV = stdnse.module(name, require "strict");
+-- @param env The environment to modify.
+local function strict (env)
+ local mt = getmetatable(env) or setmetatable(env, {}) and getmetatable(env);
+ local _newindex, _index = mt.__newindex, mt.__index;
+
+ mt.__declared = {};
+
+ function mt.__newindex (t, n, v)
+ if type(_newindex) == "function" then
+ _newindex(t, n, v); -- hook it
+ end
+ if not mt.__declared[n] then
+ local w = what();
+ if w ~= "main" and w ~= "C" then
+ error("assign to undeclared variable '"..n.."'", 2);
+ end
+ mt.__declared[n] = true;
+ end
+ rawset(t, n, v);
+ end
+
+ function mt.__index (t, n)
+ if type(_index) == "function" then
+ local v = _index(t, n); -- hook it
+ if v ~= nil then return v end
+ elseif _index ~= nil then
+ local v = _index[n];
+ if v ~= nil then return v end
+ end
+ if not mt.__declared[n] and what() ~= "C" then
+ error("variable '"..n.."' is not declared", 2);
+ end
+ return rawget(t, n);
+ end
+
+ return env;
+end
+
+return strict;
--- /dev/null
+++ b/lib/lua/nseport/stringaux.lua
@@ -1,0 +1,140 @@
+--- Auxiliary functions for string manipulation
+--
+-- @author Daniel Miller
+-- @copyright Same as Nmap--See https://nmap.org/book/man-legal.html
+-- @class module
+-- @name stringaux
+
+local assert = assert
+local type = type
+
+local string = require "string"
+local byte = string.byte
+local find = string.find
+local match = string.match
+local sub = string.sub
+local gsub = string.gsub
+local format = string.format
+local lower = string.lower
+local upper = string.upper
+
+local table = require "table"
+local concat = table.concat
+
+local _ENV = {}
+
+--- Join a list of strings with a separator string.
+--
+-- This is Lua's <code>table.concat</code> function with the parameters
+-- swapped for coherence.
+-- @usage
+-- stringaux.strjoin(", ", {"Anna", "Bob", "Charlie", "Dolores"})
+-- --> "Anna, Bob, Charlie, Dolores"
+-- @param delimiter String to delimit each element of the list.
+-- @param list Array of strings to concatenate.
+-- @return Concatenated string.
+function strjoin(delimiter, list)
+ assert(type(delimiter) == "string" or type(delimiter) == nil, "delimiter is of the wrong type! (did you get the parameters backward?)")
+
+ return concat(list, delimiter);
+end
+
+--- Split a string at a given delimiter, which may be a pattern.
+--
+-- If you want to loop over the resulting values, consider using string.gmatch instead.
+-- @usage
+-- stringaux.strsplit(",%s*", "Anna, Bob, Charlie, Dolores")
+-- --> { "Anna", "Bob", "Charlie", "Dolores" }
+-- @param pattern Pattern that separates the desired strings.
+-- @param text String to split.
+-- @return Array of substrings without the separating pattern.
+-- @see string.gmatch
+function strsplit(pattern, text)
+ local list, pos = {}, 1;
+
+ assert(pattern ~= "", "delimiter matches empty string!");
+
+ while true do
+ local first, last = find(text, pattern, pos);
+ if first then -- found?
+ list[#list+1] = sub(text, pos, first-1);
+ pos = last+1;
+ else
+ list[#list+1] = sub(text, pos);
+ break;
+ end
+ end
+ return list;
+end
+
+-- This pattern must match the percent sign '%' since it is used in
+-- escaping.
+local FILESYSTEM_UNSAFE = "[^a-zA-Z0-9._-]"
+local function _escape_helper (c)
+ return format("%%%02x", byte(c))
+end
+---
+-- Escape a string to remove bytes and strings that may have meaning to
+-- a filesystem, such as slashes.
+--
+-- All bytes are escaped, except for:
+-- * alphabetic <code>a</code>-<code>z</code> and <code>A</code>-<code>Z</code>
+-- * digits 0-9
+-- * <code>.</code> <code>_</code> <code>-</code>
+-- In addition, the strings <code>"."</code> and <code>".."</code> have
+-- their characters escaped.
+--
+-- Bytes are escaped by a percent sign followed by the two-digit
+-- hexadecimal representation of the byte value.
+-- * <code>filename_escape("filename.ext") --> "filename.ext"</code>
+-- * <code>filename_escape("input/output") --> "input%2foutput"</code>
+-- * <code>filename_escape(".") --> "%2e"</code>
+-- * <code>filename_escape("..") --> "%2e%2e"</code>
+-- This escaping is somewhat like that of JavaScript
+-- <code>encodeURIComponent</code>, except that fewer bytes are
+-- whitelisted, and it works on bytes, not Unicode characters or UTF-16
+-- code points.
+function filename_escape(s)
+ if s == "." then
+ return "%2e"
+ elseif s == ".." then
+ return "%2e%2e"
+ else
+ return (gsub(s, FILESYSTEM_UNSAFE, _escape_helper))
+ end
+end
+
+--- Returns the case insensitive pattern of given parameter
+--
+-- Useful while doing case insensitive pattern match using string library.
+-- https://stackoverflow.com/questions/11401890/case-insensitive-lua-pattern-matching/11402486#11402486
+--
+-- @usage stringaux.ipattern("user")
+-- --> "[uU][sS][eE][rR]"
+-- @param pattern The string
+-- @return A case insensitive patterned string
+function ipattern(pattern)
+ local in_brackets = false
+ -- Find an optional '%' (group 2) followed by any character (group 3)
+ local p = gsub(pattern, "(%%?)(.)", function(percent, letter)
+ if percent ~= "" then
+ -- It's a %-escape, return as-is
+ return nil
+ elseif not match(letter, "%a") then
+ -- It's not alpha. Update bracket status and return as-is
+ if letter == "[" then
+ in_brackets = true
+ elseif letter == "]" then
+ in_brackets = false
+ end
+ return nil
+ elseif not in_brackets then
+ -- Else, return a case-insensitive character class of the matched letter
+ return format("[%s%s]", lower(letter), upper(letter))
+ end
+ end)
+
+ return p
+end
+
+return _ENV
--- /dev/null
+++ b/lib/lua/nseport/tableaux.lua
@@ -1,0 +1,91 @@
+--- Auxiliary functions for table manipulation
+--
+-- @author Daniel Miller
+-- @copyright Same as Nmap--See https://nmap.org/book/man-legal.html
+-- @class module
+-- @name tableaux
+
+local next = next
+local pairs = pairs
+local ipairs = ipairs
+local type = type
+local _ENV = {}
+
+local tcopy_local
+--- Recursively copy a table.
+--
+-- Uses simple assignment to copy keys and values from a table, recursing into
+-- subtables as necessary.
+-- @param t the table to copy
+-- @return a deep copy of the table
+function tcopy (t)
+ local tc = {};
+ for k,v in pairs(t) do
+ if type(v) == "table" then
+ tc[k] = tcopy_local(v);
+ else
+ tc[k] = v;
+ end
+ end
+ return tc;
+end
+tcopy_local = tcopy
+
+--- Copy one level of a table.
+--
+-- Iterates over the keys of a table and copies their values into a new table.
+-- If any values are tables, they are copied by reference only, and modifying
+-- the copy will modify the original table value as well.
+-- @param t the table to copy
+-- @return a shallow copy of the table
+function shallow_tcopy(t)
+ local k = next(t)
+ local out = {}
+ while k do
+ out[k] = t[k]
+ k = next(t, k)
+ end
+ return out
+end
+
+--- Invert a one-to-one mapping
+-- @param t the table to invert
+-- @return an inverted mapping
+function invert(t)
+ local out = {}
+ for k, v in pairs(t) do
+ out[v] = k
+ end
+ return out
+end
+
+--- Check for the presence of a value in a table
+--@param t the table to search into
+--@param item the searched value
+--@array (optional) If true, then use ipairs to only search the array indices of the table.
+--@return Boolean true if the item was found, false if not
+--@return The index or key where the value was found, or nil
+function contains(t, item, array)
+ local iter = array and ipairs or pairs
+ for k, val in iter(t) do
+ if val == item then
+ return true, k
+ end
+ end
+ return false, nil
+end
+
+--- Returns the keys of a table as an array
+-- @param t The table
+-- @return A table of keys
+function keys(t)
+ local ret = {}
+ local k, v = next(t)
+ while k ~= nil do
+ ret[#ret+1] = k
+ k, v = next(t, k)
+ end
+ return ret
+end
+
+return _ENV
--- /dev/null
+++ b/lib/lua/nseport/unicode.lua
@@ -1,0 +1,441 @@
+---
+-- Library methods for handling unicode strings.
+--
+-- @author Daniel Miller
+-- @copyright Same as Nmap--See https://nmap.org/book/man-legal.html
+
+
+local string = require "string"
+local table = require "table"
+local stdnse = require "stdnse"
+--local unittest = require "unittest"
+local tableaux = require "tableaux"
+local utf8 = require "utf8"
+_ENV = stdnse.module("unicode", stdnse.seeall)
+
+-- Localize a few functions for a tiny speed boost, since these will be looped
+-- over every char of a string
+local byte = string.byte
+local char = string.char
+local pack = string.pack
+local unpack = string.unpack
+local concat = table.concat
+local pcall = pcall
+
+
+---Decode a buffer containing Unicode data.
+--@param buf The string/buffer to be decoded
+--@param decoder A Unicode decoder function (such as utf8_dec)
+--@param bigendian For encodings that care about byte-order (such as UTF-16),
+-- set this to true to force big-endian byte order. Default:
+-- false (little-endian)
+--@return A list-table containing the code points as numbers
+function decode(buf, decoder, bigendian)
+ if decoder == utf8_dec then
+ return {utf8.codepoint(buf, 1, -1)}
+ end
+ local cp = {}
+ local pos = 1
+ while pos <= #buf do
+ pos, cp[#cp+1] = decoder(buf, pos, bigendian)
+ end
+ return cp
+end
+
+---Encode a list of Unicode code points
+--@param list A list-table of code points as numbers
+--@param encoder A Unicode encoder function (such as utf8_enc)
+--@param bigendian For encodings that care about byte-order (such as UTF-16),
+-- set this to true to force big-endian byte order. Default:
+-- false (little-endian)
+--@return An encoded string
+function encode(list, encoder, bigendian)
+ if encoder == utf8_enc then
+ return utf8.char(table.unpack(list))
+ end
+ local buf = {}
+ for i, cp in ipairs(list) do
+ buf[i] = encoder(cp, bigendian)
+ end
+ return table.concat(buf, "")
+end
+
+---Transcode a string from one format to another
+--
+--The string will be decoded and re-encoded in one pass. This saves some
+--overhead vs simply passing the output of <code>unicode.encode</code> to
+--<code>unicode.decode</code>.
+--@param buf The string/buffer to be transcoded
+--@param decoder A Unicode decoder function (such as utf16_dec)
+--@param encoder A Unicode encoder function (such as utf8_enc)
+--@param bigendian_dec Set this to true to force big-endian decoding.
+--@param bigendian_enc Set this to true to force big-endian encoding.
+--@return An encoded string
+function transcode(buf, decoder, encoder, bigendian_dec, bigendian_enc)
+ local out = {}
+ local cp
+ local pos = 1
+ -- Take advantage of Lua's built-in utf8 functions
+ if decoder == utf8_dec then
+ for _, cp in utf8.codes(buf) do
+ out[#out+1] = encoder(cp, bigendian_enc)
+ end
+ elseif encoder == utf8_enc then
+ while pos <= #buf do
+ pos, cp = decoder(buf, pos, bigendian_dec)
+ out[#out+1] = utf8.char(cp)
+ end
+ else
+ while pos <= #buf do
+ pos, cp = decoder(buf, pos, bigendian_dec)
+ out[#out+1] = encoder(cp, bigendian_enc)
+ end
+ end
+ return table.concat(out)
+end
+
+--- Determine (poorly) the character encoding of a string
+--
+-- First, the string is checked for a Byte-order Mark (BOM). This can be
+-- examined to determine UTF-16 with endianness or UTF-8. If no BOM is found,
+-- the string is examined.
+--
+-- If null bytes are encountered, UTF-16 is assumed. Endianness is determined
+-- by byte position, assuming the null is the high-order byte. Otherwise, if
+-- byte values over 127 are found, UTF-8 decoding is attempted. If this fails,
+-- the result is 'other', otherwise it is 'utf-8'. If no high bytes are found,
+-- the result is 'ascii'.
+--
+--@param buf The string/buffer to be identified
+--@param len The number of bytes to inspect in order to identify the string.
+-- Default: 100
+--@return A string describing the encoding: 'ascii', 'utf-8', 'utf-16be',
+-- 'utf-16le', or 'other' meaning some unidentified 8-bit encoding
+function chardet(buf, len)
+ local limit = len or 100
+ if limit > #buf then
+ limit = #buf
+ end
+ -- Check BOM
+ if limit >= 2 then
+ local bom1, bom2 = byte(buf, 1, 2)
+ if bom1 == 0xff and bom2 == 0xfe then
+ return 'utf-16le'
+ elseif bom1 == 0xfe and bom2 == 0xff then
+ return 'utf-16be'
+ elseif limit >= 3 then
+ local bom3 = byte(buf, 3)
+ if bom1 == 0xef and bom2 == 0xbb and bom3 == 0xbf then
+ return 'utf-8'
+ end
+ end
+ end
+ -- Try bytes
+ local pos = 1
+ local high = false
+ local is_utf8 = true
+ while pos < limit do
+ local c = byte(buf, pos)
+ if c == 0 then
+ if pos % 2 == 0 then
+ return 'utf-16le'
+ else
+ return 'utf-16be'
+ end
+ is_utf8 = false
+ pos = pos + 1
+ elseif c > 127 then
+ if not high then
+ high = true
+ end
+ if is_utf8 then
+ local p, cp = utf8_dec(buf, pos)
+ if not p then
+ is_utf8 = false
+ else
+ pos = p
+ end
+ end
+ if not is_utf8 then
+ pos = pos + 1
+ end
+ else
+ pos = pos + 1
+ end
+ end
+ if high then
+ if is_utf8 then
+ return 'utf-8'
+ else
+ return 'other'
+ end
+ else
+ return 'ascii'
+ end
+end
+
+---Encode a Unicode code point to UTF-16. See RFC 2781.
+--
+-- Windows OS prior to Windows 2000 only supports UCS-2, so beware using this
+-- function to encode code points above 0xFFFF.
+--@param cp The Unicode code point as a number
+--@param bigendian Set this to true to encode big-endian UTF-16. Default is
+-- false (little-endian)
+--@return A string containing the code point in UTF-16 encoding.
+function utf16_enc(cp, bigendian)
+ local fmt = "<I2"
+ if bigendian then
+ fmt = ">I2"
+ end
+
+ if cp % 1.0 ~= 0.0 or cp < 0 then
+ -- Only defined for nonnegative integers.
+ return nil
+ elseif cp <= 0xFFFF then
+ return pack(fmt, cp)
+ elseif cp <= 0x10FFFF then
+ cp = cp - 0x10000
+ return pack(fmt .. fmt, 0xD800 + (cp >> 10), 0xDC00 + (cp & 0x3FF))
+ else
+ return nil
+ end
+end
+
+---Decodes a UTF-16 character.
+--
+-- Does not check that the returned code point is a real character.
+-- Specifically, it can be fooled by out-of-order lead- and trail-surrogate
+-- characters.
+--@param buf A string containing the character
+--@param pos The index in the string where the character begins
+--@param bigendian Set this to true to encode big-endian UTF-16. Default is
+-- false (little-endian)
+--@return pos The index in the string where the character ended
+--@return cp The code point of the character as a number
+function utf16_dec(buf, pos, bigendian)
+ local fmt = "<I2"
+ if bigendian then
+ fmt = ">I2"
+ end
+
+ local cp
+ cp, pos = unpack(fmt, buf, pos)
+ if cp >= 0xD800 and cp <= 0xDFFF then
+ local high = (cp - 0xD800) << 10
+ cp, pos = unpack(fmt, buf, pos)
+ cp = 0x10000 + high + cp - 0xDC00
+ end
+ return pos, cp
+end
+
+---Encode a Unicode code point to UTF-8. See RFC 3629.
+--
+-- Does not check that cp is a real character; that is, doesn't exclude the
+-- surrogate range U+D800 - U+DFFF and a handful of others.
+-- @class function
+--@param cp The Unicode code point as a number
+--@return A string containing the code point in UTF-8 encoding.
+utf8_enc = utf8.char
+
+---Decodes a UTF-8 character.
+--
+-- Does not check that the returned code point is a real character.
+--@param buf A string containing the character
+--@param pos The index in the string where the character begins
+--@return pos The index in the string where the character ended or nil on error
+--@return cp The code point of the character as a number, or an error string
+function utf8_dec(buf, pos)
+ pos = pos or 1
+ local status, cp = pcall(utf8.codepoint, buf, pos)
+ if status then
+ return utf8.offset(buf, 2, pos), cp
+ else
+ return nil, cp
+ end
+end
+
+-- Code Page 437, native US-English Windows OEM code page
+local cp437_decode = {
+ [0x80] = 0x00c7,
+ [0x81] = 0x00fc,
+ [0x82] = 0x00e9,
+ [0x83] = 0x00e2,
+ [0x84] = 0x00e4,
+ [0x85] = 0x00e0,
+ [0x86] = 0x00e5,
+ [0x87] = 0x00e7,
+ [0x88] = 0x00ea,
+ [0x89] = 0x00eb,
+ [0x8a] = 0x00e8,
+ [0x8b] = 0x00ef,
+ [0x8c] = 0x00ee,
+ [0x8d] = 0x00ec,
+ [0x8e] = 0x00c4,
+ [0x8f] = 0x00c5,
+ [0x90] = 0x00c9,
+ [0x91] = 0x00e6,
+ [0x92] = 0x00c6,
+ [0x93] = 0x00f4,
+ [0x94] = 0x00f6,
+ [0x95] = 0x00f2,
+ [0x96] = 0x00fb,
+ [0x97] = 0x00f9,
+ [0x98] = 0x00ff,
+ [0x99] = 0x00d6,
+ [0x9a] = 0x00dc,
+ [0x9b] = 0x00a2,
+ [0x9c] = 0x00a3,
+ [0x9d] = 0x00a5,
+ [0x9e] = 0x20a7,
+ [0x9f] = 0x0192,
+ [0xa0] = 0x00e1,
+ [0xa1] = 0x00ed,
+ [0xa2] = 0x00f3,
+ [0xa3] = 0x00fa,
+ [0xa4] = 0x00f1,
+ [0xa5] = 0x00d1,
+ [0xa6] = 0x00aa,
+ [0xa7] = 0x00ba,
+ [0xa8] = 0x00bf,
+ [0xa9] = 0x2310,
+ [0xaa] = 0x00ac,
+ [0xab] = 0x00bd,
+ [0xac] = 0x00bc,
+ [0xad] = 0x00a1,
+ [0xae] = 0x00ab,
+ [0xaf] = 0x00bb,
+ [0xb0] = 0x2591,
+ [0xb1] = 0x2592,
+ [0xb2] = 0x2593,
+ [0xb3] = 0x2502,
+ [0xb4] = 0x2524,
+ [0xb5] = 0x2561,
+ [0xb6] = 0x2562,
+ [0xb7] = 0x2556,
+ [0xb8] = 0x2555,
+ [0xb9] = 0x2563,
+ [0xba] = 0x2551,
+ [0xbb] = 0x2557,
+ [0xbc] = 0x255d,
+ [0xbd] = 0x255c,
+ [0xbe] = 0x255b,
+ [0xbf] = 0x2510,
+ [0xc0] = 0x2514,
+ [0xc1] = 0x2534,
+ [0xc2] = 0x252c,
+ [0xc3] = 0x251c,
+ [0xc4] = 0x2500,
+ [0xc5] = 0x253c,
+ [0xc6] = 0x255e,
+ [0xc7] = 0x255f,
+ [0xc8] = 0x255a,
+ [0xc9] = 0x2554,
+ [0xca] = 0x2569,
+ [0xcb] = 0x2566,
+ [0xcc] = 0x2560,
+ [0xcd] = 0x2550,
+ [0xce] = 0x256c,
+ [0xcf] = 0x2567,
+ [0xd0] = 0x2568,
+ [0xd1] = 0x2564,
+ [0xd2] = 0x2565,
+ [0xd3] = 0x2559,
+ [0xd4] = 0x2558,
+ [0xd5] = 0x2552,
+ [0xd6] = 0x2553,
+ [0xd7] = 0x256b,
+ [0xd8] = 0x256a,
+ [0xd9] = 0x2518,
+ [0xda] = 0x250c,
+ [0xdb] = 0x2588,
+ [0xdc] = 0x2584,
+ [0xdd] = 0x258c,
+ [0xde] = 0x2590,
+ [0xdf] = 0x2580,
+ [0xe0] = 0x03b1,
+ [0xe1] = 0x00df,
+ [0xe2] = 0x0393,
+ [0xe3] = 0x03c0,
+ [0xe4] = 0x03a3,
+ [0xe5] = 0x03c3,
+ [0xe6] = 0x00b5,
+ [0xe7] = 0x03c4,
+ [0xe8] = 0x03a6,
+ [0xe9] = 0x0398,
+ [0xea] = 0x03a9,
+ [0xeb] = 0x03b4,
+ [0xec] = 0x221e,
+ [0xed] = 0x03c6,
+ [0xee] = 0x03b5,
+ [0xef] = 0x2229,
+ [0xf0] = 0x2261,
+ [0xf1] = 0x00b1,
+ [0xf2] = 0x2265,
+ [0xf3] = 0x2264,
+ [0xf4] = 0x2320,
+ [0xf5] = 0x2321,
+ [0xf6] = 0x00f7,
+ [0xf7] = 0x2248,
+ [0xf8] = 0x00b0,
+ [0xf9] = 0x2219,
+ [0xfa] = 0x00b7,
+ [0xfb] = 0x221a,
+ [0xfc] = 0x207f,
+ [0xfd] = 0x00b2,
+ [0xfe] = 0x25a0,
+ [0xff] = 0x00a0,
+}
+local cp437_encode = tableaux.invert(cp437_decode)
+
+---Encode a Unicode code point to CP437
+--
+-- Returns nil if the code point cannot be found in CP437
+--@param cp The Unicode code point as a number
+--@return A string containing the related CP437 character
+function cp437_enc(cp)
+ if cp < 0x80 then
+ return char(cp)
+ else
+ local bv = cp437_encode[cp]
+ if bv == nil then
+ return nil
+ else
+ return char(bv)
+ end
+ end
+end
+
+---Decodes a CP437 character
+--@param buf A string containing the character
+--@param pos The index in the string where the character begins
+--@return pos The index in the string where the character ended
+--@return cp The code point of the character as a number
+function cp437_dec(buf, pos)
+ pos = pos or 1
+ local bv = byte(buf, pos)
+ if bv < 0x80 then
+ return pos + 1, bv
+ else
+ return pos + 1, cp437_decode[bv]
+ end
+end
+
+---Helper function for the common case of UTF-16 to UTF-8 transcoding, such as
+--from a Windows/SMB unicode string to a printable ASCII (subset of UTF-8)
+--string.
+--@param from A string in UTF-16, little-endian
+--@return The string in UTF-8
+function utf16to8(from)
+ return transcode(from, utf16_dec, utf8_enc, false, nil)
+end
+
+---Helper function for the common case of UTF-8 to UTF-16 transcoding, such as
+--from a printable ASCII (subset of UTF-8) string to a Windows/SMB unicode
+--string.
+--@param from A string in UTF-8
+--@return The string in UTF-16, little-endian
+function utf8to16(from)
+ return transcode(from, utf8_dec, utf16_enc, nil, false)
+end
+
+return _ENV
--- /dev/null
+++ b/lib/lua/r2pipe.lua
@@ -1,0 +1,104 @@
+---
+-- r2pipe for lua
+---
+
+local net = require "net"
+local json = require "json"
+
+local r2pipe = {}
+
+---
+-- tcp protocol
+---
+
+local R2Tcp = {
+ new = function(self, host, port)
+ local o = {}
+ setmetatable(o, self)
+ self.__index = self
+
+ o.socket = net.Socket:new()
+ o.host = host
+ o.port = port
+
+ return o
+
+ end,
+
+ send = function(self, data)
+ self.socket:dial(self.host, self.port)
+ return self.socket:send(data)
+ end,
+
+ read = function(self)
+ local b, done
+ done = false
+
+ status, b = self.socket:receive_all()
+ if not(status) then
+ return false, b
+ end
+
+ self.socket:close()
+ return true, b
+ end,
+
+ close = function(self)
+ self.socket:close()
+ end,
+}
+
+---
+-- R2Comm
+---
+
+local R2Comm = {
+ new = function(self, proto)
+ local o = {}
+ setmetatable(o, self)
+ self.__index = self
+ o.proto = proto
+ return o
+ end,
+
+ cmd = function(self, cmd)
+ local status, err, response
+ status, err = self.proto:send(string.format("%s\n", cmd))
+ if not(status) then
+ return false, err
+ end
+ status, response = self.proto:read()
+ if not(status) then
+ return false, response
+ end
+ return true, response
+ end,
+
+ cmdj = function(self, cmd)
+ local status, response
+ status, response = self.proto:send(string.format("%s\n", cmd))
+ if not(status) then
+ return false, response
+ end
+ status, response = self.proto:read()
+ if not(status) then
+ return false, response
+ end
+ status, response = json.parse(response)
+ if not(status) then
+ return false, response
+ end
+ return true, response
+ end,
+
+ close = function(self)
+ self.proto:close()
+ end,
+}
+
+local r2pipe = {
+ R2Tcp = R2Tcp,
+ R2Comm = R2Comm,
+}
+
+return r2pipe