nim-chronos/chronos/apps/http/httpcommon.nim

357 lines
12 KiB
Nim
Raw Permalink Normal View History

2021-01-27 06:14:17 +00:00
#
# Chronos HTTP/S common types
# (c) Copyright 2021-Present
2021-01-27 06:14:17 +00:00
# Status Research & Development GmbH
#
# Licensed under either of
# Apache License, version 2.0, (LICENSE-APACHEv2)
# MIT license (LICENSE-MIT)
{.push raises: [].}
import std/[strutils, uri]
2023-11-13 09:56:19 +00:00
import results, httputils
import ../../asyncloop, ../../asyncsync
import ../../streams/[asyncstream, boundstream]
export asyncloop, asyncsync, results, httputils, strutils
2021-01-27 06:14:17 +00:00
const
HttpServerUnsecureConnectionTrackerName* =
"httpserver.unsecure.connection"
HttpServerSecureConnectionTrackerName* =
"httpserver.secure.connection"
HttpServerRequestTrackerName* =
"httpserver.request"
HttpServerResponseTrackerName* =
"httpserver.response"
HeadersMark* = @[0x0d'u8, 0x0a'u8, 0x0d'u8, 0x0a'u8]
2021-01-30 18:18:06 +00:00
PostMethods* = {MethodPost, MethodPatch, MethodPut, MethodDelete}
2021-01-27 06:14:17 +00:00
MaximumBodySizeError* = "Maximum size of request's body reached"
UserAgentHeader* = "user-agent"
DateHeader* = "date"
HostHeader* = "host"
ConnectionHeader* = "connection"
AcceptHeaderName* = "accept"
ContentLengthHeader* = "content-length"
TransferEncodingHeader* = "transfer-encoding"
ContentEncodingHeader* = "content-encoding"
ContentTypeHeader* = "content-type"
ExpectHeader* = "expect"
ServerHeader* = "server"
LocationHeader* = "location"
AuthorizationHeader* = "authorization"
ContentDispositionHeader* = "content-disposition"
UrlEncodedContentType* = MediaType.init("application/x-www-form-urlencoded")
MultipartContentType* = MediaType.init("multipart/form-data")
2021-01-27 06:14:17 +00:00
type
HttpMessage* = object
code*: HttpCode
contentType*: MediaType
message*: string
2021-01-27 06:14:17 +00:00
HttpResult*[T] = Result[T, string]
HttpResultCode*[T] = Result[T, HttpCode]
HttpResultMessage*[T] = Result[T, HttpMessage]
2021-01-27 06:14:17 +00:00
HttpError* = object of AsyncError
HttpInterruptError* = object of HttpError
HttpTransportError* = object of HttpError
HttpAddressError* = object of HttpTransportError
HttpRedirectError* = object of HttpTransportError
HttpConnectionError* = object of HttpTransportError
HttpReadError* = object of HttpTransportError
HttpReadLimitError* = object of HttpReadError
HttpDisconnectError* = object of HttpReadError
HttpWriteError* = object of HttpTransportError
HttpProtocolError* = object of HttpError
code*: HttpCode
HttpCriticalError* = object of HttpProtocolError # deprecated
HttpRecoverableError* = object of HttpProtocolError # deprecated
HttpRequestError* = object of HttpProtocolError
HttpRequestHeadersError* = object of HttpRequestError
HttpRequestBodyError* = object of HttpRequestError
HttpRequestHeadersTooLargeError* = object of HttpRequestHeadersError
HttpRequestBodyTooLargeError* = object of HttpRequestBodyError
HttpResponseError* = object of HttpProtocolError
HttpInvalidUsageError* = object of HttpError
HttpUseClosedError* = object of HttpInvalidUsageError
2021-01-27 19:39:14 +00:00
KeyValueTuple* = tuple
key: string
value: string
2021-01-27 19:39:14 +00:00
TransferEncodingFlags* {.pure.} = enum
Identity, Chunked, Compress, Deflate, Gzip
ContentEncodingFlags* {.pure.} = enum
Identity, Br, Compress, Deflate, Gzip
2021-01-27 06:14:17 +00:00
QueryParamsFlag* {.pure.} = enum
CommaSeparatedArray ## Enable usage of comma symbol as separator of array
## items
HttpState* {.pure.} = enum
Alive, Closing, Closed
HttpAddressErrorType* {.pure.} = enum
InvalidUrlScheme,
InvalidPortNumber,
MissingHostname,
InvalidIpHostname,
NameLookupFailed,
NoAddressResolved
const
CriticalHttpAddressError* = {
HttpAddressErrorType.InvalidUrlScheme,
HttpAddressErrorType.InvalidPortNumber,
HttpAddressErrorType.MissingHostname,
HttpAddressErrorType.InvalidIpHostname
}
RecoverableHttpAddressError* = {
HttpAddressErrorType.NameLookupFailed,
HttpAddressErrorType.NoAddressResolved
}
func isCriticalError*(error: HttpAddressErrorType): bool =
error in CriticalHttpAddressError
func isRecoverableError*(error: HttpAddressErrorType): bool =
error in RecoverableHttpAddressError
func toString*(error: HttpAddressErrorType): string =
case error
of HttpAddressErrorType.InvalidUrlScheme:
"URL scheme not supported"
of HttpAddressErrorType.InvalidPortNumber:
"Invalid URL port number"
of HttpAddressErrorType.MissingHostname:
"Missing URL hostname"
of HttpAddressErrorType.InvalidIpHostname:
"Invalid IPv4/IPv6 address in hostname"
of HttpAddressErrorType.NameLookupFailed:
"Could not resolve remote address"
of HttpAddressErrorType.NoAddressResolved:
"No address has been resolved"
proc raiseHttpRequestBodyTooLargeError*() {.
noinline, noreturn, raises: [HttpRequestBodyTooLargeError].} =
raise (ref HttpRequestBodyTooLargeError)(
code: Http413, msg: MaximumBodySizeError)
proc raiseHttpCriticalError*(msg: string, code = Http400) {.
noinline, noreturn, raises: [HttpCriticalError].} =
2021-02-17 00:03:12 +00:00
raise (ref HttpCriticalError)(code: code, msg: msg)
2021-01-27 19:39:14 +00:00
proc raiseHttpDisconnectError*() {.
noinline, noreturn, raises: [HttpDisconnectError].} =
2021-02-17 00:03:12 +00:00
raise (ref HttpDisconnectError)(msg: "Remote peer disconnected")
2021-01-27 19:39:14 +00:00
proc raiseHttpConnectionError*(msg: string) {.
noinline, noreturn, raises: [HttpConnectionError].} =
raise (ref HttpConnectionError)(msg: msg)
proc raiseHttpInterruptError*() {.
noinline, noreturn, raises: [HttpInterruptError].} =
raise (ref HttpInterruptError)(msg: "Connection was interrupted")
proc raiseHttpReadError*(msg: string) {.
noinline, noreturn, raises: [HttpReadError].} =
raise (ref HttpReadError)(msg: msg)
proc raiseHttpProtocolError*(msg: string) {.
noinline, noreturn, raises: [HttpProtocolError].} =
raise (ref HttpProtocolError)(code: Http400, msg: msg)
proc raiseHttpProtocolError*(code: HttpCode, msg: string) {.
noinline, noreturn, raises: [HttpProtocolError].} =
raise (ref HttpProtocolError)(code: code, msg: msg)
proc raiseHttpProtocolError*(msg: HttpMessage) {.
noinline, noreturn, raises: [HttpProtocolError].} =
raise (ref HttpProtocolError)(code: msg.code, msg: msg.message)
proc raiseHttpWriteError*(msg: string) {.
noinline, noreturn, raises: [HttpWriteError].} =
raise (ref HttpWriteError)(msg: msg)
proc raiseHttpRedirectError*(msg: string) {.
noinline, noreturn, raises: [HttpRedirectError].} =
raise (ref HttpRedirectError)(msg: msg)
proc raiseHttpAddressError*(msg: string) {.
noinline, noreturn, raises: [HttpAddressError].} =
raise (ref HttpAddressError)(msg: msg)
template newHttpInterruptError*(): ref HttpInterruptError =
newException(HttpInterruptError, "Connection was interrupted")
template newHttpReadError*(message: string): ref HttpReadError =
newException(HttpReadError, message)
template newHttpWriteError*(message: string): ref HttpWriteError =
newException(HttpWriteError, message)
template newHttpUseClosedError*(): ref HttpUseClosedError =
newException(HttpUseClosedError, "Connection was already closed")
func init*(t: typedesc[HttpMessage], code: HttpCode, message: string,
contentType: MediaType): HttpMessage =
HttpMessage(code: code, message: message, contentType: contentType)
func init*(t: typedesc[HttpMessage], code: HttpCode, message: string,
contentType: string): HttpMessage =
HttpMessage(code: code, message: message,
contentType: MediaType.init(contentType))
func init*(t: typedesc[HttpMessage], code: HttpCode,
message: string): HttpMessage =
HttpMessage(code: code, message: message,
contentType: MediaType.init("text/plain"))
func init*(t: typedesc[HttpMessage], code: HttpCode): HttpMessage =
HttpMessage(code: code)
iterator queryParams*(query: string,
flags: set[QueryParamsFlag] = {}): KeyValueTuple =
2021-01-27 19:39:14 +00:00
## Iterate over url-encoded query string.
for pair in query.split('&'):
let items = pair.split('=', maxsplit = 1)
let k = items[0]
if len(k) > 0:
let v = if len(items) > 1: items[1] else: ""
if CommaSeparatedArray in flags:
for av in decodeUrl(v).split(','):
yield (decodeUrl(k), av)
else:
yield (decodeUrl(k), decodeUrl(v))
2021-01-27 19:39:14 +00:00
func getTransferEncoding*(
ch: openArray[string]
): HttpResult[set[TransferEncodingFlags]] =
2021-01-27 19:39:14 +00:00
## Parse value of multiple HTTP headers ``Transfer-Encoding`` and return
## it as set of ``TransferEncodingFlags``.
var res: set[TransferEncodingFlags] = {}
if len(ch) == 0:
res.incl(TransferEncodingFlags.Identity)
ok(res)
else:
for header in ch:
for item in header.split(","):
case strip(item.toLowerAscii())
of "identity":
res.incl(TransferEncodingFlags.Identity)
of "chunked":
res.incl(TransferEncodingFlags.Chunked)
of "compress":
res.incl(TransferEncodingFlags.Compress)
of "deflate":
res.incl(TransferEncodingFlags.Deflate)
of "gzip":
res.incl(TransferEncodingFlags.Gzip)
2021-02-10 13:13:36 +00:00
of "x-gzip":
res.incl(TransferEncodingFlags.Gzip)
2021-01-27 19:39:14 +00:00
of "":
res.incl(TransferEncodingFlags.Identity)
else:
return err("Incorrect Transfer-Encoding value")
ok(res)
func getContentEncoding*(
ch: openArray[string]
): HttpResult[set[ContentEncodingFlags]] =
2021-01-27 19:39:14 +00:00
## Parse value of multiple HTTP headers ``Content-Encoding`` and return
## it as set of ``ContentEncodingFlags``.
var res: set[ContentEncodingFlags] = {}
if len(ch) == 0:
res.incl(ContentEncodingFlags.Identity)
ok(res)
else:
for header in ch:
for item in header.split(","):
case strip(item.toLowerAscii()):
of "identity":
res.incl(ContentEncodingFlags.Identity)
of "br":
res.incl(ContentEncodingFlags.Br)
of "compress":
res.incl(ContentEncodingFlags.Compress)
of "deflate":
res.incl(ContentEncodingFlags.Deflate)
of "gzip":
res.incl(ContentEncodingFlags.Gzip)
2021-02-10 13:13:36 +00:00
of "x-gzip":
res.incl(ContentEncodingFlags.Gzip)
2021-01-27 19:39:14 +00:00
of "":
res.incl(ContentEncodingFlags.Identity)
else:
return err("Incorrect Content-Encoding value")
ok(res)
func getContentType*(ch: openArray[string]): HttpResult[ContentTypeData] =
2021-01-27 19:39:14 +00:00
## Check and prepare value of ``Content-Type`` header.
if len(ch) == 0:
err("No Content-Type values found")
elif len(ch) > 1:
2021-01-27 19:39:14 +00:00
err("Multiple Content-Type values found")
else:
let res = getContentType(ch[0])
if res.isErr():
return err($res.error())
ok(res.get())
2021-12-10 09:55:25 +00:00
proc bytesToString*(src: openArray[byte], dst: var openArray[char]) =
## Convert array of bytes to array of characters.
##
## Note, that this procedure assume that `sizeof(byte) == sizeof(char) == 1`.
## If this equation is not correct this procedures MUST not be used.
doAssert(len(src) == len(dst))
if len(src) > 0:
copyMem(addr dst[0], unsafeAddr src[0], len(src))
2021-12-10 09:55:25 +00:00
proc stringToBytes*(src: openArray[char], dst: var openArray[byte]) =
## Convert array of characters to array of bytes.
##
## Note, that this procedure assume that `sizeof(byte) == sizeof(char) == 1`.
## If this equation is not correct this procedures MUST not be used.
doAssert(len(src) == len(dst))
if len(src) > 0:
copyMem(addr dst[0], unsafeAddr src[0], len(src))
2021-12-10 09:55:25 +00:00
func bytesToString*(src: openArray[byte]): string =
## Convert array of bytes to a string.
##
## Note, that this procedure assume that `sizeof(byte) == sizeof(char) == 1`.
## If this equation is not correct this procedures MUST not be used.
var default: string
if len(src) > 0:
var dst = newString(len(src))
bytesToString(src, dst)
dst
else:
default
2021-12-10 09:55:25 +00:00
func stringToBytes*(src: openArray[char]): seq[byte] =
## Convert string to sequence of bytes.
##
## Note, that this procedure assume that `sizeof(byte) == sizeof(char) == 1`.
## If this equation is not correct this procedures MUST not be used.
var default: seq[byte]
if len(src) > 0:
var dst = newSeq[byte](len(src))
stringToBytes(src, dst)
dst
else:
default