\ $Id: httpd.fr,v 1.7 2009/10/01 10:00:57 alex Exp alex $ \ ****************************************************************************** \ \ HTTPD - is a simple web (HTTP) server that can handle multiple clients \ simultaneously. \ \ Bernd Paysan wrote a very powerful Gforth web server, which is \ documented at: \ \ http://www.jwdt.com/~paysan/httpd-en.html \ \ I read his documentation many times, trying to wrap my inexperienced \ FORTH brain around his code. Naturally, I borrowed some of his ideas. \ His web server runs off UNIX's inetd and thus handles a single client \ via standard I/O. My web server uses actual network connections and \ handles multiple clients simultaneously. (Inetd can, of course, \ spawn multiple instances of the Gforth web server to handle multiple \ clients.) Again, Bernd's web server is very powerful and more \ feature-full than mine. And, adding insult to injury, he says, \ "The following code was created in just a few hours of work ..."! \ \ HTTPD revolves around an I/O event dispatcher. In the MAIN LOOP \ section at the bottom of this program, the dispatcher is created, \ the TCP/IP listening port is created, and the listening port is \ registered with the dispatcher as an input source. Control is \ then turned over to the dispatcher to monitor (i) the listening \ port for incoming connection requests and (ii) any future client \ connections. \ \ Whenever a client tries to connect to the server on port 80, the \ dispatcher detects the pending connection request and invokes the \ NEW-CLIENT callback word. NEW-CLIENT accepts the connection request, \ the operating system creates a new data connection for the client, \ and the new data connection is registered with the dispatcher as \ another input source. (Thus, at any given time, HTTPD will have \ the listening port and the data connections of all connected clients \ registered with the dispatcher.) \ \ ... to be documented further ... \ \ \ The code is divided into the following sections (section 4 is \ incomplete, but the other sections are complete and have been \ tested): \ \ \ (1) File-extension-to-MIME-type Translation - loads the file extension \ to MIME type mappings from a "mime.types" file into a separate Forth \ wordlist. \ \ lookup-mime - translates a file's extension to the file's MIME type. \ mime-of - is a defining word for suffix-to-type mappings. \ map-mime-type - defines a suffix-to-type map using the mime-of word. \ read-mime-file - reads the file-suffix-to-MIME-type mappings from a \ "mime.types" file and enters the mappings into the dictionary. \ \ \ (2) Session Object - defines a session object structure. Each client that \ connects to the server is allocated a session object for the purpose \ of accumulating information about the session while the client's HTTP \ request is being read. \ \ sobj-request@/sobj-request! - gets/sets the session's request string. \ sobj-pathname@/sobj-pathname! - gets/sets the session's URI string. \ sobj-version@/sobj-version! - gets/sets the session's version string. \ session-create - creates and initializes a session object. \ session-cancel-cb - cancels a session's input or timeout callback. \ session-destroy - destroys a session object. \ \ \ (3) Build Pathnames - from the path in HTTP GET and HEAD requests. \ \ replace-% - replaces "%xx" sequences in a string with the specified \ hexadecimal values. \ split-name-path - extracts the user name and path from a GET or HEAD \ request's pathname. \ translate-~ - gets the value of the TILDE_TRANSLATION environment \ variable and uses it as a C format string for formatting the \ final pathname from the GET or HEAD request. \ append-/ - checks to see if a pathname without a trailing "/" refers \ to a directory and, if so, appends a "/" to the pathname. \ check-for-index-html - checks to see if a directory has a default \ HTML file in it. \ build-pathname - builds the full pathname for the file specified in an \ HTTP GET or HEAD request. \ \ \ (4) HTTP Request Handling - processes GET and HEAD requests. \ \ http-request? - returns true if a string is an HTTP request (GET, \ HEAD, or POST). \ parse-request - parses a GET or HEAD HTTP request and returns the \ fully resolved pathname and the HTTP version string. \ write-stream - writes an escaped string out to a network stream. \ report-error - generates an HTTP error response header. \ generate-header-wrapped - wrapped header generator word. \ generate-header - generates the HTTP response header for a request. \ strip-directory - strips the directory information from a pathname. \ show-header - generates a directory listing HTML header. \ show-directory - generates an HTML list item for a directory. \ show-file - generates an HTML list item for a file. \ list-directory - generates a formatted list of a directory's contents \ and writes the list out to the client's network connection. \ transfer-file - reads the contents of a file and writes the contents \ out to the client's network connection. \ GET - returns a file to the client. \ HEAD - returns file information to the client. \ keepalive-timeout-cb - is invoked by the I/O event dispatcher when \ no input has been received from a client after N seconds; the \ network connection to the client is closed and the session is \ destroyed. \ add-timeout-cb - registers a keepalive timeout callback. \ process-request - processes an HTTP request. \ \ \ (5) Network Client I/O - handles the addition of new clients and reading \ their HTTP requests. \ \ read-client - reads and processes HTTP requests from a client. \ new-client - accepts a network connection request, creates a session \ object for the new client, and registers the client as an input \ source. \ new-listener - creates a TCP/IP listening endpoint for HTTP connection \ requests. \ \ \ (6) Main Loop - is the start-up code for the HTTP server. The server's \ listening port is created, the MIME file is loaded, and then the \ server loops forever, monitoring network I/O and dispatching \ requests. \ \ ****************************************************************************** s" utility.fr" INCLUDED s" loading httpd.fr" type cr \ ****************************************************************************** \ \ Load the file-suffix-to-MIME-type mappings from the "mime.types" file(s). \ The mappings are entered into the dictionary in a separate wordlist, \ MIME-TYPES, to prevent conflicts with existing FORTH word definitions. \ The wordlist is used as a lookup table. \ \ Depending on the size of the "mime.types" file, the mappings may consume \ a lot of dictionary space. Consequently, you may need to increase the \ dictionary size by setting environment variable FICL_DICTIONARY_SIZE \ before running FINC. (FICL's default size is 12288 cells, of which \ about 7500 cells are used by standard FICL. A comprehensive, 750-line \ "mime.types" file I downloaded from the Internet overflowed the default \ dictionary size.) \ \ ****************************************************************************** 128 constant MAX_INPUT_LINE wordlist constant mime-types \ Define MIME types separately. \ ****************************************************************************** \ LOOKUP-MIME - looks up a file's suffix in the MIME-TYPES wordlist and \ returns the corresponding MIME type string. If the suffix is not \ found, the default MIME type, "application/octet-stream", is returned. \ ****************************************************************************** s" application/octet-stream" strcdup constant DEFAULT-MIME-TYPE : lookup-mime ( c-addr1 u1 -- c-addr2 u2 ) 2dup [char] . strrchr ?dup IF \ Locate the file suffix. 1 /string \ Advance past the period. 2swap 2drop THEN MIME-TYPES search-wordlist IF execute \ Retrieve the MIME type. ELSE DEFAULT-MIME-TYPE count \ Use the default MIME type. THEN ; \ ****************************************************************************** \ MIME-OF: - is a defining word for suffix-to-type mappings. At compile \ time, the file suffix is entered as a new word into the dictionary, \ with its data value being the corresponding MIME type. (NOTE that \ the MIME type string is passed in on the stack as a counted string.) \ When a file suffix word is executed, the corresponding MIME type \ string is pushed on the stack. \ ****************************************************************************** : mime-of: CREATE ( c-addr -- ) \ Counted MIME-type string. , DOES> ( -- c-addr u ) @ count ; \ ****************************************************************************** \ MAP-MIME-TYPE - processes a line from the mime.types file. The line is \ structured as follows: " [ ... ]", where \ there may be zero or more file suffixes associated with the MIME type. \ For each suffix, " mime-of " is evaluated to define \ the suffix as a FORTH word that returns its MIME type as a string. \ ****************************************************************************** : map-mime-type ( c-addr u -- ) getword-ws \ Get the mime type. 2over -leading-ws nip IF strcdup ELSE 2drop 0 THEN \ Make a single copy for all suffixes. { MIME-TYPE } BEGIN getword-ws dup 0 > \ Get the next file suffix. WHILE MAX_INPUT_LINE allocate abort" Error allocating command buffer!" >r MIME-TYPE -rot \ r@ 0 s" mime-of: " strcat \ mime-of: 2swap strcat \ evaluate r> free drop REPEAT 2drop 2drop ; \ ****************************************************************************** \ READ-MIME-FILE - reads the file-suffix-to-MIME-type mappings from a \ "mime.types" file and enters the mappings into the dictionary. \ ****************************************************************************** : read-mime-file ( c-addr u -- ) s" Loading MIME types from " type 2dup type s" ." type cr MAX_INPUT_LINE allocate abort" Error allocating input buffer!" { INBUF } r/o open-file abort" Error opening MIME types file!" BEGIN dup INBUF MAX_INPUT_LINE rot read-line abort" Error reading MIME types file!" WHILE INBUF swap squeeze-ws over c@ [char] # = \ Comment? over 0= \ Blank line? logical-or IF \ Ignore? 2drop ELSE \ Mime-type specification. map-mime-type THEN REPEAT drop close-file drop INBUF free drop ; \ ****************************************************************************** \ A session object, created when a client connects to HTTPD, is used to \ maintain client-specific information as its HTTP request is being received. \ This information includes: \ (1) the LF-terminated network stream to the client, \ (2) the callback invoked when input is detected from the client, \ (3) the callback invoked after N seconds of the client being idle, \ (2) the HTTP request (GET, HEAD, or POST), \ (3) the full pathname of the requested file, and \ (4) the HTTP version string. \ ****************************************************************************** begin-structure session-object field: sobj-stream \ LF-terminated network stream to client. field: sobj-input-cb \ Registered input callback. field: sobj-timeout-cb \ Registered timeout callback. sfield: sobj-request \ GET/HEAD/POST request string. sfield: sobj-pathname \ Pathname string from HTTP request. sfield: sobj-version \ HTTP version. field: sobj-close \ Close session and connection. end-structure \ SOBJ-REQUEST@ - gets the HTTP request string. : sobj-request@ ( a-addr -- c-addr u ) sobj-request 2@ ; \ SOBJ-REQUEST! - sets the HTTP request string. If the previous string was \ dynamically allocated (indicated by a non-zero length), free it before \ storing the new string. : sobj-request! ( c-addr u a-addr -- ) sobj-request dup 2@ IF free THEN drop 2! ; \ SOBJ-PATHNAME@ - gets the pathname string. : sobj-pathname@ ( a-addr -- c-addr u ) sobj-pathname 2@ ; \ SOBJ-PATHNAME! - sets the pathname string. If the previous string was \ dynamically allocated (indicated by a non-zero length), free it before \ storing the new string. : sobj-pathname! ( c-addr u a-addr -- ) sobj-pathname dup 2@ IF free THEN drop 2! ; \ SOBJ-VERSION@ - gets the version string. : sobj-version@ ( a-addr -- c-addr u ) sobj-version 2@ ; \ SOBJ-VERSION! - sets the version string. If the previous string was \ dynamically allocated (indicated by a non-zero length), free it before \ storing the new string. : sobj-version! ( c-addr u a-addr -- ) sobj-version dup 2@ IF free THEN drop 2! ; \ SESSION-CREATE - dynamically allocates a session object. : session-create ( -- a-addr ) session-object allocate abort" Error allocating session object!" 0 over sobj-stream ! 0 over sobj-input-cb ! 0 over sobj-timeout-cb ! 0 over sobj-close ! dup s" " rot sobj-request 2! dup s" " rot sobj-pathname 2! dup s" " rot sobj-version 2! ; \ SESSION-CANCEL-CB - cancels the specfied callback. : session-cancel-cb ( field -- ) dup @ ?dup IF iox-cancel drop 0 swap ! ELSE drop THEN ; \ SESSION-DESTROY - frees a session object. The network connection to the \ client is closed, any active callbacks are cancelled, and dynamically \ allocated strings are freed. Finally, the storage for the session \ object itself is freed. : session-destroy ( a-addr -- ) dup sobj-stream @ ?dup IF lfn-destroy drop THEN dup sobj-input-cb session-cancel-cb dup sobj-timeout-cb session-cancel-cb dup s" " rot sobj-request! \ Free the request string. dup s" " rot sobj-pathname! \ Free the pathname. dup s" " rot sobj-version! \ Free the version string. free drop \ Free the session object. ; \ ****************************************************************************** \ Build Pathnames - from the path in HTTP GET and HEAD requests. \ ****************************************************************************** \ ****************************************************************************** \ REPLACE-% - returns a pathname string with "%xx" sequences representing \ special characters replaced by the special characters. In such a \ sequence, "xx" is the hexadecimal value of a special character. These \ sequences are most frequently used for embedded blanks in file names. \ ****************************************************************************** : replace-% ( c-addr u -- c-addr u1 ) BEGIN 2dup [char] % strchr \ Search string for "%". dup IF dup 3 < IF \ 3 characters required for "%xx". 2drop 0 THEN THEN ?dup WHILE 3 - \ c-addr u1 c-addr' u' -- c-addr u1 c-addr' u'-3 swap dup char+ 2 \ -- c-addr u1 u'-3 c-addr' c-addr'+1 2 hex2char \ Convert "xx" to the special character. over c! \ Store the special character in the string. char+ dup char+ char+ \ -- c-addr u1 u'-3 c-addr'+1 c-addr'+3 swap rot \ -- c-addr u1 c-addr'+3 c-addr'+1 u'-3 cmove \ Move the remaining string down 2 characters. 2 - \ Adjust the length of the overall string. REPEAT ; \ ****************************************************************************** \ SPLIT-NAME-PATH - extracts the user name and path from a GET or HEAD \ request's pathname. The two items are returned on the stack as \ strings, with the name on top and the path below it. \ \ The pathnames in a GET or HEAD request are of the following forms: \ \ Unnamed: / /software/ /software/index.html \ Named: /~alex/ /~alex/software/ /~alex/software/index.html \ \ Split-name-path gets the user name from "~", if present. The \ remaining path is extracted as follows. In a request without a user \ name, return everything following the first "/". In a request with \ a user name, return everything following the second "/". \ ****************************************************************************** : split-name-path ( c-addr1 u1 -- c-addr2 u2 c-addr3 u3 ) 2dup nip -rot \ -- u1 c-addr1 u1 s" /~" search IF \ "/~/" present? 2 /string \ -- u1 c-addr3 u' 2dup s" /" search IF \ Search for following "/". nip - \ -- u1 c-addr3 u3 ELSE 2drop \ -- u1 c-addr3 u3 THEN ELSE \ No "/~/"; use c-addr3=c-addr1, u3=0. drop 0 \ -- u1 c-addr3 0 THEN { 2:USER-NAME } \ Save c-addr3/u3 and now determine c-addr2/u2. USER-NAME chars + \ -- u1 c-addr2-1 over USER-NAME nip \ -- u1 c-addr2-1 u1 u3 dup IF 2 + THEN \ If user name present, account for "/~". - \ -- u1 c-addr2-1 u1-u3=u2+1 1 /string \ -- u1 c-addr2 u2 rot drop USER-NAME \ -- c-addr2 u2 c-addr3 u3 ; \ ****************************************************************************** \ TRANSLATE-~ - gets the value of the TILDE_TRANSLATION environment variable \ and uses it as a C format string for formatting the final pathname from \ the GET or HEAD request. \ \ Examples of the TILDE_TRANSLATION format string: \ UNIX: /home/%s/public_html/%s \ Nintendo: %0.0s/public_html/%s \ \ The first "%s" in the format string is replaced by the user name \ (c-addr2/u2). (If the first "%s" is really "%0.0s", as in the \ Nintendo example, the user name is discarded and the "%0.0s" is \ replaced by "".) The second "%s" in the format string is replaced \ by the path (c-addr1/u1). \ \ The fully formatted pathname is stored in a dynamically allocated \ string and returned on the stack as c-addr3/u3. \ ****************************************************************************** : translate-~ ( c-addr1 u1 c-addr2 u2 -- c-addr3 u3 ) { 2:PATH-NAME 2:USER-NAME } s" TILDE_TRANSLATION" getenv ?dup 0= IF \ TILDE_TRANSLATION not defined? Use default. s" /home/%s/public_html/%s" THEN \ Determine length of formatted pathname ... dup PATH-NAME nip USER-NAME nip + + 32 + strndup \ Copy format string with extra space. s" %0.0s" strdup 2>r \ Save dynamically allocated "%0.0s". 2r@ search IF 2r@ s" " s/// \ Replace "%0.0s" with "". ELSE s" %s" USER-NAME s/// \ Replace "%s" with user name. THEN drop 2r> drop free drop \ Restore and free allocated "%0.0s". s" %s" PATH-NAME s/// drop \ Replace "%s" with path name. ; \ ****************************************************************************** \ APPEND-/ - checks to see if a pathname without a trailing "/" refers to a \ directory instead of a file. In the former case, a "/" is appended to \ the pathname. The caller is responsible for passing in a string large \ enough to be expanded by one character. \ ****************************************************************************** : append-/ ( c-addr u -- c-addr u|u+1 ) 2dup c[]@ [char] / <> IF \ No trailing "/" ... 2dup drs-directory? IF \ ... and is directory? s" /" strcat \ Append "/". THEN THEN ; \ ****************************************************************************** \ CHECK-FOR-INDEX-HTML - checks to see if a directory has a default HTML \ file in it. The possible file names, in order, are "index.html", \ "index.htm", "default.html", and "default.htm". The input arguments, \ c-addr/u1, specify the pathname of the directory, ending with a "/". \ If one of the files is found, the directory name with the file name \ appended, c-addr/u2, is returned on the stack along with a true flag. \ If none of the files is found, the original directory name, c-addr/u1, \ is returned along with a false flag. \ ****************************************************************************** : check-for-index-html ( c-addr u1 -- c-addr u1|u2 flag ) { 2:BASE-PATH } BASE-PATH s" index.html" strcat 2dup file-status nip 0= IF true EXIT THEN 2drop BASE-PATH s" index.htm" strcat 2dup file-status nip 0= IF true EXIT THEN 2drop BASE-PATH s" default.html" strcat 2dup file-status nip 0= IF true EXIT THEN 2drop BASE-PATH s" default.htm" strcat 2dup file-status nip 0= IF true EXIT THEN 2drop BASE-PATH false ; \ ****************************************************************************** \ BUILD-PATHNAME - builds the full pathname for the file specified in an \ HTTP GET or HEAD request. \ ****************************************************************************** : build-pathname ( c-addr1 u1 -- c-addr2 u2 ) dup 2 * strndup \ Dynamically allocate a copy of the file name. replace-% 2dup split-name-path translate-~ 2swap drop free drop \ Free the copy of the file name. append-/ 2dup drs-directory? IF check-for-index-html drop THEN ; \ ****************************************************************************** \ HTTP Request Handling. \ ****************************************************************************** \ ****************************************************************************** \ HTTP-REQUEST? - returns true if a string is an HTTP request (GET, HEAD, or \ POST). \ ****************************************************************************** : http-request? ( c-addr u -- flag ) getword-ws 2swap 2drop 2dup s" GET" compare 0= IF 2drop true ELSE 2dup s" HEAD" compare 0= IF 2drop true ELSE s" POST" compare 0= IF true ELSE false THEN THEN THEN ; \ ****************************************************************************** \ PARSE-REQUEST - parses a GET or HEAD HTTP request (c-addr1/u1) and \ returns the fully resolved pathname (c-addr2/u2) and the HTTP \ version string (c-addr3/u3). \ ****************************************************************************** : parse-request ( c-addr1 u1 -- c-addr2 u2 c-add3 u3 ) getword-ws 2drop \ GET or HEAD. getword-ws build-pathname 2swap \ Resolve pathname. getword-ws 2swap 2drop \ HTTP version string. ; \ ******************************************************************************* \ WRITE-STREAM - writes a string to a network stream. Upon entry to the \ word, the string is immediately duplicated (to prevent S" shenanigans) \ and escape sequences translated to their charcter counterparts. The \ resulting string is output to the network stream and then FREE'd. \ ******************************************************************************* : write-stream ( c-addr u stream -- ior ) -rot strdup escaped \ -- stream c-addr1 u1 over >r \ Save allocated string. rot 3 -1E0 \ -- c-addr1 u1 stream crlf timeout lfn-putline \ -- ior r> free drop \ Restore and free allocated string. ; \ ****************************************************************************** \ GENERATE-HEADER - generates the HTTP response header for a request. \ If the requested file exists, a 200 success code is returned to \ the client: \ 200 \ Content-type: \ Content-length: <# of bytes> \ If the requested file doesn't exist, a 404 error code is returned \ to the client: \ 404 \ Content-length: 0 \ ****************************************************************************** s" ErrorDirectory cannot be viewed.

