/usr/share/gauche-0.9/0.9.5/lib/rfc/http.scm is in gauche 0.9.5-1build1.
This file is owned by root:root, with mode 0o644.
The actual contents of the file can be viewed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 | ;;;
;;; http.scm - HTTP 1.1
;;;
;;; Copyright (c) 2000-2016 Shiro Kawai <shiro@acm.org>
;;;
;;; Redistribution and use in source and binary forms, with or without
;;; modification, are permitted provided that the following conditions
;;; are met:
;;;
;;; 1. Redistributions of source code must retain the above copyright
;;; notice, this list of conditions and the following disclaimer.
;;;
;;; 2. Redistributions in binary form must reproduce the above copyright
;;; notice, this list of conditions and the following disclaimer in the
;;; documentation and/or other materials provided with the distribution.
;;;
;;; 3. Neither the name of the authors nor the names of its contributors
;;; may be used to endorse or promote products derived from this
;;; software without specific prior written permission.
;;;
;;; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
;;; "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
;;; LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
;;; A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
;;; OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
;;; SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
;;; TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
;;; PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
;;; LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
;;; NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
;;; SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
;;;
;; HTTP handling routines.
;; RFC2616 Hypertext Transfer Protocol -- HTTP/1.1
;; http://www.ietf.org/rfc/rfc2616.txt
;; RFC2617 HTTP Authentication: Basic and Digest Access Authentication
;; http://www.ietf.org/rfc/rfc2617.txt
;; RFC2388 Returning Values from Forms: multipart/form-data
;; http://www.ietf.org/rfc/rfc2388.txt
;; HTTP 1.1 has lots of features. This module doesn't yet cover all of them.
;; The features required for typical client usage are implemented first.
(define-module rfc.http
(use srfi-11)
(use srfi-13)
(use rfc.822)
(use rfc.uri)
(use rfc.base64)
(use gauche.net)
(use gauche.parameter)
(use gauche.charconv)
(use gauche.sequence)
(use gauche.uvector)
(use util.match)
(use text.tree)
(export <http-error>
http-user-agent make-http-connection reset-http-connection
http-compose-query http-compose-form-data
http-status-code->description
http-proxy http-request
http-null-receiver http-string-receiver http-oport-receiver
http-file-receiver http-cond-receiver
http-null-sender http-string-sender http-blob-sender
http-file-sender http-multipart-sender
http-get http-head http-post http-put http-delete
http-default-auth-handler
http-default-redirect-handler
http-secure-connection-available?
)
)
(select-module rfc.http)
;;
(autoload rfc.mime
mime-compose-message
mime-compose-message-string
mime-compose-parameters
mime-parse-content-type)
(autoload rfc.tls
make-tls tls-destroy tls-connect tls-input-port tls-output-port tls-close)
(autoload file.util file-size find-file-in-paths null-device)
;;==============================================================
;; Conditions
;;
(define-condition-type <http-error> <error> #f)
;;==============================================================
;; Global parameters
;;
;; default string to be used for user-agent.
(define http-user-agent
(make-parameter #"gauche.http/~(gauche-version)"))
;; global proxy settings. can be overridden by :proxy keyword
;; argument.
(define http-proxy (make-parameter #f))
;; The default redirect handler
;;
(define http-default-redirect-handler
(make-parameter
(^[method code headers body]
(and-let* ([loc (rfc822-header-ref headers "location")])
(case (x->integer code)
[(300 301 305 307)
(case method [(GET HEAD) `(,method . ,loc)] [else #f])]
[(302 303) ;found / see other
;; See rfc2616 notes - the agent isn't supposed to automatically
;; redirect 302 response of POST, but redirect with GET for it is
;; the de-facto behavior.
(case method
[(GET HEAD) `(,method . ,loc)]
[else `(GET . ,loc)])]
[else #f])))))
;;==============================================================
;; Higher-level API
;;
;; Higher-level API is for conventional call-return API.
;;
;; The METHOD argument specifies the http request method by a symbol.
;; GET, HEAD, POST, PUT and DELETE is currently supported.
;;
;; The SERVER argument maybe a string naming the server (and
;; optionally a port number by the format of "server:port"), or
;; an <http-connection> object. Using a server name is suitable
;; for easy one-shot http access; the connection and related states
;; are discarded once the procedure returns.
;; On the other hand, a connection object can keep the states such
;; as persistent connection and authentication tokens, suitable for
;; a series of communications to a server.
;;
;; The REQUEST-URI argument can be a string as specified in RFC2616,
;; or a list in the form of (<path> (<name> <value>) ...). In the
;; latter form, (<name> <value>) assoc list is converted into a
;; url query form as defined in HTML4 (application/x-www-form-urlencoded)
;; and appended to <path>.
;;
;; The keyword arguments are handled by various low-level routines,
;; and here's the summary:
;;
;; request-body - gives the body.
;; receiver - A procedure that handles the response.
;; It takes four arguments: response-code, response headers,
;; total response size if known (#f otherwise), and
;; a retriever thunk. The retriever thunk is, when called,
;; returns two values: an input port and an integer size.
;; The receiver must loop (1) to read as many bytes as
;; specified in the size from the input port, and (2)
;; call the retriver again, while it returns positive integer.
;; The retriever procedure returns zero when body is exhausted.
;; In that case the value(s) the receiver returns will be
;; the return value of http-request.
;; The retriever thunk can return #f as size, if we don't know the
;; chunk of response size. In that case, the receiver may read
;; up to EOF, or as much data as it wants, then call the
;; retriever. The second time the retriever returns size=0,
;; so you can do cleanup work.
;; The retriever procedure returns -1 if an error occurs
;; during comminucation. In that case, the receiver can do
;; whatever cleanup work. After the receiver returns, an
;; appropriate error is thrown from http-request.
;;
;; sender - A procedure that handles sending the request body.
;; It takes three arguments: tantative list of headers,
;; the value given to :request-encoding argument,
;; and a procedure HEADER-SINK.
;;
;; HEADER-SINK takes one argument, a list of http headers
;; in the form of (("field-name" "field-value") ...).
;; In general, besides the given tentative headers, the sender
;; needs to add either Content-Length header if it sends entire
;; contents at once, or ("transfer-encoding" "chunked") if
;; it uses chunked transfer. If neither headers exist in the
;; given header list, an error is raised and request is aborted.
;; Sender can add other headers, e.g. content-type or mime-type.
;; Sender can also remove or modify given tentative headers.
;;
;; HEADER-SINK returns a procedure BODY-SINK on success.
;; BODY-SINK takes an integer SIZE argument and returns an
;; output PORT. What the sender needs to do is to call
;; BODY-SINK with the size of data chunk, then to write out
;; the data into the returned PORT, and repeat it until
;; all the data is sent. To indicate the end of data,
;; the sender needs to call BODY-SINK with argument 0.
;;
;; host - the host name passed to the 'host' header field.
;; secure - if true, using secure connection (via gauche.tls).
;; auth-user, auth-password, auth-handler - authentication parameters.
;; request-encoding - when http-* is to construct request-uri and/or
;; request body, this argument specifies the character encoding
;; to be used as the external encoding.
;; redirect-handler - Called if the server responds with 3xx status.
;; The argument is the request method, a status code, list of
;; headers and response body (can be #f). It may return a
;; (METHOD . URL) or #f. For the first case,
;; http-request re-attempts to fetch the URL with the given method.
;; (unless we're not looping). If it returns #f, the original
;; code, headers and body are returned from http-request.
;; If given #t (default), the procedure bound to the parameter
;; http-default-redirect-handler is called.
;; no-redirect - If true, we ignore the value of redirect-handler and
;; returns without attempting retrying.
;; This is provided for the backward compatibility; newer code
;; should use :redirect-handler #f
;;
;; Other unrecognized options are passed as request headers.
(define (http-request method server request-uri
:key (host #f)
(redirect-handler #t)
(no-redirect #f)
auth-handler
auth-user
auth-password
(proxy (http-proxy))
extra-headers
(user-agent (http-user-agent))
(secure #f)
(receiver (http-string-receiver))
(sender #f)
((:request-encoding enc) (gauche-character-encoding))
:allow-other-keys opts)
(define conn (ensure-connection server auth-handler auth-user auth-password
proxy secure extra-headers))
(define redirector (if no-redirect
#f
(case redirect-handler
[(#t) (http-default-redirect-handler)]
[(#f) #f]
[else => identity])))
(define options `(:user-agent ,user-agent ,@(http-auth-headers conn) ,@opts))
(define no-body-replies '("204" "304"))
(define (get-body iport method code headers receiver)
(and (not (eq? method 'HEAD))
(not (member code no-body-replies))
(receive-body iport code headers receiver)))
;; final touch of request headers
(define (req-headers host)
(cond-list [(~ conn'persistent) @ (if (~ conn'proxy)
'(:proxy-connection keep-alive)
'(:connection keep-alive))]
[#t @ `(:host ,host :user-agent ,user-agent
,@(http-auth-headers conn) ,@opts)]))
;; If we decide to give up redirection, we read from already-retrieved
;; body of 3xx reply. This modifies reply headers if necessary.
(define (redirect-headers body rep-headers)
(if body
`(:content-length ,(string-size body)
,@(delete-keywords '(:content-length
:content-transfer-encoding)
rep-headers))
rep-headers))
;; returns either one of:
;; (reply <code> <headers> <body>)
;; (redirect-to <method> <location>)
(define (request-response in out method uri host sender)
(send-request out method uri sender (req-headers host) enc)
(receive (code rep-headers) (receive-header in)
(if-let1 consider-redirect (and (string-prefix? "3" code) redirector)
;; we retrieve body as string, not using caller-provided receiver
(let* ([body (get-body in method code rep-headers
(http-string-receiver))]
[verdict (consider-redirect method code rep-headers body)])
(if verdict
`(redirect-to ,(car verdict) ,(cdr verdict))
(let1 hdrs (redirect-headers body rep-headers)
`(reply ,code ,hdrs
,(and body
(receive-body (open-input-string body) code
hdrs receiver))))))
;; no redirection
`(reply ,code ,rep-headers
,(get-body in method code rep-headers receiver)))))
;; main loop
(let loop ([history '()]
[host host]
[method method]
[request-uri (ensure-request-uri request-uri enc)])
(receive (host uri)
(consider-proxy conn (or host (~ conn'server)) request-uri)
(let1 result
(with-connection
conn
(^[i o] (request-response i o method uri host sender)))
(match result
[('reply code rep-headers body) (values code rep-headers body)]
[('redirect-to method location)
(receive (uri proto new-server path*)
(canonical-uri conn location (ref conn'server))
(when (or (member uri history)
(> (length history) 20))
(errorf <http-error> "redirection is looping via ~a" uri))
(loop (cons uri history)
(~ (redirect-connection! conn proto new-server)'server)
method path*))])))))
;;
;; Pre-defined receivers
;;
(define (http-string-receiver)
(^[code hdrs total retr]
;; TODO: check headers for encoding
(let loop ([sink (open-output-string)])
(receive (remote size) (retr)
(cond [(eqv? size 0) (get-output-string sink)]
[(or (not size) (> size 0))
(copy-port remote sink :size size) (loop sink)])))))
(define (http-null-receiver)
(^[code hdrs total retr]
(let loop ([sink (open-output-file (null-device))])
(receive (remote size) (retr)
(cond [(and size (<= size 0)) (close-output-port sink)]
[else (copy-port remote sink :size size) (loop sink)])))))
(define (http-oport-receiver sink flusher)
(^[code hdrs total retr]
(let loop ()
(receive (remote size) (retr)
(cond [(and size (<= size 0)) (flusher sink hdrs)]
[else (copy-port remote sink :size size) (loop)])))))
(define (http-file-receiver filename :key (temporary #f))
(^[code hdrs total retr]
(receive (port tmpname) (sys-mkstemp filename)
(let loop ()
(receive (remote size) (retr)
(cond [(or (not size) (> size 0))
(copy-port remote port :size size) (loop)]
[(= size 0)
(close-output-port port)
(if temporary
tmpname
(begin (sys-rename tmpname filename) filename))]
[else (close-output-port port) (sys-unlink tmpname)]))))))
(define-syntax http-cond-receiver
(syntax-rules (else =>)
[(_) (http-null-receiver)]
[(_ [else . exprs]) (begin . exprs)]
[(_ [cc => proc] . rest)
(^[code hdrs total retr]
((if (match-status-code? cc code)
proc
(http-cond-receiver . rest))
code hdrs total retr))]
[(_ [cc . exprs] . rest)
(^[code hdrs total retr]
((if (match-status-code? cc code '(cc . exprs))
(begin . exprs)
(http-cond-receiver . rest))
code hdrs total retr))]
[(_ other . rest)
(syntax-error "invalid clause in http-cond-receiver" other)]))
(define (match-status-code? pattern code clause)
(cond [(string? pattern) (equal? pattern code)]
[(regexp? pattern) (rxmatch pattern code)]
[else (error "Invalid pattern in a clause of http-cond-receiver:"
clause)]))
;;
;; Senders
;;
(define (http-null-sender)
(^[hdrs encoding header-sink]
(let1 body-sink (header-sink `(("content-length" "0") ,@hdrs))
(body-sink 0))))
(define (http-string-sender string) ;honors encoding
(^[hdrs encoding header-sink]
(let* ([body (if (ces-equivalent? encoding (gauche-character-encoding))
string
(ces-convert string (gauche-character-encoding) encoding))]
[size (string-size body)]
[body-sink (header-sink `(("content-length" ,(x->string size))
,@hdrs))]
[oport (body-sink size)])
(display body oport)
(body-sink 0))))
(define (http-blob-sender blob) ;blob may be a string or uvector
(^[hdrs encoding header-sink]
(let* ([size (if (string? blob) (string-size blob) (uvector-size blob))]
[body-sink (header-sink `(("content-length" ,(x->string size))
,@hdrs))]
[port (body-sink size)])
(if (string? blob)
(display blob port)
(write-block blob port))
(body-sink 0))))
;; Send contents directly from the file. Encoding is ignored.
;; TODO: The file size may be changed while sending out. If it gets
;; bigger, we can just ignore the rest, but what if it gets shorter?
(define (http-file-sender filename)
(^[hdrs encoding header-sink]
(let* ([size (file-size filename)]
[body-sink (header-sink `(("content-length" ,(x->string size))
,@hdrs))]
[port (body-sink size)])
(call-with-input-file filename (cut copy-port <> port :size size))
(body-sink 0))))
;; See http-compose-form-data definition for params spec.
;; TODO: support chunked sending, instead of building entire body at once.
(define (http-multipart-sender params)
(^[hdrs encoding header-sink]
(receive (body boundary) (http-compose-form-data params #f encoding)
(let* ([size (string-size body)]
[hdrs `(("content-length" ,(x->string size))
("mime-version" "1.0")
("content-type" ,#"multipart/form-data; boundary=\"~|boundary|\"")
,@(alist-delete "content-type" hdrs equal?))]
[body-sink (header-sink hdrs)]
[port (body-sink size)])
(display body port)
(body-sink 0)))))
;;
;; Shortcuts for specific requests.
;;
(define (http-get server request-uri . options)
(apply %http-request-adaptor 'GET server request-uri #f options))
(define (http-head server request-uri . options)
(apply %http-request-adaptor 'HEAD server request-uri #f options))
(define (http-post server request-uri body . options)
(apply %http-request-adaptor 'POST server request-uri body options))
(define (http-put server request-uri body . options)
(apply %http-request-adaptor 'PUT server request-uri body options))
(define (http-delete server request-uri . options)
(apply %http-request-adaptor 'DELETE server request-uri #f options))
;; Adaptor to the new API. Converts :sink and :flusher arguments,
;; which are superseded by :receiver arguments.
(define (%http-request-adaptor method server request-uri body
:key (receiver #f) (sink #f) (flusher #f)
:allow-other-keys opts)
(define recvr
(cond [(and sink flusher)
(http-oport-receiver sink flusher)]
[(or sink flusher)
(errorf "You need to provide :sink and :flusher together to http-~a"
(string-downcase (symbol->string method)))]
[receiver]
;; fallback
[else (http-oport-receiver (open-output-string)
(^[s h] (get-output-string s)))]))
(apply http-request method server request-uri
:sender (cond [(not body) (http-null-sender)]
[(list? body) (http-multipart-sender body)]
[else (http-blob-sender body)])
:receiver recvr opts))
;;==============================================================
;; HTTP connection context
;;
;; <http-connection> object can be used to have conversations with
;; (usually) a specific server. A typical usage is to emulate a
;; browser to perform a certain transaction spanning to several
;; webpages. (For the simple "one-shot" request-response access,
;; the rfc.http APIs create a temporary connection under the hood.)
(define-class <http-connection> ()
;; All slots are private.
((server :init-keyword :server) ; server[:port]
(socket :init-value #f) ; A <socket> for persistent connection.
; If it is shutdown by the server,
; the APIs attempt to reconnect.
(secure-agent :init-value #f) ; When using secure connection via
; tls, this slot holds its handle.
(persistent :init-keyword :persistent) ; true for persistent connection.
(auth-handler :init-keyword :auth-handler :init-value #f)
(auth-user :init-keyword :auth-user :init-value #f)
(auth-password :init-keyword :auth-password :init-value #f)
(proxy :init-keyword :proxy)
(extra-headers :init-keyword :extra-headers)
(secure :init-keyword :secure) ; boolean
))
(define (make-http-connection server :key
(persistent #t)
(auth-handler #f)
(auth-user #f)
(auth-password #f)
(proxy #f)
(extra-headers '()))
(make <http-connection>
:persistent persistent
:server server
:auth-handler (or auth-handler (http-default-auth-handler))
:auth-user auth-user
:auth-password auth-password
:proxy proxy
:extra-headers extra-headers))
;; This modifies CONN.
(define (redirect-connection! conn proto new-server)
(let1 orig-server (~ conn'server)
(unless (and (string=? orig-server new-server)
(eq? (~ conn'secure) (equal? proto "https")))
(shutdown-secure-agent conn)
(and-let* ([s (~ conn'socket)])
(socket-shutdown s)
(socket-close s)
(set! (~ conn'socket) #f))
(set! (~ conn'server) new-server)
(set! (~ conn'secure) (equal? proto "https"))))
conn)
;;==============================================================
;; query and request body composition
;;
;; Query string composition.
;; NB: Query string syntax (aka application/x-www-form-urlencoded) is
;; not defined in RFC2616. In fact, it is not clearly defined at all
;; in RFC. The most definitive source might be the HTML4 specification,
;; section 17.13.4 "Form content types",
;; <http://www.w3.org/TR/html4/interact/forms.html#h-17.13.4.1>.
;; In reality it is used very frequently with http, so we put it here.
(define (http-compose-query path params
:optional (encoding (gauche-character-encoding)))
(define (esc s) (uri-encode-string (x->string s) :encoding encoding))
(define (query-1 n&v)
(match n&v
[(name value) #"~(esc name)=~(esc value)"]
[_ (error "Invalid request-uri form:" params)]))
(define (query) (string-concatenate (intersperse "&" (map query-1 params))))
(cond [(not path) (query)]
[(null? params) path]
[else #"~|path|?~(query)"]))
;; multipart/form-data composition [RFC2388]
;; <params> : (<param> ...)
;; <param> : (<name> <value>) ; same as http-compose-query
;; | (<name> <key> <value> <key2> <value2> ...)
;; <key> : :value | :file | :content-type | :content-transfer-encoding
;; | other keywords (used as a header name)
;; composed message is put to the current output port.
;; returns the boundary string.
(define (http-compose-form-data params port
:optional (encoding (gauche-character-encoding)))
(define (translate-param param)
(match param
[(name value) (translate-param `(,name :value ,value))]
[(name . kvs)
(unless (even? (length kvs))
(error "Invalid parameter format to create multipart/form-data:" param))
(let-keywords kvs ([value ""]
[file #f]
[content-type #f]
[content-transfer-encoding #f] . other-keys)
`(,(canonical-content-type (mime-parse-content-type content-type)
value file)
(("content-transfer-encoding" ,(or content-transfer-encoding "binary"))
("content-disposition" ,(make-content-disposition name file))
,@(map (cut map x->string <>) (slices other-keys 2)))
,(if file `(file ,file) (x->string value))))]))
(define (canonical-content-type ct value file)
(match ct
[#f (if (or file (string-incomplete? value))
'("application" "octet-stream")
`("text" "plain" ("charset" . ,(x->string encoding))))]
[(type subtype . options)
(if (assoc "charset" options)
ct
`(,type ,subtype ("charset" . ,(x->string encoding)) ,@options))]))
(define (make-content-disposition name file)
(with-output-to-string
(^[]
(display "form-data")
(mime-compose-parameters
`(("name" . ,name)
,@(cond-list [file `("filename" . ,file)]))))))
(if (not port)
(mime-compose-message-string (map translate-param params))
(mime-compose-message (map translate-param params) port)))
;;==============================================================
;; status codes
;;
(define *status-code-map*
(hash-table 'eqv?
'(100 . "Continue")
'(101 . "Switching Protocols")
'(200 . "OK")
'(201 . "Created")
'(202 . "Accepted")
'(203 . "Non-Authoritative Information")
'(204 . "No Content")
'(205 . "Reset Content")
'(206 . "Partial Content")
'(300 . "Multiple Choices")
'(301 . "Moved Permanently")
'(302 . "Found")
'(303 . "See Other")
'(304 . "Not Modified")
'(305 . "Use Proxy")
'(306 . "(Unused)")
'(307 . "Temporary Redirect")
'(400 . "Bad Request")
'(401 . "Unauthorized")
'(402 . "Payment Required")
'(403 . "Forbidden")
'(404 . "Not Found")
'(405 . "Method Not Allowed")
'(406 . "Not Acceptable")
'(407 . "Proxy Authentication Required")
'(408 . "Request Timeout")
'(409 . "Conflict")
'(410 . "Gone")
'(411 . "Length Required")
'(412 . "Precondition Failed")
'(413 . "Request Entity Too Large")
'(414 . "Request-URI Too Long")
'(415 . "Unsupported Media Type")
'(416 . "Requested Range Not Satisfiable")
'(417 . "Expectation Failed")
'(500 . "Internal Server Error")
'(501 . "Not Implemented")
'(502 . "Bad Gateway")
'(503 . "Service Unavailable")
'(504 . "Gateway Timeout")
'(505 . "HTTP Version Not Supported")
))
;; API
;; code can be an integer or a string, e.g. "200"
;; returns #f for unknown code
(define (http-status-code->description code)
(hash-table-get *status-code-map* (x->integer code) #f))
;;==============================================================
;; internal utilities
;;
(define (ensure-request-uri request-uri enc)
(match request-uri
[(? string?) request-uri]
[(path n&v ...) (http-compose-query path n&v enc)]
[_ (error "Invalid request-uri form for http request API:" request-uri)]))
(define (canonical-body request-body extra-headers enc)
(cond [(not request-body) (values #f extra-headers)]
[(string? request-body) (values request-body extra-headers)]
[(list? request-body)
(receive (body boundary) (http-compose-form-data request-body #f enc)
(values body
`(:mime-version "1.0"
:content-type
,#"multipart/form-data; boundary=~boundary"
,@(delete-keyword! :content-type extra-headers))))]
[else (error "Invalid request-body format:" request-body)]))
;; Always returns a connection object.
(define (ensure-connection server auth-handler auth-user auth-password
proxy secure extra-headers)
(rlet1 conn (cond
[(is-a? server <http-connection>) server]
[(string? server) (make-http-connection server :persistent #f)]
[else (error "bad type of argument for server: must be an <http-connection> object or a string of the server's name, but got:" server)])
;; TODO: Might need to reset connections if parameters are changed
(let-syntax ([check-override
(syntax-rules ()
[(_ id)
(unless (undefined? id) (set! (ref conn'id) id))])])
(check-override auth-handler)
(check-override auth-user)
(check-override auth-password)
(check-override proxy)
(check-override extra-headers)
(check-override secure))))
(define (reset-http-connection conn)
(shutdown-socket-connection conn)
(shutdown-secure-agent conn))
(define (start-socket-connection conn)
;; If address is given ipv6 format such as "[::1]:port", we have to
;; use "::1" part as the hostname, excluding [].
(define (parse-address addr)
(rxmatch-case addr
[#/^unix:(\/.*)$/ (_ path) (values #f #f path)] ;unix domain
[#/^\[([a-fA-F\d:]+)\](?::(\d+))?$/ (_ host port) (values host port #f)]
[#/^([^:]+)(?::(\d+))?$/ (_ host port) (values host port #f)]
[else (error "Unrecognized http server address:" addr)]))
(receive (host port path) (parse-address (or (~ conn'proxy) (~ conn'server)))
(set! (~ conn'socket)
(if path
(make-client-socket 'unix path)
(make-client-socket host (if port
(x->integer port)
(if (~ conn'secure) 443 80)))))))
(define (shutdown-socket-connection conn)
(when (~ conn'socket)
(guard (e [(<system-error> e) #f])
(socket-shutdown (~ conn'socket)))
(socket-close (~ conn'socket))
(set! (~ conn'socket) #f)))
(define (with-connection conn proc)
(unless (~ conn'persistent)
(unless (~ conn'socket) (start-socket-connection conn))
(when (~ conn'secure) (start-secure-agent conn)))
(unwind-protect
(apply proc (if (~ conn'secure)
`(,(tls-input-port (~ conn'secure-agent))
,(tls-output-port (~ conn'secure-agent)))
`(,(socket-input-port (~ conn'socket))
,(socket-output-port (~ conn'socket)))))
(unless (~ conn'persistent)
(when (~ conn'secure) (shutdown-secure-agent conn))
(shutdown-socket-connection conn))))
;; canonicalize uri for the sake of redirection.
;; URI is a request-uri given to the API, or the redirect location specified
;; in 3xx response. It can be a full URI or just a path w.r.t. the current
;; accessing server, so we pass the current server name as HOST in order to
;; fill the URI if necessary.
;; Returns three values; the full URI to access (it is used to detect a loop
;; in redirections), the server name, and the new request uri.
(define (canonical-uri conn uri host)
(let*-values ([(scheme specific) (uri-scheme&specific uri)]
[(h p q f) (uri-decompose-hierarchical specific)])
(let ([scheme (or scheme (if (~ conn'secure) "https" "http"))]
[host (or h host)])
(values (uri-compose :scheme scheme :host host
:path p :query q :fragment f)
scheme
(or h host)
;; drop "//"
(string-drop (uri-compose :path p :query q :fragment f) 2)))))
;; canonicalize host and uri w.r.t. proxy
(define (consider-proxy conn host uri)
(if (ref conn'proxy)
(values host (uri-compose :scheme "http" :host (ref conn'server) :path* uri))
(values host uri)))
;; send
(define (send-request out method uri sender headers enc)
(define request-line #"~method ~uri HTTP/1.1\r\n")
(define request-headers
($ map (cut map (^s (if (keyword? s) (keyword->string s) (x->string s))) <>)
$ slices headers 2))
(case method
[(POST PUT)
(sender request-headers enc
(^[hdrs]
(send-headers request-line hdrs out)
(let ([chunked?
(equal? (rfc822-header-ref hdrs "transfer-encoding")
"chunked")]
[first-time #t])
(^[size]
(when chunked?
(unless first-time (display "\r\n" out))
(format out "~x\r\n" size))
(flush out)
out))))]
[else (send-headers request-line request-headers out)]))
;; NB: We try to send the request line and headers in one packet if possible,
;; since some http servers assumes important headers can be read in single
;; read() call.
(define (send-headers request-line hdrs out)
(display (tree->string
`(,request-line
,@(map (^h `(,(car h)": ",(cadr h)"\r\n")) hdrs)
"\r\n"))
out)
(flush out))
;; receive
(define (receive-header remote)
(receive (code reason) (parse-status-line (read-line remote))
(values code (rfc822-header->list remote))))
(define (parse-status-line line)
(cond [(eof-object? line)
(error <http-error> "http reply contains no data")]
[(#/\w+\s+(\d\d\d)\s+(.*)/ line) => (^m (values (m 1) (m 2)))]
[else (error <http-error> "bad reply from server" line)]))
(define (receive-body remote code headers receiver)
(let1 total (and-let* ([p (assoc "content-length" headers)])
(x->integer (cadr p)))
(if-let1 enc (assoc "transfer-encoding" headers)
(if (equal? (cadr enc) "chunked")
(receive-body-chunked remote code headers total receiver)
(error <http-error> "unsupported transfer-encoding:" (cadr enc)))
(receive-body-once remote code headers total receiver))))
(define (receive-body-once remote code headers total receiver)
;; Callback will be called twice (unless total is 0). The first
;; time we return # of total bytes, the second time zero.
(let1 rest total
(define (callback)
(if (equal? rest 0)
(values remote 0)
(begin (set! rest 0) (values remote total))))
(receiver code headers total callback)))
;; NB: chunk extension and trailer are ignored for now.
(define (receive-body-chunked remote code headers total receiver)
(define chunk-size #f)
(define condition #f)
(define (callback)
(if (equal? chunk-size 0)
(values remote 0) ;; finalize
;; If we get an error during receiving from the server, we need
;; to return -1 to give the chance to the receiver to clean up
;; things. After the receiver returns we reraise the condition.
(guard (e [else (set! condition e) (values remote -1)])
;; If we've already handled some chunks, we need to skip
;; the trailing CRLF of the previous chunk.
(when chunk-size
(read-line remote))
(let1 line (read-line remote)
(when (eof-object? line)
(error <http-error> "chunked body ended prematurely"))
(rxmatch-if (#/^([[:xdigit:]]+)/ line) (#f digits)
(begin
(set! chunk-size (string->number digits 16))
(if (zero? chunk-size)
;; finish reading trailer
(do ([line (read-line remote) (read-line remote)])
[(or (eof-object? line) (string-null? line))
(values remote 0)])
(values remote chunk-size)))
;; something's wrong
(error <http-error> "bad line in chunked data:" line))))))
(begin0 (receiver code headers total callback)
(when condition (raise condition))))
;;==============================================================
;; secure agent handling
;;
(define (shutdown-secure-agent conn)
(when (~ conn'secure-agent)
(tls-close (~ conn'secure-agent))
(tls-destroy (~ conn'secure-agent))
(set! (~ conn'secure-agent) #f)))
(define (start-secure-agent conn)
(unless (http-secure-connection-available?)
(error "Secure connection is not available on this platform"))
(when (~ conn'secure-agent) (shutdown-secure-agent conn))
(let1 tls (make-tls)
(tls-connect tls (socket-fd (~ conn'socket)))
(set! (~ conn'secure-agent) tls)))
;; for external api
(define (http-secure-connection-available?)
(cond-expand
[gauche.net.tls #t]
[else #f]))
;;==============================================================
;; authentication handling
;;
(define (http-auth-headers conn)
(or (and-let* ([auth-handler (~ conn'auth-handler)])
(auth-handler conn))
'()))
(define (http-basic-auth-handler conn)
(and-let* ([user (~ conn 'auth-user)]
[pass (or (~ conn 'auth-password) "")])
`(:authorization ,($ format "Basic ~a"
$ base64-encode-string #"~|user|:~|pass|"))))
(define http-default-auth-handler
(make-parameter http-basic-auth-handler))
|