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
|
use core:io;
use core:net;
use crypto;
/**
* A class that represents the state associated with a HTTP client.
*
* Encapsulates the state needed for persistent connections, cookies, etc. even though those
* features are not implemented yet.
*/
class Client {
// SSL context.
private ClientContext context;
// Timeout.
private Duration timeoutDuration = 60 s;
// Regex to match the end of the response.
private lang:bnf:Regex endOfHeader = lang:bnf:Regex(".*\r\n\r\n");
// Create.
init() {
init {
context = ClientContext:systemDefault();
}
}
// Set timeout.
assign timeout(Duration t) {
timeoutDuration = t;
}
// Perform requests. Assumes that `path` is a Http url.
Result request(Url path) {
unless (proto = path.protocol as HttpProtocol)
throw HttpError("The path passed to 'request' must be a http url!");
Request request(Method:GET, Version:HTTP_1_1, path);
Str hostname = path[0];
return rawRequest(hostname, proto.secure, request);
}
// Perform a request. This is a low-level function that does minimal conversion. Use `request`
// for everyday use instead.
Result rawRequest(Str host, Bool secure, Request request) {
Nat port = if (secure) {
443n;
} else {
80n;
};
unless (socket = connect(host, port))
throw HttpError("Failed to connect to ${host}");
socket.input.timeout = timeoutDuration;
OStream out = socket.output;
IStream in = socket.input;
Session? ssl;
if (secure) {
var connected = context.connect(socket, host);
out = connected.output;
in = connected.input;
ssl = connected;
}
// Send the request.
BufferedOStream buffer(out, 4096);
request.write(buffer);
buffer.flush();
Buffer inputBuffer;
Response response = if (request.version == Version:HTTP_0_9) {
// HTTP 0.9 has no headers.
Response(request.version, Status:OK);
} else {
// Read the response.
inputBuffer = in.read(4096);
while (endOfHeader.match(inputBuffer).empty) {
if (!in.more())
throw HttpError("Invalid response header.");
if (inputBuffer.filled <= 0)
inputBuffer = inputBuffer.grow(inputBuffer.count + 4096);
in.read(inputBuffer);
}
var parsedHeader = parseResponseHeader(inputBuffer);
unless (response = parsedHeader.value) {
throw HttpError("Failed to parse response:\n${parsedHeader.error}");
}
inputBuffer.shift(parsedHeader.end);
response;
};
Nat? bytes;
if (length = response.header("content-length"))
bytes = length.nat;
return Result(Response(), socket, ssl, inputBuffer, bytes);
}
// Close any open connections and cleanup any lingering state.
void close() {
// Nothing needs to be done at the moment.
}
/**
* Stream returned from `request` to handle the stream lifetime.
*/
private class Result extends IStream {
// HTTP response. The body is always empty.
Response response;
// Socket.
private NetStream socket;
// SSL context, if any.
private Session? ssl;
// Remaining bytes to read.
private Nat? remainingBytes;
// Input stream.
private IStream input;
// Remaining input.
private Buffer remainingInput;
// Read position in the remaining input.
private Nat remainingPos;
// Create.
init(Response response, NetStream socket, Session? ssl, Buffer remainingInput, Nat? remainingBytes) {
init {
response = response;
socket = socket;
ssl = ssl;
remainingInput = remainingInput;
remainingBytes = remainingBytes;
input = if (ssl) { ssl.input; } else { socket.input; };
}
}
// More data?
Bool more() : override {
if (!input.more)
return false;
if (remainingBytes) {
return remainingBytes > 0;
} else {
return true;
}
}
// Read.
Buffer read(Buffer to) : override {
if (remainingBytes) {
if (to.free > remainingBytes) {
Buffer out = readI(buffer(remainingBytes));
for (Nat i = 0; i < out.filled; i++)
to.push(out[i]);
this.remainingBytes = remainingBytes - out.filled;
return out;
} else {
Nat oldFilled = to.filled;
Buffer out = readI(to);
this.remainingBytes = remainingBytes - (out.filled - oldFilled);
return out;
}
} else {
return readI(to);
}
}
// Helper for read.
private Buffer readI(Buffer to) {
if (remainingPos < remainingInput.filled) {
while ((remainingPos < remainingInput.filled) & (to.free > 0))
to.push(remainingInput[remainingPos++]);
if (remainingPos >= remainingInput.filled)
remainingInput = Buffer();
return to;
} else {
return input.read(to);
}
}
// Peek.
Buffer peek(Buffer to) : override {
if (remainingBytes) {
if (to.free > remainingBytes) {
Buffer out = peekI(buffer(remainingBytes));
for (Nat i = 0; i < out.filled; i++)
to.push(out[i]);
return out;
} else {
Nat oldFilled = to.filled;
return peekI(to);
}
} else {
return peekI(to);
}
}
// Helper for peek.
private Buffer peekI(Buffer to) {
if (remainingPos < remainingInput.filled) {
Nat pos = remainingPos;
while ((pos < remainingInput.filled) & (to.free > 0))
to.push(remainingInput[pos++]);
return to;
} else {
return input.peek(to);
}
}
// Close.
void close() : override {
if (ssl)
ssl.close();
socket.close();
}
// Error.
ErrorCode error() : override { input.error(); }
}
}
|