Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,39 @@ Secure flag enforcement, and expiry. Cookies are deduplicated by
name+domain+path. During redirects, cookies from every hop are stored and
re-applied for each new URL.

### Multipart uploads

```clojure
(match (Client.post-multipart "https://example.com/upload"
(the (Map String (Array String)) {})
&[(Multipart.text-part "field" "value")
(Multipart.file-part "upload" "test.txt"
"text/plain"
"file contents")])
(Result.Success r) (println* (Response.code &r))
(Result.Error e) (IO.errorln &e))
```

`post-multipart` picks the boundary with `Multipart.boundary-for`, which
checks it against the parts and extends it until it occurs in none of them.
RFC 2046 §5.1.1 requires that, and without it any upload whose contents
happen to contain the delimiter is split in the wrong places by the receiver.

To build the body yourself, pick the boundary the same way:

```clojure
(let [parts [(Multipart.text-part "name" "Carp")]
boundary (Multipart.boundary-for &parts)]
(Client.post url
{@"Content-Type" [(Multipart.content-type-header &boundary)]}
&(Multipart.encode &parts &boundary)))
```

A CR or LF in a part name, filename or content type is percent-encoded as
`%0D` and `%0A`, so an untrusted field name cannot inject header lines or a
further part into the body. Quotes are backslash-escaped. Values without
those characters are emitted unchanged.

## API

### `Client`
Expand All @@ -125,6 +158,8 @@ re-applied for each new URL.
| `Client.del-with-config url config` | DELETE with request config |
| `Client.head-with-config url config` | HEAD with request config |
| `Client.patch-with-config url headers body config` | PATCH with request config |
| `Client.post-multipart url headers parts` | POST a multipart/form-data body |
| `Client.post-multipart-with-config url headers parts config` | Multipart POST with request config |
| `Client.request-with-config verb url headers body config` | Generic request with request config |
| `Client.request-stream-with-config verb url headers body config` | Streaming with request config |
| `Client.get-with-jar url jar` | GET with cookie jar |
Expand All @@ -149,6 +184,17 @@ A relative `Location` is resolved against the URL of the hop that produced it,
following RFC 3986 §5. A `Location` that carries its own scheme is followed as
given.

### `Multipart`

| Function | Purpose |
|----------|---------|
| `Multipart.text-part name value` | A text form field |
| `Multipart.file-part name filename content-type data` | A file upload part |
| `Multipart.boundary-for parts` | A boundary that occurs in no part |
| `Multipart.generate-boundary` | A boundary from the clock, unchecked against any payload |
| `Multipart.content-type-header boundary` | The `Content-Type` value for a boundary |
| `Multipart.encode parts boundary` | The encoded body |

### `RequestConfig`

| Function | Purpose |
Expand Down
5 changes: 5 additions & 0 deletions docs/Client.html
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@
CookieJar
</a>
</li>
<li>
<a href="Multipart.html">
Multipart
</a>
</li>
</ul>
</div>
</div>
Expand Down
5 changes: 5 additions & 0 deletions docs/Connection.html
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@
CookieJar
</a>
</li>
<li>
<a href="Multipart.html">
Multipart
</a>
</li>
</ul>
</div>
</div>
Expand Down
5 changes: 5 additions & 0 deletions docs/CookieJar.html
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@
CookieJar
</a>
</li>
<li>
<a href="Multipart.html">
Multipart
</a>
</li>
</ul>
</div>
</div>
Expand Down
235 changes: 235 additions & 0 deletions docs/Multipart.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
<!DOCTYPE HTML>

<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
<link rel="stylesheet" href="../style.css">
</head>
<body>
<div class="content">
<div class="logo">
<a href="">
<img src="">
</a>
<div class="title">
http-client
</div>
<div class="index">
<ul>
<li>
<a href="Client.html">
Client
</a>
</li>
<li>
<a href="Connection.html">
Connection
</a>
</li>
<li>
<a href="CookieJar.html">
CookieJar
</a>
</li>
<li>
<a href="Multipart.html">
Multipart
</a>
</li>
</ul>
</div>
</div>
<div class="module">
<h1>
Multipart
</h1>
<div class="module-description">
<p>provides multipart/form-data encoding for HTTP requests
(RFC 7578).</p>
<h2>Encoding manually</h2>
<pre><code>(let [parts [(Multipart.text-part &quot;name&quot; &quot;Carp&quot;)
(Multipart.file-part &quot;upload&quot; &quot;test.txt&quot;
&quot;text/plain&quot; &quot;file contents&quot;)]
boundary (Multipart.boundary-for &amp;parts)]
(Client.post url
{@&quot;Content-Type&quot; [(Multipart.content-type-header &amp;boundary)]}
&amp;(Multipart.encode &amp;parts &amp;boundary)))
</code></pre>
<p>Pick the boundary with <code>Multipart.boundary-for</code>, not with
<code>Multipart.generate-boundary</code>: only the former is checked against the parts,
which is what keeps a body that happens to contain the delimiter from
splitting the message.</p>
<h2>Convenience function</h2>
<pre><code>(Client.post-multipart url {}
&amp;[(Multipart.text-part &quot;field&quot; &quot;value&quot;)])
</code></pre>

</div>
<div class="binder">
<a class="anchor" href="#boundary-for">
<h3 id="boundary-for">
boundary-for
</h3>
</a>
<div class="description">
defn
</div>
<p class="sig">
(Fn [(Ref (Array Part) a)] String)
</p>
<pre class="args">
(boundary-for parts)
</pre>
<p class="doc">
<p>returns a boundary that occurs nowhere in <code>parts</code>, as RFC 2046 §5.1.1
requires of the delimiter.</p>
<p>Starts from <code>Multipart.generate-boundary</code> and, while the candidate still
occurs in some part name, filename, content type or body, appends the
<code>bcharsnospace</code> character that follows the fewest of those occurrences. Each
round therefore divides the occurrence count by 62, so the boundary stays far
inside the 70-character limit even for a payload built to defeat it. Every
round copies and scans each name, filename, content type and body, so a large
upload pays a full pass per round.</p>

