/http.arc

http://github.com/alimoeeny/arc · Unknown · 192 lines · 153 code · 39 blank · 0 comment · 0 complexity · fcc17826d6dc8471a68d4d88c4e18bff MD5 · raw file

  1. ;;; http.arc: dealing with the HTTP protocol
  2. (deftem http-msg
  3. prot nil ; protocol "HTTP/1.1"
  4. hds nil) ; headers (("Content-Type" "html") ("Location" "/new"))
  5. ; A "request" is a message from the client to the server
  6. (deftem (http-req http-msg)
  7. meth nil ; method [downcased sym] get, post
  8. path nil ; path "/some/thing"
  9. qs nil ; query string "foo=bar&baz=42"
  10. args nil ; args of the qs/form post (("foo" "bar") ("baz" "42"))
  11. cooks nil) ; sent cookies (("sessid" "MTgY4h2"))
  12. ; A "response" is a message from the server to the client
  13. (deftem (http-resp http-msg)
  14. sta nil ; status code 404
  15. rea nil) ; reason "Not Found"
  16. (= http-ok+ "200 OK"
  17. http-created+ "201 Created"
  18. http-found+ "302 Found"
  19. http-notmod+ "304 Not Modified"
  20. http-bad+ "400 Bad Request"
  21. http-forbidden+ "403 Forbidden"
  22. http-notfound+ "404 Not Found")
  23. (def read-headers ((o from (stdin)))
  24. (unless (is (peekc from) #\newline) ; for suckers using \n instead of \r\n
  25. (let line (readline from)
  26. (awhen (pos #\: line)
  27. (cons (list (normalize-hdname:cut line 0 it)
  28. (trim:cut line (+ it 1)))
  29. (read-headers from))))))
  30. (def normalize-hdname (name) ; "content-type" -> "Content-Type"
  31. (string:intersperse #\- (map capitalize (tokens name #\-))))
  32. (def capitalize (word) ; "foobar" -> "Foobar"
  33. (+ (upcase word.0) (cut word 1)))
  34. (def read-req ((o from (stdin)))
  35. (withs ((m pa pro) (read-reqline from)
  36. (rpa qs) (tokens pa #\?)
  37. hds (read-headers from))
  38. (inst 'http-req 'prot pro 'meth (sym:downcase m)
  39. 'path rpa 'qs qs 'hds hds
  40. 'cooks (parse-cooks hds)
  41. 'args (only.parse-args qs))))
  42. (def read-reqline ((o from (stdin))) (tokens:readline from))
  43. (def parse-args (argstr) ; "foo=bar&baz=42" -> (("foo" "bar") ("baz" "42"))
  44. (map [map urldecode (tokens _ #\=)] (tokens argstr #\&)))
  45. (def parse-cooks (reqhds)
  46. (reduce join
  47. (map [map [tokens (trim _) #\=] (tokens _.1 #\;)]
  48. (keep [caris _ "Cookie"] reqhds))))
  49. (def read-resp ((o from (stdin)))
  50. (let (pro st . reas) (tokens (readline from))
  51. (inst 'http-resp 'prot pro 'sta (int st)
  52. 'rea (string:intersperse " " reas)
  53. 'hds (read-headers from))))
  54. (def pr-headers (hds)
  55. (each (n v) hds (prrn n ": " v))
  56. (prrn))
  57. (def prrn args ; print with \r\n at the end
  58. (map1 disp args)
  59. (prn #\return))
  60. ; we call "head" the top part of an HTTP message,
  61. ; i.e: the status or request line plus the headers
  62. (def reqhead (meth path hds)
  63. (prrn upcase.meth " " path " HTTP/1.0")
  64. ; 1.0 because a 1.1 client should be able to deal with
  65. ; "Transfert-Encoding: chunked" (and we don't, at least yet)
  66. (pr-headers hds))
  67. (def resphead ((o sta http-ok+) (o hds httpd-hds*))
  68. (prrn "HTTP/1.1 " sta)
  69. (pr-headers hds))
  70. (def redirect (loc (o sta http-found+) (o hds httpd-hds*))
  71. (resphead sta (copy hds 'Location loc)))
  72. ;; httpd: generic HTTP server.
  73. ; put it behind a reverse proxy, and code your own "framework".
  74. ; doesn't deal with logging, gzipping, slow and bad clients,
  75. ; keep-alive, limits of req/<time>: nginx can do it for us
  76. (= httpd-hds* (obj Server "http.arc"
  77. Content-Type "text/html" ; set encoding in your HTML
  78. Connection "closed")
  79. stop-httpd* nil
  80. httpd-handler nil) ; ** the function your web app has to define **
  81. (def httpd-serve ((o port 8080))
  82. (w/socket s port
  83. (until stop-httpd*
  84. (let (in out ip) (socket-accept s)
  85. (thread:handle-req in out ip)))))
  86. (def handle-req (in out ip)
  87. (after
  88. (let req (read-req in)
  89. (= req!ip ip) ; TODO: check and use X-Real-IP
  90. (read-body req in)
  91. (w/stdout out (httpd-handler req)))
  92. (close in out)))
  93. (def read-body (req (o from (stdin)))
  94. (awhen (aand (alref req!hds "Content-Length") (errsafe:int it))
  95. (= req!body (readbytes it from))
  96. (when (findsubseq "x-www-form-urlencoded" (alref req!hds "Content-Type"))
  97. (= req!args (join req!args (parse-args:string (map [coerce _ 'char] req!body)))))))
  98. (def start-httpd ((o port 8080))
  99. (wipe stop-httpd*)
  100. (prn "httpd: serving on port " port)
  101. (thread:httpd-serve port))
  102. ;; Very basic HTTP client. still a work in progress: incomplete/ugly
  103. ;
  104. ; /!\ To have the code below working, you need to patch Arc to get
  105. ; client sockets. here the function is called 'client-socket
  106. (def parse-url (url)
  107. (with (prot "http" host nil port 80 path "/")
  108. (awhen (findsubseq "://" url) ; explicit protocol?
  109. (= prot (downcase:cut url 0 it)
  110. url (cut url (+ it 3))))
  111. (aif (pos #\/ url) ; deal with host & path
  112. (= host (cut url 0 it)
  113. path (cut url it))
  114. (= host url))
  115. (awhen (pos #\: host) ; explicit port?
  116. (= port (int (cut host inc.it))
  117. host (cut host 0 it)))
  118. (list prot host port path)))
  119. (def mk-http-req (method host path (o hds) (o port 80) (o body))
  120. (let (in out) (client-socket host port)
  121. (w/stdout out
  122. (reqhead (upcase method) path hds)
  123. (prt body)
  124. (flushout))
  125. (after (list (read-resp in) in)
  126. (close out))))
  127. (def http-get (url) ; consume the headers and return the output stream
  128. (let (prot host port path) (parse-url url)
  129. (cadr (mk-http-req 'GET host path (obj Host host
  130. Connection "close") port))))
  131. ; hard drives crash, files get lost, cool URLs don't die
  132. (let _infile infile
  133. (def infile (url)
  134. (if (begins (downcase url) "http://")
  135. (http-get url)
  136. (_infile url)))
  137. )
  138. ; arc> (filechars "http://www.faqs.org/rfcs/rfc2616.html")
  139. ; arc> (load "http://hacks.catdancer.ws/json.arc")
  140. ;; todo:
  141. ; * http-ok+ & co: remove the "+"? "*"?
  142. ; not sure about "httpd" too. at least rename 'httpd-serve to 'serve-http?
  143. ;
  144. ; * handle file uploads
  145. ;
  146. ; * deal with user@pwd in 'parse-url
  147. ;
  148. ; * actually wrong to use a table for httpd-hds*: it's legal to use the
  149. ; same header twice. normally should be not break to change to an assoc
  150. ; list ('pr-headers would still work). should make it.
  151. ;
  152. ; * maybe make it event-based or rewrite Arc to have a sane, really
  153. ; lightweight threading facility Ă  la Erlang