" strcdup constant ERROR_403_HTML s" ErrorFile not found.

" strcdup constant ERROR_404_HTML \ REPORT-ERROR - reports an HTTP error code (u). The error header is written \ to the client's network stream (st), along with an HTML message (c-addr1) \ indicating the error. A temporary response buffer (c-addr3) and the \ URI (c-addr2/u2) in question are supplied by the caller. : report-error ( c-addr1 c-addr2 u2 u st c-addr3 -- ) { STREAM RESPONSE } \ -- c-addr1 c-addr2 u2 u RESPONSE 0 s" HTTP/1.1 " strcat \ -- c-addr1 c-addr2 u2 u c-addr3 u3 rot 0 <# #s #> strcat \ -- c-addr1 c-addr2 u2 c-addr3 u3' s" " strcat 2swap strcat \ -- c-addr1 c-addr3 u3'' STREAM write-stream drop \ HTTP error code. s" Content-type: text/html" \ -- c-addr1 c-addr4 u4 STREAM write-stream drop \ MIME type. dup RESPONSE 0 s" Content-length: " strcat \ c-addr1 -- rot count nip 2 + 0 <# #s #> strcat \ -- c-addr1 c-addr3 u3' STREAM write-stream drop \ Length of HTML error message. s" " \ -- c-addr1 STREAM write-stream drop \ Blank line. count \ -- c-addr1 u1 STREAM write-stream drop \ HTML error message. ; \ GENERATE-HEADER-WRAPPED - generates an HTTP header; this word is "wrapped" \ by the GENERATE-HEADER word to ensure that the dynamically allocated \ response buffer is FREE'd in the event of an error. GENERATE-HEADER \ is passed the response buffer (c-addr) and the client's session \ object (a-addr). True or false is returned on the stack to indicate \ to the GET word whether or not the file transfer is to be performed. : generate-header-wrapped ( c-addr a-addr -- flag ) dup sobj-stream @ swap sobj-pathname@ { RESPONSE STREAM 2:PATHNAME } false PATHNAME drs-directory? IF s" HTTP/1.0 200" STREAM write-stream IF EXIT THEN s" Connection: close" STREAM write-stream IF EXIT THEN s" Content-type: text/html" STREAM write-stream IF EXIT THEN s" " STREAM write-stream IF EXIT THEN drop true \ Substitute true for false. ELSE PATHNAME file-status nip 0= IF s" HTTP/1.1 200" STREAM write-stream IF EXIT THEN s" Connection: Keep-Alive" STREAM write-stream IF EXIT THEN RESPONSE 0 s" Content-type: " strcat PATHNAME lookup-mime strcat STREAM write-stream IF EXIT THEN RESPONSE 0 s" Content-length: " strcat PATHNAME r/o bin open-file drop dup file-size drop rot close-file drop <# #s #> strcat STREAM write-stream IF EXIT THEN s" " STREAM write-stream IF EXIT THEN drop true \ Substitute true for false. ELSE ERROR_404_HTML PATHNAME 404 STREAM RESPONSE report-error THEN THEN ; \ GENERATE-HEADER - is a user-called wrapper around the work-performing \ GENERATE-HEADER-WRAPPED. GENERATE-HEADER dynamically allocates a \ response buffer to be used by GENERATE-HEADER-WRAPPED. When \ GENERATE-HEADER-WRAPPED returns, whether in error or not, GENERATE-HEADER \ then FREEs the response buffer. The client's session object (a-addr) is \ passed into GENERATE-HEADER; the transfer-file flag returned by \ GENERATE-HEADER-WRAPPED is returned as is. : generate-header ( a-addr -- flag ) dup >r sobj-pathname@ dup 32 + strndup drop dup \ Allocate response string. r> \ -- c-addr c-addr a-addr generate-header-wrapped swap free drop \ Free response string; ; \ ****************************************************************************** \ STRIP-DIRECTORY - strips the directory information from a pathname, \ leaving only the file name and extension. The original pathname \ is returned if no directory separator is found. \ ****************************************************************************** : strip-directory ( c-addr u -- c-addr1 u1) 2dup [char] / strrchr ?dup IF 1 /string \ Directory information stripped. 2swap 2drop THEN ; \ ****************************************************************************** \ LIST-DIRECTORY - lists the contents of a directory and writes the contents \ out to the client's network connection. True is returned on the \ stack if the listing was completed successfully; false is returned \ in the event of a network error. \ ****************************************************************************** \ SHOW-HEADER - generates the HTML header for a directory listing. : show-header ( session -- ) dup sobj-stream @ { SESSION STREAM } s" " STREAM write-stream IF EXIT THEN SESSION sobj-pathname@ STREAM write-stream IF EXIT THEN s" " STREAM write-stream IF EXIT THEN s"

