Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

sending MultiPart in http client Fix #1005 #1876

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
138 changes: 136 additions & 2 deletions http/vibe/http/client.d
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import vibe.core.log;
import vibe.data.json;
import vibe.inet.message;
import vibe.inet.url;
import vibe.inet.webform : MultiPart, MultiPartBody;
import vibe.stream.counting;
import vibe.stream.tls;
import vibe.stream.operations;
Expand Down Expand Up @@ -786,6 +787,7 @@ final class HTTPClientRequest : HTTPRequest {
FixedAppender!(string, 22) m_contentLengthBuffer;
TCPConnection m_rawConn;
TLSCertificateInformation m_peerCertificate;
string m_multipartBoundary;
}


Expand Down Expand Up @@ -891,9 +893,138 @@ final class HTTPClientRequest : HTTPRequest {
}
}

void writePart(MultiPart part)
/**
Writes the body as multipart request that can upload files.

Also sets the `Content-Length` if it can be calculated.
*/
void writeMultiPartBody(MultiPartBody part)
{
string boundary = randomMultipartBoundary();
auto length = part.length(boundary);
if (length != 0)
headers["Content-Length"] = length.to!string;
headers["Content-Type"] = part.contentType ~ "; boundary=\"" ~ boundary ~ "\"";

// call part.write directly instead of begin/write/finalize because it
// also calculates the length for us and expects it to write itself.
part.write(boundary, bodyWriter);
finalize();
}

/**
Starts manually writing a multipart request with `writePart` calls
following this call and finalizing using `finalizeMultiPart`.

This API is for manually writing out the parts, use `writeMultiPartBody`
to do it all in one step instead.

Sets the content type to the given content type with the boundary.

Params:
preamble = Text to write in the preamble. It is ignored by HTTP
servers but can be used for example to include additional
information when writing a mail to a non multipart conforming
reader for the user to see at the start.
boundary = The multipart boundary to use to separate the different
parts. If this is null or empty, this function will
automatically generate a cryptographically secure random
boundary to separate the parts. May be at most 70 characters,
otherwise it will be trimmed.
*/
void beginMultiPart(string content_type = "multipart/form-data", string preamble = null, string boundary = null)
{
if (!boundary.length)
boundary = randomMultipartBoundary;

if (boundary.length > 70) {
logTrace("Boundary '%s' is longer than 70 characters, truncating", boundary);
boundary = boundary[0 .. 70];
}

if ("Content-Type" !in headers)
headers["Content-Type"] = content_type ~ "; boundary=\"" ~ boundary ~ "\"";

m_multipartBoundary = boundary;

if (preamble.length) {
bodyWriter.write(preamble);
bodyWriter.write("\r\n");
}
}

/**
Writes a single multipart part, which is essentially one input in a form
request which can contain headers to specify what it is. You need to
start with `beginMultiPart` and end with `finalizeMultiPart` with this
API.

Alternatively you can use `writeMultiPartBody` to do everything in one
step.
*/
void writePart(InputStream)(string field_name, InputStream data,
string content_type = "text/plain; charset=\"utf-8\"", bool binary = false)
if (isInputStream!InputStream)
{
assert(false, "TODO");
scope InetHeaderMap headers;
headers["Content-Disposition"] = "form-data; name=\"" ~ field_name ~ "\"";
if (content_type.length)
ret.headers["Content-Type"] = content_type;
if (binary)
ret.headers["Content-Transfer-Encoding"] = "binary";
writePart(data, headers);
}

/// ditto
void writePart(InputStream)(InputStream data, scope const ref InetHeaderMap headers)
if (isInputStream!InputStream)
{
assert(m_multipartBoundary.length, "need to call beginMultiPart and finalizeMultiPart with writePart");

bodyWriter.write("--");
bodyWriter.write(m_multipartBoundary);
bodyWriter.write("\r\n");
foreach (k, v; headers.byKeyValue) {
bodyWriter.write(k);
bodyWriter.write(": ");
bodyWriter.write(v);
bodyWriter.write("\r\n");
}
bodyWriter.write("\r\n");
pipe(data, bodyWriter);
bodyWriter.write("\r\n");
}

/**
Finishes writing a multipart response by sending the ending boundary and
finalizing the request.
*/
void finalizeMultiPart(string epilogue = null)
{
bodyWriter.write("--");
bodyWriter.write(m_multipartBoundary);
bodyWriter.write("--\r\n");
if (epilogue.length)
{
bodyWriter.write(epilogue);
bodyWriter.write("\r\n");
}
m_multipartBoundary = null;
finalize();
}

///
unittest {
import vibe.core.file : openFile;
import vibe.inet.webform : MultiPart, MultiPartBody;

void test(HTTPClientRequest req) {
MultiPartBody part = new MultiPartBody;
part.parts ~= MultiPart.formData("name", "bob");
part.parts ~= MultiPart.singleFile("picture", "picture.png", "image/png", openFile("res/profilepicture.png"));
part.parts ~= MultiPart.singleFile("upload", NativePath("file.zip")); // auto read & mime detection from filename
req.writeMultiPartBody(part);
}
}

/**
Expand Down Expand Up @@ -954,6 +1085,9 @@ final class HTTPClientRequest : HTTPRequest {
if (m_headerWritten && !m_bodyWriter)
return;

assert(!m_multipartBoundary.length,
"Closed HTTPClientRequest without calling finalizeMultiPart but called beginMultiPart");

// force the request to be sent
if (!m_headerWritten) writeHeader();
else {
Expand Down
37 changes: 30 additions & 7 deletions http/vibe/http/common.d
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public import vibe.http.status;

import vibe.core.log;
import vibe.core.net;
import vibe.core.path;
import vibe.inet.message;
import vibe.stream.operations;
import vibe.textfilter.urlencode : urlEncode, urlDecode;
Expand Down Expand Up @@ -340,13 +341,35 @@ class HTTPStatusException : Exception {
string debugMessage;
}


final class MultiPart {
string contentType;

InputStream stream;
//JsonValue json;
string[string] form;
/**
* Creates a random multipart boundary string starting with hyphens containing
* random text data for separation of data in data uploads.
*
* Uses a cryptographically secure random to generate the boundary string. Use
* this if you plan to manually write a multipart/form-data document somewhere.
*
* You should use $(LREF MultiPart) for an easy to use and compatible API
* instead.
*/
string randomMultipartBoundary()
WebFreak001 marked this conversation as resolved.
Show resolved Hide resolved
@safe {
import vibe.crypto.cryptorand : secureRNG;
import std.ascii : digits, letters;

// simple characters which should be supported everywhere without conflict.
// this is 64 characters which makes the random modulo have a very
// convenient uniform distribution.
static immutable string boundaryChars = digits ~ letters ~ "_-"; // ~ "'()+_,-./:=?";

auto rng = secureRNG();
char[64] ret; // can be up to 70 according to spec, 64 should be enough
ubyte[64] randomBuffer;
rng.read(randomBuffer[]);

ret[0 .. 16] = '-'; // some padding before random
for (int i = 16; i < ret.length; i++)
ret[i] = boundaryChars[randomBuffer[i] % $];
return ret[].idup;
}

/**
Expand Down
Loading