git.fiddlerwoaroof.com
Raw Blame History
;;;; 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)))

(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)

(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))))