<?xml version="1.0"?>
<paste-with-annotations>
  <paste>
    <number>
      <integer>12803</integer>
    </number>
    <user>
      <string>brianr</string>
    </user>
    <title>
      <string>TRIVIAL-HTTP with user defined headers</string>
    </title>
    <contents>
      <string>(defpackage :trivial-http
  (:use :cl :trivial-sockets)
  (:nicknames :thttp)
  (:export :http-get :http-post :escape-url-query))
(in-package :trivial-http)

(defparameter *user-agent* &quot;Trivial HTTP for Common Lisp&quot;)

(defun url-port (url)
  (assert (string-equal url &quot;http://&quot; :end1 7))
  (let ((path-start (position #\/ url :start 7)))
    (let ((port-start (position #\: url :start 7 :end path-start)))
      (if port-start (parse-integer url :start (1+ port-start) :junk-allowed t) 80))))

(defun url-host (url)
  (assert (string-equal url &quot;http://&quot; :end1 7))
  (let* ((port-start (position #\: url :start 7))
	 (host-end (min (or (position #\/ url :start 7) (length url))
			(or port-start (length url)))))
    (subseq url 7 host-end)))

(defconstant +crlf+
  (if (boundp '+crlf+)
      (symbol-value '+crlf+)
      (concatenate 'string
                   (string (code-char 13))
                   (string (code-char 10)))))

(defun response-read-code (stream)
  (let* ((l (read-line stream))
            (space (position #\Space l)))
       (parse-integer l :start (1+ space) :junk-allowed t)))

(defun response-read-headers (stream)
  (loop for line = (read-line stream nil nil)
     until (or (eql (length line) 0)
	       (eql (elt line 0) (code-char 13))
	       (eql (elt line 0) (code-char 10)))
     collect
       (let ((colon (position #\: line)))
	 (cons (intern (string-upcase (subseq line 0 colon)) :keyword)
	       (string-trim (list #\Space (code-char 13) (code-char 10))
			    (subseq line (1+ colon)))))))

(defun http-get (url &amp;key headers connection)
  &quot;headers is an alist&quot;
  (let* ((host (url-host url))
         (port (url-port url))
	 (http-headers (add-headers
			`((&quot;Host&quot; . ,host) (&quot;User-Agent&quot; . ,*user-agent*))
			headers :overwrite nil))
	 (stream (if connection
		     connection
		     (open-stream host port))))

    ;; Send the request
    (format stream &quot;GET ~A HTTP/1.0~A&quot; url +crlf+)
    (write-headers http-headers stream)
    (format stream &quot;~A&quot; +crlf+)	; write-headers writes a trailing +crlf+
    (force-output stream)

    ;; Read in the response
    (list
     (response-read-code stream)
     (response-read-headers stream)
     stream)))

(defun http-post (url content-type content &amp;key headers connection)
  (let* ((host (url-host url))
         (port (url-port url))
	 (http-headers (add-headers
			`((&quot;Host&quot; . ,host) (&quot;User-Agent&quot; . ,*user-agent*)
			  (&quot;Content-Type&quot; . ,content-type)
			  (&quot;Content-Length&quot; . ,(length content)))
			headers :overwrite nil))
         (stream (if connection
		     connection
		     (open-stream host port))))
    
    ;; Send the request
    (format stream &quot;POST ~A HTTP/1.0~A&quot; url +crlf+)
    (write-headers http-headers stream)
    (format stream &quot;~A&quot; +crlf+) ; write-headers writes a trailing +crlf+
    (format stream &quot;~A&quot; content)
    (force-output stream)

    ;; Read in the response
    (list
     (response-read-code stream)
     (response-read-headers stream)
     stream)))

;; this next method stolen from Araneida

(defun url-reserved-character-p (c)
  (not (or (member c '(#\- #\_ #\. #\! #\~ #\* #\' #\( #\) ))
           (alphanumericp c))))

(defun escape-url-query (query)
  (apply #'concatenate 'string
   (loop for c across query
         if (url-reserved-character-p c)
         collect (format nil &quot;%~2,'0X&quot; (char-code c))
         else
         collect (string c))))

(defun write-headers (headers stream)
  (dolist (header headers)
    (format stream &quot;~A: ~A~A&quot; (car header) (cdr header) +crlf+)))

(defun add-headers (new-headers header-list &amp;key (overwrite t))
  (dolist (header new-headers)
    (if (assoc (first header) header-list :test #'string=)
	(when overwrite
	  (setf (cdr (assoc (car header) header-list :test #'string=))
		(cdr header)))
	(push header header-list)))
  header-list)</string>
    </contents>
    <universal-time>
      <integer>3339119734</integer>
    </universal-time>
    <channel>
      <string>None</string>
    </channel>
    <colorization-mode>
      <string>Common Lisp</string>
    </colorization-mode>
    <maybe-spam>
      <null/>
    </maybe-spam>
    <is-unicode>
      <null/>
    </is-unicode>
  </paste>
</paste-with-annotations>