factor/basis/ftp/server/server.factor

357 lines
9.2 KiB
Factor

! Copyright (C) 2008 Doug Coleman.
! See http://factorcode.org/license.txt for BSD license.
USING: accessors assocs byte-arrays calendar classes combinators
combinators.short-circuit concurrency.promises continuations
destructors ftp io io.backend io.directories io.encodings
io.encodings.binary tools.files io.encodings.utf8 io.files
io.files.info io.pathnames io.servers.connection io.sockets
io.streams.duplex io.streams.string io.timeouts kernel make math
math.bitwise math.parser namespaces sequences splitting threads
unicode.case logging calendar.format strings io.files.links
io.files.types io.encodings.8-bit.latin1 simple-tokenizer ;
IN: ftp.server
SYMBOL: server
SYMBOL: client
TUPLE: ftp-server < threaded-server { serving-directory string } ;
TUPLE: ftp-client user password extra-connection ;
TUPLE: ftp-command raw tokenized ;
: <ftp-command> ( str -- obj )
dup \ <ftp-command> DEBUG log-message
ftp-command new
over >>raw
swap tokenize >>tokenized ;
TUPLE: ftp-get path ;
: <ftp-get> ( path -- obj )
ftp-get new
swap >>path ;
TUPLE: ftp-put path ;
: <ftp-put> ( path -- obj )
ftp-put new
swap >>path ;
TUPLE: ftp-list ;
C: <ftp-list> ftp-list
TUPLE: ftp-disconnect ;
C: <ftp-disconnect> ftp-disconnect
: (send-response) ( n string separator -- )
[ number>string write ] 2dip write ftp-send ;
: send-response ( ftp-response -- )
[ n>> ] [ strings>> ] bi
[ but-last-slice [ "-" (send-response) ] with each ]
[ first " " (send-response) ] 2bi ;
: server-response ( string n -- )
2dup number>string swap ":" glue \ server-response DEBUG log-message
<ftp-response>
swap >>n
swap add-response-line
send-response ;
: serving? ( path -- ? )
resolve-symlinks server get serving-directory>> head? ;
: can-serve-directory? ( path -- ? )
{ [ exists? ] [ file-info directory? ] [ serving? ] } 1&& ;
: can-serve-file? ( path -- ? )
{
[ exists? ]
[ file-info type>> +regular-file+ = ]
[ serving? ]
} 1&& ;
: ftp-error ( string -- ) 500 server-response ;
: ftp-unimplemented ( string -- ) 502 server-response ;
: send-banner ( -- )
"Welcome to " host-name append 220 server-response ;
: anonymous-only ( -- )
"This FTP server is anonymous only." 530 server-response ;
: handle-QUIT ( obj -- )
drop "Goodbye." 221 server-response ;
: handle-USER ( ftp-command -- )
[
tokenized>> second client get user<<
"Please specify the password." 331 server-response
] [
2drop "bad USER" ftp-error
] recover ;
: handle-PASS ( ftp-command -- )
[
tokenized>> second client get password<<
"Login successful" 230 server-response
] [
2drop "PASS error" ftp-error
] recover ;
ERROR: type-error type ;
: parse-type ( string -- string' )
>upper {
{ "IMAGE" [ "Binary" ] }
{ "I" [ "Binary" ] }
[ type-error ]
} case ;
: handle-TYPE ( obj -- )
[
tokenized>> second parse-type
"Switching to " " mode" surround 200 server-response
] [
2drop "TYPE is binary only" ftp-error
] recover ;
: random-local-server ( -- server )
remote-address get class new 0 >>port binary <server> ;
: port>bytes ( port -- hi lo )
[ -8 shift ] keep [ 8 bits ] bi@ ;
: handle-PWD ( obj -- )
drop
current-directory get "\"" dup surround 257 server-response ;
: handle-SYST ( obj -- )
drop
"UNIX Type: L8" 215 server-response ;
: start-directory ( -- )
"Here comes the directory listing." 150 server-response ;
: transfer-outgoing-file ( path -- )
[ "Opening BINARY mode data connection for " ] dip
[ file-name ] [
file-info size>> number>string
"(" " bytes)." surround
] bi " " glue append 150 server-response ;
: transfer-incoming-file ( path -- )
"Opening BINARY mode data connection for " prepend
150 server-response ;
: finish-file-transfer ( -- )
"File send OK." 226 server-response ;
GENERIC: handle-passive-command ( stream obj -- )
: passive-loop ( server -- )
[
[
|dispose
30 seconds over set-timeout
accept drop &dispose
client get extra-connection>>
30 seconds ?promise-timeout
handle-passive-command
]
[ client get f >>extra-connection drop ]
[ drop ] cleanup
] with-destructors ;
: finish-directory ( -- )
"Directory send OK." 226 server-response ;
M: ftp-list handle-passive-command ( stream obj -- )
drop
start-directory [
utf8 encode-output
[ current-directory get directory. ] with-string-writer string-lines
harvest [ ftp-send ] each
] with-output-stream finish-directory ;
M: ftp-get handle-passive-command ( stream obj -- )
[
path>>
[ transfer-outgoing-file ]
[ binary <file-reader> swap stream-copy ] bi
finish-file-transfer
] [
3drop "File transfer failed" ftp-error
] recover ;
M: ftp-put handle-passive-command ( stream obj -- )
[
path>>
[ transfer-incoming-file ]
[ binary <file-writer> stream-copy ] bi
finish-file-transfer
] [
3drop "File transfer failed" ftp-error
] recover ;
M: ftp-disconnect handle-passive-command ( stream obj -- )
drop dispose ;
: fulfill-client ( obj -- )
client get extra-connection>> [
fulfill
] [
drop
"Establish an active or passive connection first" ftp-error
] if* ;
: handle-STOR ( obj -- )
tokenized>> second
dup can-serve-file? [
<ftp-put> fulfill-client
] [
drop
<ftp-disconnect> fulfill-client
] if ;
: handle-LIST ( obj -- )
drop current-directory get
can-serve-directory? [
<ftp-list> fulfill-client
] [
<ftp-disconnect> fulfill-client
] if ;
: not-a-plain-file ( path -- )
": not a plain file." append ftp-error ;
: handle-RETR ( obj -- )
tokenized>> second
dup can-serve-file? [
<ftp-get> fulfill-client
] [
not-a-plain-file
<ftp-disconnect> fulfill-client
] if ;
: handle-SIZE ( obj -- )
tokenized>> second
dup can-serve-file? [
file-info size>> number>string 213 server-response
] [
not-a-plain-file
] if ;
: expect-connection ( -- port )
<promise> client get extra-connection<<
random-local-server
[ [ passive-loop ] curry in-thread ]
[ addr>> port>> ] bi ;
: handle-PASV ( obj -- )
drop
expect-connection port>bytes [ number>string ] bi@ "," glue
"Entering Passive Mode (127,0,0,1," ")" surround
221 server-response ;
: handle-EPSV ( obj -- )
drop
client get f >>extra-connection drop
expect-connection number>string
"Entering Extended Passive Mode (|||" "|)" surround
229 server-response ;
: handle-MDTM ( obj -- )
tokenized>> 1 swap ?nth [
dup file-info dup directory? [
drop not-a-plain-file
] [
nip
modified>> timestamp>mdtm
213 server-response
] if
] [
"" not-a-plain-file
] if* ;
ERROR: not-a-directory ;
ERROR: no-directory-permissions ;
: directory-change-success ( -- )
"Directory successully changed." 250 server-response ;
: directory-change-failed ( -- )
"Failed to change directory." 553 server-response ;
: handle-CWD ( obj -- )
tokenized>> 1 swap ?nth [
dup can-serve-directory? [
set-current-directory
directory-change-success
] [
drop
directory-change-failed
] if
] [
directory-change-success
] if* ;
: unrecognized-command ( obj -- )
raw>> "Unrecognized command: " prepend ftp-error ;
: client-loop-dispatch ( str/f -- ? )
dup tokenized>> first >upper {
{ "QUIT" [ handle-QUIT f ] }
{ "USER" [ handle-USER t ] }
{ "PASS" [ handle-PASS t ] }
{ "SYST" [ handle-SYST t ] }
{ "ACCT" [ drop "ACCT unimplemented" ftp-unimplemented t ] }
{ "PWD" [ handle-PWD t ] }
{ "TYPE" [ handle-TYPE t ] }
{ "CWD" [ handle-CWD t ] }
{ "PASV" [ handle-PASV t ] }
{ "EPSV" [ handle-EPSV t ] }
{ "LIST" [ handle-LIST t ] }
{ "STOR" [ handle-STOR t ] }
{ "RETR" [ handle-RETR t ] }
{ "SIZE" [ handle-SIZE t ] }
{ "MDTM" [ handle-MDTM t ] }
[ drop unrecognized-command t ]
} case ;
: read-command ( -- ftp-command/f )
readln [ f ] [ <ftp-command> ] if-empty ;
: handle-client-loop ( -- )
read-command [
client-loop-dispatch
[ handle-client-loop ] when
] when* ;
: serve-directory ( server -- )
serving-directory>> [
send-banner
handle-client-loop
] with-directory ;
M: ftp-server handle-client* ( server -- )
[
"New client" \ handle-client* DEBUG log-message
ftp-client new client set
[ server set ] [ serve-directory ] bi
] with-destructors ;
: <ftp-server> ( directory port -- server )
latin1 ftp-server new-threaded-server
swap >>insecure
swap resolve-symlinks >>serving-directory
"ftp.server" >>name
5 minutes >>timeout ;
: ftpd ( directory port -- )
<ftp-server> start-server ;
: ftpd-main ( path -- ) 2100 ftpd ;
MAIN: ftpd-main
! sudo tcpdump -i en1 -A -s 10000 tcp port 21