" STREAM write-stream IF EXIT THEN SESSION sobj-pathname@ STREAM write-stream IF EXIT THEN s"

" STREAM write-stream IF EXIT THEN s"
    " STREAM write-stream IF EXIT THEN s"
  • Parent Directory
  • " STREAM write-stream drop ; \ SHOW-DIRECTORY - generates an HTML list item for a directory. : show-directory ( c-addr u stream -- ) { STREAM } s"
  • " STREAM write-stream IF EXIT THEN STREAM write-stream IF EXIT THEN s" /
  • " STREAM write-stream drop ; \ SHOW-FILE - generates an HTML list item for a file. : show-file ( size c-addr u stream -- ) { STREAM } rot >r \ Save file size. \ c-addr u s"
  • " STREAM write-stream IF EXIT THEN \ c-addr u STREAM write-stream IF EXIT THEN s"   (" STREAM write-stream IF EXIT THEN r> 1023 + 1024 / 0 <# #s #> STREAM write-stream IF EXIT THEN s" K)
  • " STREAM write-stream drop ; \ LIST-DIRECTORY - generates an HTML-formatted directory listing. : list-directory ( a-addr -- ) dup sobj-stream @ { SESSION STREAM } SESSION show-header SESSION sobj-pathname@ 2dup c[]@ [char] / = IF dup 1+ strndup s" *" strcat ELSE dup 2 + strndup s" /*" strcat THEN 2dup drs-create IF drop free drop EXIT \ Error creating directory scan. ELSE -rot drop free drop \ c-addr u scan -- scan THEN dup drs-first BEGIN ?dup WHILE \ File size? 2dup r/o open-file 0= IF dup file-size 2drop \ c-addr u fileid -- c-addr u fileid size swap close-file drop \ -- c-addr u size ELSE drop 0 \ c-addr u fileid -- c-addr u size THEN -rot \ -- size c-addr u \ Directory? 2dup drs-directory? -rot \ -- size flag c-addr u strip-directory \ -- size flag c-addr1 u1 rot IF STREAM show-directory drop \ size c-addr1 u1 -- ELSE STREAM show-file \ size c-addr1 u1 -- THEN dup drs-next REPEAT drs-destroy drop s"
" STREAM write-stream drop s" " STREAM write-stream drop -1 SESSION sobj-close ! ; \ ****************************************************************************** \ TRANSFER-FILE - reads the contents of a file and writes the contents \ out to the client's network connection. True is returned on the \ stack if the transfer was completed successfully; false is returned \ in the event of a network error. \ ****************************************************************************** 32 1024 * constant MAXBUF : transfer-file ( a-addr -- flag ) { SESSION } SESSION sobj-pathname@ r/o bin open-file IF drop false EXIT \ Error opening file. THEN MAXBUF allocate drop \ Allocate an input buffer. { INBUF } BEGIN true over INBUF MAXBUF rot read-file drop ?dup WHILE \ Data read from file? INBUF swap SESSION sobj-stream @ -1E0 lfn-write 0= WHILE \ Data written to network stream? 2drop REPEAT 2drop false THEN swap close-file drop \ Close the file. INBUF free drop \ Free the input buffer. ; \ ****************************************************************************** \ GET - returns a file to the client. \ ****************************************************************************** : GET ( a-addr -- ) { SESSION } SESSION sobj-request@ \ -- c-addr u parse-request \ c-addr u -- caddr u1 caddr2 u2 strdup SESSION sobj-version! \ -- caddr u1 strdup SESSION sobj-pathname! \ -- SESSION generate-header \ a-addr -- flag IF SESSION sobj-pathname@ drs-directory? IF SESSION list-directory ELSE SESSION transfer-file drop THEN THEN ; \ ****************************************************************************** \ HEAD - returns file information to the client. \ ****************************************************************************** : HEAD ( a-addr -- ) { SESSION } SESSION sobj-request@ parse-request strdup SESSION sobj-version! strdup SESSION sobj-pathname! SESSION generate-header drop ; \ ****************************************************************************** \ KEEPALIVE-TIMEOUT-CB - is invoked by the I/O event dispatcher when no \ input has been received from a client after N seconds. The network \ connection to the client is closed and the session is destroyed. \ ****************************************************************************** : keepalive-timeout-cb ( cb c-addr n -- ) drop nip 0 over sobj-timeout-cb ! session-destroy ; \ ****************************************************************************** \ ADD-TIMEOUT-CB - registers a keepalive timeout callback. If the timer \ expires before any further input is received from the client, the \ client connection is closed and the session destroyed. If input \ is received before the timer expires, the timer is cancelled and, \ after the new request is processed, a new timer is created. \ ****************************************************************************** 60E0 fconstant KEEPALIVE-TIMEOUT : add-timeout-cb ( a-addr -- ) dup sobj-timeout-cb session-cancel-cb >r KEEPALIVE-TIMEOUT \ Timeout in seconds. r@ \ Session object. ['] keepalive-timeout-cb \ Word to execute when timer expires. over sobj-input-cb @ iox-dispatcher \ I/O event dispatcher. iox-after \ Register timer callback. r> sobj-timeout-cb ! \ Save callback handle in session object. ; \ ****************************************************************************** \ PROCESS-REQUEST - processes an HTTP request. \ ****************************************************************************** : process-request ( a-addr -- ) dup \ -- a-addr a-addr dup sobj-request@ \ -- a-addr a-addr c-addr u getword-ws 2swap 2drop \ -- a-addr a-addr c-addr' u' ?dup IF evaluate \ a-addr a-addr c-addr' u' -- a-addr dup s" " rot sobj-request! \ a-addr -- a-addr ELSE 2drop \ a-addr a-addr c-addr' -- a-addr THEN add-timeout-cb ; \ ****************************************************************************** \ ADD-INPUT - registers a network file descriptor as an input source with \ an I/O dispatcher. When input is detected on the file descriptor, \ the dispatcher executes the specified word to handle the input. \ ****************************************************************************** : add-input ( dp fd addr xt -- cb ) rot IOX_READ \ dp fd addr xt -- dp addr xt fd 1 2swap \ -- dp fd 1 addr xt 4 roll \ -- fd 1 addr xt dp iox-onio \ fd 1 addr xt dp -- cb ; \ ****************************************************************************** \ READ-CLIENT - is invoked by the I/O dispatcher when an HTTP request is \ available to be read from a client. As the request is read, the \ request information is stored in the client's session object. When \ the request is complete, PROCESS-REQUEST is called to process the \ request. If there is a read error or if the connection is closed \ by the client, the client's session object is destroyed. \ ****************************************************************************** : read-client ( cb c-addr n -- ) drop nip \ Save the session in a local variable. { SESSION } SESSION sobj-timeout-cb session-cancel-cb SESSION sobj-stream @ \ Get the network stream. BEGIN dup lfn-readable? \ Pending input on network connection? SESSION sobj-close @ 0= \ No explicit call for connection close? logical-and WHILE dup -1E+0 lfn-getline \ Read the next line of input. 0= IF 2dup http-request? IF \ HTTP request (GET, HEAD, POST)? 2dup strdup SESSION sobj-request! \ Save the request in the session. THEN dup 0= IF \ End of request (empty line)? SESSION process-request THEN THEN 2drop REPEAT lfn-up? 0= \ Network connection gone down? SESSION sobj-close @ \ Explicit call for connection close? logical-or IF SESSION session-destroy \ Close it and discard the session. THEN ; \ ****************************************************************************** \ NEW-CLIENT - is invoked by the I/O dispatcher when a client is requesting \ a network connection to the server. The connection request is \ accepted and the new client data connection is registered with the \ I/O dispatcher as an input source. When data is received on the \ connection, the I/O dispatcher automatically invokes READ-CLIENT \ to read and process the data. \ ****************************************************************************** : new-client ( cb c-addr n -- ) drop -1E+0 tcp-answer drop \ Answer the connection request. s" " lfn-create drop \ Create a LF-terminated network stream. swap iox-dispatcher swap \ cb st -- dp st dup lfn-fd swap \ -- dp fd st session-create dup >r \ Create a session for the client. dup -rot sobj-stream ! \ dp fd st a-addr -- dp fd a-addr ['] read-client \ Word to execute on client input. add-input \ Register an input callback. r> sobj-input-cb ! \ Save callback in session object. ; \ ****************************************************************************** \ NEW-LISTENER - creates a TCP/IP listening endpoint for HTTP connection \ requests. If a port number is present on the data stack, HTTPD \ listens at that port; otherwise, it listens at default port 80. \ The listening endpoint is registered with the I/O dispatcher, after \ which NEW-CLIENT is invoked to handle incoming connection requests. \ ****************************************************************************** : new-listener ( [n] dp -- dp ) depth 1 = IF 80 swap THEN \ ( [n] dp -- n dp ) dup rot \ ( n dp -- dp dp n ) s" Listening on port " type dup s>d <# #s #> type s" ..." type cr 0 tcp-listen \ ( dp dp n -- dp dp ep ior ) abort" Error creating listening socket!" dup tcp-fd swap ['] new-client \ ( dp dp fd ep -- dp dp fd ep xt ) add-input drop \ ( dp dp fd ep xt -- dp ) ; \ ****************************************************************************** \ MAIN LOOP - Create an I/O dispatcher, create the server's listening port, \ and loop forever. \ ****************************************************************************** 0 iox-debug 0 lfn-debug 0 tcp-debug skt-startup abort" Unable to start up networking!\n" [DEFINED] G-DISPATCHER [IF] G-DISPATCHER \ (nicl) Use NICL's I/O dispatcher. [ELSE] iox-create drop \ (finc) Create an I/O dispatcher. [THEN] new-listener \ Create the HTTP listening port. mime-types set-current \ Load the MIME-type mappings. s" MIME_TYPES" getenv ?dup 0= [IF] s" /etc/mime.types" strdup [THEN] read-mime-file definitions [DEFINED] G-DISPATCHER [IF] drop [ELSE] s" Monitoring ..." type cr -1E+0 iox-monitor \ Monitor connection requests and client I/O. s" Done monitoring?" type cr \ Shouldn't reach here! [THEN]