1 ! Copyright (C) 2008 Doug Coleman.
2 ! See http://factorcode.org/license.txt for BSD license.
3 USING: combinators.short-circuit accessors combinators io
4 io.encodings.8-bit io.encodings io.encodings.binary
5 io.encodings.utf8 io.files io.files.info io.directories
6 io.pathnames io.sockets kernel math.parser namespaces make
7 sequences ftp io.launcher.unix.parser unicode.case splitting
8 assocs classes io.servers.connection destructors calendar
9 io.timeouts io.streams.duplex threads continuations math
10 concurrency.promises byte-arrays io.backend tools.hexdump
11 tools.files io.streams.string ;
14 TUPLE: ftp-client url mode state command-promise user password ;
16 : <ftp-client> ( url -- ftp-client )
22 : ftp-server-directory ( -- str )
23 \ ftp-server-directory get-global "resource:temp" or
26 TUPLE: ftp-command raw tokenized ;
28 : <ftp-command> ( -- obj )
33 : <ftp-get> ( path -- obj )
39 : <ftp-put> ( path -- obj )
45 C: <ftp-list> ftp-list
47 : read-command ( -- ftp-command )
49 [ >>raw ] [ tokenize-command >>tokenized ] bi ;
51 : (send-response) ( n string separator -- )
52 rot number>string write write ftp-send ;
54 : send-response ( ftp-response -- )
55 [ n>> ] [ strings>> ] bi
56 [ but-last-slice [ "-" (send-response) ] with each ]
57 [ first " " (send-response) ] 2bi ;
59 : server-response ( n string -- )
61 swap add-response-line
65 : ftp-error ( string -- )
66 500 "Unrecognized command: " rot append server-response ;
69 220 "Welcome to " host-name append server-response ;
71 : anonymous-only ( -- )
72 530 "This FTP server is anonymous only." server-response ;
74 : handle-QUIT ( obj -- )
75 drop 221 "Goodbye." server-response ;
77 : handle-USER ( ftp-command -- )
79 tokenized>> second client get (>>user)
80 331 "Please specify the password." server-response
82 2drop "bad USER" ftp-error
85 : handle-PASS ( ftp-command -- )
87 tokenized>> second client get (>>password)
88 230 "Login successful" server-response
90 2drop "PASS error" ftp-error
93 ERROR: type-error type ;
95 : parse-type ( string -- string' )
97 { "IMAGE" [ "Binary" ] }
102 : handle-TYPE ( obj -- )
104 tokenized>> second parse-type
105 200 "Switching to " rot " mode" 3append server-response
107 2drop "TYPE is binary only" ftp-error
110 : random-local-server ( -- server )
111 remote-address get class new 0 >>port binary <server> ;
113 : port>bytes ( port -- hi lo )
114 [ -8 shift ] keep [ HEX: ff bitand ] bi@ ;
116 : handle-PWD ( obj -- )
118 257 current-directory get "\"" "\"" surround server-response ;
120 : handle-SYST ( obj -- )
122 215 "UNIX Type: L8" server-response ;
124 : if-command-promise ( quot -- )
125 [ client get command-promise>> ] dip
126 [ "Establish an active or passive connection first" ftp-error ] if* ;
128 : handle-STOR ( obj -- )
131 [ [ <ftp-put> ] dip fulfill ] if-command-promise
137 ! : handle-EPRT ( obj -- )
138 ! tokenized>> second "|" split harvest ;
140 : start-directory ( -- )
141 150 "Here comes the directory listing." server-response ;
143 : finish-directory ( -- )
144 226 "Directory send OK." server-response ;
146 GENERIC: service-command ( stream obj -- )
148 M: ftp-list service-command ( stream obj -- )
152 [ current-directory get directory. ] with-string-writer string-lines
153 harvest [ ftp-send ] each
157 : transfer-outgoing-file ( path -- )
158 150 "Opening BINARY mode data connection for "
161 " " swap file-info size>> number>string
162 "(" " bytes)." surround append
163 ] bi 3append server-response ;
165 : transfer-incoming-file ( path -- )
166 150 "Opening BINARY mode data connection for " rot append
169 : finish-file-transfer ( -- )
170 226 "File send OK." server-response ;
172 M: ftp-get service-command ( stream obj -- )
175 [ transfer-outgoing-file ]
176 [ binary <file-reader> swap stream-copy ] bi
179 3drop "File transfer failed" ftp-error
182 M: ftp-put service-command ( stream obj -- )
185 [ transfer-incoming-file ]
186 [ binary <file-writer> stream-copy ] bi
189 3drop "File transfer failed" ftp-error
192 : passive-loop ( server -- )
196 30 seconds over set-timeout
198 client get command-promise>>
199 30 seconds ?promise-timeout
202 [ client get f >>command-promise drop ]
206 : handle-LIST ( obj -- )
208 [ [ <ftp-list> ] dip fulfill ] if-command-promise ;
210 : handle-SIZE ( obj -- )
212 tokenized>> second file-info size>>
213 213 swap number>string server-response
216 550 "Could not get file size" server-response
219 : handle-RETR ( obj -- )
220 [ tokenized>> second <ftp-get> swap fulfill ]
221 curry if-command-promise ;
223 : expect-connection ( -- port )
225 client get <promise> >>command-promise drop
226 [ [ passive-loop ] curry in-thread ]
227 [ addr>> port>> ] bi ;
229 : handle-PASV ( obj -- )
230 drop client get passive >>mode drop
233 "Entering Passive Mode (127,0,0,1," %
234 port>bytes [ number>string ] bi@ "," glue %
236 ] "" make 227 swap server-response ;
238 : handle-EPSV ( obj -- )
240 client get command-promise>> [
241 "You already have a passive stream" ftp-error
243 229 "Entering Extended Passive Mode (|||"
244 expect-connection number>string
245 "|)" 3append server-response
248 ! LPRT 6,16,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,242,186
249 ! : handle-LPRT ( obj -- ) tokenized>> "," split ;
251 ERROR: not-a-directory ;
252 ERROR: no-permissions ;
254 : handle-CWD ( obj -- )
256 tokenized>> second dup normalize-path
257 dup ftp-server-directory head? [
261 file-info directory? [
262 set-current-directory
263 250 "Directory successully changed." server-response
269 550 "Failed to change directory." server-response
272 : unrecognized-command ( obj -- ) raw>> ftp-error ;
274 : handle-client-loop ( -- )
276 USE: prettyprint global [ dup . flush ] bind
278 [ tokenize-command >>tokenized ] bi
279 dup tokenized>> first >upper {
280 { "USER" [ handle-USER t ] }
281 { "PASS" [ handle-PASS t ] }
282 { "ACCT" [ drop "ACCT unimplemented" ftp-error t ] }
283 { "CWD" [ handle-CWD t ] }
288 ! { "REIN" [ drop client get reset-ftp-client t ] }
289 { "QUIT" [ handle-QUIT f ] }
291 ! { "PORT" [ ] } ! TODO
292 { "PASV" [ handle-PASV t ] }
294 { "TYPE" [ handle-TYPE t ] }
299 { "STOR" [ handle-STOR t ] }
301 { "RETR" [ handle-RETR t ] }
302 { "LIST" [ handle-LIST t ] }
303 { "SIZE" [ handle-SIZE t ] }
308 ! { "DELE" [ handle-DELE t ] }
309 ! { "RMD" [ handle-RMD t ] }
310 ! ! { "XRMD" [ handle-XRMD t ] }
311 ! { "MKD" [ handle-MKD t ] }
312 { "PWD" [ handle-PWD t ] }
315 { "SYST" [ handle-SYST t ] }
322 ! { "EPRT" [ handle-EPRT ] }
323 ! { "LPRT" [ handle-LPRT ] }
324 { "EPSV" [ handle-EPSV t ] }
325 ! { "LPSV" [ drop handle-LPSV t ] }
326 [ drop unrecognized-command t ]
327 } case [ handle-client-loop ] when ;
329 TUPLE: ftp-server < threaded-server ;
331 M: ftp-server handle-client* ( server -- )
334 ftp-server-directory [
335 host-name <ftp-client> client set
336 send-banner handle-client-loop
340 : <ftp-server> ( port -- server )
341 ftp-server new-threaded-server
348 <ftp-server> start-server ;
350 : ftpd-main ( -- ) 2100 ftpd ;
354 ! sudo tcpdump -i en1 -A -s 10000 tcp port 21