client.scm 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516
  1. ;;; Web client
  2. ;; Copyright (C) 2011, 2012, 2013, 2014, 2015, 2016, 2017 Free Software Foundation, Inc.
  3. ;; This library is free software; you can redistribute it and/or
  4. ;; modify it under the terms of the GNU Lesser General Public
  5. ;; License as published by the Free Software Foundation; either
  6. ;; version 3 of the License, or (at your option) any later version.
  7. ;;
  8. ;; This library is distributed in the hope that it will be useful,
  9. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  11. ;; Lesser General Public License for more details.
  12. ;;
  13. ;; You should have received a copy of the GNU Lesser General Public
  14. ;; License along with this library; if not, write to the Free Software
  15. ;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
  16. ;; 02110-1301 USA
  17. ;;; Commentary:
  18. ;;;
  19. ;;; (web client) is a simple HTTP URL fetcher for Guile.
  20. ;;;
  21. ;;; In its current incarnation, (web client) is synchronous. If you
  22. ;;; want to fetch a number of URLs at once, probably the best thing to
  23. ;;; do is to write an event-driven URL fetcher, similar in structure to
  24. ;;; the web server.
  25. ;;;
  26. ;;; Another option, good but not as performant, would be to use threads,
  27. ;;; possibly via a thread pool.
  28. ;;;
  29. ;;; Code:
  30. (define-module (web client)
  31. #:use-module (rnrs bytevectors)
  32. #:use-module (ice-9 binary-ports)
  33. #:use-module (ice-9 iconv)
  34. #:use-module (ice-9 rdelim)
  35. #:use-module (web request)
  36. #:use-module (web response)
  37. #:use-module (web uri)
  38. #:use-module (web http)
  39. #:use-module (srfi srfi-1)
  40. #:use-module (srfi srfi-9)
  41. #:use-module (srfi srfi-9 gnu)
  42. #:use-module ((rnrs io ports)
  43. #:prefix rnrs-ports:)
  44. #:export (current-http-proxy
  45. open-socket-for-uri
  46. http-get
  47. http-get*
  48. http-head
  49. http-post
  50. http-put
  51. http-delete
  52. http-trace
  53. http-options))
  54. (define %http-receive-buffer-size
  55. ;; Size of the HTTP receive buffer.
  56. 65536)
  57. ;; Autoload GnuTLS so that this module can be used even when GnuTLS is
  58. ;; not available. At compile time, this yields "possibly unbound
  59. ;; variable" warnings, but these are OK: we know that the variables will
  60. ;; be bound if we need them, because (guix download) adds GnuTLS as an
  61. ;; input in that case.
  62. ;; XXX: Use this hack instead of #:autoload to avoid compilation errors.
  63. ;; See <http://bugs.gnu.org/12202>.
  64. (module-autoload! (current-module)
  65. '(gnutls) '(make-session connection-end/client))
  66. (define gnutls-module
  67. (delay
  68. (catch 'misc-error
  69. (lambda ()
  70. (let ((module (resolve-interface '(gnutls))))
  71. ;; In some 2.1/2.2 installations installed alongside Guile 2.0, gnutls
  72. ;; can be imported but the bindings are broken as "unknown type".
  73. ;; Here we check that gnutls-version is the right type (a procedure)
  74. ;; to make sure the bindings are ok.
  75. (if (procedure? (module-ref module 'gnutls-version))
  76. module
  77. #f)))
  78. (const #f))))
  79. (define (ensure-gnutls)
  80. (if (not (force gnutls-module))
  81. (throw 'gnutls-not-available "(gnutls) module not available")))
  82. (define current-http-proxy
  83. (make-parameter (let ((proxy (getenv "http_proxy")))
  84. (and (not (equal? proxy ""))
  85. proxy))))
  86. (define (tls-wrap port server)
  87. "Return PORT wrapped in a TLS connection to SERVER. SERVER must be a DNS
  88. host name without trailing dot."
  89. (define (log level str)
  90. (format (current-error-port)
  91. "gnutls: [~a|~a] ~a" (getpid) level str))
  92. (ensure-gnutls)
  93. (let ((session (make-session connection-end/client)))
  94. ;; Some servers such as 'cloud.github.com' require the client to support
  95. ;; the 'SERVER NAME' extension. However, 'set-session-server-name!' is
  96. ;; not available in older GnuTLS releases. See
  97. ;; <http://bugs.gnu.org/18526> for details.
  98. (if (module-defined? (force gnutls-module)
  99. 'set-session-server-name!)
  100. (set-session-server-name! session server-name-type/dns server)
  101. (format (current-error-port)
  102. "warning: TLS 'SERVER NAME' extension not supported~%"))
  103. (set-session-transport-fd! session (fileno port))
  104. (set-session-default-priority! session)
  105. ;; The "%COMPAT" bit allows us to work around firewall issues (info
  106. ;; "(gnutls) Priority Strings"); see <http://bugs.gnu.org/23311>.
  107. ;; Explicitly disable SSLv3, which is insecure:
  108. ;; <https://tools.ietf.org/html/rfc7568>.
  109. (set-session-priorities! session "NORMAL:%COMPAT:-VERS-SSL3.0")
  110. (set-session-credentials! session (make-certificate-credentials))
  111. ;; Uncomment the following lines in case of debugging emergency.
  112. ;;(set-log-level! 10)
  113. ;;(set-log-procedure! log)
  114. (handshake session)
  115. ;; FIXME: It appears that session-record-port is entirely
  116. ;; sufficient; it's already a port. The only value of this code is
  117. ;; to keep a reference on "port", to keep it alive! To fix this we
  118. ;; need to arrange to either hand GnuTLS its own fd to close, or to
  119. ;; arrange a reference from the session-record-port to the
  120. ;; underlying socket.
  121. (let ((record (session-record-port session)))
  122. (define (read! bv start count)
  123. (define read-bv (get-bytevector-some record))
  124. (if (eof-object? read-bv)
  125. 0 ; read! returns 0 on eof-object
  126. (let ((read-bv-len (bytevector-length read-bv)))
  127. (bytevector-copy! read-bv 0 bv start (min read-bv-len count))
  128. (when (< count read-bv-len)
  129. (unget-bytevector record bv count (- read-bv-len count)))
  130. read-bv-len)))
  131. (define (write! bv start count)
  132. (put-bytevector record bv start count)
  133. (force-output record)
  134. count)
  135. (define (get-position)
  136. (rnrs-ports:port-position record))
  137. (define (set-position! new-position)
  138. (rnrs-ports:set-port-position! record new-position))
  139. (define (close)
  140. (unless (port-closed? port)
  141. (close-port port))
  142. (unless (port-closed? record)
  143. (close-port record)))
  144. (setvbuf record 'block)
  145. (make-custom-binary-input/output-port "gnutls wrapped port" read! write!
  146. get-position set-position!
  147. close))))
  148. (define (ensure-uri-reference uri-or-string)
  149. (cond
  150. ((string? uri-or-string) (string->uri-reference uri-or-string))
  151. ((uri-reference? uri-or-string) uri-or-string)
  152. (else (error "Invalid URI-reference" uri-or-string))))
  153. (define (open-socket-for-uri uri-or-string)
  154. "Return an open input/output port for a connection to URI."
  155. (define http-proxy (current-http-proxy))
  156. (define uri (ensure-uri-reference (or http-proxy uri-or-string)))
  157. (define addresses
  158. (let ((port (uri-port uri)))
  159. (delete-duplicates
  160. (getaddrinfo (uri-host uri)
  161. (cond (port => number->string)
  162. ((uri-scheme uri) => symbol->string)
  163. (else (error "Not an absolute URI" uri)))
  164. (if port
  165. AI_NUMERICSERV
  166. 0))
  167. (lambda (ai1 ai2)
  168. (equal? (addrinfo:addr ai1) (addrinfo:addr ai2))))))
  169. (define https?
  170. (eq? 'https (uri-scheme uri)))
  171. (define (open-socket)
  172. (let loop ((addresses addresses))
  173. (let* ((ai (car addresses))
  174. (s (with-fluids ((%default-port-encoding #f))
  175. ;; Restrict ourselves to TCP.
  176. (socket (addrinfo:fam ai) SOCK_STREAM IPPROTO_IP))))
  177. (catch 'system-error
  178. (lambda ()
  179. (connect s (addrinfo:addr ai))
  180. ;; Buffer input and output on this port.
  181. (setvbuf s 'block)
  182. ;; If we're using a proxy, make a note of that.
  183. (when http-proxy (set-http-proxy-port?! s #t))
  184. s)
  185. (lambda args
  186. ;; Connection failed, so try one of the other addresses.
  187. (close s)
  188. (if (null? (cdr addresses))
  189. (apply throw args)
  190. (loop (cdr addresses))))))))
  191. (let-syntax ((with-https-proxy
  192. (syntax-rules ()
  193. ((_ exp)
  194. ;; For HTTPS URIs, honor 'https_proxy', not 'http_proxy'.
  195. ;; FIXME: Proxying is not supported for https.
  196. (let ((thunk (lambda () exp)))
  197. (if (and https?
  198. current-http-proxy)
  199. (parameterize ((current-http-proxy #f))
  200. (when (and=> (getenv "https_proxy")
  201. (negate string-null?))
  202. (format (current-error-port)
  203. "warning: 'https_proxy' is ignored~%"))
  204. (thunk))
  205. (thunk)))))))
  206. (with-https-proxy
  207. (let ((s (open-socket)))
  208. ;; Buffer input and output on this port.
  209. (setvbuf s 'block %http-receive-buffer-size)
  210. (if https?
  211. (tls-wrap s (uri-host uri))
  212. s)))))
  213. (define (extend-request r k v . additional)
  214. (let ((r (set-field r (request-headers)
  215. (assoc-set! (copy-tree (request-headers r))
  216. k v))))
  217. (if (null? additional)
  218. r
  219. (apply extend-request r additional))))
  220. ;; -> request body
  221. (define (sanitize-request request body)
  222. "\"Sanitize\" the given request and body, ensuring that they are
  223. complete and coherent. This method is most useful for methods that send
  224. data to the server, like POST, but can be used for any method. Return
  225. two values: a request and a bytevector, possibly the same ones that were
  226. passed as arguments.
  227. If BODY is a string, encodes the string to a bytevector, in an encoding
  228. appropriate for REQUEST. Adds a ‘content-length’ and ‘content-type’
  229. header, as necessary.
  230. If BODY is a procedure, it is called with a port as an argument, and the
  231. output collected as a bytevector. In the future we might try to instead
  232. use a compressing, chunk-encoded port, and call this procedure later.
  233. Authors are advised not to rely on the procedure being called at any
  234. particular time.
  235. Note that we rely on the request itself already having been validated,
  236. as is the case by default with a request returned by `build-request'."
  237. (cond
  238. ((not body)
  239. (let ((length (request-content-length request)))
  240. (if length
  241. ;; FIXME make this stricter: content-length header should be
  242. ;; prohibited if there's no body, even if the content-length
  243. ;; is 0.
  244. (unless (zero? length)
  245. (error "content-length, but no body"))
  246. (when (assq 'transfer-encoding (request-headers request))
  247. (error "transfer-encoding not allowed with no body")))
  248. (values request #vu8())))
  249. ((string? body)
  250. (let* ((type (request-content-type request '(text/plain)))
  251. (declared-charset (assq-ref (cdr type) 'charset))
  252. (charset (or declared-charset "utf-8")))
  253. (sanitize-request
  254. (if declared-charset
  255. request
  256. (extend-request request 'content-type
  257. `(,@type (charset . ,charset))))
  258. (string->bytevector body charset))))
  259. ((procedure? body)
  260. (let* ((type (request-content-type request
  261. '(text/plain)))
  262. (declared-charset (assq-ref (cdr type) 'charset))
  263. (charset (or declared-charset "utf-8")))
  264. (sanitize-request
  265. (if declared-charset
  266. request
  267. (extend-request request 'content-type
  268. `(,@type (charset . ,charset))))
  269. (call-with-encoded-output-string charset body))))
  270. ((not (bytevector? body))
  271. (error "unexpected body type"))
  272. (else
  273. (values (let ((rlen (request-content-length request))
  274. (blen (bytevector-length body)))
  275. (cond
  276. (rlen (if (= rlen blen)
  277. request
  278. (error "bad content-length" rlen blen)))
  279. (else (extend-request request 'content-length blen))))
  280. body))))
  281. (define (decode-response-body response body)
  282. ;; `body' is either #f or a bytevector.
  283. (cond
  284. ((not body) body)
  285. ((bytevector? body)
  286. (let ((rlen (response-content-length response))
  287. (blen (bytevector-length body)))
  288. (cond
  289. ((and rlen (not (= rlen blen)))
  290. (error "bad content-length" rlen blen))
  291. ((response-content-type response)
  292. => (lambda (type)
  293. (cond
  294. ((text-content-type? (car type))
  295. ;; RFC 2616 3.7.1: "When no explicit charset parameter is
  296. ;; provided by the sender, media subtypes of the "text"
  297. ;; type are defined to have a default charset value of
  298. ;; "ISO-8859-1" when received via HTTP."
  299. (bytevector->string body (or (assq-ref (cdr type) 'charset)
  300. "iso-8859-1")))
  301. (else body))))
  302. (else body))))
  303. (else
  304. (error "unexpected body type" body))))
  305. ;; We could expose this to user code if there is demand.
  306. (define* (request uri #:key
  307. (body #f)
  308. (port (open-socket-for-uri uri))
  309. (method 'GET)
  310. (version '(1 . 1))
  311. (keep-alive? #f)
  312. (headers '())
  313. (decode-body? #t)
  314. (streaming? #f)
  315. (request
  316. (build-request
  317. (ensure-uri-reference uri)
  318. #:method method
  319. #:version version
  320. #:headers (if keep-alive?
  321. headers
  322. (cons '(connection close) headers))
  323. #:port port)))
  324. (call-with-values (lambda () (sanitize-request request body))
  325. (lambda (request body)
  326. (let ((request (write-request request port)))
  327. (when body
  328. (write-request-body request body))
  329. (force-output (request-port request))
  330. (let ((response (read-response port)))
  331. (cond
  332. ((eq? (request-method request) 'HEAD)
  333. (unless keep-alive?
  334. (close-port port))
  335. (values response #f))
  336. (streaming?
  337. (values response
  338. (response-body-port response
  339. #:keep-alive? keep-alive?
  340. #:decode? decode-body?)))
  341. (else
  342. (let ((body (read-response-body response)))
  343. (unless keep-alive?
  344. (close-port port))
  345. (values response
  346. (if decode-body?
  347. (decode-response-body response body)
  348. body))))))))))
  349. (define* (http-get uri #:key
  350. (body #f)
  351. (port (open-socket-for-uri uri))
  352. (version '(1 . 1)) (keep-alive? #f)
  353. ;; #:headers is the new name of #:extra-headers.
  354. (extra-headers #f) (headers (or extra-headers '()))
  355. (decode-body? #t) (streaming? #f))
  356. "Connect to the server corresponding to URI and ask for the
  357. resource, using the ‘GET’ method. If you already have a port open,
  358. pass it as PORT. The port will be closed at the end of the
  359. request unless KEEP-ALIVE? is true. Any extra headers in the
  360. alist HEADERS will be added to the request.
  361. If BODY is not ‘#f’, a message body will also be sent with the HTTP
  362. request. If BODY is a string, it is encoded according to the
  363. content-type in HEADERS, defaulting to UTF-8. Otherwise BODY should be
  364. a bytevector, or ‘#f’ for no body. Although it's allowed to send a
  365. message body along with any request, usually only POST and PUT requests
  366. have bodies. See ‘http-put’ and ‘http-post’ documentation, for more.
  367. If DECODE-BODY? is true, as is the default, the body of the
  368. response will be decoded to string, if it is a textual content-type.
  369. Otherwise it will be returned as a bytevector.
  370. However, if STREAMING? is true, instead of eagerly reading the response
  371. body from the server, this function only reads off the headers. The
  372. response body will be returned as a port on which the data may be read.
  373. Unless KEEP-ALIVE? is true, the port will be closed after the full
  374. response body has been read.
  375. Returns two values: the response read from the server, and the response
  376. body as a string, bytevector, #f value, or as a port (if STREAMING? is
  377. true)."
  378. (when extra-headers
  379. (issue-deprecation-warning
  380. "The #:extra-headers argument to http-get has been renamed to #:headers. "
  381. "Please update your code."))
  382. (request uri #:method 'GET #:body body
  383. #:port port #:version version #:keep-alive? keep-alive?
  384. #:headers headers #:decode-body? decode-body?
  385. #:streaming? streaming?))
  386. (define* (http-get* uri #:key
  387. (body #f)
  388. (port (open-socket-for-uri uri))
  389. (version '(1 . 1)) (keep-alive? #f)
  390. ;; #:headers is the new name of #:extra-headers.
  391. (extra-headers #f) (headers (or extra-headers '()))
  392. (decode-body? #t))
  393. "Deprecated in favor of (http-get #:streaming? #t)."
  394. (issue-deprecation-warning
  395. "`http-get*' has been deprecated. "
  396. "Instead, use `http-get' with the #:streaming? #t keyword argument.")
  397. (http-get uri #:body body
  398. #:port port #:version version #:keep-alive? keep-alive?
  399. #:headers headers #:decode-body? #t #:streaming? #t))
  400. (define-syntax-rule (define-http-verb http-verb method doc)
  401. (define* (http-verb uri #:key
  402. (body #f)
  403. (port (open-socket-for-uri uri))
  404. (version '(1 . 1))
  405. (keep-alive? #f)
  406. (headers '())
  407. (decode-body? #t)
  408. (streaming? #f))
  409. doc
  410. (request uri
  411. #:body body #:method method
  412. #:port port #:version version #:keep-alive? keep-alive?
  413. #:headers headers #:decode-body? decode-body?
  414. #:streaming? streaming?)))
  415. (define-http-verb http-head
  416. 'HEAD
  417. "Fetch message headers for the given URI using the HTTP \"HEAD\"
  418. method.
  419. This function is similar to ‘http-get’, except it uses the \"HEAD\"
  420. method. See ‘http-get’ for full documentation on the various keyword
  421. arguments that are accepted by this function.
  422. Returns two values: the resulting response, and ‘#f’. Responses to HEAD
  423. requests do not have a body. The second value is only returned so that
  424. other procedures can treat all of the http-foo verbs identically.")
  425. (define-http-verb http-post
  426. 'POST
  427. "Post data to the given URI using the HTTP \"POST\" method.
  428. This function is similar to ‘http-get’, except it uses the \"POST\"
  429. method. See ‘http-get’ for full documentation on the various keyword
  430. arguments that are accepted by this function.
  431. Returns two values: the resulting response, and the response body.")
  432. (define-http-verb http-put
  433. 'PUT
  434. "Put data at the given URI using the HTTP \"PUT\" method.
  435. This function is similar to ‘http-get’, except it uses the \"PUT\"
  436. method. See ‘http-get’ for full documentation on the various keyword
  437. arguments that are accepted by this function.
  438. Returns two values: the resulting response, and the response body.")
  439. (define-http-verb http-delete
  440. 'DELETE
  441. "Delete data at the given URI using the HTTP \"DELETE\" method.
  442. This function is similar to ‘http-get’, except it uses the \"DELETE\"
  443. method. See ‘http-get’ for full documentation on the various keyword
  444. arguments that are accepted by this function.
  445. Returns two values: the resulting response, and the response body.")
  446. (define-http-verb http-trace
  447. 'TRACE
  448. "Send an HTTP \"TRACE\" request.
  449. This function is similar to ‘http-get’, except it uses the \"TRACE\"
  450. method. See ‘http-get’ for full documentation on the various keyword
  451. arguments that are accepted by this function.
  452. Returns two values: the resulting response, and the response body.")
  453. (define-http-verb http-options
  454. 'OPTIONS
  455. "Query characteristics of an HTTP resource using the HTTP \"OPTIONS\"
  456. method.
  457. This function is similar to ‘http-get’, except it uses the \"OPTIONS\"
  458. method. See ‘http-get’ for full documentation on the various keyword
  459. arguments that are accepted by this function.
  460. Returns two values: the resulting response, and the response body.")