sunet/ftpd.scm

1018 lines
31 KiB
Scheme
Raw Normal View History

2000-09-26 11:32:01 -04:00
; RFC 959 ftp daemon
; Mike Sperber <sperber@informatik.uni-tuebingen.de>
; Copyright (c) 1998 Michael Sperber.
; It doesn't support the following desirable things:
;
; - Login by user; this requires crypt which scsh doesn't have
; - RESTART support
; - Banners from files on CWD
; - Lots of fancy stuff like ProFTPD, http://www.proftpd.org/
2001-04-27 12:19:34 -04:00
(define-record session
control-input-port
control-output-port
anonymous-home
2001-04-27 12:19:34 -04:00
(logged-in? #f)
(authenticated? #f)
(anonymous? #f)
(root-directory #f)
(current-directory "")
(to-be-renamed #f)
(reverse-replies '())
(reply-code #f) ; the last one wins
(type 'ascii) ; PLEASE set this to bin
(data-socket #f)
(passive-socket #f))
(define session (make-fluid #f))
(define (make-fluid-selector selector)
(lambda () (selector (fluid session))))
(define (make-fluid-setter setter)
(lambda (value)
(setter (fluid session) value)))
(define session-control-input-port (make-fluid-selector session:control-input-port))
(define session-control-output-port (make-fluid-selector session:control-output-port))
(define session-anonymous-home (make-fluid-selector session:anonymous-home))
2001-04-27 12:19:34 -04:00
(define session-logged-in? (make-fluid-selector session:logged-in?))
(define session-authenticated? (make-fluid-selector session:authenticated?))
(define session-anonymous? (make-fluid-selector session:anonymous?))
(define session-root-directory (make-fluid-selector session:root-directory))
(define session-current-directory (make-fluid-selector session:current-directory))
(define session-to-be-renamed (make-fluid-selector session:to-be-renamed))
(define session-reverse-replies (make-fluid-selector session:reverse-replies))
(define session-reply-code (make-fluid-selector session:reply-code))
(define session-type (make-fluid-selector session:type))
(define session-data-socket (make-fluid-selector session:data-socket))
(define session-passive-socket (make-fluid-selector session:passive-socket))
(define set-session-control-input-port
(make-fluid-setter set-session:control-input-port))
(define set-session-control-output-port
(make-fluid-setter set-session:control-output-port))
(define set-session-logged-in? (make-fluid-setter set-session:logged-in?))
(define set-session-authenticated? (make-fluid-setter set-session:authenticated?))
(define set-session-anonymous? (make-fluid-setter set-session:anonymous?))
(define set-session-root-directory (make-fluid-setter set-session:root-directory))
(define set-session-current-directory (make-fluid-setter set-session:current-directory))
(define set-session-to-be-renamed (make-fluid-setter set-session:to-be-renamed))
(define set-session-reverse-replies (make-fluid-setter set-session:reverse-replies))
(define set-session-reply-code (make-fluid-setter set-session:reply-code))
(define set-session-type (make-fluid-setter set-session:type))
(define set-session-data-socket (make-fluid-setter set-session:data-socket))
(define set-session-passive-socket (make-fluid-setter set-session:passive-socket))
2001-06-29 11:10:28 -04:00
;;; LOG -------------------------------------------------------
(define (log level format-message . args)
(syslog level
2001-06-29 11:10:28 -04:00
(apply format #f (string-append "(thread ~D) " format-message)
(thread-uid (current-thread)) args)))
;;; CONVERTERS ------------------------------------------------
(define (protocol-family->string protocol-family)
(cond ((= protocol-family protocol-family/unspecified)
"unspecified")
((= protocol-family protocol-family/internet)
"internet")
((= protocol-family protocol-family/unix)
"unix")
(else "unknown")))
(define (socket->string socket)
2001-06-29 11:10:28 -04:00
(format #f "family: ~A, ~&local address: ~A, ~&remote address: ~A, ~&input-port ~A, ~&output-port ~A"
(protocol-family->string (socket:family socket))
(socket-address->string (socket-local-address socket))
(socket-address->string (socket-remote-address socket))
(socket:inport socket)
(socket:outport socket)))
2001-06-29 11:10:28 -04:00
(define (socket-address->string socket-address)
(call-with-values
(lambda () (socket-address->internet-address socket-address))
(lambda (host-address service-port)
(format #f "~A:~A"
(format-internet-host-address host-address)
(format-port service-port)))))
2001-06-29 11:10:28 -04:00
;;; ftpd -------------------------------------------------------
(define (ftpd anonymous-home . maybe-port)
2000-09-26 11:32:01 -04:00
(let ((port (optional maybe-port 21)))
2001-06-26 09:15:56 -04:00
(with-syslog-destination
"ftpd"
(syslog-options log-pid)
(syslog-facility daemon)
syslog-mask-all
(lambda ()
(log (syslog-level notice)
"starting on port ~D with ~S as anonymous home"
port (expand-file-name anonymous-home (cwd)))
(bind-listen-accept-loop
protocol-family/internet
(lambda (socket address)
(log (syslog-level info)
2001-06-26 09:15:56 -04:00
"new connection with ~S"
(socket-address->string address))
(log (syslog-level debug)
"got connection with socket ~S and address ~S"
(socket->string socket)
(socket-address->string address))
(set-ftp-socket-options! socket)
(spawn
(lambda ()
(handle-connection (socket:inport socket)
(socket:outport socket)
(file-name-as-directory anonymous-home))
(call-with-current-continuation
(lambda (exit)
(with-errno-handler*
(lambda (errno packet)
(cond
;; I dunno why SHUTDOWN-SOCKET can die this way, but it
;; can and does
((= errno errno/notconn)
(log (syslog-level warning)
"socket not connected any more - exiting thread")
(exit 'fick-dich-ins-knie))))
(lambda ()
(log (syslog-level debug)
"shutting down socket ~S"
(socket->string socket))
(shutdown-socket socket shutdown/sends+receives)))))
(log (syslog-level info)
"closing socket")
(close-socket socket))
(socket-address->string address))) ; use remote address as thread-name
port)))))
2000-09-26 11:32:01 -04:00
(define (ftpd-inetd anonymous-home)
2001-06-26 09:15:56 -04:00
(with-syslog-destination
"ftpd"
(syslog-option log-pid)
(syslog-facility daemon)
#f
(lambda ()
(log (syslog-level info)
"new connection on current input- and output-port with ~S as anonymous home"
(expand-file-name anonymous-home (cwd)))
(log (syslog-level debug)
"new connection on current input-port ~A and current output-port ~A with ~S as anonymous home"
(current-input-port)
(current-output-port)
(expand-file-name anonymous-home (cwd)))
(handle-connection (current-input-port)
(current-output-port)
(file-name-as-directory anonymous-home)))))
2000-09-26 11:32:01 -04:00
(define (set-ftp-socket-options! socket)
;; If the client closes the connection, we won't lose when we try to
;; close the socket by trying to flush the output buffer.
(set-port-buffering (socket:outport socket) bufpol/none)
2000-09-26 11:32:01 -04:00
(set-socket-option socket level/socket socket/oob-inline #t))
(define (handle-connection input-port output-port anonymous-home)
2001-06-29 11:10:28 -04:00
(log (syslog-level debug)
"handling connection with input-port ~A, outputport ~A and home ~A"
input-port
output-port
anonymous-home)
2000-09-26 11:32:01 -04:00
(call-with-current-continuation
(lambda (escape)
(with-handler
(lambda (condition more)
2001-06-29 11:10:28 -04:00
(log (syslog-level debug)
"hit error condition ~A -- exiting"
(condition-type condition))
2001-04-27 12:19:34 -04:00
(display condition (current-error-port))
2000-09-26 11:32:01 -04:00
(escape 'fick-dich-ins-knie))
(lambda ()
(let-fluid session (make-session input-port output-port
anonymous-home)
2001-04-27 12:19:34 -04:00
(lambda ()
(display-banner)
(handle-commands))))))))
2000-09-26 11:32:01 -04:00
(define (display-banner)
2001-06-29 11:10:28 -04:00
(log (syslog-level debug)
"displaying banner")
2000-09-26 11:32:01 -04:00
(register-reply! 220
(string-append
"Scheme Untergrund ftp server ("
*ftpd-version*
") ready.")))
(define-condition-type 'ftpd-quit '())
(define ftpd-quit? (condition-predicate 'ftpd-quit))
(define-condition-type 'ftpd-irregular-quit '())
(define ftpd-irregular-quit? (condition-predicate 'ftpd-irregular-quit))
2000-09-26 11:32:01 -04:00
(define-condition-type 'ftpd-error '())
(define ftpd-error? (condition-predicate 'ftpd-error))
2001-04-27 12:19:34 -04:00
2000-09-26 11:32:01 -04:00
(define (handle-commands)
2001-06-29 11:10:28 -04:00
(log (syslog-level debug) "handling commands")
(call-with-current-continuation
(lambda (exit)
(with-handler
(lambda (condition more)
(if (ftpd-quit? condition)
2001-06-29 11:10:28 -04:00
(begin
(log (syslog-level debug) "quitting")
(with-handler
(lambda (condition ignore)
(more))
(lambda ()
(write-replies)
(exit 'fick-dich-ins-knie))))
(more)))
(lambda ()
2001-06-29 11:10:28 -04:00
(log (syslog-level debug)
"starting write-accept-loop")
(let loop ()
(write-replies)
(accept-command)
(loop)))))))
2000-09-26 11:32:01 -04:00
(define (accept-command)
2001-06-29 11:10:28 -04:00
(let* ((timeout-seconds 90)
(command-line (read-crlf-line-timeout (session-control-input-port)
#f
(* 1000 timeout-seconds);timeout
500))) ; max interval
(log (syslog-level debug)
"Command line: ~A"
command-line)
(cond ((eq? command-line 'timeout)
(log (syslog-level debug)
"hit timelimit (~D seconds) -- closing control connection."
timeout-seconds)
(register-reply!
421
(format #f "Timeout (~D seconds): closing control connection."
timeout-seconds)
(signal 'ftpd-quit)))
2001-04-27 12:19:34 -04:00
(else
(call-with-values
(lambda () (parse-command-line command-line))
(lambda (command arg)
(handle-command command arg)))))))
2000-09-26 11:32:01 -04:00
(define (handle-command command arg)
2001-06-29 11:10:28 -04:00
(log (syslog-level debug)
"handling command ~S with argument ~S"
command arg)
2000-09-26 11:32:01 -04:00
(call-with-current-continuation
(lambda (escape)
(with-handler
(lambda (condition more)
(cond
((error? condition)
2001-06-29 11:10:28 -04:00
(log (syslog-level notice)
"internal error occured: ~S -- replying (451) and escaping"
condition)
2000-09-26 11:32:01 -04:00
(register-reply! 451
(format #f "Internal error: ~S"
(condition-stuff condition)))
(escape 'fick-dich-ins-knie))
((ftpd-error? condition)
2001-06-29 11:10:28 -04:00
(log (syslog-level notice)
2001-07-07 11:19:52 -04:00
"ftpd error occured: ~S -- escaping"
; must this occur everytime CDUP is called in ftp-root-path?
2001-06-29 11:10:28 -04:00
(condition-stuff condition))
2000-09-26 11:32:01 -04:00
(escape 'fick-dich-ins-knie))
(else
(more))))
(lambda ()
(with-errno-handler*
(lambda (errno packet)
2001-06-29 11:10:28 -04:00
(log (syslog-level notice)
"unix error occured: ~S -- replying (451) and escaping"
(car packet))
2000-09-26 11:32:01 -04:00
(register-reply! 451
(format #f "Unix error: ~A." (car packet)))
(escape 'fick-dich-ins-knie))
(lambda ()
(dispatch-command command arg))))))))
(define (dispatch-command command arg)
2001-06-29 11:10:28 -04:00
(log (syslog-level debug)
"dispatching command ~S with argument ~S"
command arg)
2000-09-26 11:32:01 -04:00
(cond
((assoc command *command-alist*)
=> (lambda (pair)
2001-07-07 11:19:52 -04:00
(log (syslog-level debug)
"command ~S was found in command-list and is executed with argument ~S"
(car pair) arg)
2000-09-26 11:32:01 -04:00
((cdr pair) arg)))
(else
2001-07-07 11:19:52 -04:00
(log (syslog-level debug) "rejecting unknown command ~S (500) (argument: ~S)"
command arg)
2000-09-26 11:32:01 -04:00
(register-reply! 500
(string-append
(format #f "Unknown command: \"~A\"" command)
(if (string=? "" arg)
"."
(format #f " (argument(s) \"~A\")." arg)))))))
(define (handle-user name)
2001-07-07 11:19:52 -04:00
(log (syslog-level debug) "handling USER-command with name ~S"
name)
2000-09-26 11:32:01 -04:00
(cond
2001-04-27 12:19:34 -04:00
((session-logged-in?)
2001-07-07 11:19:52 -04:00
(log (syslog-level debug) "user ~S is already logged in (230)"
name)
2000-09-26 11:32:01 -04:00
(register-reply! 230
"You are already logged in."))
((or (string=? "anonymous" name)
(string=? "ftp" name))
(handle-user-anonymous))
(else
2001-07-07 11:19:52 -04:00
(log (syslog-level debug) "rejecting non-anonymous login (530)")
2000-09-26 11:32:01 -04:00
(register-reply! 530
"Only anonymous logins allowed."))))
(define (handle-user-anonymous)
2001-07-07 11:19:52 -04:00
(log (syslog-level debug) "anonymous user login (230)")
(set-session-logged-in? #t)
(set-session-authenticated? #t)
(set-session-anonymous? #t)
(set-session-root-directory (session-anonymous-home))
(set-session-current-directory "")
(register-reply! 230 "Anonymous user logged in."))
2000-09-26 11:32:01 -04:00
(define (handle-pass password)
2001-07-07 11:19:52 -04:00
(log (syslog-level debug) "handling PASS-command with password ~S"
password)
2000-09-26 11:32:01 -04:00
(cond
2001-04-27 12:19:34 -04:00
((not (session-logged-in?))
2001-07-07 11:19:52 -04:00
(log (syslog-level debug) "Rejecting password as user is not logged in yet. (530)")
2000-09-26 11:32:01 -04:00
(register-reply! 530 "You have not logged in yet."))
2001-04-27 12:19:34 -04:00
((session-anonymous?)
2001-07-07 11:19:52 -04:00
(log (syslog-level debug) "Accepting password as user is logged in. (200)")
2000-09-26 11:32:01 -04:00
(register-reply! 200 "Thank you."))
(else
2001-07-07 11:19:52 -04:00
(log (syslog-level notice) "Reached unreachable case-branch while handling password! (502)")
2000-09-26 11:32:01 -04:00
(register-reply! 502 "This can't happen."))))
(define (handle-quit foo)
2001-07-07 11:19:52 -04:00
(log (syslog-level debug) "handling QUIT-command (221)")
2000-09-26 11:32:01 -04:00
(register-reply! 221 "Goodbye! Au revoir! Auf Wiedersehen!")
(signal 'ftpd-quit))
(define (handle-syst foo)
2001-07-07 11:19:52 -04:00
(log (syslog-level debug) "telling system type (215)")
2000-09-26 11:32:01 -04:00
(register-reply! 215 "UNIX Type: L8"))
(define (handle-cwd path)
2001-07-07 11:19:52 -04:00
(log (syslog-level debug) "handling CWD-command with ~S as path-argument"
path)
2000-09-26 11:32:01 -04:00
(ensure-authenticated-login)
(let ((current-directory (assemble-path path)))
(with-errno-handler*
(lambda (errno packet)
2001-07-07 11:19:52 -04:00
(log (syslog-level debug)
"can't change to directory \"~A\": ~A (550)"
path (car packet))
2000-09-26 11:32:01 -04:00
(signal-error! 550
(format #f "Can't change directory to \"~A\": ~A."
path
(car packet))))
(lambda ()
(with-cwd*
(file-name-as-directory
2001-04-27 12:19:34 -04:00
(string-append (session-root-directory) current-directory))
2000-09-26 11:32:01 -04:00
(lambda () ; I hate gratuitous syntax
2001-07-07 11:19:52 -04:00
(log (syslog-level debug)
"changing current directory to \"/~A\" (250)"
current-directory)
2001-04-27 12:19:34 -04:00
(set-session-current-directory current-directory)
2000-09-26 11:32:01 -04:00
(register-reply! 250
(format #f "Current directory changed to \"/~A\"."
current-directory))))))))
(define (handle-cdup foo)
2001-07-07 11:19:52 -04:00
(log (syslog-level debug) "handling CDUP-command as \"CWD ..\"")
2000-09-26 11:32:01 -04:00
(handle-cwd ".."))
(define (handle-pwd foo)
2001-07-07 11:19:52 -04:00
(log (syslog-level debug) "handling PWD-command")
2000-09-26 11:32:01 -04:00
(ensure-authenticated-login)
2001-07-07 11:19:52 -04:00
(log (syslog-level debug) "replying \"/~A\" as current directory (257)"
(session-current-directory))
2000-09-26 11:32:01 -04:00
(register-reply! 257
(format #f "Current directory is \"/~A\"."
2001-04-27 12:19:34 -04:00
(session-current-directory))))
2000-09-26 11:32:01 -04:00
(define (make-file-action-handler error-format-string action)
(lambda (path)
(ensure-authenticated-login)
(if (string=? "" path)
(signal-error! 500 "No argument."))
2001-04-27 12:19:34 -04:00
(let ((full-path (string-append (session-root-directory)
2000-09-26 11:32:01 -04:00
(assemble-path path))))
(with-errno-handler*
(lambda (errno packet)
(signal-error! 550
(format #f error-format-string
path (car packet))))
(lambda ()
(action path full-path))))))
(define handle-dele
(make-file-action-handler
"Could not delete \"~A\": ~A."
(lambda (path full-path)
(delete-file full-path)
(register-reply! 250 (format #f "Deleted \"~A\"." path)))))
(define handle-mdtm
(make-file-action-handler
"Could not get info on \"~A\": ~A."
(lambda (path full-path)
(let* ((info (file-info full-path))
(the-date (date (file-info:mtime info) 0)))
(register-reply! 213
(format-date "~Y~m~d~H~M~S" the-date))))))
(define handle-mkd
(make-file-action-handler
"Could not make directory \"~A\": ~A."
(lambda (path full-path)
(create-directory full-path #o755)
(register-reply! 257
(format #f "Created directory \"~A\"." path)))))
(define handle-rmd
(make-file-action-handler
"Could not remove directory \"~A\": ~A."
(lambda (path full-path)
(delete-directory full-path)
(register-reply! 250
(format #f "Deleted directory \"~A\"." path)))))
(define handle-rnfr
(make-file-action-handler
"Could not get info on file \"~A\": ~A."
(lambda (path full-path)
(file-info full-path)
(register-reply! 350 "RNFR accepted. Gimme a RNTO next.")
2001-04-27 12:19:34 -04:00
(set-session-to-be-renamed full-path))))
2000-09-26 11:32:01 -04:00
(define (handle-rnto path)
(ensure-authenticated-login)
2001-04-27 12:19:34 -04:00
(if (not (session-to-be-renamed))
2000-09-26 11:32:01 -04:00
(signal-error! 503 "Need RNFR before RNTO."))
(if (string=? "" path)
(signal-error! 500 "No argument."))
2001-04-27 12:19:34 -04:00
(let ((full-path (string-append (session-root-directory)
2000-09-26 11:32:01 -04:00
(assemble-path path))))
(if (file-exists? full-path)
(signal-error!
550
(format #f "Rename failed---\"~A\" already exists or is protected."
path)))
(with-errno-handler*
(lambda (errno packet)
(signal-error! 550
(format #f "Could not rename: ~A." path)))
(lambda ()
2001-06-20 05:22:59 -04:00
(rename-file (session-to-be-renamed) full-path)
2000-09-26 11:32:01 -04:00
(register-reply! 250 "File renamed.")
2001-04-27 12:19:34 -04:00
(set-session-to-be-renamed #f)))))
2000-09-26 11:32:01 -04:00
(define handle-size
(make-file-action-handler
"Could not get info on file \"~A\": ~A."
(lambda (path full-path)
(let ((info (file-info full-path)))
(if (not (eq? 'regular (file-info:type info)))
(signal-error! 550
(format #f "\"~A\" is not a regular file."
path)))
(register-reply! 213 (number->string (file-info:size info)))))))
(define (handle-type arg)
(cond
((string-ci=? "A" arg)
2001-04-27 12:19:34 -04:00
(set-session-type 'ascii))
2000-09-26 11:32:01 -04:00
((string-ci=? "I" arg)
2001-04-27 12:19:34 -04:00
(set-session-type 'image))
2000-09-26 11:32:01 -04:00
((string-ci=? "L8" arg)
2001-04-27 12:19:34 -04:00
(set-session-type 'image))
2000-09-26 11:32:01 -04:00
(else
(signal-error! 504
(format #f "Unknown TYPE: ~A." arg))))
(register-reply! 200
(format #f "TYPE is now ~A."
2001-04-27 12:19:34 -04:00
(case (session-type)
2000-09-26 11:32:01 -04:00
((ascii) "ASCII")
((image) "8-bit binary")
(else "somethin' weird, man")))))
(define (handle-mode arg)
(cond
((string=? "" arg)
(register-reply! 500
"No arguments. Not to worry---I'd ignore them anyway."))
((string-ci=? "S" arg)
(register-reply! 200 "Using stream mode to transfer files."))
(else
(register-reply! 504 (format #f "Mode \"~A\" is not supported."
arg)))))
(define (handle-stru arg)
(cond
((string=? "" arg)
(register-reply! 500
"No arguments. Not to worry---I'd ignore them anyway."))
((string-ci=? "F" arg)
(register-reply! 200 "Using file structure to transfer files."))
(else
(register-reply! 504
(format #f "File structure \"~A\" is not supported."
arg)))))
(define (handle-noop arg)
(register-reply! 200 "Done nothing, but successfully."))
(define *port-arg-regexp*
(make-regexp "^([0-9]+),([0-9]+),([0-9]+),([0-9]+),([0-9]+),([0-9]+)$"))
(define (parse-port-arg string)
(cond
((regexp-exec *port-arg-regexp* string)
=> (lambda (match)
(let ((components
(map (lambda (match-index)
(string->number
(match:substring match match-index)))
'(1 2 3 4 5 6))))
(if (any? (lambda (component)
(> component 255))
components)
(signal-error! 501
"Invalid arguments to PORT."))
(apply
(lambda (a1 a2 a3 a4 p1 p2)
(values (+ (arithmetic-shift a1 24)
(arithmetic-shift a2 16)
(arithmetic-shift a3 8)
a4)
2000-09-26 11:32:01 -04:00
(+ (arithmetic-shift p1 8)
p2)))
components))))
(else
(signal-error! 500
"Syntax error in argument to PORT."))))
(define (handle-port stuff)
(ensure-authenticated-login)
(maybe-close-data-connection)
(call-with-values
(lambda () (parse-port-arg stuff))
(lambda (address port)
(let ((socket (create-socket protocol-family/internet
socket-type/stream)))
(set-socket-option socket level/socket socket/reuse-address #t)
(connect-socket socket
(internet-address->socket-address
address port))
2001-04-27 12:19:34 -04:00
(set-session-data-socket socket)
2000-09-26 11:32:01 -04:00
(register-reply! 200
(format #f "Connected to ~A, port ~A."
(format-internet-host-address address)
port))))))
(define (handle-pasv stuff)
(ensure-authenticated-login)
(maybe-close-data-connection)
(let ((socket (create-socket protocol-family/internet
socket-type/stream)))
(set-socket-option socket level/socket socket/reuse-address #t)
(bind-socket socket
(internet-address->socket-address (this-host-address)
2000-09-26 11:32:01 -04:00
0))
(listen-socket socket 1)
(let ((address (socket-local-address socket)))
(call-with-values
(lambda () (socket-address->internet-address address))
(lambda (host-address port)
2001-04-27 12:19:34 -04:00
(set-session-passive-socket socket)
2000-09-26 11:32:01 -04:00
(register-reply! 227
(format #f "Passive mode OK (~A,~A)"
(format-internet-host-address host-address ",")
(format-port port))))))))
(define (this-host-address)
(call-with-values
(lambda ()
(socket-address->internet-address
(socket-local-address (port->socket (session-control-input-port)
protocol-family/internet))))
(lambda (host-address control-port)
host-address)))
2000-09-26 11:32:01 -04:00
(define (format-internet-host-address address . maybe-separator)
(define (extract shift)
(number->string
(bitwise-and (arithmetic-shift address (- shift))
255)))
(let ((separator (optional maybe-separator ".")))
(string-append
(extract 24) separator (extract 16) separator
(extract 8) separator (extract 0))))
2000-09-26 11:32:01 -04:00
(define (format-port port)
(string-append
(number->string (bitwise-and (arithmetic-shift port -8) 255))
","
(number->string (bitwise-and port 255))))
(define (handle-nlst arg)
(handle-listing arg '()))
(define (handle-list arg)
(handle-listing arg '(long)))
(define (handle-listing arg preset-flags)
(ensure-authenticated-login)
(with-data-connection
(lambda ()
(let ((args (split-arguments arg)))
(call-with-values
(lambda ()
(partition-list
(lambda (arg)
(and (not (string=? "" arg))
(char=? #\- (string-ref arg 0))))
args))
(lambda (flag-args rest-args)
(if (and (not (null? rest-args))
(not (null? (cdr rest-args))))
(signal-error! 501 "More than one path argument."))
(let ((path (if (null? rest-args)
""
(car rest-args)))
(flags (arguments->ls-flags flag-args)))
(if (not flags)
(signal-error! 501 "Invalid flag(s)."))
(generate-listing path (append preset-flags flags)))))))))
; Note this doesn't call ENSURE-AUTHENTICATED-LOGIN or
; ENSURE-DATA-CONNECTION.
(define (generate-listing path flags)
2001-04-27 12:19:34 -04:00
(let ((full-path (string-append (session-root-directory)
2000-09-26 11:32:01 -04:00
(assemble-path path))))
(with-errno-handler*
(lambda (errno packet)
(signal-error! 451
(format #f "Can't access directory at ~A: ~A."
path
(car packet))))
(lambda ()
(with-cwd*
(file-name-directory full-path)
(lambda ()
(let ((nondir (file-name-nondirectory full-path)))
(ls flags
(list
;; work around OLIN BUG
(if (string=? nondir "")
"."
2001-06-09 05:33:37 -04:00
nondir))
(socket:outport (session-data-socket))))))))))
2000-09-26 11:32:01 -04:00
(define (handle-abor foo)
(maybe-close-data-connection)
(register-reply! 226 "Closing data connection."))
(define (handle-retr path)
(ensure-authenticated-login)
2001-04-27 12:19:34 -04:00
(let ((full-path (string-append (session-root-directory)
2000-09-26 11:32:01 -04:00
(assemble-path path))))
(with-fatal-error-handler* ; CALL-WITH-INPUT-FILE doesn't go through ERRNO
(lambda (condition more)
(signal-error! 550
(format #f "Can't open \"~A\" for reading."
path)))
(lambda ()
(let ((info (file-info full-path)))
(if (not (eq? 'regular (file-info:type info)))
(signal-error! 450
(format #f "\"~A\" is not a regular file."
path)))
(call-with-input-file full-path
(lambda (file-port)
(with-data-connection
(lambda ()
2001-04-27 12:19:34 -04:00
(case (session-type)
2000-09-26 11:32:01 -04:00
((image)
(copy-port->port-binary
file-port
2001-04-27 12:19:34 -04:00
(socket:outport (session-data-socket))))
2000-09-26 11:32:01 -04:00
((ascii)
(copy-port->port-ascii
file-port
2001-04-27 12:19:34 -04:00
(socket:outport (session-data-socket))))))))))))))
2000-09-26 11:32:01 -04:00
(define (handle-stor path)
(ensure-authenticated-login)
2001-04-27 12:19:34 -04:00
(let ((full-path (string-append (session-root-directory)
2000-09-26 11:32:01 -04:00
(assemble-path path))))
(with-fatal-error-handler*
(lambda (condition more)
(signal-error! 550
(format #f "Can't open \"~A\" for writing."
path)))
(lambda ()
(call-with-output-file full-path
(lambda (file-port)
(with-data-connection
(lambda ()
2001-04-27 12:19:34 -04:00
(case (session-type)
2000-09-26 11:32:01 -04:00
((image)
(copy-port->port-binary
2001-04-27 12:19:34 -04:00
(socket:inport (session-data-socket))
2000-09-26 11:32:01 -04:00
file-port))
((ascii)
(copy-ascii-port->port
2001-04-27 12:19:34 -04:00
(socket:inport (session-data-socket))
2000-09-26 11:32:01 -04:00
file-port)))))))))))
(define (assemble-path path)
2001-07-07 11:19:52 -04:00
(log (syslog-level debug) "assembling path ~S"
path)
2000-09-26 11:32:01 -04:00
(let* ((interim-path
(if (not (file-name-rooted? path))
2001-04-27 12:19:34 -04:00
(string-append (file-name-as-directory
(session-current-directory))
2000-09-26 11:32:01 -04:00
path)
path))
(complete-path (if (file-name-rooted? interim-path)
(file-name-sans-rooted interim-path)
interim-path)))
2001-07-07 11:19:52 -04:00
(log (syslog-level debug) "path ~S assembled to ~S"
path complete-path)
2000-09-26 11:32:01 -04:00
(cond
((normalize-path complete-path)
=> (lambda (assembled-path) assembled-path))
(else
2001-07-07 11:19:52 -04:00
(log (syslog-level debug)
"invalid pathname -- tried to pass root directory (501)")
2000-09-26 11:32:01 -04:00
(signal-error! 501 "Invalid pathname")))))
(define (ensure-authenticated-login)
2001-04-27 12:19:34 -04:00
(if (or (not (session-logged-in?))
2001-07-07 11:19:52 -04:00
(not (session-authenticated?)))
(begin
(log (syslog-level debug) "login authentication failed - user is not logged in (530)")
(signal-error! 530 "You're not logged in yet."))
(log (syslog-level debug) "authenticated login ensured.")))
2000-09-26 11:32:01 -04:00
(define (with-data-connection thunk)
(dynamic-wind ensure-data-connection
thunk
maybe-close-data-connection))
(define *window-size* 51200)
(define (ensure-data-connection)
2001-04-27 12:19:34 -04:00
(if (and (not (session-data-socket))
(not (session-passive-socket)))
2000-09-26 11:32:01 -04:00
(signal-error! 425 "No data connection."))
2001-04-27 12:19:34 -04:00
(if (session-passive-socket)
2000-09-26 11:32:01 -04:00
(call-with-values
2001-04-27 12:19:34 -04:00
(lambda () (accept-connection (session-passive-socket)))
2000-09-26 11:32:01 -04:00
(lambda (socket socket-address)
2001-04-27 12:19:34 -04:00
(set-session-data-socket socket))))
2000-09-26 11:32:01 -04:00
(register-reply! 150 "Opening data connection.")
(write-replies)
2001-04-27 12:19:34 -04:00
(set-socket-option (session-data-socket) level/socket
2000-09-26 11:32:01 -04:00
socket/send-buffer *window-size*)
2001-04-27 12:19:34 -04:00
(set-socket-option (session-data-socket) level/socket
2000-09-26 11:32:01 -04:00
socket/receive-buffer *window-size*))
(define (maybe-close-data-connection)
2001-04-27 12:19:34 -04:00
(if (or (session-data-socket) (session-passive-socket))
2000-09-26 11:32:01 -04:00
(close-data-connection)))
(define (close-data-connection)
2001-04-27 12:19:34 -04:00
(if (session-data-socket)
(close-socket (session-data-socket)))
(if (session-passive-socket)
(close-socket (session-passive-socket)))
2000-09-26 11:32:01 -04:00
(register-reply! 226 "Closing data connection.")
2001-04-27 12:19:34 -04:00
(set-session-data-socket #f)
(set-session-passive-socket #f))
2000-09-26 11:32:01 -04:00
(define *command-alist*
(list
(cons "NOOP" handle-noop)
(cons "USER" handle-user)
(cons "PASS" handle-pass)
(cons "QUIT" handle-quit)
(cons "SYST" handle-syst)
(cons "CWD" handle-cwd)
(cons "PWD" handle-pwd)
(cons "CDUP" handle-cdup)
(cons "DELE" handle-dele)
(cons "MDTM" handle-mdtm)
(cons "MKD" handle-mkd)
(cons "RMD" handle-rmd)
(cons "RNFR" handle-rnfr)
(cons "RNTO" handle-rnto)
(cons "SIZE" handle-size)
(cons "TYPE" handle-type)
(cons "MODE" handle-mode)
(cons "STRU" handle-stru)
(cons "PORT" handle-port)
(cons "PASV" handle-pasv)
(cons "NLST" handle-nlst)
(cons "LIST" handle-list)
(cons "RETR" handle-retr)
(cons "STOR" handle-stor)
(cons "ABOR" handle-abor)))
(define (parse-command-line line)
(if (eof-object? line) ; Netscape does this
(signal 'ftpd-irregular-quit)
2000-09-26 11:32:01 -04:00
(let* ((line (trim-spaces line))
2001-04-27 12:19:34 -04:00
(split-position (string-index line #\space)))
2000-09-26 11:32:01 -04:00
(if split-position
(values (upcase-string (substring line 0 split-position))
(trim-spaces (substring line
(+ 1 split-position)
(string-length line))))
(values (upcase-string line) "")))))
; Path names
; This removes all internal ..'s from a path.
; NORMALIZE-PATH returns #f if PATH points to a parent directory.
(define (normalize-path path)
(let loop ((components (split-file-name (simplify-file-name path)))
(reverse-result '()))
(cond
((null? components)
(path-list->file-name (reverse reverse-result)))
((null? (cdr components))
(if (string=? ".." (car components))
#f
(path-list->file-name
(reverse (cons (car components) reverse-result)))))
((string=? ".." (cadr components))
(loop (cddr components) reverse-result))
(else
(loop (cdr components) (cons (car components) reverse-result))))))
(define (file-name-rooted? file-name)
(and (not (string=? "" file-name))
(char=? #\/ (string-ref file-name 0))))
(define (file-name-sans-rooted file-name)
(substring file-name 1 (string-length file-name)))
(define split-arguments
(infix-splitter (make-regexp " +")))
2000-09-26 11:32:01 -04:00
; Reply handling
; Replies must be synchronous with requests and actions. Therefore,
; they are queued on generation via REGISTER-REPLY!. The messages are
; printed via WRITE-REPLIES. For the nature of the replies, see RFC
; 959.
(define (write-replies)
2001-04-27 12:19:34 -04:00
(if (not (null? (session-reverse-replies)))
(let loop ((messages (reverse (session-reverse-replies))))
2000-09-26 11:32:01 -04:00
(if (null? (cdr messages))
(write-final-reply (car messages))
(begin
(write-nonfinal-reply (car messages))
(loop (cdr messages))))))
2001-04-27 12:19:34 -04:00
(set-session-reverse-replies '()))
2000-09-26 11:32:01 -04:00
(define (write-final-reply line)
2001-04-27 12:19:34 -04:00
(format (session-control-output-port) "~D ~A" (session-reply-code) line)
;; (format #t "Reply: ~D ~A~%" (session-reply-code) line)
(write-crlf (session-control-output-port)))
2000-09-26 11:32:01 -04:00
(define (write-nonfinal-reply line)
2001-04-27 12:19:34 -04:00
(format (session-control-output-port) "~D-~A" (session-reply-code) line)
;; (format #t "Reply: ~D-~A~%" (session-reply-code) line)
(write-crlf (session-control-output-port)))
2000-09-26 11:32:01 -04:00
(define (signal-error! code message)
(register-reply! code message)
(signal 'ftpd-error))
(define (register-reply! code message)
2001-04-27 12:19:34 -04:00
(set-session-reverse-replies
(cons message (session-reverse-replies)))
(set-session-reply-code code))
2000-09-26 11:32:01 -04:00
; Version
2001-07-07 11:19:52 -04:00
(define *ftpd-version* "$Revision: 1.20 $")
2000-09-26 11:32:01 -04:00
(define (copy-port->port-binary input-port output-port)
(let ((buffer (make-string *window-size*)))
(let loop ()
(cond
((read-string! buffer input-port)
=> (lambda (length)
(write-string buffer output-port 0 length)
(loop))))))
(force-output output-port))
(define (copy-port->port-ascii input-port output-port)
(let loop ()
(let ((line (read-line input-port 'concat)))
(if (not (eof-object? line))
(let ((length (string-length line)))
(cond
((zero? length)
'fick-dich-ins-knie)
((char=? #\newline (string-ref line (- length 1)))
(write-string line output-port 0 (- length 1))
(write-crlf output-port))
(else
(write-string line output-port)))
(loop)))))
(force-output output-port))
(define (copy-ascii-port->port input-port output-port)
(let loop ()
2001-04-27 12:19:34 -04:00
(let* ((line (read-crlf-line input-port
#f
90000 ; timeout
500)) ; max interval
2000-09-26 11:32:01 -04:00
(length (string-length line)))
(if (not (eof-object? line))
(begin
(write-string line output-port 0 length)
(newline output-port)
(loop)))))
(force-output output-port))
; Utilities
(define (optional maybe-arg default-exp)
(cond
((null? maybe-arg) default-exp)
((null? (cdr maybe-arg)) (car maybe-arg))
(else (error "too many optional arguments" maybe-arg))))