; RFC 959 ftp daemon ; Mike Sperber ; 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/ (define (ftpd . maybe-port) (let ((port (optional maybe-port 21))) (bind-listen-accept-loop protocol-family/internet (lambda (socket address) (set-ftp-socket-options! socket) (fork (lambda () (handle-connection (socket:inport socket) (socket:outport socket)) (reap-zombies) (shutdown-socket socket shutdown/sends+receives)))) port))) (define (ftpd-inetd) (handle-connection (current-input-port) (current-output-port))) (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) (set-socket-option socket level/socket socket/oob-inline #t)) ; We're stateful anyway, so what the hell ... (define *control-input-port* #f) (define *control-output-port* #f) (define (handle-connection input-port output-port) (call-with-current-continuation (lambda (escape) (with-handler (lambda (condition more) (escape 'fick-dich-ins-knie)) (lambda () (set! *control-input-port* input-port) (set! *control-output-port* output-port) (display-banner) (handle-commands)))))) (define (display-banner) (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-error '()) (define ftpd-error? (condition-predicate 'ftpd-error)) (define (handle-commands) (with-handler (lambda (condition more) ;; this in really only for ftpd-quit (write-replies) (more)) (lambda () (let loop () (write-replies) (accept-command) (loop))))) (define (accept-command) (let ((command-line (read-crlf-line *control-input-port*))) ;; (format #t "Command line: ~A~%" command-line) (call-with-values (lambda () (parse-command-line command-line)) (lambda (command arg) (handle-command command arg))))) (define (handle-command command arg) (call-with-current-continuation (lambda (escape) (with-handler (lambda (condition more) (cond ((error? condition) (register-reply! 451 (format #f "Internal error: ~S" (condition-stuff condition))) (escape 'fick-dich-ins-knie)) ((ftpd-error? condition) (escape 'fick-dich-ins-knie)) (else (more)))) (lambda () (with-errno-handler* (lambda (errno packet) (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) (cond ((assoc command *command-alist*) => (lambda (pair) ((cdr pair) arg))) (else (register-reply! 500 (string-append (format #f "Unknown command: \"~A\"" command) (if (string=? "" arg) "." (format #f " (argument(s) \"~A\")." arg))))))) (define *logged-in?* #f) (define *authenticated?* #f) (define *anonymous?* #f) (define *root-directory* #f) (define *current-directory* "") (define (handle-user name) (cond (*logged-in?* (register-reply! 230 "You are already logged in.")) ((or (string=? "anonymous" name) (string=? "ftp" name)) (handle-user-anonymous)) (else (register-reply! 530 "Only anonymous logins allowed.")))) (define (handle-user-anonymous) (let ((ftp-info (user-info "ftp"))) (set-gid (user-info:gid ftp-info)) (set-uid (user-info:uid ftp-info)) (set! *logged-in?* #t) (set! *authenticated?* #t) (set! *anonymous?* #t) (set! *root-directory* (file-name-as-directory (user-info:home-dir ftp-info))) (set! *current-directory* "") (register-reply! 230 "Anonymous user logged in."))) (define (handle-pass password) (cond ((not *logged-in?*) (register-reply! 530 "You have not logged in yet.")) (*anonymous?* (register-reply! 200 "Thank you.")) (else (register-reply! 502 "This can't happen.")))) (define (handle-quit foo) (register-reply! 221 "Goodbye! Au revoir! Auf Wiedersehen!") (signal 'ftpd-quit)) (define (handle-syst foo) (register-reply! 215 "UNIX Type: L8")) (define (handle-cwd path) (ensure-authenticated-login) (let ((current-directory (assemble-path path))) (with-errno-handler* (lambda (errno packet) (signal-error! 550 (format #f "Can't change directory to \"~A\": ~A." path (car packet)))) (lambda () (with-cwd* (file-name-as-directory (string-append *root-directory* current-directory)) (lambda () ; I hate gratuitous syntax (set! *current-directory* current-directory) (register-reply! 250 (format #f "Current directory changed to \"/~A\"." current-directory)))))))) (define (handle-cdup foo) (handle-cwd "..")) (define (handle-pwd foo) (ensure-authenticated-login) (register-reply! 257 (format #f "Current directory is \"/~A\"." *current-directory*))) (define (make-file-action-handler error-format-string action) (lambda (path) (ensure-authenticated-login) (if (string=? "" path) (signal-error! 500 "No argument.")) (let ((full-path (string-append *root-directory* (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 *to-be-renamed* #f) (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.") (set! *to-be-renamed* full-path)))) (define (handle-rnto path) (ensure-authenticated-login) (if (not *to-be-renamed*) (signal-error! 503 "Need RNFR before RNTO.")) (if (string=? "" path) (signal-error! 500 "No argument.")) (let ((full-path (string-append *root-directory* (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 () (rename-file *to-be-renamed* full-path) (register-reply! 250 "File renamed.") (set! *to-be-renamed* #f))))) (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 *type* 'ascii) (define (handle-type arg) (cond ((string-ci=? "A" arg) (set! *type* 'ascii)) ((string-ci=? "I" arg) (set! *type* 'image)) ((string-ci=? "L8" arg) (set! *type* 'image)) (else (signal-error! 504 (format #f "Unknown TYPE: ~A." arg)))) (register-reply! 200 (format #f "TYPE is now ~A." (case *type* ((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) (+ (arithmetic-shift p1 8) p2))) components)))) (else (signal-error! 500 "Syntax error in argument to PORT.")))) (define *data-socket* #f) (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)) (set! *data-socket* socket) (register-reply! 200 (format #f "Connected to ~A, port ~A." (format-internet-host-address address) port)))))) (define *passive-socket* #f) (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) ;; kludge (bind-socket socket (internet-address->socket-address (this-host-address) 0)) (listen-socket socket 1) (let ((address (socket-local-address socket))) (call-with-values (lambda () (socket-address->internet-address address)) (lambda (host-address port) (set! *passive-socket* socket) (register-reply! 227 (format #f "Passive mode OK (~A,~A)" (format-internet-host-address host-address ",") (format-port port)))))))) ; This doesn't look right. But I can't look into the socket of the ; control connection if we're running under inetd---there's no way to ; coerce a port to a socket as there is in C. (define (this-host-address) (car (host-info:addresses (host-info (system-name))))) (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)))) (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) (let ((full-path (string-append *root-directory* (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 () (ls flags (list full-path) (socket:outport *data-socket*)))))) (define (handle-abor foo) (maybe-close-data-connection) (register-reply! 226 "Closing data connection.")) (define (handle-retr path) (ensure-authenticated-login) (let ((full-path (string-append *root-directory* (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 () (case *type* ((image) (copy-port->port-binary file-port (socket:outport *data-socket*))) ((ascii) (copy-port->port-ascii file-port (socket:outport *data-socket*))))))))))))) (define (handle-stor path) (ensure-authenticated-login) (let ((full-path (string-append *root-directory* (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 () (case *type* ((image) (copy-port->port-binary (socket:inport *data-socket*) file-port)) ((ascii) (copy-ascii-port->port (socket:inport *data-socket*) file-port))))))))))) (define (assemble-path path) (let* ((interim-path (if (not (file-name-rooted? path)) (string-append (file-name-as-directory *current-directory*) path) path)) (complete-path (if (file-name-rooted? interim-path) (file-name-sans-rooted interim-path) interim-path))) (cond ((normalize-path complete-path) => (lambda (assembled-path) assembled-path)) (else (signal-error! 501 "Invalid pathname"))))) (define (ensure-authenticated-login) (if (or (not *logged-in?*) (not *authenticated?*)) (signal-error! 530 "You're not logged in yet."))) (define (with-data-connection thunk) (dynamic-wind ensure-data-connection thunk maybe-close-data-connection)) (define *window-size* 51200) (define (ensure-data-connection) (if (and (not *data-socket*) (not *passive-socket*)) (signal-error! 425 "No data connection.")) (if *passive-socket* (call-with-values (lambda () (accept-connection *passive-socket*)) (lambda (socket socket-address) (set! *data-socket* socket)))) (register-reply! 150 "Opening data connection.") (write-replies) (set-socket-option *data-socket* level/socket socket/send-buffer *window-size*) (set-socket-option *data-socket* level/socket socket/receive-buffer *window-size*)) (define (maybe-close-data-connection) (if (or *data-socket* *passive-socket*) (close-data-connection))) (define (close-data-connection) (if *data-socket* (close-socket *data-socket*)) (if *passive-socket* (close-socket *passive-socket*)) (register-reply! 226 "Closing data connection.") (set! *data-socket* #f) (set! *passive-socket* #f)) (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 (values "QUIT" "") (let* ((line (trim-spaces line)) (split-position (index line #\space))) (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 " +")) ; 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 *reverse-replies* '()) (define *reply-code* #f) ; the last one wins (define (write-replies) (if (not (null? *reverse-replies*)) (let loop ((messages (reverse *reverse-replies*))) (if (null? (cdr messages)) (write-final-reply (car messages)) (begin (write-nonfinal-reply (car messages)) (loop (cdr messages)))))) (set! *reverse-replies* '())) (define (write-final-reply line) (format *control-output-port* "~D ~A" *reply-code* line) ;; (format #t "Reply: ~D ~A~%" *reply-code* line) (write-crlf *control-output-port*)) (define (write-nonfinal-reply line) (format *control-output-port* "~D-~A" *reply-code* line) ;; (format #t "Reply: ~D-~A~%" *reply-code* line) (write-crlf *control-output-port*)) (define (signal-error! code message) (register-reply! code message) (signal 'ftpd-error)) (define (register-reply! code message) (set! *reverse-replies* (cons message *reverse-replies*)) (set! *reply-code* code)) ; Version (define *ftpd-version* "$Revision: 1.1 $") (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 () (let* ((line (read-crlf-line input-port)) (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)))) ; Stuff from Big Scheme ; We can't open BIG-SCHEME because we use virgin SIGNALS. Sigh. (define any? (structure-ref big-scheme any?)) (define partition-list (structure-ref big-scheme partition-list))