-
Notifications
You must be signed in to change notification settings - Fork 2.4k
/
afp.lua
2120 lines (1801 loc) · 70 KB
/
afp.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
---
-- This library was written by Patrik Karlsson <patrik@cqure.net> to facilitate
-- communication with the Apple AFP Service. It is not feature complete and
-- still missing several functions.
--
-- The library currently supports
-- * Authentication using the DHX UAM (CAST128)
-- * File reading and writing
-- * Listing sharepoints
-- * Listing directory contents
-- * Querying ACLs and mapping user identities (UIDs)
--
-- The library was built based on the following reference:
-- http://developer.apple.com/mac/library/documentation/Networking/Reference/AFP_Reference/Reference/reference.html
-- http://developer.apple.com/mac/library/documentation/Networking/Conceptual/AFP/AFPSecurity/AFPSecurity.html#//apple_ref/doc/uid/TP40000854-CH232-CHBBAGCB
--
-- Most functions have been tested against both Mac OS X 10.6.2 and Netatalk 2.0.3
--
-- The library contains the following four classes
-- * <code>Response</code>
-- ** A class used as return value by functions in the <code>Proto</code> class.
-- ** The response class acts as a wrapper and holds the response data and any error information.
-- * <code>Proto</code>
-- ** This class contains all the AFP specific functions and calls.
-- ** The functions can be accessed directly but the preferred method is through the <code>Helper</code> class.
-- ** The function names closely resemble those described in the Apple documentation.
-- ** Some functions may lack some of the options outlined in Apple's documentation.
-- * <code>Helper</code>
-- ** The helper class wraps the <code>Proto</code> class using functions with a more descriptive name.
-- ** Functions are task-oriented. For example, <code>ReadFile</code> and usually call several functions in the <code>Proto</code> class.
-- ** The purpose of this class is to give developers easy access to some of the common AFP tasks.
-- * <code>Util</code>
-- ** The <code>Util</code> class contains a number of static functions mainly used to convert data.
--
-- The following information will describe how to use the AFP Helper class to communicate with an AFP server.
--
-- The short version:
-- <code>
-- helper = afp.Helper:new()
-- status, response = helper:OpenSession( host, port )
-- status, response = helper:Login()
-- .. do some fancy AFP stuff ..
-- status, response = helper:Logout()
-- status, response = helper:CloseSession()
-- </code>
--
-- Here's the longer version, with some explanatory text. To start using the Helper class,
-- the script has to create it's own instance. We do this by issuing the following:
-- <code>
-- helper = afp.Helper:new()
-- </code>
--
-- Next a session to the AFP server must be established, this is done using the OpenSession method of the
-- Helper class, like this:
-- <code>
-- status, response = helper:OpenSession( host, port )
-- </code>
--
-- The next step needed to be performed is to authenticate to the server. We need to do this even for
-- functions that are available publically. In order to authenticate as the public user simply
-- authenticate using nil for both username and password. This can be achieved by calling the Login method
-- without any parameters, like this:
-- <code>
-- status, response = helper:Login()
-- </code>
--
-- To authenticate to the server using the username 'admin' and password 'nimda' we do this instead:
-- <code>
-- status, response = helper:Login('admin', 'nimda')
-- </code>
--
-- At this stage we're authenticated and can call any of the AFP functions we're authorized to.
-- For the purpose of this documentation, we will attempt to list the servers share points.
-- We do this by issuing the following:
-- <code>
-- status, shares = helper:ListShares()
-- </code>
--
-- Once we're finnished, we need to logout and close the AFP session this is done by calling the
-- following two methods of the Helper class:
-- <code>
-- status, response = helper:Logout()
-- status, response = helper:CloseSession()
-- </code>
--
-- Consult the documentation of each function to learn more about their respective return values.
--
--@author Patrik Karlsson <patrik@cqure.net>
--@copyright Same as Nmap--See http://nmap.org/book/man-legal.html
--
-- @args afp.username The username to use for authentication.
-- @args afp.password The password to use for authentication.
--
-- Version 0.5
--
-- Created 01/03/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
-- Revised 01/20/2010 - v0.2 - updated all bitmaps to hex for better readability
-- Revised 02/15/2010 - v0.3 - added a bunch of new functions and re-designed the code to be OO
--
-- New functionality added as of v0.3
-- o File reading, writing
-- o Authentication
-- o Helper functions for most AFP functions
-- o More robust error handling
--
-- Revised 03/05/2010 - v0.4 - changed output table of Helper:Dir to include type and ID
-- - added support for --without-openssl
--
-- Revised 03/09/2010 - v0.5 - documentation, documenation and more documentation
-- Revised 04/03/2011 - v0.6 - add support for getting file- sizes, dates and Unix ACLs
-- - moved afp.username & afp.password arguments to library
local bin = require "bin"
local bit = require "bit"
local nmap = require "nmap"
local os = require "os"
local stdnse = require "stdnse"
local string = require "string"
local table = require "table"
_ENV = stdnse.module("afp", stdnse.seeall);
local HAVE_SSL, openssl = pcall(require,'openssl')
-- Table of valid REQUESTs
local REQUEST = {
CloseSession = 0x01,
OpenSession = 0x04,
Command = 0x02,
GetStatus = 0x03,
Write = 0x06,
}
-- Table of headers flags to be set accordingly in requests and responses
local FLAGS = {
Request = 0,
Response = 1
}
-- Table of possible AFP_COMMANDs
COMMAND = {
FPCloseVol = 0x02,
FPCloseFork = 0x04,
FPCopyFile = 0x05,
FPCreateDir = 0x06,
FPCreateFile = 0x07,
FPGetSrvrInfo = 0x0f,
FPGetSrvParms = 0x10,
FPLogin = 0x12,
FPLoginCont = 0x13,
FPLogout = 0x14,
FPMapId = 0x15,
FPMapName = 0x16,
FPGetUserInfo = 0x25,
FPOpenVol = 0x18,
FPOpenFork = 0x1a,
FPGetFileDirParams = 0x22,
FPChangePassword = 0x24,
FPReadExt = 0x3c,
FPWriteExt = 0x3d,
FPGetAuthMethods = 0x3e,
FPLoginExt = 0x3f,
FPEnumerateExt2 = 0x44,
}
USER_BITMAP = {
UserId = 0x01,
PrimaryGroupId = 0x2,
UUID = 0x4
}
VOL_BITMAP = {
Attributes = 0x1,
Signature = 0x2,
CreationDate = 0x4,
ModificationDate = 0x8,
BackupDate = 0x10,
ID = 0x20,
BytesFree = 0x40,
BytesTotal = 0x80,
Name = 0x100,
ExtendedBytesFree = 0x200,
ExtendedBytesTotal = 0x400,
BlockSize = 0x800
}
FILE_BITMAP = {
Attributes = 0x1,
ParentDirId = 0x2,
CreationDate = 0x4,
ModificationDate = 0x8,
BackupDate = 0x10,
FinderInfo = 0x20,
LongName = 0x40,
ShortName = 0x80,
NodeId = 0x100,
DataForkSize = 0x200,
ResourceForkSize = 0x400,
ExtendedDataForkSize = 0x800,
LaunchLimit = 0x1000,
UTF8Name = 0x2000,
ExtendedResourceForkSize = 0x4000,
UnixPrivileges = 0x8000,
ALL = 0xFFFF
}
DIR_BITMAP = {
Attributes = 0x1,
ParentDirId = 0x2,
CreationDate = 0x4,
ModificationDate = 0x8,
BackupDate = 0x10,
FinderInfo = 0x20,
LongName = 0x40,
ShortName = 0x80,
NodeId = 0x100,
OffspringCount = 0x200,
OwnerId = 0x400,
GroupId = 0x800,
AccessRights = 0x1000,
UTF8Name = 0x2000,
UnixPrivileges = 0x8000,
ALL = 0xBFFF,
}
PATH_TYPE = {
ShortName = 1,
LongName = 2,
UTF8Name = 3,
}
ACCESS_MODE = {
Read = 0x1,
Write = 0x2,
DenyRead = 0x10,
DenyWrite = 0x20
}
-- Access controls
ACLS = {
OwnerSearch = 0x1,
OwnerRead = 0x2,
OwnerWrite = 0x4,
GroupSearch = 0x100,
GroupRead = 0x200,
GroupWrite = 0x400,
EveryoneSearch = 0x10000,
EveryoneRead = 0x20000,
EveryoneWrite = 0x40000,
UserSearch = 0x100000,
UserRead = 0x200000,
UserWrite = 0x400000,
BlankAccess = 0x10000000,
UserIsOwner = 0x80000000
}
-- User authentication modules
UAM =
{
NoUserAuth = "No User Authent",
ClearText = "Cleartxt Passwrd",
RandNum = "Randnum Exchange",
TwoWayRandNum = "2-Way Randnum",
DHCAST128 = "DHCAST128",
DHX2 = "DHX2",
Kerberos = "Client Krb v2",
Reconnect = "Recon1",
}
ERROR =
{
SocketError = 1000,
CustomError = 0xdeadbeef,
FPNoErr = 0,
FPAccessDenied = -5000,
FPAuthContinue = -5001,
FPBadUAM = -5002,
FPBadVersNum = -5003,
FPBitmapErr = - 5004,
FPCantMove = - 5005,
FPEOFErr = -5009,
FPItemNotFound = -5012,
FPLockErr = -5013,
FPMiscErr = -5014,
FPObjectExists = -5017,
FPObjectNotFound = -5018,
FPParamErr = -5019,
FPUserNotAuth = -5023,
FPCallNotSupported = -5024,
}
MAP_ID =
{
UserIDToName = 1,
GroupIDToName = 2,
UserIDToUTF8Name = 3,
GroupIDToUTF8Name = 4,
UserUUIDToUTF8Name = 5,
GroupUUIDToUTF8Name = 6
}
MAP_NAME =
{
NameToUserID = 1,
NameToGroupID = 2,
UTF8NameToUserID = 3,
UTF8NameToGroupID = 4,
UTF8NameToUserUUID = 5,
UTF8NameToGroupUUID = 6
}
SERVERFLAGS =
{
CopyFile = 0x01,
ChangeablePasswords = 0x02,
NoPasswordSaving = 0x04,
ServerMessages = 0x08,
ServerSignature = 0x10,
TCPoverIP = 0x20,
ServerNotifications = 0x40,
Reconnect = 0x80,
OpenDirectory = 0x100,
UTF8ServerName = 0x200,
UUIDs = 0x400,
SuperClient = 0x8000
}
local ERROR_MSG = {
[ERROR.FPAccessDenied]="Access Denied",
[ERROR.FPAuthContinue]="Authentication is not yet complete",
[ERROR.FPBadUAM]="Specified UAM is unknown",
[ERROR.FPBadVersNum]="Server does not support the specified AFP version",
[ERROR.FPBitmapErr]="Attempt was made to get or set a parameter that cannot be obtained or set with this command, or a required bitmap is null",
[ERROR.FPCantMove]="Attempt was made to move a directory into one of its descendent directories.",
[ERROR.FPEOFErr]="No more matches or end of fork reached.",
[ERROR.FPLockErr]="Some or all of the requested range is locked by another user; a lock range conflict exists.",
[ERROR.FPMiscErr]="Non-AFP error occurred.",
[ERROR.FPObjectNotFound]="Input parameters do not point to an existing directory, file, or volume.",
[ERROR.FPParamErr]="Parameter error.",
[ERROR.FPObjectExists] = "File or directory already exists.",
[ERROR.FPUserNotAuth] = "UAM failed (the specified old password doesn't match); no user is logged in yet for the specified session; authentication failed; password is incorrect.",
[ERROR.FPItemNotFound] = "Specified APPL mapping, comment, or icon was not found in the Desktop database; specified ID is unknown.",
[ERROR.FPCallNotSupported] = "Server does not support this command.",
}
-- Check if all the bits in flag are set in bitmap.
local function flag_is_set(bitmap, flag)
return bit.band(bitmap, flag) == flag
end
-- Response class returned by all functions in Proto
Response = {
new = function(self,o)
o = o or {}
setmetatable(o, self)
self.__index = self
return o
end,
--- Sets the error code
--
-- @param code number containing the error code
setErrorCode = function( self, code )
self.error_code = code
end,
--- Gets the error code
--
-- @return code number containing the error code
getErrorCode = function( self )
return self.error_code
end,
--- Gets the error message
--
-- @return msg string containing the error
getErrorMessage = function(self)
if self.error_msg then
return self.error_msg
else
return ERROR_MSG[self.error_code] or ("Unknown error (%d) occured"):format(self.error_code)
end
end,
--- Sets the error message
--
-- @param msg string containing the error message
setErrorMessage = function(self, msg)
self.error_code = ERROR.CustomError
self.error_msg = msg
end,
--- Sets the result
--
-- @param result result to set
setResult = function(self, result)
self.result = result
end,
--- Get the result
--
-- @return result
getResult = function(self)
return self.result
end,
--- Sets the packet
setPacket = function( self, packet )
self.packet = packet
end,
getPacket = function( self )
return self.packet
end,
--- Gets the packet data
getPacketData = function(self)
return self.packet.data
end,
--- Gets the packet header
getPacketHeader = function(self)
return self.packet.header
end,
}
--- Proto class containing all AFP specific code
--
-- For more details consult:
-- http://developer.apple.com/mac/library/documentation/Networking/Reference/AFP_Reference/Reference/reference.html
Proto = {
RequestId = 1,
new = function(self,o)
o = o or {}
setmetatable(o, self)
self.__index = self
return o
end,
setSocket = function(self, socket)
self.socket = socket
end,
--- Creates an AFP packet
--
-- @param command number should be one of the commands in the COMMAND table
-- @param data_offset number holding the offset to the data
-- @param data the actual data of the request
create_fp_packet = function( self, command, data_offset, data )
local reserved = 0
local data = data or ""
local data_len = data:len()
local header = bin.pack("CC>SIII", FLAGS.Request, command, self.RequestId, data_offset, data_len, reserved)
self.RequestId = self.RequestId + 1
return header .. data
end,
--- Parses the FP header (first 16-bytes of packet)
--
-- @param packet string containing the raw packet
-- @return table with header data containing <code>flags</code>, <code>command</code>,
-- <code>request_id</code>, <code>error_code</code>, <code>length</code> and <code>reserved</code> fields
parse_fp_header = function( self, packet )
local header = {}
local pos
pos, header.flags, header.command, header.request_id = bin.unpack( "CC>S", packet )
pos, header.error_code, header.length, header.reserved = bin.unpack( ">i>II", packet:sub(5) )
if header.error_code ~= 0 then
header.error_msg = ERROR_MSG[header.error_code] or ("Unknown error: %d"):format(header.error_code)
header.error_msg = "ERROR: " .. header.error_msg
end
header.raw = packet:sub(1,16)
return header
end,
--- Reads a AFP packet of the socket
--
-- @return Response object
read_fp_packet = function( self )
local packet = {}
local buf = ""
local status, response
status, buf = self.socket:receive_bytes(16)
if ( not status ) then
response = Response:new()
response:setErrorCode(ERROR.SocketError)
response:setErrorMessage(buf)
return response
end
packet.header = self:parse_fp_header( buf )
while buf:len() < packet.header.length + packet.header.raw:len() do
local tmp
status, tmp = self.socket:receive_bytes( packet.header.length + 16 - buf:len() )
if not status then
response = Response:new()
response:setErrorCode(ERROR.SocketError)
response:setErrorMessage(buf)
return response
end
buf = buf .. tmp
end
packet.data = buf:len() > 16 and buf:sub( 17 ) or ""
response = Response:new()
response:setErrorCode(packet.header.error_code)
response:setPacket(packet)
return response
end,
--- Sends the raw packet over the socket
--
-- @param packet containing the raw data
-- @return Response object
send_fp_packet = function( self, packet )
return self.socket:send(packet)
end,
--- Sends an DSIOpenSession request to the server and handles the response
--
-- @return Response object
dsi_open_session = function( self, host, port )
local data_offset = 0
local option = 0x01 -- Attention Quantum
local option_len = 4
local quantum = 1024
local data, packet, status
data = bin.pack( "CCI", option, option_len, quantum )
packet = self:create_fp_packet( REQUEST.OpenSession, data_offset, data )
self:send_fp_packet( packet )
return self:read_fp_packet()
end,
--- Sends an DSICloseSession request to the server and handles the response
dsi_close_session = function( self )
local data_offset = 0
local option = 0x01 -- Attention Quantum
local option_len = 4
local quantum = 1024
local data, packet, status
data = ""
packet = self:create_fp_packet( REQUEST.CloseSession, data_offset, data )
self:send_fp_packet( packet )
end,
-- Sends an FPCopyFile request to the server
--
-- @param src_vol number containing the ID of the src file volume
-- @param srd_did number containing the directory id of the src file
-- @param src_path string containingt the file path/name of the src file
-- @param dst_vol number containing the ID of the dst file volume
-- @param dst_did number containing the id of the dest. directory
-- @param dst_path string containing the dest path (can be nil or "")
-- @param new_name string containign the new name of the destination
-- @return Response object
fp_copy_file = function(self, src_vol, src_did, src_path, dst_vol, dst_did, dst_path, new_name )
local pad, data_offset = 0, 0
local unicode_names, unicode_hint = 0x03, 0x08000103
local data, packet, response
-- make sure we have empty names rather than nil values
local dst_path = dst_path or ""
local src_path = src_path or ""
local new_name = new_name or ""
data = bin.pack(">CCSISI", COMMAND.FPCopyFile, pad, src_vol, src_did, dst_vol, dst_did )
data = data .. bin.pack(">CIP", unicode_names, unicode_hint, src_path )
data = data .. bin.pack(">CIP", unicode_names, unicode_hint, dst_path )
data = data .. bin.pack(">CIP", unicode_names, unicode_hint, new_name )
packet = self:create_fp_packet( REQUEST.Command, data_offset, data )
self:send_fp_packet( packet )
return self:read_fp_packet()
end,
--- Sends an GetStatus DSI request (which is basically a FPGetSrvrInfo
-- AFP request) to the server and handles the response
--
-- @return status (true or false)
-- @return table with server information (if status is true) or error string
-- (if status is false)
fp_get_server_info = function(self)
local packet
local data_offset = 0
local pad = 0
local response, result = {}, {}
local offsets = {}
local pos
local _
local status
local data = bin.pack("CC", COMMAND.FPGetSrvrInfo, 0)
packet = self:create_fp_packet(REQUEST.GetStatus, data_offset, data)
self:send_fp_packet(packet)
response = self:read_fp_packet()
if response:getErrorCode() ~= ERROR.FPNoErr then
return response
end
packet = response.packet
-- parse and store the offsets in the 'header'
pos, offsets.machine_type, offsets.afp_version_count,
offsets.uam_count, offsets.volume_icon_and_mask
= bin.unpack(">SSSS", packet.data, pos)
-- the flags are directly in the 'header'
result.flags = {}
pos, result.flags.raw = bin.unpack(">S", packet.data, pos)
-- the short server name is stored directly in the 'header' as
-- well
pos, result.server_name = bin.unpack("p", packet.data, pos)
-- Server offset should begin at an even boundary see link below
-- http://developer.apple.com/mac/library/documentation/Networking/Reference/AFP_Reference/Reference/reference.html#//apple_ref/doc/uid/TP40003548-CH3-CHDIEGED
if (pos + 1) % 2 ~= 0 then
pos = pos + 1
end
-- and some more offsets
pos, offsets.server_signature, offsets.network_addresses_count,
offsets.directory_names_count, offsets.utf8_server_name
= bin.unpack(">SSSS", packet.data, pos)
-- this sets up all the server flaqs in the response table as booleans
result.flags.SuperClient = flag_is_set(result.flags.raw, SERVERFLAGS.SuperClient)
result.flags.UUIDs = flag_is_set(result.flags.raw, SERVERFLAGS.UUIDs)
result.flags.UTF8ServerName = flag_is_set(result.flags.raw, SERVERFLAGS.UTF8ServerName)
result.flags.OpenDirectory = flag_is_set(result.flags.raw, SERVERFLAGS.OpenDirectory)
result.flags.Reconnect = flag_is_set(result.flags.raw, SERVERFLAGS.Reconnect)
result.flags.ServerNotifications = flag_is_set(result.flags.raw, SERVERFLAGS.ServerNotifications)
result.flags.TCPoverIP = flag_is_set(result.flags.raw, SERVERFLAGS.TCPoverIP)
result.flags.ServerSignature = flag_is_set(result.flags.raw, SERVERFLAGS.ServerSignature)
result.flags.ServerMessages = flag_is_set(result.flags.raw, SERVERFLAGS.ServerMessages)
result.flags.NoPasswordSaving = flag_is_set(result.flags.raw, SERVERFLAGS.NoPasswordSaving)
result.flags.ChangeablePasswords = flag_is_set(result.flags.raw, SERVERFLAGS.ChangeablePasswords)
result.flags.CopyFile = flag_is_set(result.flags.raw, SERVERFLAGS.CopyFile)
-- store the machine type
_, result.machine_type = bin.unpack("p", packet.data, offsets.machine_type + 1)
-- this tells us the number of afp versions supported
pos, result.afp_version_count = bin.unpack("C", packet.data, offsets.afp_version_count + 1)
-- now we loop through them all, storing for the response
result.afp_versions = {}
for i = 1,result.afp_version_count do
pos, _ = bin.unpack("p", packet.data, pos)
table.insert(result.afp_versions, _)
end
-- same idea as the afp versions here
pos, result.uam_count = bin.unpack("C", packet.data, offsets.uam_count + 1)
result.uams = {}
for i = 1,result.uam_count do
pos, _ = bin.unpack("p", packet.data, pos)
table.insert(result.uams, _)
end
-- volume_icon_and_mask would normally be parsed out here,
-- however the apple docs say it is deprecated in Mac OS X, so
-- we don't bother with it
-- server signature is 16 bytes
result.server_signature = string.sub(packet.data, offsets.server_signature + 1, offsets.server_signature + 16)
-- this is the same idea as afp_version and uam above
pos, result.network_addresses_count = bin.unpack("C", packet.data, offsets.network_addresses_count + 1)
result.network_addresses = {}
-- gets a little complicated in here, basically each entry has
-- a length byte, a tag byte, and then the data. We parse
-- differently based on the tag
for i = 1, result.network_addresses_count do
local length
local tag
pos, length = bin.unpack("C", packet.data, pos)
pos, tag = bin.unpack("C", packet.data, pos)
if tag == 0x00 then
-- reserved, shouldn't ever come up, maybe this should
-- return an error? maybe not, lets just ignore this
elseif tag == 0x01 then
-- four byte ip
local octet = {}
pos, octet[1], octet[2], octet[3], octet[4] = bin.unpack("CCCC", packet.data, pos)
table.insert(result.network_addresses, string.format("%d.%d.%d.%d", octet[1], octet[2], octet[3], octet[4]))
elseif tag == 0x02 then
-- four byte ip and two byte port
local octet = {}
local port
pos, octet[1], octet[2], octet[3], octet[4], port = bin.unpack(">CCCCS", packet.data, pos)
table.insert(result.network_addresses, string.format("%d.%d.%d.%d:%d", octet[1], octet[2], octet[3], octet[4], port))
elseif tag == 0x03 then
-- ddp address (two byte network, one byte
-- node, one byte socket) not tested, anyone
-- use ddp anymore?
local network
local node
local socket
pos, network = bin.unpack(">S", packet.data, pos)
pos, node = bin.unpack("C", packet.data, pos)
pos, socket = bin.unpack("C", packet.data, pos)
table.insert(result.network_addresses, string.format("ddp %d.%d:%d", network, node, socket))
elseif tag == 0x04 then
-- dns name (string)
local temp
pos, temp = bin.unpack("z", packet.data:sub(1,pos+length-3), pos)
table.insert(result.network_addresses, temp)
elseif tag == 0x05 then
-- four byte ip and two byte port, client
-- should use ssh. not tested, should work as it
-- is the same as tag 0x02
local octet = {}
local port
pos, octet[1], octet[2], octet[3], octet[4], port = bin.unpack(">CCCCS", packet.data, pos)
table.insert(result.network_addresses, string.format("ssh://%d.%d.%d.%d:%d", octet[1], octet[2], octet[3], octet[4], port))
elseif tag == 0x06 then
-- 16 byte ipv6
-- not tested, but should work (next tag is
-- tested)
local octet = {}
local j
local addr
for j = 1, 8 do
pos, octet[j] = bin.unpack(">S", packet.data, pos)
end
for j = 1, 7 do
addr = addr .. string.format("%04x:", octet[j])
end
addr = addr .. string.format("%04x", octet[8])
table.insert(result.network_addresses, addr)
elseif tag == 0x07 then
-- 16 byte ipv6 and two byte port
local octet = {}
local port
local j
local addr
for j = 1, 8 do
pos, octet[j] = bin.unpack(">S", packet.data, pos)
end
pos, port = bin.unpack(">S", packet.data, pos)
addr = "["
for j = 1, 7 do
addr = addr .. string.format("%04x:", octet[j])
end
addr = addr .. string.format("%04x]:%d", octet[8], port)
table.insert(result.network_addresses, addr)
end
end
-- same idea as the others here
pos, result.directory_names_count = bin.unpack("C", packet.data, offsets.directory_names_count + 1)
result.directory_names = {}
for i = 1, result.directory_names_count do
local dirname
pos, dirname = bin.unpack("p", packet.data, pos)
table.insert(result.directory_names, dirname)
end
-- only one utf8 server name. note this string has a two-byte length.
_, result.utf8_server_name = bin.unpack(">P", packet.data, offsets.utf8_server_name + 1)
response.result = result
return response
end,
--- Sends an FPGetUserInfo AFP request to the server and handles the response
--
-- @return response object with the following result <code>user_bitmap</code> and
-- <code>uid</code> fields
fp_get_user_info = function( self )
local packet, pos, status, response
local data_offset = 0
local flags = 1 -- Default User
local uid = 0
local bitmap = USER_BITMAP.UserId
local result = {}
local data = bin.pack( "CCI>S", COMMAND.FPGetUserInfo, flags, uid, bitmap )
packet = self:create_fp_packet( REQUEST.Command, data_offset, data )
self:send_fp_packet( packet )
response = self:read_fp_packet()
if response:getErrorCode() ~= ERROR.FPNoErr then
return response
end
pos, response.result.user_bitmap, response.result.uid = bin.unpack(">S>I", packet.data)
return response
end,
--- Sends an FPGetSrvrParms AFP request to the server and handles the response
--
-- @return response object with the following result <code>server_time</code>,
-- <code>vol_count</code>, <code>volumes</code> fields
fp_get_srvr_parms = function(self)
local packet, status, data
local data_offset = 0
local response = {}
local pos = 0
local parms = {}
data = bin.pack("CC", COMMAND.FPGetSrvParms, 0)
packet = self:create_fp_packet( REQUEST.Command, data_offset, data )
self:send_fp_packet( packet )
response = self:read_fp_packet()
if response:getErrorCode() ~= ERROR.FPNoErr then
return response
end
data = response:getPacketData()
pos, parms.server_time, parms.vol_count = bin.unpack("IC", data)
-- we should now be at the leading zero preceeding the first volume name
-- next is the length of the volume name, move pos there
pos = pos + 1
parms.volumes = {}
for i=1, parms.vol_count do
local _, vol_len = bin.unpack("C", data:sub(pos))
local volume_name = data:sub(pos + 1, pos + 1 + vol_len)
pos = pos + vol_len + 2
table.insert(parms.volumes, string.format("%s", volume_name) )
end
response:setResult(parms)
return response
end,
--- Sends an FPLogin request to the server and handles the response
--
-- This function currently only supports the 3.1 through 3.3 protocol versions
-- It currently supports the following authentication methods:
-- o No User Authent
-- o DHCAST128
--
-- The DHCAST128 UAM should work against most servers even though it's
-- superceeded by the DHX2 UAM.
--
-- @param afp_version string (AFP3.3|AFP3.2|AFP3.1)
-- @param uam string containing authentication information
-- @return Response object
fp_login = function( self, afp_version, uam, username, password, options )
local packet, status, data
local data_offset = 0
local status, response
if not HAVE_SSL then
response = Response:new()
response:setErrorMessage("OpenSSL not available, aborting ...")
return response
end
-- currently we only support AFP3.3
if afp_version == nil or ( afp_version ~= "AFP3.3" and afp_version ~= "AFP3.2" and afp_version ~= "AFP3.1" ) then
response = Response:new()
response:setErrorMessage("Incorrect AFP version")
return response
end
if ( uam == "No User Authent" ) then
data = bin.pack( "CCACA", COMMAND.FPLogin, afp_version:len(), afp_version, uam:len(), uam )
packet = self:create_fp_packet( REQUEST.Command, data_offset, data )
self:send_fp_packet( packet )
return self:read_fp_packet( )
elseif( uam == "DHCAST128" ) then
local dhx_s2civ, dhx_c2civ = 'CJalbert', 'LWallace'
local p, g, Ra, Ma, Mb, K, nonce
local EncData, PlainText, K_bin, auth_response
local _, Id
local username = username or ""
local password = password or ""
if ( bit.mod(username:len(), 2) == 0 ) then
username = username .. string.char(0)
end
p = openssl.bignum_hex2bn("BA2873DFB06057D43F2024744CEEE75B")
g = openssl.bignum_dec2bn("7")
Ra = openssl.bignum_hex2bn("86F6D3C0B0D63E4B11F113A2F9F19E3BBBF803F28D30087A1450536BE979FD42")
Ma = openssl.bignum_mod_exp(g, Ra, p)
data = bin.pack( "CpppA", COMMAND.FPLogin, afp_version, uam, username, openssl.bignum_bn2bin(Ma) )
packet = self:create_fp_packet( REQUEST.Command, data_offset, data )
self:send_fp_packet( packet )
response = self:read_fp_packet( )
if ( response:getErrorCode() ~= ERROR.FPAuthContinue ) then
return response
end
if ( response.packet.header.length ~= 50 ) then
response:setErrorMessage("LoginContinue packet contained invalid data")
return response
end
_, Id, Mb, EncData = bin.unpack(">SH16A32", response.packet.data )
Mb = openssl.bignum_hex2bn( Mb )
K = openssl.bignum_mod_exp (Mb, Ra, p)
K_bin = openssl.bignum_bn2bin(K)
nonce = openssl.decrypt("cast5-cbc", K_bin, dhx_s2civ, EncData, false ):sub(1,16)
nonce = openssl.bignum_add( openssl.bignum_bin2bn(nonce), openssl.bignum_dec2bn("1") )
PlainText = openssl.bignum_bn2bin(nonce) .. Util.ZeroPad(password, 64)
auth_response = openssl.encrypt( "cast5-cbc", K_bin, dhx_c2civ, PlainText, true)
data = bin.pack( "CC>SA", COMMAND.FPLoginCont, 0, Id, auth_response )
packet = self:create_fp_packet( REQUEST.Command, data_offset, data )
self:send_fp_packet( packet )
response = self:read_fp_packet( )
if ( response:getErrorCode() ~= ERROR.FPNoErr ) then
return response
end
return response
end
response:setErrorMessage("Unsupported uam: " .. uam or "nil")
return response
end,
-- Terminates sessions and frees server resources established by FPLoginand FPLoginExt.
--
-- @return response object
fp_logout = function( self )
local packet, data, response
local data_offset, pad = 0, 0
data = bin.pack("CC", COMMAND.FPLogout, pad)
packet = self:create_fp_packet( REQUEST.Command, data_offset, data )
self:send_fp_packet( packet )
return self:read_fp_packet( )
end,
--- Sends an FPOpenVol request to the server and handles the response
--
-- @param bitmap number bitmask of volume information to request
-- @param volume_name string containing the volume name to query
-- @return response object with the following result <code>bitmap</code> and
-- <code>volume_id</code> fields
fp_open_vol = function( self, bitmap, volume_name )
local packet, status, pos, data
local data_offset, pad = 0, 0
local response, volume = {}, {}
data = bin.pack("CC>SCA", COMMAND.FPOpenVol, pad, bitmap, volume_name:len(), volume_name )
packet = self:create_fp_packet( REQUEST.Command, data_offset, data )
self:send_fp_packet( packet )
response = self:read_fp_packet()
if response:getErrorCode() ~= ERROR.FPNoErr then
return response
end
pos, volume.bitmap, volume.volume_id = bin.unpack(">S>S", response.packet.data)
response:setResult(volume)
return response
end,
--- Sends an FPGetFileDirParms request to the server and handles the response
--
-- @param volume_id number containing the id of the volume to query
-- @param did number containing the id of the directory to query