"Fossies" - the Fresh Open Source Software Archive

Member "nmap-7.91/nselib/afp.lua" (9 Oct 2020, 74827 Bytes) of package /linux/misc/nmap-7.91.tgz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Lua source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. See also the latest Fossies "Diffs" side-by-side code changes report for "afp.lua": 7.90_vs_7.91.

    1 ---
    2 -- This library was written by Patrik Karlsson <patrik@cqure.net> to facilitate
    3 -- communication with the Apple AFP Service. It is not feature complete and
    4 -- still missing several functions.
    5 --
    6 -- The library currently supports
    7 -- * Authentication using the DHX UAM (CAST128)
    8 -- * File reading and writing
    9 -- * Listing sharepoints
   10 -- * Listing directory contents
   11 -- * Querying ACLs and mapping user identities (UIDs)
   12 --
   13 -- The library was built based on the following reference:
   14 -- http://developer.apple.com/mac/library/documentation/Networking/Reference/AFP_Reference/Reference/reference.html
   15 -- http://developer.apple.com/mac/library/documentation/Networking/Conceptual/AFP/AFPSecurity/AFPSecurity.html#//apple_ref/doc/uid/TP40000854-CH232-CHBBAGCB
   16 --
   17 -- Most functions have been tested against both Mac OS X 10.6.2 and Netatalk 2.0.3
   18 --
   19 -- The library contains the following four classes
   20 -- * <code>Response</code>
   21 -- ** A class used as return value by functions in the <code>Proto</code> class.
   22 -- ** The response class acts as a wrapper and holds the response data and any error information.
   23 -- * <code>Proto</code>
   24 -- ** This class contains all the AFP specific functions and calls.
   25 -- ** The functions can be accessed directly but the preferred method is through the <code>Helper</code> class.
   26 -- ** The function names closely resemble those described in the Apple documentation.
   27 -- ** Some functions may lack some of the options outlined in Apple's documentation.
   28 -- * <code>Helper</code>
   29 -- ** The helper class wraps the <code>Proto</code> class using functions with a more descriptive name.
   30 -- ** Functions are task-oriented. For example, <code>ReadFile</code> and usually call several functions in the <code>Proto</code> class.
   31 -- ** The purpose of this class is to give developers easy access to some of the common AFP tasks.
   32 -- * <code>Util</code>
   33 -- ** The <code>Util</code> class contains a number of static functions mainly used to convert data.
   34 --
   35 -- The following information will describe how to use the AFP Helper class to communicate with an AFP server.
   36 --
   37 -- The short version:
   38 -- <code>
   39 -- helper = afp.Helper:new()
   40 -- status, response = helper:OpenSession( host, port )
   41 -- status, response = helper:Login()
   42 -- .. do some fancy AFP stuff ..
   43 -- status, response = helper:Logout()
   44 -- status, response = helper:CloseSession()
   45 -- </code>
   46 --
   47 -- Here's the longer version, with some explanatory text. To start using the Helper class,
   48 -- the script has to create its own instance. We do this by issuing the following:
   49 -- <code>
   50 -- helper = afp.Helper:new()
   51 -- </code>
   52 --
   53 -- Next a session to the AFP server must be established, this is done using the OpenSession method of the
   54 -- Helper class, like this:
   55 -- <code>
   56 -- status, response = helper:OpenSession( host, port )
   57 -- </code>
   58 --
   59 -- The next step needed to be performed is to authenticate to the server. We need to do this even for
   60 -- functions that are available publicly. In order to authenticate as the public user simply
   61 -- authenticate using nil for both username and password. This can be achieved by calling the Login method
   62 -- without any parameters, like this:
   63 -- <code>
   64 -- status, response = helper:Login()
   65 -- </code>
   66 --
   67 -- To authenticate to the server using the username 'admin' and password 'nimda' we do this instead:
   68 -- <code>
   69 -- status, response = helper:Login('admin', 'nimda')
   70 -- </code>
   71 --
   72 -- At this stage we're authenticated and can call any of the AFP functions we're authorized to.
   73 -- For the purpose of this documentation, we will attempt to list the servers share points.
   74 -- We do this by issuing the following:
   75 -- <code>
   76 -- status, shares = helper:ListShares()
   77 -- </code>
   78 --
   79 -- Once we're finished, we need to logout and close the AFP session this is done by calling the
   80 -- following two methods of the Helper class:
   81 -- <code>
   82 -- status, response = helper:Logout()
   83 -- status, response = helper:CloseSession()
   84 -- </code>
   85 --
   86 -- Consult the documentation of each function to learn more about their respective return values.
   87 --
   88 --@author Patrik Karlsson <patrik@cqure.net>
   89 --@copyright Same as Nmap--See https://nmap.org/book/man-legal.html
   90 --
   91 -- @args afp.username The username to use for authentication.
   92 -- @args afp.password The password to use for authentication.
   93 
   94 --
   95 -- Version 0.5
   96 --
   97 -- Created 01/03/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
   98 -- Revised 01/20/2010 - v0.2 - updated all bitmaps to hex for better readability
   99 -- Revised 02/15/2010 - v0.3 - added a bunch of new functions and re-designed the code to be OO
  100 --
  101 --   New functionality added as of v0.3
  102 --    o File reading, writing
  103 --    o Authentication
  104 --    o Helper functions for most AFP functions
  105 --    o More robust error handling
  106 --
  107 -- Revised 03/05/2010 - v0.4 - changed output table of Helper:Dir to include type and ID
  108 --                           - added support for --without-openssl
  109 --
  110 -- Revised 03/09/2010 - v0.5 - documentation, documentation and more documentation
  111 -- Revised 04/03/2011 - v0.6 - add support for getting file- sizes, dates and Unix ACLs
  112 --                           - moved afp.username & afp.password arguments to library
  113 
  114 local datetime = require "datetime"
  115 local ipOps = require "ipOps"
  116 local nmap = require "nmap"
  117 local os = require "os"
  118 local stdnse = require "stdnse"
  119 local string = require "string"
  120 local stringaux = require "stringaux"
  121 local table = require "table"
  122 _ENV = stdnse.module("afp", stdnse.seeall);
  123 
  124 local HAVE_SSL, openssl = pcall(require,'openssl')
  125 
  126 -- Table of valid REQUESTs
  127 local REQUEST = {
  128   CloseSession = 0x01,
  129   OpenSession = 0x04,
  130   Command = 0x02,
  131   GetStatus = 0x03,
  132   Write = 0x06,
  133 }
  134 
  135 -- Table of headers flags to be set accordingly in requests and responses
  136 local FLAGS = {
  137   Request = 0,
  138   Response = 1
  139 }
  140 
  141 -- Table of possible AFP_COMMANDs
  142 COMMAND = {
  143   FPCloseVol = 0x02,
  144   FPCloseFork = 0x04,
  145   FPCopyFile = 0x05,
  146   FPCreateDir = 0x06,
  147   FPCreateFile = 0x07,
  148   FPGetSrvrInfo = 0x0f,
  149   FPGetSrvParms = 0x10,
  150   FPLogin = 0x12,
  151   FPLoginCont = 0x13,
  152   FPLogout = 0x14,
  153   FPMapId = 0x15,
  154   FPMapName = 0x16,
  155   FPGetUserInfo = 0x25,
  156   FPOpenVol = 0x18,
  157   FPOpenFork = 0x1a,
  158   FPGetFileDirParams = 0x22,
  159   FPChangePassword = 0x24,
  160   FPReadExt = 0x3c,
  161   FPWriteExt = 0x3d,
  162   FPGetAuthMethods = 0x3e,
  163   FPLoginExt = 0x3f,
  164   FPEnumerateExt2 = 0x44,
  165 }
  166 
  167 USER_BITMAP = {
  168   UserId = 0x01,
  169   PrimaryGroupId = 0x2,
  170   UUID = 0x4
  171 }
  172 
  173 VOL_BITMAP = {
  174   Attributes = 0x1,
  175   Signature = 0x2,
  176   CreationDate = 0x4,
  177   ModificationDate = 0x8,
  178   BackupDate = 0x10,
  179   ID = 0x20,
  180   BytesFree = 0x40,
  181   BytesTotal = 0x80,
  182   Name = 0x100,
  183   ExtendedBytesFree = 0x200,
  184   ExtendedBytesTotal = 0x400,
  185   BlockSize = 0x800
  186 }
  187 
  188 FILE_BITMAP = {
  189   Attributes = 0x1,
  190   ParentDirId = 0x2,
  191   CreationDate = 0x4,
  192   ModificationDate = 0x8,
  193   BackupDate = 0x10,
  194   FinderInfo = 0x20,
  195   LongName = 0x40,
  196   ShortName = 0x80,
  197   NodeId = 0x100,
  198   DataForkSize = 0x200,
  199   ResourceForkSize = 0x400,
  200   ExtendedDataForkSize = 0x800,
  201   LaunchLimit = 0x1000,
  202   UTF8Name = 0x2000,
  203   ExtendedResourceForkSize = 0x4000,
  204   UnixPrivileges = 0x8000,
  205   ALL = 0xFFFF
  206 }
  207 
  208 DIR_BITMAP = {
  209   Attributes = 0x1,
  210   ParentDirId = 0x2,
  211   CreationDate = 0x4,
  212   ModificationDate = 0x8,
  213   BackupDate = 0x10,
  214   FinderInfo = 0x20,
  215   LongName = 0x40,
  216   ShortName = 0x80,
  217   NodeId = 0x100,
  218   OffspringCount = 0x200,
  219   OwnerId = 0x400,
  220   GroupId = 0x800,
  221   AccessRights = 0x1000,
  222   UTF8Name = 0x2000,
  223   UnixPrivileges = 0x8000,
  224   ALL = 0xBFFF,
  225 }
  226 
  227 PATH_TYPE = {
  228   ShortName = 1,
  229   LongName = 2,
  230   UTF8Name = 3,
  231 }
  232 
  233 ACCESS_MODE = {
  234   Read = 0x1,
  235   Write = 0x2,
  236   DenyRead = 0x10,
  237   DenyWrite = 0x20
  238 }
  239 
  240 -- Access controls
  241 ACLS = {
  242   OwnerSearch = 0x1,
  243   OwnerRead = 0x2,
  244   OwnerWrite = 0x4,
  245 
  246   GroupSearch = 0x100,
  247   GroupRead = 0x200,
  248   GroupWrite = 0x400,
  249 
  250   EveryoneSearch = 0x10000,
  251   EveryoneRead = 0x20000,
  252   EveryoneWrite = 0x40000,
  253 
  254   UserSearch = 0x100000,
  255   UserRead = 0x200000,
  256   UserWrite = 0x400000,
  257 
  258   BlankAccess = 0x10000000,
  259   UserIsOwner = 0x80000000
  260 }
  261 
  262 -- User authentication modules
  263 UAM =
  264 {
  265   NoUserAuth = "No User Authent",
  266   ClearText = "Cleartxt Passwrd",
  267   RandNum = "Randnum Exchange",
  268   TwoWayRandNum = "2-Way Randnum",
  269   DHCAST128 = "DHCAST128",
  270   DHX2 = "DHX2",
  271   Kerberos = "Client Krb v2",
  272   Reconnect = "Recon1",
  273 }
  274 
  275 ERROR =
  276 {
  277   SocketError = 1000,
  278   CustomError = 0xdeadbeef,
  279 
  280   FPNoErr = 0,
  281   FPAccessDenied = -5000,
  282   FPAuthContinue = -5001,
  283   FPBadUAM = -5002,
  284   FPBadVersNum = -5003,
  285   FPBitmapErr = - 5004,
  286   FPCantMove = - 5005,
  287   FPEOFErr = -5009,
  288   FPItemNotFound = -5012,
  289   FPLockErr = -5013,
  290   FPMiscErr = -5014,
  291   FPObjectExists = -5017,
  292   FPObjectNotFound = -5018,
  293   FPParamErr = -5019,
  294   FPUserNotAuth = -5023,
  295   FPCallNotSupported = -5024,
  296 }
  297 
  298 MAP_ID =
  299 {
  300   UserIDToName = 1,
  301   GroupIDToName = 2,
  302   UserIDToUTF8Name = 3,
  303   GroupIDToUTF8Name = 4,
  304   UserUUIDToUTF8Name = 5,
  305   GroupUUIDToUTF8Name = 6
  306 }
  307 
  308 MAP_NAME =
  309 {
  310   NameToUserID = 1,
  311   NameToGroupID = 2,
  312   UTF8NameToUserID = 3,
  313   UTF8NameToGroupID = 4,
  314   UTF8NameToUserUUID = 5,
  315   UTF8NameToGroupUUID = 6
  316 }
  317 
  318 
  319 SERVERFLAGS =
  320 {
  321   CopyFile = 0x01,
  322   ChangeablePasswords = 0x02,
  323   NoPasswordSaving = 0x04,
  324   ServerMessages = 0x08,
  325   ServerSignature = 0x10,
  326   TCPoverIP = 0x20,
  327   ServerNotifications = 0x40,
  328   Reconnect = 0x80,
  329   OpenDirectory = 0x100,
  330   UTF8ServerName = 0x200,
  331   UUIDs = 0x400,
  332   SuperClient = 0x8000
  333 }
  334 
  335 local ERROR_MSG = {
  336   [ERROR.FPAccessDenied]="Access Denied",
  337   [ERROR.FPAuthContinue]="Authentication is not yet complete",
  338   [ERROR.FPBadUAM]="Specified UAM is unknown",
  339   [ERROR.FPBadVersNum]="Server does not support the specified AFP version",
  340   [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",
  341   [ERROR.FPCantMove]="Attempt was made to move a directory into one of its descendant directories.",
  342   [ERROR.FPEOFErr]="No more matches or end of fork reached.",
  343   [ERROR.FPLockErr]="Some or all of the requested range is locked by another user; a lock range conflict exists.",
  344   [ERROR.FPMiscErr]="Non-AFP error occurred.",
  345   [ERROR.FPObjectNotFound]="Input parameters do not point to an existing directory, file, or volume.",
  346   [ERROR.FPParamErr]="Parameter error.",
  347   [ERROR.FPObjectExists] = "File or directory already exists.",
  348   [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.",
  349   [ERROR.FPItemNotFound] = "Specified APPL mapping, comment, or icon was not found in the Desktop database; specified ID is unknown.",
  350   [ERROR.FPCallNotSupported] = "Server does not support this command.",
  351 }
  352 
  353 -- Dates are shifted forward one day to avoid referencing 12/31/1969 UTC
  354 -- when specifying 1/1/1970 (local) in a timezone that is ahead of UTC
  355 local TIME_OFFSET = os.time({year=2000, month=1, day=2, hour=0}) - os.time({year=1970, month=1, day=2, hour=0})
  356 
  357 -- Check if all the bits in flag are set in bitmap.
  358 local function flag_is_set(bitmap, flag)
  359   return (bitmap & flag) == flag
  360 end
  361 
  362 -- Serialize path of a given type
  363 -- NB: For now the actual UTF-8 encoding is ignored
  364 local function encode_path (path)
  365   if path.type == PATH_TYPE.ShortName or path.type == PATH_TYPE.LongName then
  366     return string.pack("Bs1", path.type, path.name)
  367   elseif path.type == PATH_TYPE.UTF8Name then
  368     return string.pack(">BI4s2", path.type, 0x08000103, path.name)
  369   end
  370   assert(false, ("Unrecognized path type '%s'"):format(tostring(path.type)))
  371 end
  372 
  373 -- Response class returned by all functions in Proto
  374 Response = {
  375 
  376   new = function(self,o)
  377     o = o or {}
  378     setmetatable(o, self)
  379     self.__index = self
  380     return o
  381   end,
  382 
  383   --- Sets the error code
  384   --
  385   -- @param code number containing the error code
  386   setErrorCode = function( self, code )
  387     self.error_code = code
  388   end,
  389 
  390   --- Gets the error code
  391   --
  392   -- @return code number containing the error code
  393   getErrorCode = function( self )
  394     return self.error_code
  395   end,
  396 
  397   --- Gets the error message
  398   --
  399   -- @return msg string containing the error
  400   getErrorMessage = function(self)
  401     if self.error_msg then
  402       return self.error_msg
  403     else
  404       return ERROR_MSG[self.error_code] or ("Unknown error (%d) occurred"):format(self.error_code)
  405     end
  406   end,
  407 
  408   --- Sets the error message
  409   --
  410   -- @param msg string containing the error message
  411   setErrorMessage = function(self, msg)
  412     self.error_code = ERROR.CustomError
  413     self.error_msg = msg
  414   end,
  415 
  416   --- Sets the result
  417   --
  418   -- @param result result to set
  419   setResult = function(self, result)
  420     self.result = result
  421   end,
  422 
  423   --- Get the result
  424   --
  425   -- @return result
  426   getResult = function(self)
  427     return self.result
  428   end,
  429 
  430   --- Sets the packet
  431   setPacket = function( self, packet )
  432     self.packet = packet
  433   end,
  434 
  435   getPacket = function( self )
  436     return self.packet
  437   end,
  438 
  439   --- Gets the packet data
  440   getPacketData = function(self)
  441     return self.packet.data
  442   end,
  443 
  444   --- Gets the packet header
  445   getPacketHeader = function(self)
  446     return self.packet.header
  447   end,
  448 }
  449 
  450 --- Proto class containing all AFP specific code
  451 --
  452 -- For more details consult:
  453 -- http://developer.apple.com/mac/library/documentation/Networking/Reference/AFP_Reference/Reference/reference.html
  454 Proto = {
  455 
  456   RequestId = 1,
  457 
  458   new = function(self,o)
  459     o = o or {}
  460     setmetatable(o, self)
  461     self.__index = self
  462     return o
  463   end,
  464 
  465   setSocket = function(self, socket)
  466     self.socket = socket
  467   end,
  468 
  469   --- Creates an AFP packet
  470   --
  471   -- @param command number should be one of the commands in the COMMAND table
  472   -- @param data_offset number holding the offset to the data
  473   -- @param data the actual data of the request
  474   create_fp_packet = function( self, command, data_offset, data )
  475     local reserved = 0
  476     local data = data or ""
  477     local data_len = data:len()
  478     local header = string.pack(">BBI2I4I4I4", FLAGS.Request, command, self.RequestId, data_offset, data_len, reserved)
  479 
  480     self.RequestId = self.RequestId + 1
  481     return header .. data
  482   end,
  483 
  484   --- Parses the FP header (first 16-bytes of packet)
  485   --
  486   -- @param packet string containing the raw packet
  487   -- @return table with header data containing <code>flags</code>, <code>command</code>,
  488   -- <code>request_id</code>, <code>error_code</code>, <code>length</code> and <code>reserved</code> fields
  489   parse_fp_header = function( self, packet )
  490     local header = {}
  491     local pos
  492 
  493     header.flags, header.command, header.request_id, pos = string.unpack( ">BBI2", packet )
  494     header.error_code, header.length, header.reserved, pos = string.unpack( ">i4I4I4", packet, pos )
  495 
  496     if header.error_code ~= 0 then
  497       header.error_msg = ERROR_MSG[header.error_code] or ("Unknown error: %d"):format(header.error_code)
  498       header.error_msg = "ERROR: " .. header.error_msg
  499     end
  500     header.raw = packet:sub(1,16)
  501     return header
  502   end,
  503 
  504   --- Reads a AFP packet of the socket
  505   --
  506   -- @return Response object
  507   read_fp_packet = function( self )
  508 
  509     local packet = {}
  510     local buf = ""
  511     local status, response
  512 
  513     status, buf = self.socket:receive_bytes(16)
  514     if ( not status ) then
  515       response = Response:new()
  516       response:setErrorCode(ERROR.SocketError)
  517       response:setErrorMessage(buf)
  518       return response
  519     end
  520 
  521     packet.header = self:parse_fp_header( buf )
  522     while buf:len() < packet.header.length + packet.header.raw:len() do
  523       local tmp
  524       status, tmp = self.socket:receive_bytes( packet.header.length + 16 - buf:len() )
  525       if not status then
  526         response = Response:new()
  527         response:setErrorCode(ERROR.SocketError)
  528         response:setErrorMessage(buf)
  529         return response
  530       end
  531       buf = buf .. tmp
  532     end
  533 
  534     packet.data = buf:len() > 16 and buf:sub( 17 ) or ""
  535     response = Response:new()
  536     response:setErrorCode(packet.header.error_code)
  537     response:setPacket(packet)
  538 
  539     return response
  540   end,
  541 
  542   --- Sends the raw packet over the socket
  543   --
  544   -- @param packet containing the raw data
  545   -- @return Response object
  546   send_fp_packet = function( self, packet )
  547     return self.socket:send(packet)
  548   end,
  549 
  550   --- Sends an DSIOpenSession request to the server and handles the response
  551   --
  552   -- @return Response object
  553   dsi_open_session = function( self, host, port )
  554     local data_offset = 0
  555     local option = 0x01 -- Attention Quantum
  556     local option_len = 4
  557     local quantum = 1024
  558     local data, packet, status
  559 
  560     data = string.pack( ">BBI4", option, option_len, quantum  )
  561     packet = self:create_fp_packet( REQUEST.OpenSession, data_offset, data )
  562 
  563     self:send_fp_packet( packet )
  564     return self:read_fp_packet()
  565   end,
  566 
  567   --- Sends an DSICloseSession request to the server and handles the response
  568   dsi_close_session = function( self )
  569     local data_offset = 0
  570     local option = 0x01 -- Attention Quantum
  571     local option_len = 4
  572     local quantum = 1024
  573     local data, packet, status
  574 
  575     data = ""
  576     packet = self:create_fp_packet( REQUEST.CloseSession, data_offset, data )
  577 
  578     self:send_fp_packet( packet )
  579   end,
  580 
  581   -- Sends an FPCopyFile request to the server
  582   --
  583   -- @param src_vol number containing the ID of the src file volume
  584   -- @param srd_did number containing the directory id of the src file
  585   -- @param src_path string containing the file path/name of the src file
  586   -- @param dst_vol number containing the ID of the dst file volume
  587   -- @param dst_did number containing the id of the dest. directory
  588   -- @param dst_path string containing the dest path (can be nil or "")
  589   -- @param new_name string containing the new name of the destination
  590   -- @return Response object
  591   fp_copy_file = function(self, src_vol, src_did, src_path, dst_vol, dst_did, dst_path, new_name )
  592     local data_offset = 0
  593     local unicode_names, unicode_hint = 0x03, 0x08000103
  594     local data, packet, response
  595 
  596     -- make sure we have empty names rather than nil values
  597     local dst_path = dst_path or ""
  598     local src_path = src_path or ""
  599     local new_name = new_name or ""
  600 
  601     data = string.pack(">BxI2I4I2I4", COMMAND.FPCopyFile, src_vol, src_did, dst_vol, dst_did )
  602            .. encode_path({type=PATH_TYPE.UTF8Name, name=src_path})
  603            .. encode_path({type=PATH_TYPE.UTF8Name, name=dst_path})
  604            .. encode_path({type=PATH_TYPE.UTF8Name, name=new_name})
  605 
  606     packet = self:create_fp_packet( REQUEST.Command, data_offset, data )
  607     self:send_fp_packet( packet )
  608     return self:read_fp_packet()
  609   end,
  610 
  611   --- Sends an GetStatus DSI request (which is basically a FPGetSrvrInfo
  612   -- AFP request) to the server and handles the response
  613   --
  614   -- @return status (true or false)
  615   -- @return table with server information (if status is true) or error string
  616   -- (if status is false)
  617   fp_get_server_info = function(self)
  618     local packet
  619     local data_offset = 0
  620     local response, result = {}, {}
  621     local offsets = {}
  622     local pos
  623     local status
  624 
  625     local data = string.pack("Bx", COMMAND.FPGetSrvrInfo)
  626     packet = self:create_fp_packet(REQUEST.GetStatus, data_offset, data)
  627     self:send_fp_packet(packet)
  628     response = self:read_fp_packet()
  629 
  630     if response:getErrorCode() ~= ERROR.FPNoErr then
  631       return response
  632     end
  633 
  634     packet = response.packet
  635 
  636     -- parse and store the offsets in the 'header'
  637     offsets.machine_type, offsets.afp_version_count,
  638       offsets.uam_count, offsets.volume_icon_and_mask, pos
  639       = string.unpack(">I2I2I2I2", packet.data)
  640 
  641     -- the flags are directly in the 'header'
  642     result.flags = {}
  643     result.flags.raw, pos = string.unpack(">I2", packet.data, pos)
  644 
  645     -- the short server name is stored directly in the 'header' as
  646     -- well
  647     result.server_name, pos = string.unpack("s1", packet.data, pos)
  648 
  649     -- Server offset should begin at an even boundary see link below
  650     -- http://developer.apple.com/mac/library/documentation/Networking/Reference/AFP_Reference/Reference/reference.html#//apple_ref/doc/uid/TP40003548-CH3-CHDIEGED
  651     if (pos + 1) % 2 ~= 0 then
  652       pos = pos + 1
  653     end
  654 
  655     -- and some more offsets
  656     offsets.server_signature, offsets.network_addresses_count,
  657     offsets.directory_names_count, offsets.utf8_server_name, pos
  658       = string.unpack(">I2I2I2I2", packet.data, pos)
  659 
  660     -- this sets up all the server flags in the response table as booleans
  661     result.flags.SuperClient = flag_is_set(result.flags.raw, SERVERFLAGS.SuperClient)
  662     result.flags.UUIDs = flag_is_set(result.flags.raw, SERVERFLAGS.UUIDs)
  663     result.flags.UTF8ServerName = flag_is_set(result.flags.raw, SERVERFLAGS.UTF8ServerName)
  664     result.flags.OpenDirectory = flag_is_set(result.flags.raw, SERVERFLAGS.OpenDirectory)
  665     result.flags.Reconnect = flag_is_set(result.flags.raw, SERVERFLAGS.Reconnect)
  666     result.flags.ServerNotifications = flag_is_set(result.flags.raw, SERVERFLAGS.ServerNotifications)
  667     result.flags.TCPoverIP = flag_is_set(result.flags.raw, SERVERFLAGS.TCPoverIP)
  668     result.flags.ServerSignature = flag_is_set(result.flags.raw, SERVERFLAGS.ServerSignature)
  669     result.flags.ServerMessages = flag_is_set(result.flags.raw, SERVERFLAGS.ServerMessages)
  670     result.flags.NoPasswordSaving = flag_is_set(result.flags.raw, SERVERFLAGS.NoPasswordSaving)
  671     result.flags.ChangeablePasswords = flag_is_set(result.flags.raw, SERVERFLAGS.ChangeablePasswords)
  672     result.flags.CopyFile = flag_is_set(result.flags.raw, SERVERFLAGS.CopyFile)
  673 
  674     -- store the machine type
  675     result.machine_type = string.unpack("s1", packet.data, offsets.machine_type + 1)
  676 
  677     -- this tells us the number of afp versions supported
  678     result.afp_version_count, pos = string.unpack("B", packet.data, offsets.afp_version_count + 1)
  679 
  680     -- now we loop through them all, storing for the response
  681     result.afp_versions = {}
  682     for i = 1,result.afp_version_count do
  683       local v
  684       v, pos = string.unpack("s1", packet.data, pos)
  685       table.insert(result.afp_versions, v)
  686     end
  687 
  688     -- same idea as the afp versions here
  689     result.uam_count, pos = string.unpack("B", packet.data, offsets.uam_count + 1)
  690 
  691     result.uams = {}
  692     for i = 1,result.uam_count do
  693       local uam
  694       uam, pos = string.unpack("s1", packet.data, pos)
  695       table.insert(result.uams, uam)
  696     end
  697 
  698     -- volume_icon_and_mask would normally be parsed out here,
  699     -- however the apple docs say it is deprecated in Mac OS X, so
  700     -- we don't bother with it
  701 
  702     -- server signature is 16 bytes
  703     result.server_signature = string.sub(packet.data, offsets.server_signature + 1, offsets.server_signature + 16)
  704 
  705     -- this is the same idea as afp_version and uam above
  706     result.network_addresses_count, pos = string.unpack("B", packet.data, offsets.network_addresses_count + 1)
  707 
  708     result.network_addresses = {}
  709 
  710     -- gets a little complicated in here, basically each entry has
  711     -- a length byte, a tag byte, and then the data. We parse
  712     -- differently based on the tag
  713     for i = 1, result.network_addresses_count do
  714       local length
  715       local tag
  716 
  717       length, tag, pos = string.unpack("BB", packet.data, pos)
  718 
  719       if tag == 0x00 then
  720         -- reserved, shouldn't ever come up, maybe this should
  721         -- return an error? maybe not, lets just ignore this
  722       elseif tag == 0x01 then
  723         -- four byte ip
  724         local ip
  725         ip, pos = string.unpack("c4", packet.data, pos)
  726         table.insert(result.network_addresses, ipOps.str_to_ip(ip))
  727       elseif tag == 0x02 then
  728         -- four byte ip and two byte port
  729         local ip, port
  730         ip, port, pos = string.unpack("c4 >I2", packet.data, pos)
  731         table.insert(result.network_addresses, string.format("%s:%d", ipOps.str_to_ip(ip), port))
  732       elseif tag == 0x03 then
  733         -- ddp address (two byte network, one byte
  734         -- node, one byte socket) not tested, anyone
  735         -- use ddp anymore?
  736         local network, node, socket
  737         network, node, socket, pos = string.unpack(">I2BB", packet.data, pos)
  738         table.insert(result.network_addresses, string.format("ddp %d.%d:%d", network, node, socket))
  739       elseif tag == 0x04 then
  740         -- dns name (string)
  741         local temp
  742         temp, pos = string.unpack("z", packet.data:sub(1,pos+length-3), pos)
  743         table.insert(result.network_addresses, temp)
  744       elseif tag == 0x05 then
  745         -- four byte ip and two byte port, client
  746         -- should use ssh. not tested, should work as it
  747         -- is the same as tag 0x02
  748         local ip, port
  749         ip, port, pos = string.unpack("c4 >I2", packet.data, pos)
  750         table.insert(result.network_addresses, string.format("ssh://%s:%d", ipOps.str_to_ip(ip), port))
  751       elseif tag == 0x06 then
  752         -- 16 byte ipv6
  753         -- not tested, but should work (next tag is
  754         -- tested)
  755         local ip
  756         ip, pos = string.unpack("c16", packet.data, pos)
  757 
  758         table.insert(result.network_addresses, ipOps.str_to_ip(ip))
  759       elseif tag == 0x07 then
  760         -- 16 byte ipv6 and two byte port
  761         local ip, port
  762         ip, port, pos = string.unpack(">c16 I2", packet.data, pos)
  763 
  764         table.insert(result.network_addresses,
  765           string.format("[%s]:%d", ipOps.str_to_ip(ip), port))
  766       end
  767     end
  768 
  769     -- same idea as the others here
  770     result.directory_names_count, pos = string.unpack("B", packet.data, offsets.directory_names_count + 1)
  771 
  772     result.directory_names = {}
  773     for i = 1, result.directory_names_count do
  774       local dirname
  775       dirname, pos = string.unpack("s1", packet.data, pos)
  776       table.insert(result.directory_names, dirname)
  777     end
  778 
  779     -- only one utf8 server name. note this string has a two-byte length.
  780     result.utf8_server_name = string.unpack(">s2", packet.data, offsets.utf8_server_name + 1)
  781     response.result = result
  782 
  783     return response
  784   end,
  785 
  786 
  787   --- Sends an FPGetUserInfo AFP request to the server and handles the response
  788   --
  789   -- @return response object with the following result <code>user_bitmap</code> and
  790   --     <code>uid</code> fields
  791   fp_get_user_info = function( self )
  792 
  793     local packet, pos, status, response
  794     local data_offset = 0
  795     local flags = 1 -- Default User
  796     local uid = 0
  797     local bitmap = USER_BITMAP.UserId
  798     local result = {}
  799 
  800     local data = string.pack( ">BBI4I2", COMMAND.FPGetUserInfo, flags, uid, bitmap )
  801     packet = self:create_fp_packet( REQUEST.Command, data_offset, data )
  802 
  803     self:send_fp_packet( packet )
  804     response = self:read_fp_packet()
  805     if response:getErrorCode() ~= ERROR.FPNoErr then
  806       return response
  807     end
  808 
  809     response.result.user_bitmap, response.result.uid, pos = string.unpack(">I2I4", packet.data)
  810 
  811     return response
  812   end,
  813 
  814   --- Sends an FPGetSrvrParms AFP request to the server and handles the response
  815   --
  816   -- @return response object with the following result <code>server_time</code>,
  817   -- <code>vol_count</code>, <code>volumes</code> fields
  818   fp_get_srvr_parms = function(self)
  819     local packet, status, data
  820     local data_offset = 0
  821     local response = {}
  822     local pos = 0
  823     local parms = {}
  824 
  825     data = string.pack("Bx", COMMAND.FPGetSrvParms)
  826     packet = self:create_fp_packet( REQUEST.Command, data_offset, data )
  827     self:send_fp_packet( packet )
  828     response = self:read_fp_packet()
  829 
  830     if response:getErrorCode() ~= ERROR.FPNoErr then
  831       return response
  832     end
  833 
  834     data = response:getPacketData()
  835     parms.server_time, parms.vol_count, pos = string.unpack(">I4B", data)
  836 
  837     parms.volumes = {}
  838 
  839     for i=1, parms.vol_count do
  840       local volume_name
  841       -- pos+1 to skip over the volume bitmap
  842       volume_name, pos = string.unpack("s1", data, pos + 1)
  843       table.insert(parms.volumes, string.format("%s", volume_name) )
  844     end
  845 
  846     response:setResult(parms)
  847 
  848     return response
  849   end,
  850 
  851 
  852   --- Sends an FPLogin request to the server and handles the response
  853   --
  854   -- This function currently only supports the 3.1 through 3.3 protocol versions
  855   -- It currently supports the following authentication methods:
  856   --   o No User Authent
  857   --   o DHCAST128
  858   --
  859   -- The DHCAST128 UAM should work against most servers even though it's
  860   -- superceded by the DHX2 UAM.
  861   --
  862   -- @param afp_version string (AFP3.3|AFP3.2|AFP3.1)
  863   -- @param uam string containing authentication information
  864   -- @return Response object
  865   fp_login = function( self, afp_version, uam, username, password, options )
  866     local packet, status, data
  867     local data_offset = 0
  868     local status, response
  869 
  870     if not HAVE_SSL then
  871       response = Response:new()
  872       response:setErrorMessage("OpenSSL not available, aborting ...")
  873       return response
  874     end
  875 
  876     -- currently we only support AFP3.3
  877     if afp_version == nil or ( afp_version ~= "AFP3.3" and afp_version ~= "AFP3.2" and afp_version ~= "AFP3.1" ) then
  878       response = Response:new()
  879       response:setErrorMessage("Incorrect AFP version")
  880       return response
  881     end
  882 
  883     if ( uam == "No User Authent" ) then
  884       data = string.pack( "Bs1s1", COMMAND.FPLogin, afp_version, uam )
  885       packet = self:create_fp_packet( REQUEST.Command, data_offset, data )
  886       self:send_fp_packet( packet )
  887       return self:read_fp_packet( )
  888     elseif( uam == "DHCAST128" ) then
  889       local dhx_s2civ, dhx_c2civ = 'CJalbert', 'LWallace'
  890       local p, g, Ra, Ma, Mb, K, nonce
  891       local EncData, PlainText, K_bin, auth_response
  892       local Id
  893       local username = username or ""
  894       local password = password or ""
  895 
  896       username = username .. string.rep('\0', (#username + 1) % 2)
  897 
  898       p = openssl.bignum_hex2bn("BA2873DFB06057D43F2024744CEEE75B")
  899       g = openssl.bignum_dec2bn("7")
  900       Ra = openssl.bignum_hex2bn("86F6D3C0B0D63E4B11F113A2F9F19E3BBBF803F28D30087A1450536BE979FD42")
  901       Ma = openssl.bignum_mod_exp(g, Ra, p)
  902 
  903       data = string.pack( "Bs1s1s1", COMMAND.FPLogin, afp_version, uam, username) .. openssl.bignum_bn2bin(Ma)
  904       packet = self:create_fp_packet( REQUEST.Command, data_offset, data )
  905       self:send_fp_packet( packet )
  906       response = self:read_fp_packet( )
  907       if ( response:getErrorCode() ~= ERROR.FPAuthContinue ) then
  908         return response
  909       end
  910 
  911       if ( response.packet.header.length ~= 50 ) then
  912         response:setErrorMessage("LoginContinue packet contained invalid data")
  913         return response
  914       end
  915 
  916       Id, Mb, EncData = string.unpack(">I2c16c32", response.packet.data )
  917 
  918       Mb = openssl.bignum_bin2bn( Mb )
  919       K = openssl.bignum_mod_exp (Mb, Ra, p)
  920       K_bin = openssl.bignum_bn2bin(K)
  921       nonce = openssl.decrypt("cast5-cbc", K_bin, dhx_s2civ, EncData, false ):sub(1,16)
  922       nonce = openssl.bignum_add( openssl.bignum_bin2bn(nonce), openssl.bignum_dec2bn("1") )
  923       PlainText = openssl.bignum_bn2bin(nonce) .. Util.ZeroPad(password, 64)
  924       auth_response = openssl.encrypt( "cast5-cbc", K_bin, dhx_c2civ, PlainText, true)
  925 
  926       data = string.pack( ">BBI2", COMMAND.FPLoginCont, 0, Id) .. auth_response
  927       packet = self:create_fp_packet( REQUEST.Command, data_offset, data )
  928       self:send_fp_packet( packet )
  929       response = self:read_fp_packet( )
  930       if ( response:getErrorCode() ~= ERROR.FPNoErr ) then
  931         return response
  932       end
  933       return response
  934     end
  935     response:setErrorMessage("Unsupported uam: " .. uam or "nil")
  936     return response
  937   end,
  938 
  939   -- Terminates sessions and frees server resources established by FPLoginand FPLoginExt.
  940   --
  941   -- @return response object
  942   fp_logout = function( self )
  943     local packet, data, response
  944     local data_offset = 0
  945 
  946     data = string.pack("Bx", COMMAND.FPLogout)
  947     packet = self:create_fp_packet( REQUEST.Command, data_offset, data )
  948     self:send_fp_packet( packet )
  949     return self:read_fp_packet( )
  950   end,
  951 
  952   --- Sends an FPOpenVol request to the server and handles the response
  953   --
  954   -- @param bitmap number bitmask of volume information to request
  955   -- @param volume_name string containing the volume name to query
  956   -- @return response object with the following result <code>bitmap</code> and
  957   --     <code>volume_id</code> fields
  958   fp_open_vol = function( self, bitmap, volume_name )
  959     local packet, status, pos, data
  960     local data_offset = 0
  961     local response, volume = {}, {}
  962 
  963     data = string.pack(">BxI2s1", COMMAND.FPOpenVol, bitmap, volume_name)
  964     packet = self:create_fp_packet( REQUEST.Command, data_offset, data )
  965     self:send_fp_packet( packet )
  966     response = self:read_fp_packet()
  967     if response:getErrorCode() ~= ERROR.FPNoErr then
  968       return response
  969     end
  970 
  971     volume.bitmap, volume.volume_id, pos = string.unpack(">I2I2", response.packet.data)
  972     response:setResult(volume)
  973     return response
  974   end,
  975 
  976 
  977   --- Sends an FPGetFileDirParms request to the server and handles the response
  978   --
  979   -- @param volume_id number containing the id of the volume to query
  980   -- @param did number containing the id of the directory to query
  981   -- @param file_bitmap number bitmask of file information to query
  982   -- @param dir_bitmap number bitmask of directory information to query
  983   -- @param path table containing the name and the name encoding type of the directory to query
  984   -- @return response object with the following result <code>file_bitmap</code>, <code>dir_bitmap</code>,
  985   --     <code>file_type</code> and (<code>dir<code> or <code>file</code> tables) depending on whether
  986   --     <code>did</code> is a file or directory
  987   fp_get_file_dir_parms = function( self, volume_id, did, file_bitmap, dir_bitmap, path )
  988 
  989     local packet, status, data
  990     local data_offset = 0
  991     local response, parms = {}, {}
  992     local pos
  993 
  994     if ( did == nil ) then
  995       response = Response:new()
  996       response:setErrorMessage("No Directory Id supplied")
  997       return response
  998     end
  999 
 1000     if ( volume_id == nil ) then
 1001       response = Response:new()
 1002       response:setErrorMessage("No Volume Id supplied")
 1003       return response
 1004     end
 1005 
 1006     data = string.pack(">BxI2I4I2I2", COMMAND.FPGetFileDirParams, volume_id, did, file_bitmap, dir_bitmap)
 1007            .. encode_path(path)
 1008     packet = self:create_fp_packet( REQUEST.Command, data_offset, data )
 1009     self:send_fp_packet( packet )
 1010     response = self:read_fp_packet()
 1011 
 1012     if response:getErrorCode() ~= ERROR.FPNoErr then
 1013       return response
 1014     end
 1015 
 1016     parms.file_bitmap, parms.dir_bitmap, parms.file_type, pos = string.unpack( ">I2I2Bx", response.packet.data )
 1017 
 1018     -- file or dir?
 1019     if ( parms.file_type == 0x80 ) then
 1020       pos, parms.dir = Util.decode_dir_bitmap( parms.dir_bitmap, response.packet.data, pos )
 1021     else
 1022       -- file
 1023       pos, parms.file = Util.decode_file_bitmap( parms.file_bitmap, response.packet.data, pos )
 1024     end
 1025 
 1026     response:setResult(parms)
 1027     return response
 1028   end,
 1029 
 1030   --- Sends an FPEnumerateExt2 request to the server and handles the response
 1031   --
 1032   -- @param volume_id number containing the id of the volume to query
 1033   -- @param did number containing the id of the directory to query
 1034   -- @param file_bitmap number bitmask of file information to query
 1035   -- @param dir_bitmap number bitmask of directory information to query
 1036   -- @param req_count number
 1037   -- @param start_index number
 1038   -- @param reply_size number
 1039   -- @param path table containing the name and the name encoding type of the directory to query
 1040   -- @return response object with the following result set to a table of tables containing
 1041   --   <code>file_bitmap</code>, <code>dir_bitmap</code>, <code>req_count</code> fields
 1042   fp_enumerate_ext2 = function( self, volume_id, did, file_bitmap, dir_bitmap, req_count, start_index, reply_size, path )
 1043 
 1044     local packet, pos, status
 1045     local data_offset = 0
 1046     local response,records = {}, {}
 1047 
 1048     local data = string.pack( ">BxI2I4I2I2", COMMAND.FPEnumerateExt2, volume_id, did, file_bitmap, dir_bitmap )
 1049                 .. string.pack( ">I2I4I4", req_count, start_index, reply_size)
 1050                 .. encode_path(path)
 1051     packet = self:create_fp_packet( REQUEST.Command, data_offset, data )
 1052 
 1053     self:send_fp_packet( packet )
 1054     response = self:read_fp_packet( )
 1055 
 1056     if response:getErrorCode() ~= ERROR.FPNoErr then
 1057       return response
 1058     end
 1059 
 1060     file_bitmap, dir_bitmap, req_count, pos = string.unpack(">I2I2I2", response.packet.data)
 1061 
 1062     records = {}
 1063 
 1064     for i=1, req_count do
 1065       local record = {}
 1066       local len, _, ftype
 1067 
 1068       len, ftype, pos = string.unpack(">I2Bx", response.packet.data, pos)
 1069 
 1070       if ( ftype == 0x80 ) then
 1071         _, record = Util.decode_dir_bitmap( dir_bitmap, response.packet.data, pos )
 1072       else
 1073         -- file
 1074         _, record = Util.decode_file_bitmap( file_bitmap, response.packet.data, pos )
 1075       end
 1076 
 1077       if ( len % 2 ) ~= 0 then
 1078         len = len + 1
 1079       end
 1080 
 1081       pos = pos + ( len - 4 )
 1082 
 1083       record.type = ftype
 1084       table.insert(records, record)
 1085     end
 1086 
 1087     response:setResult(records)
 1088     return response
 1089   end,
 1090 
 1091   --- Sends an FPOpenFork request to the server and handles the response
 1092   --
 1093   -- @param flag number
 1094   -- @param volume_id number containing the id of the volume to query
 1095   -- @param did number containing the id of the directory to query
 1096   -- @param file_bitmap number bitmask of file information to query
 1097   -- @param access_mode number containing bitmask of options from <code>ACCESS_MODE</code>
 1098   -- @param path string containing the name of the directory to query
 1099   -- @return response object with the following result contents <code>file_bitmap</code> and <code>fork_id</code>
 1100   fp_open_fork = function( self, flag, volume_id, did, file_bitmap, access_mode, path )
 1101 
 1102     local packet
 1103     local data_offset = 0
 1104     local response, fork = {}, {}
 1105 
 1106     local data = string.pack( ">BBI2I4I2I2", COMMAND.FPOpenFork, flag, volume_id, did, file_bitmap, access_mode )
 1107                  .. encode_path(path)
 1108 
 1109     packet = self:create_fp_packet( REQUEST.Command, data_offset, data )
 1110     self:send_fp_packet( packet )
 1111     response = self:read_fp_packet()
 1112 
 1113     if response:getErrorCode() ~= ERROR.FPNoErr then
 1114       return response
 1115     end
 1116 
 1117     fork.file_bitmap, fork.fork_id = string.unpack(">I2I2", response.packet.data)
 1118     response:setResult(fork)
 1119     return response
 1120   end,
 1121 
 1122   --- FPCloseFork
 1123   --
 1124   -- @param fork number containing the fork to close
 1125   -- @return response object
 1126   fp_close_fork = function( self, fork )
 1127     local packet
 1128     local data_offset = 0
 1129     local response = {}
 1130 
 1131     local data = string.pack( ">BxI2", COMMAND.FPCloseFork, fork )
 1132 
 1133     packet = self:create_fp_packet( REQUEST.Command, data_offset, data )
 1134     self:send_fp_packet( packet )
 1135     return self:read_fp_packet( )
 1136   end,
 1137 
 1138   --- FPCreateDir
 1139   --
 1140   -- @param vol_id number containing the volume id
 1141   -- @param dir_id number containing the directory id
 1142   -- @param path table containing the name and name encoding type of the directory to query
 1143   -- @return response object
 1144   fp_create_dir = function( self, vol_id, dir_id, path )
 1145     local packet
 1146     local data_offset = 0
 1147     local response = {}
 1148 
 1149     local data = string.pack( ">BxI2I4", COMMAND.FPCreateDir, vol_id, dir_id)
 1150                  .. encode_path(path)
 1151 
 1152     packet = self:create_fp_packet( REQUEST.Command, data_offset, data )
 1153     self:send_fp_packet( packet )
 1154     return self:read_fp_packet( )
 1155   end,
 1156 
 1157   --- Sends an FPCloseVol request to the server and handles the response
 1158   --
 1159   -- @param volume_id number containing the id of the volume to close
 1160   -- @return response object
 1161   fp_close_vol = function( self, volume_id )
 1162     local packet
 1163     local data_offset = 0
 1164     local response = {}
 1165 
 1166     local data = string.pack( ">BxI2", COMMAND.FPCloseVol, volume_id )
 1167 
 1168     packet = self:create_fp_packet( REQUEST.Command, data_offset, data )
 1169     self:send_fp_packet( packet )
 1170     return self:read_fp_packet( )
 1171   end,
 1172 
 1173   --- FPReadExt
 1174   --
 1175   -- @param fork number containing the open fork
 1176   -- @param offset number containing the offset from where writing should start. Negative value indicates offset from the end of the fork
 1177   -- @param count number containing the number of bytes to be written
 1178   -- @return response object
 1179   fp_read_ext = function( self, fork, offset, count )
 1180     local packet, response
 1181     local data_offset = 0
 1182     local block_size = 1024
 1183     local data = string.pack( ">BxI2I8I8", COMMAND.FPReadExt, fork, offset, count  )
 1184 
 1185     packet = self:create_fp_packet( REQUEST.Command, data_offset, data )
 1186     self:send_fp_packet( packet )
 1187     response = self:read_fp_packet( )
 1188 
 1189     if ( response:getErrorCode() == ERROR.FPEOFErr and response.packet.header.length > 0 ) then
 1190       response:setErrorCode( ERROR.FPNoErr )
 1191     end
 1192 
 1193     response:setResult( response.packet.data )
 1194     return response
 1195   end,
 1196 
 1197   --- FPWriteExt
 1198   --
 1199   -- @param flag number indicates whether Offset is relative to the beginning or end of the fork.
 1200   -- @param fork number containing the open fork
 1201   -- @param offset number containing the offset from where writing should start. Negative value indicates offset from the end of the fork
 1202   -- @param count number containing the number of bytes to be written
 1203   -- @param fdata string containing the data to be written
 1204   -- @return response object
 1205   fp_write_ext = function( self, flag, fork, offset, count, fdata )
 1206     local packet
 1207     local data_offset = 20
 1208     local data
 1209 
 1210     if count > fdata:len() then
 1211       local err = Response:new()
 1212       err:setErrorMessage("fp_write_ext: Count is greater than the amount of data")
 1213       return err
 1214     end
 1215     if count < 0 then
 1216       local err = Response:new()
 1217       err:setErrorMessage("fp_write_ext: Count must exceed zero")
 1218       return err
 1219     end
 1220 
 1221     data = string.pack( ">BBI2I8I8", COMMAND.FPWriteExt, flag, fork, offset, count) .. fdata
 1222     packet = self:create_fp_packet( REQUEST.Write, data_offset, data )
 1223     self:send_fp_packet( packet )
 1224     return self:read_fp_packet( )
 1225   end,
 1226 
 1227   --- FPCreateFile
 1228   --
 1229   -- @param flag number where 0 indicates a soft create and 1 indicates a hard create.
 1230   -- @param vol_id number containing the volume id
 1231   -- @param did number containing the ancestor directory id
 1232   -- @param path string containing the path, including the volume, path and file name
 1233   -- @return response object
 1234   fp_create_file = function(self, flag, vol_id, did, path )
 1235     local packet
 1236     local data_offset = 0
 1237     local data = string.pack(">BBI2I4", COMMAND.FPCreateFile, flag, vol_id, did)
 1238                  .. encode_path(path)
 1239 
 1240     packet = self:create_fp_packet( REQUEST.Command, data_offset, data )
 1241     self:send_fp_packet( packet )
 1242     return self:read_fp_packet()
 1243   end,
 1244 
 1245   --- FPMapId
 1246   --
 1247   -- @param subfunc number containing the subfunction to call
 1248   -- @param id number containing th id to translate
 1249   -- @return response object with the id in the <code>result</code> field
 1250   fp_map_id = function( self, subfunc, id )
 1251     local packet, response
 1252     local data_offset = 0
 1253     local data = string.pack( "BB", COMMAND.FPMapId, subfunc )
 1254 
 1255     if ( subfunc == MAP_ID.UserUUIDToUTF8Name or subfunc == MAP_ID.GroupUUIDToUTF8Name ) then
 1256       data = data .. string.pack(">I8", id)
 1257     else
 1258       data = data .. string.pack(">I4", id)
 1259     end
 1260 
 1261     packet = self:create_fp_packet( REQUEST.Command, data_offset, data )
 1262     self:send_fp_packet( packet )
 1263     response = self:read_fp_packet( )
 1264 
 1265     if response:getErrorCode() ~= ERROR.FPNoErr then
 1266       return response
 1267     end
 1268 
 1269     -- Netatalk returns the name with 1-byte length prefix,
 1270     -- Mac OS has a 2-byte (UTF-8) length prefix
 1271     local len = string.unpack("B", response.packet.data)
 1272 
 1273     -- if length is zero assume 2-byte length (UTF-8 name)
 1274     if len == 0 then
 1275       response:setResult(string.unpack(">s2", response.packet.data))
 1276     else
 1277       response:setResult(string.unpack("s1", response.packet.data ))
 1278     end
 1279     return response
 1280   end,
 1281 
 1282   --- FPMapName
 1283   --
 1284   -- @param subfunc number containing the subfunction to call
 1285   -- @param name string containing name to map
 1286   -- @return response object with the mapped name in the <code>result</code> field
 1287   fp_map_name = function( self, subfunc, name )
 1288     local packet
 1289     local data_offset = 0
 1290     local data = string.pack(">BBs2", COMMAND.FPMapName, subfunc, name )
 1291     local response
 1292 
 1293     packet = self:create_fp_packet( REQUEST.Command, data_offset, data )
 1294     self:send_fp_packet( packet )
 1295     response = self:read_fp_packet( )
 1296 
 1297     if response:getErrorCode() ~= ERROR.FPNoErr then
 1298       return response
 1299     end
 1300 
 1301     response:setResult(string.unpack(">I4", response.packet.data))
 1302     return response
 1303   end,
 1304 }
 1305 
 1306 --- The helper class wraps the protocol class and their functions. It contains
 1307 -- high-level functions with descriptive names, facilitating the use and
 1308 -- minimizing the need to fully understand the AFP low-level protocol details.
 1309 Helper = {
 1310 
 1311   --- Creates a new helper object
 1312   new = function(self,o)
 1313     local o = {}
 1314     setmetatable(o, self)
 1315     self.__index = self
 1316     o.username = stdnse.get_script_args("afp.username")
 1317     o.password = stdnse.get_script_args("afp.password")
 1318     return o
 1319   end,
 1320 
 1321   --- Connects to the remote server and establishes a new AFP session
 1322   --
 1323   -- @param host table as received by the action function of the script
 1324   -- @param port table as received by the action function of the script
 1325   -- @return status boolean
 1326   -- @return string containing error message (if status is false)
 1327   OpenSession = function( self, host, port )
 1328     local status, response
 1329 
 1330     self.socket = nmap.new_socket()
 1331     self.socket:set_timeout( 5000 )
 1332     status = self.socket:connect(host, port)
 1333     if not status then
 1334       return false, "Socket connection failed"
 1335     end
 1336 
 1337     self.proto = Proto:new( { socket=self.socket} )
 1338     response = self.proto:dsi_open_session(self.socket)
 1339 
 1340     if response:getErrorCode() ~= ERROR.FPNoErr then
 1341       self.socket:close()
 1342       return false, response:getErrorMessage()
 1343     end
 1344 
 1345     return true
 1346   end,
 1347 
 1348   --- Closes the AFP session and then the socket
 1349   --
 1350   -- @return status boolean
 1351   -- @return string containing error message (if status is false)
 1352   CloseSession = function( self )
 1353     local status, packet = self.proto:dsi_close_session( )
 1354     self.socket:close()
 1355 
 1356     return status, packet
 1357   end,
 1358 
 1359   --- Terminates the connection, without closing the AFP session
 1360   --
 1361   -- @return status (always true)
 1362   -- @return string (always "")
 1363   Terminate = function( self )
 1364     self.socket:close()
 1365     return true,""
 1366   end,
 1367 
 1368   --- Logs in to an AFP service
 1369   --
 1370   -- @param username (optional) string containing the username
 1371   -- @param password (optional) string containing the user password
 1372   -- @param options table containing additional options <code>uam</code>
 1373   Login = function( self, username, password, options )
 1374     local uam = ( options and options.UAM ) and options.UAM or "DHCAST128"
 1375     local response
 1376 
 1377     -- username and password arguments override the ones supplied using the
 1378     -- script arguments afp.username and afp.password
 1379     local username = username or self.username
 1380     local password = password or self.password
 1381 
 1382     if ( username and uam == "DHCAST128" ) then
 1383       response = self.proto:fp_login( "AFP3.1", "DHCAST128", username, password )
 1384     elseif( username ) then
 1385       return false, ("Unsupported UAM: %s"):format(uam)
 1386     else
 1387       response = self.proto:fp_login( "AFP3.1", "No User Authent" )
 1388     end
 1389 
 1390     if response:getErrorCode() ~= ERROR.FPNoErr then
 1391       return false, response:getErrorMessage()
 1392     end
 1393 
 1394     return true, "Success"
 1395   end,
 1396 
 1397   --- Logs out from the AFP service
 1398   Logout = function(self)
 1399     return self.proto:fp_logout()
 1400   end,
 1401 
 1402   --- Walks the directory tree specified by <code>str_path</code> and returns the node information
 1403   --
 1404   -- @param str_path string containing the directory
 1405   -- @return status boolean true on success, otherwise false
 1406   -- @return item table containing node information <code>DirectoryId</code> and <code>DirectoryName</code>
 1407   WalkDirTree = function( self, str_path )
 1408     local status, response
 1409     local elements = stringaux.strsplit( "/", str_path )
 1410     local f_bm = FILE_BITMAP.NodeId + FILE_BITMAP.ParentDirId + FILE_BITMAP.LongName
 1411     local d_bm = DIR_BITMAP.NodeId + DIR_BITMAP.ParentDirId + DIR_BITMAP.LongName
 1412     local item = { DirectoryId = 2 }
 1413 
 1414     response = self.proto:fp_open_vol( VOL_BITMAP.ID, elements[1] )
 1415     if response:getErrorCode() ~= ERROR.FPNoErr then
 1416       return false, response:getErrorMessage()
 1417     end
 1418 
 1419     item.VolumeId = response.result.volume_id
 1420     item.DirectoryName = str_path
 1421 
 1422     for i=2, #elements do
 1423       local path = { type=PATH_TYPE.LongName, name=elements[i] }
 1424       response = self.proto:fp_get_file_dir_parms( item.VolumeId, item.DirectoryId, f_bm, d_bm, path )
 1425       if response:getErrorCode() ~= ERROR.FPNoErr then
 1426         return false, response:getErrorMessage()
 1427       end
 1428       item.DirectoryId = response.result.dir.NodeId
 1429       item.DirectoryName = response.result.dir.LongName
 1430     end
 1431 
 1432     return true, item
 1433   end,
 1434 
 1435   --- Reads a file on the AFP server
 1436   --
 1437   -- @param str_path string containing the AFP sharepoint, path and filename eg. HR/Documents/File.doc
 1438   -- @return status boolean true on success, false on failure
 1439   -- @return content string containing the file contents
 1440   ReadFile = function( self, str_path )
 1441     local status, response, fork, content, vol_name
 1442     local offset, count, did = 0, 1024, 2
 1443     local status, path, vol_id
 1444     local p = Util.SplitPath( str_path )
 1445 
 1446     status, response = self:WalkDirTree( p.dir )
 1447     if ( not status ) then
 1448       return false, response
 1449     end
 1450 
 1451     vol_id = response.VolumeId
 1452     did = response.DirectoryId
 1453 
 1454     path = { type=PATH_TYPE.LongName, name=p.file }
 1455 
 1456     response = self.proto:fp_open_fork(0, vol_id, did, 0, ACCESS_MODE.Read, path )
 1457     if response:getErrorCode() ~= ERROR.FPNoErr then
 1458       return false, response:getErrorMessage()
 1459     end
 1460 
 1461     fork = response.result.fork_id
 1462     content = ""
 1463 
 1464     while true do
 1465       response = self.proto:fp_read_ext( fork, offset, count )
 1466       if response:getErrorCode() ~= ERROR.FPNoErr then
 1467         break
 1468       end
 1469       content = content .. response.result
 1470       offset = offset + count
 1471     end
 1472 
 1473     response = self.proto:fp_close_fork( fork )
 1474     if response:getErrorCode() ~= ERROR.FPNoErr then
 1475       return false, response:getErrorMessage()
 1476     end
 1477 
 1478     return true, content
 1479   end,
 1480 
 1481   --- Writes a file to the AFP server
 1482   --
 1483   -- @param str_path string containing the AFP sharepoint, path and filename eg. HR/Documents/File.doc
 1484   -- @param fdata string containing the data to write to the file
 1485   -- @return status boolean true on success, false on failure
 1486   -- @return error string containing error message if status is false
 1487   WriteFile = function( self, str_path, fdata )
 1488     local status, response, fork, content
 1489     local offset, count = 1, 1024
 1490     local status, vol_id, did, path
 1491     local p = Util.SplitPath( str_path )
 1492 
 1493     status, response = self:WalkDirTree( p.dir )
 1494     vol_id = response.VolumeId
 1495     did = response.DirectoryId
 1496 
 1497     if ( not status ) then
 1498       return false, response
 1499     end
 1500 
 1501     path = { type=PATH_TYPE.LongName, name=p.file }
 1502 
 1503     status, response = self.proto:fp_create_file( 0, vol_id, did, path )
 1504     if not status then
 1505       if ( response.header.error_code ~= ERROR.FPObjectExists ) then
 1506         return false, response.header.error_msg
 1507       end
 1508     end
 1509 
 1510     response = self.proto:fp_open_fork( 0, vol_id, did, 0, ACCESS_MODE.Write, path )
 1511     if response:getErrorCode() ~= ERROR.FPNoErr then
 1512       return false, response:getErrorMessage()
 1513     end
 1514 
 1515     fork = response.result.fork_id
 1516 
 1517     response = self.proto:fp_write_ext( 0, fork, 0, fdata:len(), fdata )
 1518 
 1519     return true, nil
 1520   end,
 1521 
 1522   --- Maps a user id (uid) to a user name
 1523   --
 1524   -- @param uid number containing the uid to resolve
 1525   -- @return status boolean true on success, false on failure
 1526   -- @return username string on success
 1527   --         error string on failure
 1528   UIDToName = function( self, uid )
 1529     local response = self.proto:fp_map_id( MAP_ID.UserIDToName, uid )
 1530     if response:getErrorCode() ~= ERROR.FPNoErr then
 1531       return false, response:getErrorMessage()
 1532     end
 1533     return true, response.result
 1534   end,
 1535 
 1536   --- Maps a group id (gid) to group name
 1537   --
 1538   -- @param gid number containing the gid to lookup
 1539   -- @return status boolean true on success, false on failure
 1540   -- @return groupname string on success
 1541   --         error string on failure
 1542   GIDToName = function( self, gid )
 1543     local response = self.proto:fp_map_id( MAP_ID.GroupIDToName, gid )
 1544     if response:getErrorCode() ~= ERROR.FPNoErr then
 1545       return false, response:getErrorMessage()
 1546     end
 1547     return true, response.result
 1548   end,
 1549 
 1550   --- Maps a username to a UID
 1551   --
 1552   -- @param name string containing the username to map to an UID
 1553   -- @return status boolean true on success, false on failure
 1554   -- @return UID number on success
 1555   --         error string on failure
 1556   NameToUID = function( self, name )
 1557     local response = self.proto:fp_map_name( MAP_NAME.NameToUserID, name )
 1558     if response:getErrorCode() ~= ERROR.FPNoErr then
 1559       return false, response:getErrorMessage()
 1560     end
 1561     return true, response.result
 1562   end,
 1563 
 1564   --- List the contents of a directory
 1565   --
 1566   -- @param str_path string containing the sharepoint and directory names
 1567   -- @param options table options containing zero or more of the options
 1568   -- <code>max_depth</code> and <code>dironly</code>
 1569   -- @param depth number containing the current depth (used when called recursively)
 1570   -- @param parent table containing information about the parent object (used when called recursively)
 1571   -- @return status boolean true on success, false on failure
 1572   -- @return dir table containing a table for each directory item with the following:
 1573   --         <code>type</code>, <code>name</code>, <code>id</code>,
 1574   --         <code>fsize</code>, <code>uid</code>, <code>gid</code>,
 1575   --         <code>privs</code>, <code>create</code>, <code>modify</code>
 1576   Dir = function( self, str_path, options, depth, parent )
 1577     local status, result
 1578     local depth = depth or 1
 1579     local options = options or { max_depth = 1 }
 1580     local response, records
 1581     local f_bm = FILE_BITMAP.NodeId | FILE_BITMAP.ParentDirId
 1582                  | FILE_BITMAP.LongName | FILE_BITMAP.UnixPrivileges
 1583                  | FILE_BITMAP.CreationDate | FILE_BITMAP.ModificationDate
 1584                  | FILE_BITMAP.ExtendedDataForkSize
 1585     local d_bm = DIR_BITMAP.NodeId | DIR_BITMAP.ParentDirId
 1586                  | DIR_BITMAP.LongName | DIR_BITMAP.UnixPrivileges
 1587                  | DIR_BITMAP.CreationDate | DIR_BITMAP.ModificationDate
 1588 
 1589     local TYPE_DIR = 0x80
 1590 
 1591     if ( parent == nil ) then
 1592       status, response = self:WalkDirTree( str_path )
 1593       if ( not status ) then
 1594         return false, response
 1595       end
 1596 
 1597       parent = {}
 1598       parent.vol_id = response.VolumeId
 1599       parent.did = response.DirectoryId
 1600       parent.dir_name = response.DirectoryName or ""
 1601       parent.out_tbl = {}
 1602     end
 1603 
 1604     if ( options and options.max_depth and options.max_depth > 0 and options.max_depth < depth ) then
 1605       return false, "Max Depth Reached"
 1606     end
 1607 
 1608     local path = { type=PATH_TYPE.LongName, name="" }
 1609     response = self.proto:fp_enumerate_ext2( parent.vol_id, parent.did, f_bm, d_bm, 1000, 1, 1000 * 300, path)
 1610 
 1611     if response:getErrorCode() ~= ERROR.FPNoErr then
 1612       return false, response:getErrorMessage()
 1613     end
 1614 
 1615     records = response.result or {}
 1616     local dir_items = {}
 1617 
 1618     for _, record in ipairs( records ) do
 1619       local isdir = record.type == TYPE_DIR
 1620       -- Skip non-directories if option "dironly" is set
 1621       if isdir or not options.dironly then
 1622         local item = {type = record.type,
 1623                       name = record.LongName,
 1624                       id = record.NodeId,
 1625                       fsize = record.ExtendedDataForkSize or 0}
 1626         local privs = (record.UnixPrivileges or {}).ua_permissions
 1627         if privs then
 1628           item.uid = record.UnixPrivileges.uid
 1629           item.gid = record.UnixPrivileges.gid
 1630           item.privs = (isdir and "d" or "-") .. Util.decode_unix_privs(privs)
 1631         end
 1632         item.create = Util.time_to_string(record.CreationDate)
 1633         item.modify = Util.time_to_string(record.ModificationDate)
 1634         table.insert( dir_items, item )
 1635       end
 1636       if isdir then
 1637         self:Dir("", options, depth + 1, { vol_id = parent.vol_id, did=record.NodeId, dir_name=record.LongName, out_tbl=dir_items} )
 1638       end
 1639     end
 1640 
 1641     table.insert( parent.out_tbl, dir_items )
 1642 
 1643     return true, parent.out_tbl
 1644   end,
 1645 
 1646   --- Displays a directory tree
 1647   --
 1648   -- @param str_path string containing the sharepoint and the directory
 1649   -- @param options table options containing zero or more of the options
 1650   -- <code>max_depth</code> and <code>dironly</code>
 1651   -- @return dirtree table containing the directories
 1652   DirTree = function( self, str_path, options )
 1653     local options = options or {}
 1654     options.dironly = true
 1655     return self:Dir( str_path, options )
 1656   end,
 1657 
 1658   --- List the AFP sharepoints
 1659   --
 1660   -- @return volumes table containing the sharepoints
 1661   ListShares = function( self )
 1662     local response
 1663     response = self.proto:fp_get_srvr_parms( )
 1664 
 1665     if response:getErrorCode() ~= ERROR.FPNoErr then
 1666       return false, response:getErrorMessage()
 1667     end
 1668 
 1669     return true, response.result.volumes
 1670   end,
 1671 
 1672   --- Determine the sharepoint permissions
 1673   --
 1674   -- @param vol_name string containing the name of the volume
 1675   -- @return status boolean true on success, false on failure
 1676   -- @return acls table containing the volume acls as returned by <code>acls_to_long_string</code>
 1677   GetSharePermissions = function( self, vol_name )
 1678     local status, response, acls
 1679 
 1680     response = self.proto:fp_open_vol( VOL_BITMAP.ID, vol_name )
 1681 
 1682     if response:getErrorCode() == ERROR.FPNoErr then
 1683       local vol_id = response.result.volume_id
 1684       local path = { type = PATH_TYPE.LongName, name = "" }
 1685 
 1686       response = self.proto:fp_get_file_dir_parms( vol_id, 2, FILE_BITMAP.ALL, DIR_BITMAP.ALL, path )
 1687       if response:getErrorCode() == ERROR.FPNoErr then
 1688         if ( response.result.dir and response.result.dir.AccessRights ) then
 1689           acls = Util.acls_to_long_string(response.result.dir.AccessRights)
 1690           acls.name = nil
 1691         end
 1692       end
 1693       self.proto:fp_close_vol( vol_id )
 1694     end
 1695 
 1696     return true, acls
 1697   end,
 1698 
 1699   --- Gets the Unix permissions of a file
 1700   -- @param vol_name string containing the name of the volume
 1701   -- @param str_path string containing the name of the file
 1702   -- @return status true on success, false on failure
 1703   -- @return acls table (on success) containing the following fields
 1704   --  <code>uid</code> - a numeric user identifier
 1705   --  <code>gid</code> - a numeric group identifier
 1706   --  <code>privs</code> - a string value representing the permissions
 1707   --                       eg: drwx------
 1708   -- @return err string (on failure) containing the error message
 1709   GetFileUnixPermissions = function(self, vol_name, str_path)
 1710     local response = self.proto:fp_open_vol( VOL_BITMAP.ID, vol_name )
 1711 
 1712     if ( response:getErrorCode() ~= ERROR.FPNoErr ) then
 1713       return false, response:getErrorMessage()
 1714     end
 1715 
 1716     local vol_id = response.result.volume_id
 1717     local path = { type = PATH_TYPE.LongName, name = str_path }
 1718     response = self.proto:fp_get_file_dir_parms( vol_id, 2, FILE_BITMAP.UnixPrivileges, DIR_BITMAP.UnixPrivileges, path )
 1719     if ( response:getErrorCode() ~= ERROR.FPNoErr ) then
 1720       return false, response:getErrorMessage()
 1721     end
 1722 
 1723     local item = response.result.file or response.result.dir
 1724     local item_type = ( response.result.file ) and "-" or "d"
 1725     local privs = item.UnixPrivileges and item.UnixPrivileges.ua_permissions
 1726     if ( privs ) then
 1727       local uid = item.UnixPrivileges.uid
 1728       local gid = item.UnixPrivileges.gid
 1729       local str_privs = item_type .. Util.decode_unix_privs(privs)
 1730       return true, { uid = uid, gid = gid, privs = str_privs }
 1731     end
 1732   end,
 1733 
 1734   --- Gets the Unix permissions of a file
 1735   -- @param vol_name string containing the name of the volume
 1736   -- @param str_path string containing the name of the file
 1737   -- @return status true on success, false on failure
 1738   -- @return size containing the size of the file in bytes
 1739   -- @return err string (on failure) containing the error message
 1740   GetFileSize = function( self, vol_name, str_path )
 1741     local response = self.proto:fp_open_vol( VOL_BITMAP.ID, vol_name )
 1742 
 1743     if ( response:getErrorCode() ~= ERROR.FPNoErr ) then
 1744       return false, response:getErrorMessage()
 1745     end
 1746 
 1747     local vol_id = response.result.volume_id
 1748     local path = { type = PATH_TYPE.LongName, name = str_path }
 1749     response = self.proto:fp_get_file_dir_parms( vol_id, 2, FILE_BITMAP.ExtendedDataForkSize, 0, path )
 1750     if ( response:getErrorCode() ~= ERROR.FPNoErr ) then
 1751       return false, response:getErrorMessage()
 1752     end
 1753 
 1754     return true, response.result.file and response.result.file.ExtendedDataForkSize or 0
 1755   end,
 1756 
 1757 
 1758   --- Returns the creation, modification and backup dates of a file
 1759   -- @param vol_name string containing the name of the volume
 1760   -- @param str_path string containing the name of the file
 1761   -- @return status true on success, false on failure
 1762   -- @return dates table containing the following fields:
 1763   --  <code>create</code> - Creation date of the file
 1764   --  <code>modify</code> - Modification date of the file
 1765   --  <code>backup</code> - Date of last backup
 1766   -- @return err string (on failure) containing the error message
 1767   GetFileDates = function( self, vol_name, str_path )
 1768     local response = self.proto:fp_open_vol( VOL_BITMAP.ID, vol_name )
 1769 
 1770     if ( response:getErrorCode() ~= ERROR.FPNoErr ) then
 1771       return false, response:getErrorMessage()
 1772     end
 1773 
 1774     local vol_id = response.result.volume_id
 1775     local path = { type = PATH_TYPE.LongName, name = str_path }
 1776     local f_bm = FILE_BITMAP.CreationDate + FILE_BITMAP.ModificationDate + FILE_BITMAP.BackupDate
 1777     local d_bm = DIR_BITMAP.CreationDate + DIR_BITMAP.ModificationDate + DIR_BITMAP.BackupDate
 1778     response = self.proto:fp_get_file_dir_parms( vol_id, 2, f_bm, d_bm, path )
 1779     if ( response:getErrorCode() ~= ERROR.FPNoErr ) then
 1780       return false, response:getErrorMessage()
 1781     end
 1782 
 1783     local item = response.result.file or response.result.dir
 1784 
 1785     local create = Util.time_to_string(item.CreationDate)
 1786     local backup = Util.time_to_string(item.BackupDate)
 1787     local modify = Util.time_to_string(item.ModificationDate)
 1788 
 1789     return true, { create = create, backup = backup, modify = modify }
 1790   end,
 1791 
 1792   --- Creates a new directory on the AFP sharepoint
 1793   --
 1794   -- @param str_path containing the sharepoint and the directory
 1795   -- @return status boolean true on success, false on failure
 1796   -- @return dirId number containing the new directory id
 1797   CreateDir = function( self, str_path )
 1798     local status, response, vol_id, did
 1799     local p = Util.SplitPath( str_path )
 1800     local path = { type=PATH_TYPE.LongName, name=p.file }
 1801 
 1802 
 1803     status, response = self:WalkDirTree( p.dir )
 1804     if not status then
 1805       return false, response
 1806     end
 1807 
 1808     response = self.proto:fp_create_dir( response.VolumeId, response.DirectoryId, path )
 1809     if response:getErrorCode() ~= ERROR.FPNoErr then
 1810       return false, response:getErrorMessage()
 1811     end
 1812 
 1813     return true, response
 1814   end,
 1815 
 1816 }
 1817 
 1818 --- Util class, containing some static functions used by Helper and Proto
 1819 Util =
 1820 {
 1821   --- Pads a string with zeroes
 1822   --
 1823   -- @param str string containing the string to be padded
 1824   -- @param len number containing the length of the new string
 1825   -- @return str string containing the new string
 1826   ZeroPad = function( str, len )
 1827     return str .. string.rep('\0', len - str:len())
 1828   end,
 1829 
 1830   --- Splits a path into two pieces, directory and file
 1831   --
 1832   -- @param str_path string containing the path to split
 1833   -- @return dir table containing <code>dir</code> and <code>file</code>
 1834   SplitPath = function( str_path )
 1835     local elements = stringaux.strsplit("/", str_path)
 1836     local dir, file = "", ""
 1837 
 1838     if #elements < 2 then
 1839       return nil
 1840     end
 1841 
 1842     file = elements[#elements]
 1843 
 1844     table.remove( elements, #elements )
 1845     dir = table.concat( elements, "/" )
 1846 
 1847     return { ['dir']=dir, ['file']=file }
 1848 
 1849   end,
 1850 
 1851   --- Converts a group bitmask of Search, Read and Write to table
 1852   --
 1853   -- @param acls number containing bitmasked acls
 1854   -- @return table of ACLs
 1855   acl_group_to_long_string = function(acls)
 1856 
 1857     local acl_table = {}
 1858 
 1859     if ( acls & ACLS.OwnerSearch ) == ACLS.OwnerSearch then
 1860       table.insert( acl_table, "Search")
 1861     end
 1862 
 1863     if ( acls & ACLS.OwnerRead ) == ACLS.OwnerRead then
 1864       table.insert( acl_table, "Read")
 1865     end
 1866 
 1867     if ( acls & ACLS.OwnerWrite ) == ACLS.OwnerWrite then
 1868       table.insert( acl_table, "Write")
 1869     end
 1870 
 1871     return acl_table
 1872   end,
 1873 
 1874 
 1875   --- Converts a numeric acl to string
 1876   --
 1877   -- @param acls number containing acls as received from <code>fp_get_file_dir_parms</code>
 1878   -- @return table of long ACLs
 1879   acls_to_long_string = function( acls )
 1880 
 1881     local owner = Util.acl_group_to_long_string( ( acls & 255 ) )
 1882     local group = Util.acl_group_to_long_string( ( (acls >> 8) & 255 ) )
 1883     local everyone = Util.acl_group_to_long_string( ( (acls >> 16) & 255 ) )
 1884     local user = Util.acl_group_to_long_string( ( (acls >> 24) & 255 ) )
 1885 
 1886     local blank = ( acls & ACLS.BlankAccess ) == ACLS.BlankAccess and "Blank" or nil
 1887     local isowner = ( acls & ACLS.UserIsOwner ) == ACLS.UserIsOwner and "IsOwner" or nil
 1888 
 1889     local options = {}
 1890 
 1891     if blank then
 1892       table.insert(options, "Blank")
 1893     end
 1894 
 1895     if isowner then
 1896       table.insert(options, "IsOwner")
 1897     end
 1898 
 1899     local acls_tbl = {}
 1900 
 1901     table.insert( acls_tbl, string.format( "Owner: %s", table.concat(owner, ",") ) )
 1902     table.insert( acls_tbl, string.format( "Group: %s", table.concat(group, ",") ) )
 1903     table.insert( acls_tbl, string.format( "Everyone: %s", table.concat(everyone, ",") ) )
 1904     table.insert( acls_tbl, string.format( "User: %s", table.concat(user, ",") ) )
 1905 
 1906     if #options > 0 then
 1907       table.insert( acls_tbl, string.format( "Options: %s", table.concat(options, ",") ) )
 1908     end
 1909 
 1910     return acls_tbl
 1911 
 1912   end,
 1913 
 1914 
 1915   --- Converts AFP file timestamp to a standard text format
 1916   --
 1917   -- @param timestamp value returned by FPEnumerateExt2 or FPGetFileDirParms
 1918   -- @return string representing the timestamp
 1919   time_to_string = function (timestamp)
 1920     return timestamp and datetime.format_timestamp(timestamp + TIME_OFFSET) or nil
 1921   end,
 1922 
 1923 
 1924   --- Decodes the UnixPrivileges.ua_permissions value
 1925   --
 1926   -- @param privs number containing the UnixPrivileges.ua_permissions value
 1927   -- @return string containing the ACL characters
 1928   decode_unix_privs = function( privs )
 1929     local owner = ( ( privs & ACLS.OwnerRead ) == ACLS.OwnerRead ) and "r" or "-"
 1930     owner = owner .. (( ( privs & ACLS.OwnerWrite ) == ACLS.OwnerWrite ) and "w" or "-")
 1931     owner = owner .. (( ( privs & ACLS.OwnerSearch ) == ACLS.OwnerSearch ) and "x" or "-")
 1932 
 1933     local group = ( ( privs & ACLS.GroupRead ) == ACLS.GroupRead ) and "r" or "-"
 1934     group = group .. (( ( privs & ACLS.GroupWrite ) == ACLS.GroupWrite ) and "w" or "-")
 1935     group = group .. (( ( privs & ACLS.GroupSearch ) == ACLS.GroupSearch ) and "x" or "-")
 1936 
 1937     local other = ( ( privs & ACLS.EveryoneRead ) == ACLS.EveryoneRead ) and "r" or "-"
 1938     other = other .. (( ( privs & ACLS.EveryoneWrite ) == ACLS.EveryoneWrite ) and "w" or "-")
 1939     other = other .. (( ( privs & ACLS.EveryoneSearch ) == ACLS.EveryoneSearch ) and "x" or "-")
 1940 
 1941     return owner .. group .. other
 1942   end,
 1943 
 1944 
 1945   --- Decodes a file bitmap
 1946   --
 1947   -- @param bitmap number containing the bitmap
 1948   -- @param data string containing the data to be decoded
 1949   -- @param pos number containing the offset into data
 1950   -- @return pos number containing the new offset after decoding
 1951   -- @return file table containing the decoded values
 1952   decode_file_bitmap = function( bitmap, data, pos )
 1953     local origpos = pos
 1954     local file = {}
 1955 
 1956     if ( ( bitmap & FILE_BITMAP.Attributes ) == FILE_BITMAP.Attributes ) then
 1957       file.Attributes, pos = string.unpack(">I2", data, pos )
 1958     end
 1959     if ( ( bitmap & FILE_BITMAP.ParentDirId ) == FILE_BITMAP.ParentDirId ) then
 1960       file.ParentDirId, pos = string.unpack(">I4", data, pos )
 1961     end
 1962     if ( ( bitmap & FILE_BITMAP.CreationDate ) == FILE_BITMAP.CreationDate ) then
 1963       file.CreationDate, pos = string.unpack(">I4", data, pos )
 1964     end
 1965     if ( ( bitmap & FILE_BITMAP.ModificationDate ) == FILE_BITMAP.ModificationDate ) then
 1966       file.ModificationDate, pos = string.unpack(">I4", data, pos )
 1967     end
 1968     if ( ( bitmap & FILE_BITMAP.BackupDate ) == FILE_BITMAP.BackupDate ) then
 1969       file.BackupDate, pos = string.unpack(">I4", data, pos )
 1970     end
 1971     if ( ( bitmap & FILE_BITMAP.FinderInfo ) == FILE_BITMAP.FinderInfo ) then
 1972       file.FinderInfo, pos = string.unpack("c32", data, pos )
 1973     end
 1974     if ( ( bitmap & FILE_BITMAP.LongName ) == FILE_BITMAP.LongName ) then
 1975       local offset
 1976       offset, pos = string.unpack(">I2", data, pos)
 1977       if offset > 0 then
 1978         file.LongName = string.unpack("s1", data, origpos + offset)
 1979       end
 1980     end
 1981     if ( ( bitmap & FILE_BITMAP.ShortName ) == FILE_BITMAP.ShortName ) then
 1982       local offset
 1983       offset, pos = string.unpack(">I2", data, pos)
 1984       if offset > 0 then
 1985         file.ShortName = string.unpack("s1", data, origpos + offset)
 1986       end
 1987     end
 1988     if ( ( bitmap & FILE_BITMAP.NodeId ) == FILE_BITMAP.NodeId ) then
 1989       file.NodeId, pos = string.unpack(">I4", data, pos )
 1990     end
 1991     if ( ( bitmap & FILE_BITMAP.DataForkSize ) == FILE_BITMAP.DataForkSize ) then
 1992       file.DataForkSize, pos = string.unpack(">I4", data, pos )
 1993     end
 1994     if ( ( bitmap & FILE_BITMAP.ResourceForkSize ) == FILE_BITMAP.ResourceForkSize ) then
 1995       file.ResourceForkSize, pos = string.unpack(">I4", data, pos )
 1996     end
 1997     if ( ( bitmap & FILE_BITMAP.ExtendedDataForkSize ) == FILE_BITMAP.ExtendedDataForkSize ) then
 1998       file.ExtendedDataForkSize, pos = string.unpack(">I8", data, pos )
 1999     end
 2000     if ( ( bitmap & FILE_BITMAP.LaunchLimit ) == FILE_BITMAP.LaunchLimit ) then
 2001       -- should not be set as it's deprecated according to:
 2002       -- http://developer.apple.com/mac/library/documentation/Networking/Reference/AFP_Reference/Reference/reference.html#//apple_ref/doc/c_ref/kFPLaunchLimitBit
 2003     end
 2004     if ( ( bitmap & FILE_BITMAP.UTF8Name ) == FILE_BITMAP.UTF8Name ) then
 2005       local offset
 2006       offset, pos = string.unpack(">I2", data, pos)
 2007       if offset > 0 then
 2008         -- +4 to skip over the encoding hint
 2009         file.UTF8Name = string.unpack(">s2", data, origpos + offset + 4)
 2010       end
 2011       -- Skip over the trailing pad
 2012       pos = pos + 4
 2013     end
 2014     if ( ( bitmap & FILE_BITMAP.ExtendedResourceForkSize ) == FILE_BITMAP.ExtendedResourceForkSize ) then
 2015       file.ExtendedResourceForkSize, pos = string.unpack(">I8", data, pos )
 2016     end
 2017     if ( ( bitmap & FILE_BITMAP.UnixPrivileges ) == FILE_BITMAP.UnixPrivileges ) then
 2018       local unixprivs = {}
 2019       unixprivs.uid, unixprivs.gid, unixprivs.permissions, unixprivs.ua_permissions, pos = string.unpack(">I4I4I4I4", data, pos)
 2020       file.UnixPrivileges = unixprivs
 2021     end
 2022     return pos, file
 2023   end,
 2024 
 2025   --- Decodes a directory bitmap
 2026   --
 2027   -- @param bitmap number containing the bitmap
 2028   -- @param data string containing the data to be decoded
 2029   -- @param pos number containing the offset into data
 2030   -- @return pos number containing the new offset after decoding
 2031   -- @return dir table containing the decoded values
 2032   decode_dir_bitmap = function( bitmap, data, pos )
 2033     local origpos = pos
 2034     local dir = {}
 2035 
 2036     if ( ( bitmap & DIR_BITMAP.Attributes ) == DIR_BITMAP.Attributes ) then
 2037       dir.Attributes, pos = string.unpack(">I2", data, pos)
 2038     end
 2039     if ( ( bitmap & DIR_BITMAP.ParentDirId ) == DIR_BITMAP.ParentDirId ) then
 2040       dir.ParentDirId, pos = string.unpack(">I4", data, pos)
 2041     end
 2042     if ( ( bitmap & DIR_BITMAP.CreationDate ) == DIR_BITMAP.CreationDate ) then
 2043       dir.CreationDate, pos = string.unpack(">I4", data, pos)
 2044     end
 2045     if ( ( bitmap & DIR_BITMAP.ModificationDate ) == DIR_BITMAP.ModificationDate ) then
 2046       dir.ModificationDate, pos = string.unpack(">I4", data, pos)
 2047     end
 2048     if ( ( bitmap & DIR_BITMAP.BackupDate ) == DIR_BITMAP.BackupDate ) then
 2049       dir.BackupDate, pos = string.unpack(">I4", data, pos)
 2050     end
 2051     if ( ( bitmap & DIR_BITMAP.FinderInfo ) == DIR_BITMAP.FinderInfo ) then
 2052       dir.FinderInfo, pos = string.unpack("c32", data, pos)
 2053     end
 2054     if ( ( bitmap & DIR_BITMAP.LongName ) == DIR_BITMAP.LongName ) then
 2055       local offset
 2056       offset, pos = string.unpack(">I2", data, pos)
 2057 
 2058       -- TODO: This really needs to be addressed someway
 2059       -- Barely, never, ever happens, which makes it difficult to pin down
 2060       -- http://developer.apple.com/mac/library/documentation/Networking/Reference/AFP_Reference/Reference/reference.html#//apple_ref/doc/uid/TP40003548-CH3-CHDBEHBG
 2061 
 2062       -- [nnposter, 8/1/2020] URL above not available. Offset below (pos+4)
 2063       -- seems illogical, as it partially covers two separate fields: bottom
 2064       -- half of the file ID and the entire offspring count.
 2065       -- Disabled the hack, as it interfered with valid cases
 2066 
 2067       --[[
 2068       local justkidding = string.unpack(">I4", data, pos + 4)
 2069       if ( justkidding ~= 0 ) then
 2070         offset = 5
 2071       end
 2072       ]]
 2073 
 2074       if offset > 0 then
 2075         dir.LongName = string.unpack("s1", data, origpos + offset)
 2076       end
 2077     end
 2078     if ( ( bitmap & DIR_BITMAP.ShortName ) == DIR_BITMAP.ShortName ) then
 2079       local offset
 2080       offset, pos = string.unpack(">I2", data, pos)
 2081       if offset > 0 then
 2082         dir.ShortName = string.unpack("s1", data, origpos + offset)
 2083       end
 2084     end
 2085     if ( ( bitmap & DIR_BITMAP.NodeId ) == DIR_BITMAP.NodeId ) then
 2086       dir.NodeId, pos = string.unpack(">I4", data, pos )
 2087     end
 2088     if ( ( bitmap & DIR_BITMAP.OffspringCount ) == DIR_BITMAP.OffspringCount ) then
 2089       dir.OffspringCount, pos = string.unpack(">I2", data, pos )
 2090     end
 2091     if ( ( bitmap & DIR_BITMAP.OwnerId ) == DIR_BITMAP.OwnerId ) then
 2092       dir.OwnerId, pos = string.unpack(">I4", data, pos )
 2093     end
 2094     if ( ( bitmap & DIR_BITMAP.GroupId ) == DIR_BITMAP.GroupId ) then
 2095       dir.GroupId, pos = string.unpack(">I4", data, pos )
 2096     end
 2097     if ( ( bitmap & DIR_BITMAP.AccessRights ) == DIR_BITMAP.AccessRights ) then
 2098       dir.AccessRights, pos = string.unpack(">I4", data, pos )
 2099     end
 2100     if ( ( bitmap & DIR_BITMAP.UTF8Name ) == DIR_BITMAP.UTF8Name ) then
 2101       local offset
 2102       offset, pos = string.unpack(">I2", data, pos)
 2103       if offset > 0 then
 2104         -- +4 to skip over the encoding hint
 2105         dir.UTF8Name = string.unpack(">s2", data, origpos + offset + 4)
 2106       end
 2107       -- Skip over the trailing pad
 2108       pos = pos + 4
 2109     end
 2110     if ( ( bitmap & DIR_BITMAP.UnixPrivileges ) == DIR_BITMAP.UnixPrivileges ) then
 2111       local unixprivs = {}
 2112 
 2113       unixprivs.uid, unixprivs.gid, unixprivs.permissions, unixprivs.ua_permissions, pos = string.unpack(">I4I4I4I4", data, pos)
 2114       dir.UnixPrivileges = unixprivs
 2115     end
 2116     return pos, dir
 2117   end,
 2118 
 2119 }
 2120 
 2121 
 2122 
 2123 
 2124 return _ENV;