git.fiddlerwoaroof.com
http-streams.lisp
df3bfbd4
 ;;;; http-streams.lisp
 
 (in-package #:http-streams)
 
 ;;; "http-streams" goes here. Hacks and glory await!
 
 (define-condition no-pipe (error)
   ())
 
 (defclass piped-object ()
   ((%pipe :reader pipe :initarg :pipe :initform (error 'no-pipe))))
 
 (defun end-line (stream)
   (princ #. (coerce '(#\return #\newline) 'string) stream))
 
 (defmacro with-line ((stream) form)
   `(prog1 ,form
      (end-line ,stream)))
 
ea876b00
 (defun lookup-status (status)
   (ecase status
     (200 "OK")
     (400 "Bad Request")
     (404 "Not Found")
     (301 "Moved Permanently")
     (302 "Moved Temporarily")
     ))
 
 (defun response (status out-stream)
   (with-line (out-stream)
     (format out-stream "HTTP/1.1 ~d ~a" status (lookup-status status)))
   out-stream)
 
df3bfbd4
 (defun request (method path out-stream)
   (with-line (out-stream)
     (format out-stream "~a ~a HTTP/1.1" method path))
   out-stream)
 
 (defun header (name value out-stream)
   (with-line (out-stream)
     (format out-stream "~a: ~a" name value))
   out-stream)
 
 (defun body (value out-stream)
   (end-line out-stream)
   (etypecase value
     (stream (loop with buffer = (make-string 1000)
 	       for end-read = (read-sequence buffer value)
 	       while (/= end-read 0)
 	       do (write-sequence buffer out-stream :end end-read)))
     (string (write-sequence value out-stream))))
 
 (defmacro define-header-function (name header)
   (alexandria:once-only (header)
     `(defun ,name (value out-stream)
        (header ,header value out-stream))))
 
 (defmacro define-header-functions (() &body definitions)
   `(progn ,@(mapcar (fw.lu:destructuring-lambda ((name header))
 		      `(define-header-function ,name ,header))
 		    definitions)))
 
 (define-header-functions ()
   (content-type "Content-Type")
   (host "Host")
   (accept "Accept")
   (pragma "Pragma")
   (user-agent "User-Agent")
   (cache-control "Cache-Control")
   (accept-encoding "Accept-Encoding")
   (accept-language "Accept-Language"))
 
 (defmacro /> (name (&rest args)  &body funs)
   (alexandria:with-gensyms (out)
     `(defun ,name (,@args ,out)
        ,@(mapcar (serapeum:op `(,@_ ,out))
 		 funs))))
 
 (/> build-subreddit-request (method subreddit)
   (request method (format nil "/r/~a.rss" subreddit))
   (accept "text/html")
   (accept-language "en-US,en;q=0.8")
   (cache-control "no-cache")
   (host "www.reddit.com")
   (pragma "no-cache")
   (header "Upgrade-Insecure-Requests" "1")
   (user-agent "foobar 0.0")
   (body ""))
 
 (defun read-in-header (stream)
   (values (read-line stream)
 	  (loop with headers = ()
 	     for line = (read-line stream nil)
 	     while line
 	     until (string= line "
")
 	     do
 	       (push (map 'list 'metatilities:strip-whitespace
 			  (fwoar.string-utils:split #\: (metatilities:strip-whitespace line) :count 2))
 		     headers)
 	     finally
 	       (return (nreverse headers)))))
 
 (defmacro with-subreddit-request ((subreddit stream) &body body)
   (alexandria:with-gensyms (socket sock-stream ssl-stream)
     `(usocket:with-client-socket (,socket ,sock-stream "reddit.com" 443 :element-type '(unsigned-byte 8))
        (let* ((,ssl-stream (cl+ssl:make-ssl-client-stream ,sock-stream :hostname "reddit.com"))
 	      (,stream (flexi-streams:make-flexi-stream ,ssl-stream :external-format :iso-8859-1)))
 	 (build-subreddit-request :GET ,subreddit ,stream)
 	 (finish-output ,stream)
 	 (unwind-protect (progn ,@body)
 	   (close ,ssl-stream))))))
 
 (defun get-header (name headers)
   (cadr (assoc name headers :test 'equalp)))
 
 (defun get-content-length (headers)
   (parse-integer (get-header "Content-Length" headers)))
 
 (defmacro with-parsed-response (char-stream (result headers reply-buf content-length) &body body)
   (alexandria:once-only (char-stream)
     `(multiple-value-bind (,result ,headers) (read-in-header ,char-stream)
        (let* ((,reply-buf (make-string (get-content-length ,headers)))
 	      (,content-length (read-sequence ,reply-buf ,char-stream)))
 	 ,@body))))
 
 (defun get-subreddit-feed (&optional (subreddit "lisp"))
   (with-subreddit-request (subreddit char-stream)
     (with-parsed-response char-stream (result headers reply-buf content-length)
       (values reply-buf
 	      result
 	      headers
 	      content-length))))