;;; www/http.scm --- HTTP client library for Guile ;; Copyright (C) 1997,2001,2002 Free Software Foundation, Inc. ;; ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation; either version 2, or (at your option) ;; any later version. ;; ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; ;; You should have received a copy of the GNU General Public License ;; along with this software; see the file COPYING. If not, write to ;; the Free Software Foundation, Inc., 59 Temple Place, Suite 330, ;; Boston, MA 02111-1307 USA ;; ;;; Commentary: ;; This module exports the following variables and procedures: ;; http:version ;; http:user-agent ;; (http:message-version msg) ;; (http:message-status-code msg) ;; (http:message-status-text msg) ;; (http:message-status-ok? msg) ;; (http:status-ok? status) ;; (http:message-body msg) ;; (http:message-headers msg) ;; (http:message-header header msg) ;; (http:get url) ;; (http:open host . args) ;; (http:request method url . args) ;;; Code: (define-module (www http) :use-module (www url) :use-module (ice-9 regex)) ;;; Compatibility (or (defined? 'read-line) (use-modules (ice-9 rdelim))) ;;; Variables that affect HTTP usage. (define-public http:version "HTTP/1.0") ; bump up to 1.1 when ready (define-public http:user-agent "GuileHTTP 0.1") ;; An HTTP message is represented by a vector: ;; #(VERSION STATUS-CODE STATUS-TEXT HEADERS BODY) ;; ;; Each of VERSION, STATUS-CODE, STATUS-TEXT are strings. HEADERS ;; is an alist of headers and their contents. BODY is a single string. (define (http:make-message version statcode stattext headers body) (vector version statcode stattext headers body)) ;;;; HTTP status predicates. ;; (http:message-version MSG) ;; Returns the HTTP version in use in HTTP message MSG. ;; ;; (http:message-status-code MSG) ;; Returns the status code returned in HTTP message MSG. ;; ;; (http:message-status-text MSG) ;; Returns the text of the status line from HTTP message MSG. ;; ;; (http:message-status-ok? STATUS) ;; Returns #t if status code STATUS indicates a successful request, ;; #f otherwise. (define-public (http:message-version msg) (vector-ref msg 0)) (define-public (http:message-status-code msg) (vector-ref msg 1)) (define-public (http:message-status-text msg) (vector-ref msg 2)) (define-public (http:message-status-ok? msg) (http:status-ok? (http:status-code msg))) (define-public (http:status-ok? status) (char=? #\2 (string-ref status 0))) (define-public (http:message-body msg) (vector-ref msg 4)) ;; HTTP response headers functions ;; ;; An HTTP message header is represented here by a pair. The CAR is a ;; symbol representing the header name, and the CDR is a string ;; containing the header text. E.g.: ;; ;; '((date . "Thu, 29 May 1997 23:48:27 GMT") ;; (server . "NCSA/1.5.1") ;; (last-modified . "Tue, 06 May 1997 18:32:03 GMT") ;; (content-type . "text/html") ;; (content-length . "8097")) ;; ;; Note: these symbols are all lowercase, although the original headers ;; were mixed-case. Clients using this library should keep this in ;; mind, since Guile symbols are case-sensitive. ;; ;; FIXME: should headers with known semantics be parsed automatically? ;; I.e. should the Content-Length header automatically get string->number? ;; Should Date and Last-Modified headers be run through strptime? ;; It is advantageous to keep headers in a uniform format, but it may ;; be convenient to parse headers that have unambiguous meanings. ;; ;; (http:message-headers MSG) ;; Returns a list of the headers from HTTP message MSG. ;; (http:message-header HEADER MSG) ;; Return the header field named HEADER from HTTP message MSG, or ;; #f if no such header is present in the message. (define-public (http:message-headers msg) (vector-ref msg 3)) (define-public (http:message-header header msg) (http:fetch-header header (http:message-headers msg))) (define (http:fetch-header header header-alist) (assq-ref header-alist header)) (define header-regex (make-regexp ": *")) (define (http:header-parse hd) (let ((match (regexp-exec header-regex hd))) (cons (string->symbol (apply string (map char-downcase (string->list (match:prefix match))))) (match:suffix match)))) (define (parse-status-line statline) (let* ((first (string-index statline #\space)) (second (string-index statline #\space (1+ first)))) (list (make-shared-substring statline 0 first) (make-shared-substring statline (1+ first) second) (make-shared-substring statline (1+ second))))) ;;; HTTP connection management functions. ;; Open connections are cached on hostname in the connection-table. ;; If an HTTP connection is already open to a particular host and TCP port, ;; looking up the hostname and port number in connection-table will yield ;; a Scheme port that may be used to communicate with that server. (define connection-table '()) ;; FIXME: you can only re-use a connection if the server sends the ;; Keep-Alive header, I think. With these definitions, we were trying to ;; send more requests on connections the server assumed were dead. ;; (define (add-open-connection! host tcp-port port) ;; (set! connection-table ;; (assoc-set! connection-table (cons host tcp-port) port))) ;; (define (get-open-connection host tcp-port) ;; (assoc-ref connection-table (cons host tcp-port))) (define (add-open-connection! host tcp-port port) #f) (define (get-open-connection host tcp-port) #f) ;;; HTTP methods. ;; Common methods: GET, POST etc. (define-public (http:get url) ;; FIXME: if http:open returns an old connection that has been ;; closed remotely, this will fail. (http:request "GET" url)) ;; Connection-oriented functions: ;; ;; (http:open HOST [PORT]) ;; Return an HTTP connection to HOST on TCP port PORT (default 80). ;; If an open connection already exists, use it; otherwise, create ;; a new socket. (define-public (http:open host . args) (let ((port (cond ((null? args) 80) ((not (car args)) 80) (else (car args))))) (or (get-open-connection host port) (let* ((tcp (vector-ref (getproto "tcp") 2)) (addr (car (vector-ref (gethost host) 4))) (sock (socket AF_INET SOCK_STREAM tcp))) (connect sock AF_INET addr port) (add-open-connection! host port sock) sock)))) ;; (http:request METHOD URL [HEADERS [BODY]]) ;; Submit an HTTP request. ;; URL is a structure returned by url:parse. ;; METHOD is the name of some HTTP method, e.g. "GET" or "POST". ;; The optional HEADERS and BODY arguments are lists of strings ;; which describe HTTP messages. The `Content-Length' header ;; is calculated automatically and should not be supplied. ;; ;; Example usage: ;; (http:request "get" parsed-url ;; (list "User-Agent: GuileHTTP 0.1" ;; "Content-Type: text/plain")) ;; (http:request "post" parsed-url ;; (list "User-Agent: GuileHTTP 0.1" ;; "Content-Type: unknown/x-www-form-urlencoded") ;; (list "search=Gosper" ;; "case=no" ;; "max_hits=50")) (define-public (http:request method url . args) (let ((host (url:host url)) (tcp-port (or (url:port url) 80)) (path (format #f "/~A" (or (url:path url) "")))) (let ((sock (http:open host tcp-port)) (request (format #f "~A ~A ~A" method path http:version)) (headers (if (pair? args) (car args) '())) (body (if (and (pair? args) (pair? (cdr args))) (cadr args) '()))) (let* ((content-length (apply + (map (lambda (line) (+ 2 (string-length line))) ; + 2 for CRLF body))) (headers (if (positive? content-length) (cons (format #f "Content-Length: ~A" content-length) headers) headers))) (with-output-to-port sock (lambda () (display-with-crlf request) (for-each display-with-crlf headers) (display "\r\n") (for-each display-with-crlf body))) ;; parse and add status line ;; also cons up a list of response headers (let* ((response-status-line (sans-trailing-whitespace (read-line sock 'trim))) (response-headers (let make-header-list ((ln (sans-trailing-whitespace (read-line sock 'trim))) (hlist '())) (if (= 0 (string-length ln)) hlist (make-header-list (sans-trailing-whitespace (read-line sock 'trim)) (cons (http:header-parse ln) hlist))))) (response-status-fields (parse-status-line response-status-line)) (response-version (car response-status-fields)) (response-code (cadr response-status-fields)) (response-text (caddr response-status-fields))) ;; signal error if HTTP status is invalid ;; (or (http:status-ok? response-code) ;; (error 'http-status "HTTP server returned bad status" ;; response-status-line)) ;; Get message body: if Content-Length header was supplied, read ;; that many chars. Otherwise, read until EOF (let ((content-length (http:fetch-header "content-length" response-headers))) (let ((response-body (if (and content-length (not (string-ci=? method "HEAD"))) (read-n-chars (string->number content-length) sock) (with-output-to-string (lambda () (while (not (eof-object? (peek-char sock))) (display (read-char sock)))))))) ;; FIXME: what about keepalives? (close-port sock) (http:make-message response-version response-code response-text response-headers response-body)))))))) ;;; System interface cruft & string funcs (define (read-n-chars num . port-arg) (let ((p (if (null? port-arg) (current-input-port) (car port-arg))) (s (make-string num))) (do ((i 0 (+ i 1)) (ch (read-char p) (read-char p))) ((or (>= i num) (eof-object? ch)) s) (string-set! s i ch)))) (define (display-with-crlf line . p) (apply display line p) (apply display "\r\n" p)) ;; (sans-trailing-whitespace STR) ;; These are defined in module (ice-9 string-fun), so this code ;; will prob. be discarded when the module system and boot-9 ;; settle down. (define (sans-trailing-whitespace s) (let ((st 0) (end (string-length s))) (while (and (< 0 end) (char-whitespace? (string-ref s (1- end)))) (set! end (1- end))) (if (< end st) "" (make-shared-substring s st end)))) ;;; www/http.scm ends here