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 114local datetime = require "datetime" 115local ipOps = require "ipOps" 116local nmap = require "nmap" 117local os = require "os" 118local stdnse = require "stdnse" 119local string = require "string" 120local stringaux = require "stringaux" 121local table = require "table" 122_ENV = stdnse.module("afp", stdnse.seeall); 123 124local HAVE_SSL, openssl = pcall(require,'openssl') 125 126-- Table of valid REQUESTs 127local 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 136local FLAGS = { 137 Request = 0, 138 Response = 1 139} 140 141-- Table of possible AFP_COMMANDs 142COMMAND = { 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 167USER_BITMAP = { 168 UserId = 0x01, 169 PrimaryGroupId = 0x2, 170 UUID = 0x4 171} 172 173VOL_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 188FILE_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 208DIR_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 227PATH_TYPE = { 228 ShortName = 1, 229 LongName = 2, 230 UTF8Name = 3, 231} 232 233ACCESS_MODE = { 234 Read = 0x1, 235 Write = 0x2, 236 DenyRead = 0x10, 237 DenyWrite = 0x20 238} 239 240-- Access controls 241ACLS = { 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 263UAM = 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 275ERROR = 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 298MAP_ID = 299{ 300 UserIDToName = 1, 301 GroupIDToName = 2, 302 UserIDToUTF8Name = 3, 303 GroupIDToUTF8Name = 4, 304 UserUUIDToUTF8Name = 5, 305 GroupUUIDToUTF8Name = 6 306} 307 308MAP_NAME = 309{ 310 NameToUserID = 1, 311 NameToGroupID = 2, 312 UTF8NameToUserID = 3, 313 UTF8NameToGroupID = 4, 314 UTF8NameToUserUUID = 5, 315 UTF8NameToGroupUUID = 6 316} 317 318 319SERVERFLAGS = 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 335local 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 355local 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. 358local function flag_is_set(bitmap, flag) 359 return (bitmap & flag) == flag 360end 361 362-- Serialize path of a given type 363-- NB: For now the actual UTF-8 encoding is ignored 364local 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))) 371end 372 373-- Response class returned by all functions in Proto 374Response = { 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 454Proto = { 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. 1309Helper = { 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 1819Util = 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 2124return _ENV; 2125