shithub: scripts

Download patch

ref: 2ebee720a8af84e6f1a4d4e390f723526553b5e0
parent: a7fd3774d4706aeee559afbf04cc016b085002a1
author: kitzman <[email protected]>
date: Tue Nov 21 10:23:46 EST 2023

added lua libs

diff: cannot open b/lib/lua/nseport//null: file does not exist: 'b/lib/lua/nseport//null' diff: cannot open b/lib/lua//null: file does not exist: 'b/lib/lua//null'
--- 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