(* * Simple HTTP server * * Based on the Simple Moscow ML HTTP server * (sestoft@dina.kvl.dk) 1999 * * * This server understands only GET requests, no CGI, and no ~users. *) structure Server :> sig (* Launch http server on specified port *) val start : int -> unit end = struct structure Sig = Signals structure S = Substring type url = string val serverName = "CS312 Basic HTTP Server" fun log msg = print msg exception ShutDown local val mimetypes = [(["html", "htm"], "text/html"), (["txt"], "text/plain"), (["gif"], "image/gif"), (["jpeg", "jpg", "jpe"], "image/jpeg"), (["png"], "image/png"), (["tiff", "tif"], "image/tiff"), (["css"], "text/css"), (["eps", "ps"], "application/postscript"), (["pdf"], "application/pdf"), (["dvi"], "application/x-dvi"), (["bin", "exe", "class"], "application/octet-stream"), (["gtar"], "application/x-gtar"), (["latex"], "application/x-latex"), (["tar"], "application/x-tar"), (["tex"], "application/x-tex"), (["texinfo", "texi"], "application/x-texinfo"), (["zip"], "application/zip"), (["gz"] , "application/gzip"), (["mws"], "application/x-maple"), (["doc"], "application/msword"), (["ppt"], "application/powerpoint") ] val mimehash : (string, string) HashTable.hash_table = HashTable.mkTable (HashString.hashString,fn (x,y) => x=y) (length mimetypes, Fail "") fun addexts (exts, ty) = List.app (fn ext => HashTable.insert mimehash (ext, ty)) exts val _ = List.app addexts mimetypes fun lowercase s = CharVector.map Char.toLower s in fun mimetype ext = case Option.mapPartial (HashTable.find mimehash o lowercase) ext of NONE => "text/plain" | SOME mime => mime end fun date () = Date.toString(Date.fromTimeLocal(Time.now())) fun mkheader header status fields mimetype len = (log " "; log status; log " "; log (Int.toString len); log "\n"; concat ([header, "\n", "Date: ", date(), "\n", "Server: ", serverName, "\n"] @ fields @ ["Content-type: ", mimetype, "\n", "Content-length: ", Int.toString len, "\n\n"])) val okheader = mkheader "HTTP/1.0 200 OK" "200" [] fun movpermheader newpath = mkheader "HTTP/1.0 301 Moved permanently" "301" ["Location: " ^ newpath ^ "\n"] "text/html" val forbiddenheader = mkheader "HTTP/1.0 403 Forbidden" "403" [] "text/html" val notfoundheader = mkheader "HTTP/1.0 404 Not Found" "404" [] "text/html" val errorheader = mkheader "HTTP/1.0 500 Internal server error" "500" [] "text/html" val notimplheader = mkheader "HTTP/1.0 501 Not implemented" "501" [] "text/html" exception Notfound and Forbidden and Notimplemented and Directory of string fun listDir (s) = let fun loop (ds) = (case OS.FileSys.readDir (ds) of "" => [] before OS.FileSys.closeDir (ds) | file => file::loop (ds)) val ds = OS.FileSys.openDir (s) in loop (ds) handle e => (OS.FileSys.closeDir (ds); raise (e)) end fun indexdoc dirpath = let val pre = ["

Index of ", dirpath, "

\n"] val post = ["\n
"] val entries = ListMergeSort.sort (fn (x,y) => x>y) (listDir dirpath) fun fhref f r = "" :: f :: "" :: r fun fmtentry (f, r) = "\n" :: (if f <> ".." andalso OS.FileSys.isDir(OS.Path.concat(dirpath, f)) then fhref (f ^ "/") r else fhref f r) in concat (pre @ foldr fmtentry post (".." :: entries)) end (* Convert %xy to the character with hex code xy. No checks done. *) fun xurldecode s = let fun hexval c = if #"0" <= c andalso c <= #"9" then ord c - 48 else (ord c - 55) mod 32 fun loop [] acc = implode (rev acc) | loop (#"%" :: cr) acc = (case cr of c1 :: c2 :: cr' => loop cr' (chr (16 * hexval c1 + hexval c2) :: acc) | _ => loop cr (#"%" :: acc)) | loop (c :: cr) acc = loop cr (c :: acc) in loop (explode s) [] end datatype result = String of string * string (* a response and its MIME type *) | File of BinIO.instream * int * string (* instream, size, and MIME type *) | Failure of exn (* failure or exceptional result *) fun htmldoc title contents = String.concat("" :: title :: "\n" :: "

" :: title :: "

\n" :: contents :: ["\n"]) fun relativizeUrl (url:url,docroot:string) = OS.Path.mkRelative (url,docroot) (* dirty hack to get this to work... a more useful * way of course would exist is OS.Path.fromUnixPath * would friggin' exist! *) fun isUnix () = (case SMLofNJ.SysInfo.getOSKind () of SMLofNJ.SysInfo.UNIX => true | _ => false) fun fromAbsUnixPath (s:string):string = let fun is_sep (c:char) = (c = #"/") in if isUnix () then s else OS.Path.toString {isAbs=false, vol="", arcs= String.tokens is_sep s} end fun response (docroot:string) (inp) : result = let val _ = log (S.string (S.takel (Char.notContains "\r\n") (S.all inp))) val (method, sus1) = S.splitl Char.isAlpha (S.all inp) val _ = if S.string method <> "GET" then raise Notimplemented else () val path1 = S.string (S.trimr 1 (#1 (S.position "HTTP" (S.triml 2 sus1)))) in if (String.isPrefix "SHUTDOWN" path1) then raise ShutDown else let val path2 = xurldecode path1 (* Allow access only to docroot and its subdirectories: *) val path3 = fromAbsUnixPath (case #arcs(OS.Path.fromString (OS.Path.mkCanonical path2)) of ".." :: _ => raise Forbidden | _ => OS.Path.concat(docroot, path2)) val indexes = ["index.html", "index.htm", "INDEX.HTML", "INDEX.HTM"] open OS.FileSys fun tryindex path idx = access (OS.Path.concat(path, idx), []) fun exists p = if access (p, []) then () else raise Notfound fun readbl p = if access (p, [A_READ]) then () else raise Forbidden fun execbl p = if isUnix () then if access (p, [A_EXEC]) then () else raise Forbidden else exists p fun readdoc p = (exists p; readbl p; File (BinIO.openIn p, fileSize p, mimetype (OS.Path.ext p))) in if OS.FileSys.isDir (path3) (*OS.Path.file path3 = ""*) then (execbl path3; case List.find (tryindex path3) indexes of SOME idx => readdoc (OS.Path.concat(path3, idx)) | NONE => String (indexdoc path3, "text/html")) else if access(path3, []) andalso OS.FileSys.isDir path3 then raise Directory (OS.Path.concat(path1, "")) else readdoc path3 end handle ShutDown => raise ShutDown | e => Failure e end fun send sock vec = (Socket.sendVec(sock, { buf=vec, i=0, sz=NONE }); ()) fun sendstr sock s = send sock (Byte.stringToBytes s) fun senddoc sock header contents = (sendstr sock (header (size contents)); sendstr sock contents) fun addheader sock data = case data of String (s, mime) => senddoc sock (okheader mime) s | File (is, size, mime) => ((sendstr sock (okheader mime size); while not (BinIO.endOfStream is) do send sock (BinIO.inputN(is, 1024))) handle e => (BinIO.closeIn is; raise e); BinIO.closeIn is) | Failure (Directory newpath) => senddoc sock (movpermheader newpath) (htmldoc "301 Moved permanently" "") | Failure Forbidden => senddoc sock forbiddenheader (htmldoc "403 Forbidden" "You have no access to that document.") | Failure Notfound => senddoc sock notfoundheader (htmldoc "404 Not found" "The requested URL was not found.") | Failure Notimplemented => senddoc sock notimplheader (htmldoc "501 Not implemented" ("The server could not handle your request. Contact "^ "the administrator if you think this is too bad.")) | Failure _ => senddoc sock errorheader (htmldoc "500 Internal Server Error" ("The server encountered an internal error. Contact "^ "the administrator of the site")) fun server {port:int,docroot:string}:unit = let val sock = INetSock.TCP.socket () val buf = Word8Array.array(10000, 0w0) fun gethttprequest sock = let val got = Socket.recvArr(sock, {buf = buf, i=0, sz=NONE}) in Byte.unpackString(buf, 0, SOME got) end fun next () = let val (sock', a) = Socket.accept sock val (addr,_) = INetSock.fromAddr (a) in log (NetHostDB.toString (addr)); log " ["; log (date()); log "] "; (addheader sock' (response docroot (gethttprequest sock'))) handle Fail s => print ("[" ^ s ^ "]") | ShutDown => (Socket.close sock'; raise ShutDown); Socket.close sock'; next () end val saveIntHandler = Sig.inqHandler Sig.sigINT in SMLofNJ.Cont.callcc (fn (doneK) => (ignore (Sig.setHandler (Sig.sigINT, Sig.HANDLER (fn _ => doneK))); Socket.bind(sock, INetSock.any port); Socket.listen(sock, 150); log "Starting "; log serverName; log " on port "; log (Int.toString port); log "\n"; (next ()) handle Fail s => print s | ShutDown => log "\n" )); ignore (Sig.setHandler (Sig.sigINT, saveIntHandler)); log "Shutting down HTTP server\n"; Socket.close sock end handle Fail s => print ("HTTP server failed: " ^ s ^ "\n"); fun start (port:int): unit = let val _ = SMLofNJ.Internals.GC.messages false val current = OS.FileSys.getDir () val path = OS.Path.concat (current,"local_pages") val _ = log ("Serving pages out of \""^path^"\"...\n") in server {port=port,docroot=path} end end