</p>
</div>
<div class="binder">
<a class="anchor" href="#content-type-header">
<h3 id="content-type-header">
content-type-header
</h3>
</a>
<div class="description">
defn
</div>
<p class="sig">
(Fn [(Ref String a)] String)
</p>
<pre class="args">
(content-type-header boundary)
</pre>
<p class="doc">
<p>returns the Content-Type header value for multipart/form-data with the
given boundary.</p>

</p>
</div>
<div class="binder">
<a class="anchor" href="#encode">
<h3 id="encode">
encode
</h3>
</a>
<div class="description">
defn
</div>
<p class="sig">
(Fn [(Ref (Array Part) a), (Ref String b)] String)
</p>
<pre class="args">
(encode parts boundary)
</pre>
<p class="doc">
<p>encodes an array of parts into a multipart/form-data body string using
the given boundary (RFC 7578).</p>
<p>Pass a boundary from <code>Multipart.boundary-for</code>; <code>encode</code> cannot re-pick one,
because the caller has already committed to it in the Content-Type header.</p>
<p>A CR or LF in a part name, filename or content type would end the header line
and let the rest of the value pose as headers or as a further part, so both
are percent-encoded as %0D and %0A, following the same rule as HTML form
submission. Quotes in a name or filename are backslash-escaped. Values
without those characters are emitted unchanged.</p>

</p>
</div>
<div class="binder">
<a class="anchor" href="#file-part">
<h3 id="file-part">
file-part
</h3>
</a>
<div class="description">
defn
</div>
<p class="sig">
(Fn [(Ref String a), (Ref String b), (Ref String c), (Ref String d)] Part)
</p>
<pre class="args">
(file-part name filename content-type data)
</pre>
<p class="doc">
<p>creates a file upload part with a filename and content type.
The <code>data</code> parameter is the raw file contents as a string.</p>

</p>
</div>
<div class="binder">
<a class="anchor" href="#generate-boundary">
<h3 id="generate-boundary">
generate-boundary
</h3>
</a>
<div class="description">
defn
</div>
<p class="sig">
(Fn [] String)
</p>
<pre class="args">
(generate-boundary)
</pre>
<p class="doc">
<p>generates a boundary string for multipart encoding, using the current
time for uniqueness.</p>
<p>The result is not checked against any payload, so a part that contains it is
encoded into a message the receiver splits in the wrong places. Prefer
<code>Multipart.boundary-for</code>, which rules that out.</p>

</p>
</div>
<div class="binder">
<a class="anchor" href="#parse">
<h3 id="parse">
parse
</h3>
</a>
<div class="description">
defn
</div>
<p class="sig">
(Fn [(Ref String a), (Ref String b)] (Result (Array FormPart) String))
</p>
<pre class="args">
(parse body boundary)
</pre>
<p class="doc">
<p>decodes a <code>multipart/form-data</code> <code>body</code> with the given <code>boundary</code>
into its <code>FormPart</code>s. Fails when the opening boundary delimiter is absent.</p>

</p>
</div>
<div class="binder">
<a class="anchor" href="#text-part">
<h3 id="text-part">
text-part
</h3>
</a>
<div class="description">
defn
</div>
<p class="sig">
(Fn [(Ref String a), (Ref String b)] Part)
</p>
<pre class="args">
(text-part name value)
</pre>
<p class="doc">
<p>creates a text form field part with the given name and value.</p>

</p>
</div>
</div>
</div>
</body>
</html>
5 changes: 5 additions & 0 deletions docs/http-client_index.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@
CookieJar
</a>
</li>
<li>
<a href="Multipart.html">
Multipart
</a>
</li>
</ul>
</div>
</div>
Expand Down
5 changes: 5 additions & 0 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@
CookieJar
</a>
</li>
<li>
<a href="Multipart.html">
Multipart
</a>
</li>
</ul>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion gendocs.carp
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,5 @@ transparently over plain TCP or TLS-encrypted streams.
Requires OpenSSL for HTTPS support (via the `tls` library).
Plain HTTP works without OpenSSL.")

(save-docs Client Connection CookieJar)
(save-docs Client Connection CookieJar Multipart)
(quit)
4 changes: 2 additions & 2 deletions http-client.carp
Original file line number Diff line number Diff line change
Expand Up @@ -666,7 +666,7 @@ Returns `(Result Response String)`.
\"text/plain\" \"file contents\")])
```")
(defn post-multipart [url headers parts]
(let [boundary (Multipart.generate-boundary)
(let [boundary (Multipart.boundary-for parts)
body (Multipart.encode parts &boundary)
ct-vals [(Multipart.content-type-header &boundary)]
cl-vals [(Int.str (String.length &body))]
Expand All @@ -681,7 +681,7 @@ multipart/form-data body using the given `RequestConfig`.
Returns `(Result Response String)`.
See `RequestConfig` for timeout and redirect details.")
(defn post-multipart-with-config [url headers parts config]
(let [boundary (Multipart.generate-boundary)
(let [boundary (Multipart.boundary-for parts)
body (Multipart.encode parts &boundary)
ct-vals [(Multipart.content-type-header &boundary)]
cl-vals [(Int.str (String.length &body))]
Expand Down
Loading