Payload

  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
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
use "collections"
use "net"
use "format"
use "buffered"

primitive ChunkedTransfer
primitive StreamTransfer
primitive OneshotTransfer

type TransferMode is (ChunkedTransfer | StreamTransfer | OneshotTransfer)

class trn Payload
  """
  This class represent a single HTTP message, which can be either a
  `request` or a `response`.

  ### Transfer Modes

  HTTP provides two ways to encode the transmission of a message 'body',
  of any size. This package supports both of them:

  2. **StreamTransfer**. This is used for payload bodies where the exact
    length is known in advance, including most transfers of files. It is
    selected by calling `Payload.set_length` with an integer bytecount.
    Appication buffer sizes determine how much data is fed to the TCP
    connection at once, but the total amount must match this size.

  3. **ChunkedTransfer**. This is used when the payload length can not be
    known in advance, but can be large. It is selected by calling
    `Payload.set_length` with a parameter of `None`. On the TCP link this mode
    can be detected because there is no `Content-Length` header at all, being
    replaced by the `Transfer-Encoding: chunked` header. In addition, the
    message body is separated into chunks, each with its own bytecount. As with
    `StreamTransfer` mode, transmission can be spread out over time with the
    difference that it is the original data source that determines the chunk
    size.

    If `Payload.set_length` is never called at all, a variation on
    `StreamTransfer` called `OneshotTransfer` is used. In this case, all of
    the message body is placed into the message at once, using
    `Payload.add_chunk` calls. The size will be determined when the message is
    submitted for transmission. Care must be taken not to consume too much
    memory, especially on a server where there can be multiple messages in
    transit at once.

    The type of transfer being used by an incoming message can be determined
    from its `transfer_mode` field, which will be one of the
    [TransferMode](/http/http-TransferMode) types.

  ### Sequence

  For example, to send a message of possibly large size:

  1. Create the message with a call to `Payload.request` or `Payload.response`.
  2. Set the `session` field of the message.
  2. Call `Payload.set_length` to indicate the length of the body.
  3. Add any additional headers that may be required, such as `Content-type`.
  4. Submit the message for transmission by calling the either the
  `HTTPSession.apply` method (in servers) or the `HTTPCLient.apply` method
  in clients.
  5. Wait for the `send_body` notification.
  6. Make any number of calls to `Payload.send_chunk`.
  7. Call `Payload.finish`.

  To send a message of small, reasonable size (say, under 20KB), this
  simplified method can be used instead:

  1. Create the message with a call to `Payload.request` or `Payload.response`.
  2. Set the `session` field of the message.
  3. Add any additional headers that may be required, such as `Content-type`.
  4. Call `add_chunk` one or more times to add body data.
  4. Submit the message for transmission by calling the either the
  [HTTPSession](/http/http-HTTPSession)`.apply` method (in servers) or the
  [HTTPClient](/http/http-HTTPClient)`.apply` method in clients.
  """
  var proto: String = "HTTP/1.1"
    """The HTTP protocol string"""

  var status: U16
    """
    Internal representation of the response [Status](http-Status).

    Will be `0` for HTTP requests.
    """

  var method: String
    """
    The HTTP Method.

    `GET`, `POST`, `DELETE`, `OPTIONS`, ...

    For HTTP responses this will be the status string,
    for a `200` status this will be `200 OK`, for `404`, `404 Not Found` etc..
    """

  var url: URL
    """
    The HTTP request [URL](http-URL).
    It will be used for the HTTP path and the `Host` header.
    The `user` and `password` fields are ignored.

    For HTTP responses this will be an empty [URL](http-URL).
    """
  var _body_length: USize = 0
  var transfer_mode: TransferMode = OneshotTransfer
    """
    Determines the transfer mode of this message.

    In case of outgoing requests or responses,
    use `set_length` to control the transfer mode.

    In case of incoming requests, this field determines
    how the request is transferred.
    """
  var session: (HTTPSession | None) = None

  embed _headers: Map[String, String] = _headers.create()
  embed _body: Array[ByteSeq val] = _body.create()
  let _response: Bool
  var username: String = ""
    """
    The username extracted from an `Authentication` header of an HTTP request
    received via [HTTPServer](https://ponylang.github.io/http_server/http_server-Server/).

    This is not used and not sent using [HTTPClient](http-HTTPClient),
    use `update` to set an `Authentication` header instead.
    """
  var password: String = ""
    """
    The password extracted from an `Authentication` header of an HTTP request
    received via [HTTPServer](https://ponylang.github.io/http_server/http_server-Server/).

    This is not used and not sent using [HTTPClient](http-HTTPClient),
    use `update` to set an `Authentication` header instead.
    """

  new iso request(method': String = "GET", url': URL = URL) =>
    """
    Create an HTTP `request` message.
    """
    status = 0
    method = method'
    url = url'
    _response = false

  new iso response(status': Status = StatusOK) =>
    """
    Create an HTTP `response` message.
    """
    status = status'()
    method = status'.string()
    url = URL
    _response = true

  new iso _empty(response': Bool = true) =>
    """
    Create an empty HTTP payload.
    """
    status = 0
    method = ""
    url = URL
    _response = response'

  fun apply(key: String): String ? =>
    """
    Get a header.
    """
    _headers(key.lower())?

  fun is_safe(): Bool =>
    """
    A request method is "safe" if it does not modify state in the resource.
    These methods can be guaranteed not to have any body data.
    Return true for a safe request method, false otherwise.
    """
    match method
    | "GET"
    | "HEAD"
    | "OPTIONS" =>
      true
    else
      false
    end

  fun body(): this->Array[ByteSeq] ? =>
    """
    Get the body in `OneshotTransfer` mode.
    In the other modes it raises an error.
    """
    match transfer_mode
    | OneshotTransfer => _body
    else error
    end

  fun ref set_length(bytecount: (USize | None)) =>
    """
    Set the body length when known in advance. This determines the
    transfer mode that will be used. A parameter of 'None' will use
    Chunked Transfer Encoding. A numeric value will use Streamed
    transfer. Not calling this function at all will
    use Oneshot transfer.
    """
    match bytecount
    | None  =>
      transfer_mode = ChunkedTransfer
      _headers("Transfer-Encoding") = "chunked"
    | let n: USize =>
      try not _headers.contains("Content-Length") then
        _headers("Content-Length") = n.string()
      end
      _body_length = n
      transfer_mode = StreamTransfer
    end

  fun ref update(key: String, value: String): Payload ref^ =>
    """
    Set any header. If we've already received the header, append the value as a
    comma separated list, as per RFC 2616 section 4.2.
    """
    _headers.upsert(key.lower(),
      value,
      {(current, provided) => current + "," + provided})
    this

  fun headers(): this->Map[String, String] =>
    """
    Get all the headers.
    """
    _headers

  fun body_size(): (USize | None) =>
    """
    Get the total intended size of the body.
    `ServerConnection` accumulates actual size transferred for logging.
    """
    match transfer_mode
    | ChunkedTransfer => None
    else _body_length
    end

  fun ref add_chunk(data: ByteSeq val): Payload ref^ =>
    """
    This is how application code adds data to the body in
    `OneshotTransfer` mode. For large bodies, call `set_length`
    and use `send_chunk` instead.
    """
    _body.push(data)
    _body_length = _body_length + data.size()

    this

  fun box send_chunk(data: ByteSeq val) =>
    """
    This is how application code sends body data in `StreamTransfer` and
    `ChunkedTransfer` modes. For smaller body lengths, `add_chunk` in
    `Oneshot` mode can be used instead.
    """
    match session
    | let s: HTTPSession =>
      match transfer_mode
      | ChunkedTransfer =>
        // Wrap some body data in the Chunked Transfer Encoding format,
        // which is the length in hex, the data, and a CRLF. It is
        // important to never send a chunk of length zero, as that is
        // how the end of the body is signalled.
        s.write(Format.int[USize](data.size(), FormatHexBare))
        s.write("\r\n")
        s.write(data)
        s.write("\r\n")
      | StreamTransfer =>
        // In stream mode just send the data. Its length should have
        // already been accounted for by `set_length`.
        s.write(data)
      end
    end

  fun val finish() =>
    """
    Mark the end of body transmission. This does not do anything,
    and is unnecessary, in Oneshot mode.
    """
    match session
    | let s: HTTPSession =>
      match transfer_mode
      | ChunkedTransfer =>
        s.write("0\r\n\r\n")
        s.finish()
      | StreamTransfer =>
        s.finish()
      end
    end

  fun val respond(response': Payload) =>
    """
    Start sending a response from the server to the client.
    """
    try
      (session as HTTPSession)(consume response')
    end

  fun val _client_fail() =>
    """
    Start sending an error response.
    """
    None
    /* Not sure if we need this. Nobody calls it. But something like:
    try
      (session as HTTPSession)(
        Payload.response(StatusInternalServerError))
    end
    */

  fun val _write(keepalive: Bool = true, wr: Writer ref) =>
    """
    Writes the payload to a Writer. Requests and Responses differ
    only in the first line of text - everything after that is the same format.
    """
    if _response then
      _write_response(keepalive, wr)
    else
      _write_request(keepalive, wr)
    end

    _write_common(wr)

  fun val _write_request(keepalive: Bool, wr: Writer ref) =>
    """
    Writes the 'request' parts of an HTTP message.
    """
    wr
      .> write(method)
      .> write(" ")
      .> write(url.path)

    if url.query.size() > 0 then
      wr
        .> write("?")
        .> write(url.query)
    end

    if url.fragment.size() > 0 then
      wr
        .> write("#")
        .> write(url.fragment)
    end

    wr
      .> write(" ")
      .> write(proto)
      .> write("\r\n")

    if not keepalive then
      wr.write("Connection: close\r\n")
    end

    if url.port == url.default_port() then
      wr
        .> write("Host: ")
        .> write(url.host)
        .> write("\r\n")
    else
      wr
        .> write("Host: ")
        .> write(url.host)
        .> write(":")
        .> write(url.port.string())
        .> write("\r\n")
    end

  fun val _write_common(wr: Writer ref) =>
    """
    Writes the parts of an HTTP message common to both requests and
    responses.
    """
    _write_headers(wr)

    // In oneshot mode we send the entire stored body.
    if transfer_mode is OneshotTransfer then
      for piece in _body.values() do
        wr.write(piece)
      end
    end

  fun val _write_response(keepalive: Bool, wr: Writer ref) =>
    """
    Write the response-specific parts of an HTTP message. This is the
    status line, consisting of the protocol name, the status value,
    and a string representation of the status (carried in the `method`
    field). Since writing it out is an actor behavior call, we go to
    the trouble of packaging it into a single string before sending.
    """

    wr
      .> write(proto)
      .> write(" ")
      .> write(status.string())
      .> write(" ")
      .> write(method)
      .> write("\r\n")

    if keepalive then
      wr.write("Connection: keep-alive\r\n")
    end

  fun _write_headers(wr: Writer ref) =>
    """
    Write all of the HTTP headers to the Writer.
    """
    var saw_length: Bool = false
    for (k, v) in _headers.pairs() do
      if (k != "Host") then
        if k == "Content-Length" then saw_length = true end
        wr
          .> write(k)
          .> write(": ")
          .> write(v)
          .> write("\r\n")
      end
    end

    if (not saw_length) and (transfer_mode is OneshotTransfer) then
      wr
        .> write("Content-Length: ")
        .> write(_body_length.string())
        .> write("\r\n")
    end

    // Blank line before the body.
    wr.write("\r\n")

  fun box has_body(): Bool =>
    """
    Determines whether a message has a body portion.
    """
    if _response then
      // Errors never have bodies.
      if
        (status == 204) // no content
          or (status == 304) // not modified
          or ((status > 0) and (status < 200))
          or (status > 400)
      then
        false
      else
        true
      end
    else
      match transfer_mode
      | ChunkedTransfer => true
      else (_body_length > 0)
      end
    end