multipart.scm 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. ;;; guile-webutils -- Web application utilities for Guile
  2. ;;; Copyright © 2016 Ricardo Wurmus <rekado@elephly.net>
  3. ;;; Copyright © 2017 Christopher Allan Webber <cwebber@dustycloud.org>
  4. ;;;
  5. ;;; This program is free software: you can redistribute it and/or
  6. ;;; modify it under the terms of the GNU General Public License
  7. ;;; as published by the Free Software Foundation, either version 3 of
  8. ;;; the License, or (at your option) any later version.
  9. ;;;
  10. ;;; This program is distributed in the hope that it will be useful,
  11. ;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  13. ;;; General Public License for more details.
  14. ;;;
  15. ;;; You should have received a copy of the GNU General Public License
  16. ;;; along with this program. If not, see
  17. ;;; <http://www.gnu.org/licenses/>.
  18. (define-module (webutils multipart)
  19. #:use-module (webutils bytevectors)
  20. #:use-module (ice-9 match)
  21. #:use-module ((ice-9 binary-ports)
  22. #:select (unget-bytevector
  23. open-bytevector-input-port))
  24. #:use-module ((ice-9 iconv)
  25. #:select (bytevector->string
  26. string->bytevector))
  27. #:use-module ((rnrs io ports)
  28. #:select (get-string-all
  29. latin-1-codec
  30. get-bytevector-some
  31. put-bytevector
  32. open-bytevector-output-port))
  33. #:use-module (rnrs bytevectors)
  34. #:use-module (srfi srfi-1)
  35. #:use-module (srfi srfi-9)
  36. #:use-module ((web http) #:select (read-headers
  37. write-headers))
  38. #:use-module (web request)
  39. #:use-module (gcrypt random)
  40. #:export (parse-request-body
  41. make-part
  42. part?
  43. part-headers
  44. part-body
  45. part-content-disposition-params
  46. part-name parts-ref parts-ref-string
  47. part-content-type
  48. write-multipart-to-port
  49. format-multipart-body))
  50. ;;; Parsing
  51. ;;;
  52. (define-record-type <part>
  53. (make-part headers body)
  54. part?
  55. (headers part-headers)
  56. (body part-body))
  57. (define (parse-form-part part)
  58. "Break the PART string at the first empty line and return a part
  59. record object."
  60. (match-let
  61. (((prefix match suffix)
  62. (bytevector-partition
  63. (u8-list->bytevector '(13 10 13 10)) part)))
  64. (make-part
  65. (call-with-input-string
  66. ;; TODO: bytestring-append?
  67. (string-append (bytevector->string
  68. (bytevector-drop prefix 2) "ISO-8859-1")
  69. "\r\n\r\n")
  70. read-headers)
  71. ;; Drop last two bytes because every part body ends with "\r\n".
  72. (open-bytevector-input-port
  73. (bytevector-drop-right suffix 2)))))
  74. (define (%get-string-from-port port)
  75. "Get a string from the port and then reset it back to the beginning"
  76. (seek port 0 SEEK_SET)
  77. (let ((str (get-string-all port)))
  78. (seek port 0 SEEK_SET)
  79. str))
  80. (define (set-parts-default-encodings! parts)
  81. "Set the default encoding on each part-body port in PARTS."
  82. (let ((default-encoding
  83. (or (and=> (parts-ref parts "_charset_")
  84. (lambda (charset-part)
  85. (%get-string-from-port (part-body charset-part))))
  86. "UTF-8")))
  87. (for-each
  88. (lambda (part)
  89. (let ((charset
  90. (or (assoc-ref (cdr (part-content-type part))
  91. 'charset)
  92. default-encoding)))
  93. (set-port-encoding! (part-body part)
  94. charset)))
  95. parts))
  96. parts)
  97. (define (parse-request-body request body)
  98. "Parse the multipart/form-data request BODY and return an alist."
  99. (match-let
  100. ((('multipart/form-data ('boundary . boundary))
  101. (assoc-ref (request-headers request) 'content-type)))
  102. (set-parts-default-encodings!
  103. (map parse-form-part
  104. (split-parts (string-append "--" boundary) body)))))
  105. (define (split-parts boundary payload)
  106. "Split the bytevector PAYLOAD containing the request body at the
  107. given BOUNDARY string. Return a list of bytevectors."
  108. (define boundbv (string->bytevector boundary (latin-1-codec)))
  109. (define boundlen (bytevector-length boundbv))
  110. (let loop ((rest payload)
  111. (parts '()))
  112. (match-let
  113. (((prefix match suffix)
  114. (bytevector-partition boundbv rest)))
  115. (if suffix
  116. (loop suffix
  117. (cons prefix parts))
  118. ;; The last part is always empty
  119. (cdr (reverse parts))))))
  120. (define (part-content-disposition-params part)
  121. "Return the parameters from the Content-Disposition part of PART"
  122. (and=> (assoc-ref (part-headers part) 'content-disposition)
  123. (match-lambda
  124. (('form-data alist ...)
  125. alist)
  126. (_ '()))))
  127. (define (part-name part)
  128. "Retrieve the name of PART from the Content-Disposition."
  129. (assoc-ref (part-content-disposition-params part) 'name))
  130. (define (parts-ref parts name)
  131. "Return the part from PARTS matching NAME."
  132. (find (lambda (part)
  133. (equal? (part-name part) name))
  134. parts))
  135. (define (parts-ref-string parts name)
  136. "Return the part from PARTS matching NAME, as a string."
  137. (%get-string-from-port (part-body (parts-ref parts name))))
  138. (define (part-content-type part)
  139. "Retrieve the Content-Type of PART, or the default of '(text-plain)."
  140. (or (assoc-ref (part-headers part) 'content-type)
  141. '(text-plain)))
  142. ;;; Composing/posting
  143. ;;;
  144. (define (write-multipart-to-port parts boundary port)
  145. "Write multipart message containing PARTS separated by BOUNDARY to PORT."
  146. (define (write-crlf)
  147. (display "\r\n" port))
  148. (define (write-ddash)
  149. (display "--" port))
  150. (define (write-boundary)
  151. (write-ddash)
  152. (display boundary port))
  153. (define (write-part part)
  154. (define (write-body body)
  155. ;; TODO: Also support ports...
  156. (match body
  157. ((? string? _)
  158. (display body port))
  159. ((? bytevector? _)
  160. (put-bytevector port body))
  161. ((? port? body-port)
  162. (let lp ()
  163. (match (get-bytevector-some body-port)
  164. ((? eof-object? _)
  165. 'done)
  166. (body-bv
  167. (put-bytevector port body-bv)
  168. (lp)))))))
  169. (define (write-headers-and-body headers body)
  170. (write-headers headers port)
  171. (write-crlf)
  172. (write-body body))
  173. (match part
  174. (((? string? name) . body)
  175. (write-headers-and-body `((content-disposition form-data
  176. (name . ,name)))
  177. body))
  178. ((? part? _)
  179. (write-headers-and-body (part-headers part)
  180. (part-body part)))))
  181. (for-each (lambda (part)
  182. (write-boundary)
  183. (write-crlf)
  184. (write-part part)
  185. (write-crlf))
  186. parts)
  187. (write-boundary)(write-ddash)
  188. (write-crlf))
  189. (define* (format-multipart-body parts #:key (boundary (random-token)))
  190. "Take PARTS and use to construct submittable multirequest body.
  191. Returns two values to its continuation, the formatted body and the
  192. boundary used to write it."
  193. (call-with-values (lambda ()
  194. (open-bytevector-output-port))
  195. (lambda (bv-port get-bv)
  196. (write-multipart-to-port parts boundary bv-port)
  197. (values (get-bv) boundary))))