mirror of
https://github.com/waku-org/nwaku.git
synced 2025-01-13 08:14:43 +00:00
1940 lines
59 KiB
Nim
1940 lines
59 KiB
Nim
#
|
|
# HTTP Utilities
|
|
# (c) Copyright 2018
|
|
# Status Research & Development GmbH
|
|
#
|
|
# Licensed under either of
|
|
# Apache License, version 2.0, (LICENSE-APACHEv2)
|
|
# MIT license (LICENSE-MIT)
|
|
import std/[times, strutils, algorithm, sequtils]
|
|
import stew/results
|
|
export results
|
|
|
|
const
|
|
ALPHA* = {'a'..'z', 'A'..'Z'}
|
|
NUM* = {'0'..'9'}
|
|
TOKEND* = {'!', '#', '$', '%', '&', '\'', '*', '+', '-', '.', '^', '_', '`',
|
|
'|', '~'}
|
|
# HTTP token delimeters
|
|
URITOKEND* = {'-', '.', '_', '~', ':', '/', '?', '#', '[', ']', '@', '!',
|
|
'$', '&', '\'', '(', ')', '*', '+', ',', ';', '=', '%'}
|
|
# URI token delimeters
|
|
SPACE* = {' ', '\t'}
|
|
COLON* = {':'}
|
|
SLASH* = {'/'}
|
|
DOT* = {'.'}
|
|
CR* = char(0x0D)
|
|
LF* = char(0x0A)
|
|
ALPHANUM = ALPHA + NUM
|
|
TOKENURI = TOKEND * URITOKEND - DOT
|
|
TOKENONLY = TOKEND - URITOKEND - DOT
|
|
URIONLY = URITOKEND - TOKEND - COLON - SLASH - DOT
|
|
HEADERNAME* = ALPHANUM + TOKENURI + TOKENONLY + DOT
|
|
|
|
BSLASH = {'\\'}
|
|
FSLASH = {'/'}
|
|
COMMA = {','}
|
|
DQUOTE = {'"'}
|
|
EQUALS = {'='}
|
|
SEMCOL = {';'}
|
|
SPACEO = {'\x20'}
|
|
HOTAB = {'\x09'}
|
|
CHAR = {'\x00' .. '\x7E'}
|
|
CTL = {'\x00' .. '\x1F', '\x7F'}
|
|
SEPARATORS = {'(', ')', '<', '>', '@', ',', ';', ':',
|
|
'\\', '"', '/', '[', ']', '?', '=', '{',
|
|
'}'} + SPACEO + HOTAB
|
|
LTOKEN = CHAR - CTL - SEPARATORS
|
|
LSEP = SEPARATORS - BSLASH - DQUOTE - EQUALS - SEMCOL -
|
|
SPACEO - HOTAB
|
|
LSEP2 = LSEP - COMMA - FSLASH
|
|
LSEP3 = LSEP - FSLASH
|
|
CTL2 = CTL - HOTAB
|
|
|
|
# Legend:
|
|
# [0x81, 0x8D] - markers
|
|
# 0x81 - start HTTP request method
|
|
# 0x82 - end of HTTP request method
|
|
# 0x83 - start of HTTP request URI
|
|
# 0x84 - end of HTTP request URI
|
|
# 0x85 - start of HTTP version
|
|
# 0x86 - end of HTTP version
|
|
# 0x87 - LF
|
|
# 0x88 - start of header name
|
|
# 0x89 - end of header name
|
|
# 0x8A - start of header value
|
|
# 0x8B - end of header value
|
|
# 0x8C - last header LF
|
|
# 0x8D - header's finish
|
|
# [0xC0, 0xCF] - errors
|
|
|
|
# * ALPHA NUM TO^UR TOON URON CR LF COLON SLASH DOT SPACE PAD PAD PAD PAD
|
|
requestSM = [
|
|
0xC0, 0x81, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xCF, 0xCF, 0xCF, 0xCF, # s00: first method char
|
|
0xC1, 0x01, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1, 0x82, 0xCF, 0xCF, 0xCF, 0xCF, # s01: method
|
|
0xC2, 0x83, 0x83, 0x83, 0xC2, 0x83, 0xC2, 0xC2, 0x83, 0x83, 0x83, 0xC2, 0xCF, 0xCF, 0xCF, 0xCF, # s02: first uri char
|
|
0xC2, 0x03, 0x03, 0x03, 0xC2, 0x03, 0xC2, 0xC2, 0x03, 0x03, 0x03, 0x84, 0xCF, 0xCF, 0xCF, 0xCF, # s03: uri
|
|
0xC3, 0x85, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xCF, 0xCF, 0xCF, 0xCF, # s04: first version char
|
|
0xC3, 0x05, 0x05, 0xC3, 0xC3, 0xC3, 0x86, 0xC3, 0xC3, 0x05, 0x05, 0xC3, 0xCF, 0xCF, 0xCF, 0xCF, # s05: version
|
|
0xC4, 0xC4, 0xC4, 0xC4, 0xC4, 0xC4, 0xC4, 0x87, 0xC4, 0xC4, 0xC4, 0xC4, 0xCF, 0xCF, 0xCF, 0xCF, # s06: LF
|
|
0xC5, 0x88, 0x88, 0x88, 0x88, 0xC5, 0x8D, 0xC5, 0xC5, 0xC5, 0x88, 0xC5, 0xCF, 0xCF, 0xCF, 0xCF, # s07: first token char
|
|
0xC5, 0x08, 0x08, 0x08, 0x08, 0xC5, 0xC5, 0xC5, 0x89, 0xC5, 0x08, 0xC5, 0xCF, 0xCF, 0xCF, 0xCF, # s08: header name
|
|
0x8B, 0x8B, 0x8B, 0x8B, 0x8B, 0x8B, 0x8C, 0xC6, 0x8B, 0x8B, 0x8B, 0x8A, 0xCF, 0xCF, 0xCF, 0xCF, # s09: first header char
|
|
0x8B, 0x8B, 0x8B, 0x8B, 0x8B, 0x8B, 0x8C, 0xC6, 0x8B, 0x8B, 0x8B, 0x0A, 0xCF, 0xCF, 0xCF, 0xCF, # s0a: 1st space
|
|
0x0B, 0x0B, 0x0B, 0x0B, 0x0B, 0x0B, 0x8C, 0xC7, 0x0B, 0x0B, 0x0B, 0x0B, 0xCF, 0xCF, 0xCF, 0xCF, # s0b: header value
|
|
0xC7, 0xC7, 0xC7, 0xC7, 0xC7, 0xC7, 0xC7, 0x87, 0xC7, 0xC7, 0xC7, 0xC7, 0xCF, 0xCF, 0xCF, 0xCF, # s0c: header LF
|
|
0xC8, 0xC8, 0xC8, 0xC8, 0xC8, 0xC8, 0xC8, 0x8E, 0xC8, 0xC8, 0xC8, 0xC8, 0xCF, 0xCF, 0xCF, 0xCF # s0d: last LF
|
|
]
|
|
|
|
# Legend:
|
|
# [0x81, 0x8D] - markers
|
|
# 0x81 - start HTTP version
|
|
# 0x82 - end of HTTP version
|
|
# 0x83 - start of HTTP response code
|
|
# 0x84 - end of HTTP response code
|
|
# 0x85 - start of HTTP reason string
|
|
# 0x86 - end of HTTP reason string
|
|
# 0x87 - LF
|
|
# 0x88 - start of header name
|
|
# 0x89 - end of header name
|
|
# 0x8A - start of header value
|
|
# 0x8B - end of header value
|
|
# 0x8C - last header LF
|
|
# 0x8D - header's finish
|
|
# [0xC0, 0xCF] - errors
|
|
|
|
# * ALPHA NUM TO^UR TOON URON CR LF COLON SLASH DOT SPACE PAD PAD PAD PAD
|
|
responseSM = [
|
|
0xC0, 0x81, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xCF, 0xCF, 0xCF, 0xCF, # s00: first version char
|
|
0xC1, 0x01, 0x01, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1, 0x01, 0x01, 0x82, 0xCF, 0xCF, 0xCF, 0xCF, # s01: version
|
|
0xC2, 0xC2, 0x83, 0xC2, 0xC2, 0xC2, 0xC2, 0xC2, 0xC2, 0xC2, 0xC2, 0xC2, 0xCF, 0xCF, 0xCF, 0xCF, # s02: first code char
|
|
0xC2, 0xC2, 0x03, 0xC2, 0xC2, 0xC2, 0x84, 0xC2, 0xC2, 0xC2, 0xC2, 0x85, 0xCF, 0xCF, 0xCF, 0xCF, # s03: code
|
|
0xC2, 0xC2, 0xC2, 0xC2, 0xC2, 0xC2, 0xC2, 0x88, 0xC2, 0xC2, 0xC2, 0xC2, 0xCF, 0xCF, 0xCF, 0xCF, # s04: no reason LF
|
|
0xC3, 0x86, 0x86, 0x86, 0x86, 0x86, 0x87, 0xC3, 0x86, 0x86, 0x86, 0x86, 0xCF, 0xCF, 0xCF, 0xCF, # s05: first reason char
|
|
0xC3, 0x06, 0x06, 0x06, 0x06, 0x06, 0x87, 0xC3, 0x06, 0x06, 0x06, 0x06, 0xCF, 0xCF, 0xCF, 0xCF, # s06: reason
|
|
0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x88, 0xC3, 0xC3, 0xC3, 0xC3, 0xCF, 0xCF, 0xCF, 0xCF, # s07: LF
|
|
0xC3, 0x8A, 0x8A, 0x8A, 0x8A, 0xC3, 0x8F, 0xC3, 0xC3, 0xC3, 0x8A, 0xC4, 0xCF, 0xCF, 0xCF, 0xCF, # s08: no headers CR
|
|
0xC4, 0x8A, 0x8A, 0x8A, 0x8A, 0xC4, 0x8F, 0xC4, 0xC4, 0xC4, 0x8A, 0xC4, 0xCF, 0xCF, 0xCF, 0xCF, # s09: first token char
|
|
0xC4, 0x0A, 0x0A, 0x0A, 0x0A, 0xC4, 0xC4, 0xC4, 0x8B, 0xC4, 0x0A, 0xC4, 0xCF, 0xCF, 0xCF, 0xCF, # s0a: header name
|
|
0x8D, 0x8D, 0x8D, 0x8D, 0x8D, 0x8D, 0x8E, 0xC5, 0x8D, 0x8D, 0x8D, 0x8C, 0xCF, 0xCF, 0xCF, 0xCF, # s0b: first header char
|
|
0x8D, 0x8D, 0x8D, 0x8D, 0x8D, 0xC5, 0x8E, 0xC5, 0x8D, 0x8D, 0x8D, 0x0C, 0xCF, 0xCF, 0xCF, 0xCF, # s0c: 1st space
|
|
0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x8E, 0xC5, 0x0D, 0x0D, 0x0D, 0x0D, 0xCF, 0xCF, 0xCF, 0xCF, # s0d: header value
|
|
0xC7, 0xC7, 0xC7, 0xC7, 0xC7, 0xC7, 0xC7, 0x89, 0xC7, 0xC7, 0xC7, 0xC7, 0xCF, 0xCF, 0xCF, 0xCF, # s0e: header LF
|
|
0xC8, 0xC8, 0xC8, 0xC8, 0xC8, 0xC8, 0xC8, 0x9F, 0xC8, 0xC8, 0xC8, 0xC8, 0xCF, 0xCF, 0xCF, 0xCF, # s0f: last LF
|
|
]
|
|
|
|
# Legend:
|
|
# 0x81 - start of disposition type
|
|
# 0x82 - end of disposition type or end of token value.
|
|
# 0x83 - start of parameter name
|
|
# 0x84 - end of parameter name
|
|
# 0x85 - start of tokenized parameter value
|
|
# 0x86 - end of tokenized parameter value
|
|
# 0x87 - double quote
|
|
# 0x88 - start of quoted parameter value
|
|
# 0x89 - 0x8A - escaped quote
|
|
# 0x8B - end of quoted parameter value
|
|
# 0x8C - semicolon and spaces
|
|
# [0xC0 - 0xCC] error values
|
|
|
|
# * LTOK LSEP BSLA DQUO EQUA SEMC SPAC HOTA
|
|
contdispSM = [
|
|
0xC0, 0x81, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, # s0: start of disposition-type
|
|
0xC1, 0x01, 0xC1, 0xC1, 0xC1, 0xC1, 0x82, 0xC1, 0xC1, # s1: disposition-type
|
|
0xC2, 0x83, 0xC2, 0xC2, 0xC2, 0xC2, 0xC2, 0x02, 0x02, # s2: semicolon and spaces
|
|
0xC3, 0x03, 0xC1, 0xC1, 0xC1, 0x84, 0xC1, 0xC1, 0xC1, # s3: parm-name
|
|
0xC4, 0x85, 0xC4, 0xC4, 0x87, 0xC4, 0x8D, 0xC4, 0xC4, # s4: =
|
|
0xC5, 0x05, 0xC5, 0xC5, 0xC5, 0xC5, 0x86, 0xC5, 0xC5, # s5: parm-value as token start
|
|
0xC6, 0x83, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0x06, 0x06, # s6: parm-value as token finish
|
|
0xC7, 0x88, 0x88, 0x88, 0x8B, 0x88, 0x88, 0x88, 0x88, # s7: double quote
|
|
0xC8, 0x08, 0x08, 0x89, 0x8B, 0x08, 0x08, 0x08, 0x08, # s8: quoted parm-value
|
|
0xC9, 0x08, 0x08, 0x08, 0x8A, 0x08, 0x08, 0x08, 0x08, # s9: middle double quote
|
|
0xCA, 0x08, 0x08, 0x89, 0x8B, 0x08, 0x08, 0x08, 0x08, # sA: quoted parm-value
|
|
0xCB, 0xCB, 0xCB, 0xCB, 0xCB, 0xCB, 0x8C, 0xCB, 0xCB, # sB: finish double quote
|
|
0xCC, 0x83, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0x0C, 0x0C, # sC: semicolon and spaces
|
|
0xCD, 0x83, 0xCD, 0xCD, 0xCD, 0xCD, 0xCD, 0x0D, 0x0D, # sD: semicolon and spaces
|
|
]
|
|
|
|
# * CTL2 LTOK LSEP3 / \ ; " = SPACE
|
|
contentTypeSM = [
|
|
0xE00, 0xE00, 0x801, 0xE00, 0xE00, 0xE00, 0xE00, 0xE00, 0xE00, 0xE00, # s00: start of media-type
|
|
0xE01, 0xE01, 0x001, 0xE01, 0x802, 0xE01, 0xE01, 0xE01, 0xE01, 0xE01, # s01: media-type and forward slash
|
|
0xE02, 0xE02, 0x803, 0xE02, 0xE02, 0xE02, 0xE02, 0xE02, 0xE02, 0xE02, # s02: forward slash
|
|
0xE03, 0xE03, 0x003, 0xE03, 0xE03, 0xE03, 0x805, 0xE03, 0xE03, 0x804, # s03: media-subtype
|
|
0xE04, 0xE04, 0xE04, 0xE04, 0xE04, 0xE04, 0x005, 0xE04, 0xE04, 0x004, # s04: spaces
|
|
0xE05, 0xE05, 0x806, 0xE05, 0xE05, 0xE05, 0xE05, 0xE05, 0xE05, 0x005, # s05: semicolon and spaces
|
|
0xE06, 0xE06, 0x006, 0xE06, 0xE06, 0xE06, 0xE06, 0xE06, 0x807, 0xE06, # s06: param-name
|
|
0xE07, 0xE07, 0x808, 0xE07, 0xE07, 0xE07, 0x811, 0x80B, 0xE07, 0xE07, # s07: =
|
|
0xE08, 0xE08, 0x008, 0xE08, 0xE08, 0xE08, 0x80A, 0xE08, 0xE08, 0x809, # s08: parm-value-token
|
|
0xE09, 0xE09, 0x806, 0xE09, 0xE09, 0xE09, 0x805, 0xE09, 0xE09, 0x009, # s09: spaces after parm-value-token
|
|
0xE0A, 0xE0A, 0x806, 0xE0A, 0xE0A, 0xE0A, 0xE0A, 0xE0A, 0xE0A, 0x00A, # s0a: semicolon and spaces
|
|
0x80C, 0xE0B, 0x80C, 0x80C, 0x80C, 0x80D, 0x80C, 0x80F, 0x80C, 0x80C, # s0b: starting double quote
|
|
0x00C, 0xE0C, 0x00C, 0x00C, 0x00C, 0x80D, 0x00C, 0x80F, 0x00C, 0x00C, # s0c: parm-value-quote
|
|
0x80E, 0xE0D, 0x80E, 0x80E, 0x80E, 0x80E, 0x80E, 0x80E, 0x80E, 0x80E, # s0d: escaped character
|
|
0x00C, 0xE0E, 0x00C, 0x00C, 0x00C, 0x80D, 0x00C, 0x80F, 0x00C, 0x00C, # s0e: parm-value-quote
|
|
0xE0F, 0xE0F, 0xE0F, 0xE0F, 0xE0F, 0xE0F, 0x810, 0xE0F, 0xE0F, 0x00F, # s0f: spaces after parm-value-quote
|
|
0xE10, 0xE10, 0x806, 0xE10, 0xE10, 0xE10, 0xE10, 0xE10, 0xE10, 0x010, # s10: semicolon and spaces
|
|
0xE11, 0xE11, 0x806, 0xE11, 0xE11, 0xE11, 0xE11, 0xE11, 0xE11, 0x011 # s11: empty value, semicolon and spaces
|
|
]
|
|
|
|
# * LTOKN LSEP2 / \ , ; " = SPACE
|
|
acceptSM = [
|
|
0xE00, 0x801, 0xE00, 0xE00, 0xE00, 0x000, 0xE00, 0xE00, 0xE00, 0xE00, # s00: start of media-type
|
|
0xE01, 0x001, 0xE01, 0x802, 0xE01, 0xE01, 0xE01, 0xE01, 0xE01, 0xE01, # s01: media-type and forward slash
|
|
0xE02, 0x803, 0xE02, 0xE02, 0xE02, 0xE02, 0xE02, 0xE02, 0xE02, 0xE02, # s02: forward slash
|
|
0xE03, 0x003, 0xE03, 0xE03, 0xE03, 0x805, 0x806, 0xE03, 0xE03, 0x804, # s03: media-subtype
|
|
0xE04, 0xE04, 0xE04, 0xE04, 0xE04, 0x805, 0x806, 0xE04, 0xE04, 0x004, # s04: spaces
|
|
0xE05, 0x801, 0xE05, 0xE05, 0xE05, 0xE05, 0xE05, 0xE05, 0xE05, 0x005, # s05: comma and spaces
|
|
0xE06, 0x807, 0xE06, 0xE06, 0xE06, 0xE06, 0xE06, 0xE06, 0xE06, 0x006, # s06: semicolon and spaces
|
|
0xE07, 0x007, 0xE07, 0xE07, 0xE07, 0xE07, 0xE07, 0xE07, 0x808, 0xE07, # s07: param-name
|
|
0xE08, 0x80A, 0xE08, 0xE08, 0xE08, 0x809, 0x80D, 0x80E, 0xE08, 0xE08, # s08: =
|
|
0xE09, 0x801, 0xE09, 0xE09, 0xE09, 0xE09, 0xE09, 0xE09, 0xE09, 0x009, # s09: comma and spaces after <param_name>=,
|
|
0xE0A, 0x00A, 0xE0A, 0xE0A, 0xE0A, 0x80C, 0x80B, 0xE0A, 0xE0A, 0xE0A, # s0A: param value as token
|
|
0xE0B, 0x807, 0xE0B, 0xE0B, 0xE0B, 0xE0B, 0xE0B, 0xE0B, 0xE0B, 0x00B, # s0B: spaces after ;
|
|
0xE0C, 0x801, 0xE0C, 0xE0C, 0xE0C, 0xE0C, 0xE0C, 0xE0C, 0xE0C, 0x00C, # s0C: spaces after ,
|
|
0xE0D, 0x807, 0xE0D, 0xE0D, 0xE0D, 0xE0D, 0xE0D, 0xE0D, 0xE0D, 0x00D, # s0D: spaces after =;
|
|
0xE0E, 0x80F, 0x80F, 0x80F, 0x810, 0x80F, 0x80F, 0x812, 0x80F, 0x80F, # s0E: starting double quote
|
|
0xE0F, 0x00F, 0x00F, 0x00F, 0x810, 0x00F, 0x00F, 0x812, 0x00F, 0x00F, # s0F: quoted param-value
|
|
0xE10, 0x00F, 0x00F, 0x00F, 0x00F, 0x00F, 0x00F, 0x811, 0x00F, 0x00F, # s10: backslash
|
|
0xE11, 0x00F, 0x00F, 0x00F, 0x810, 0x00F, 0x00F, 0x812, 0x00F, 0x00F, # s11: double quote after backslash
|
|
0xE12, 0xE12, 0xE12, 0xE12, 0xE12, 0x814, 0x813, 0xE12, 0xE12, 0xE12, # s12: finish double quote
|
|
0xE13, 0x807, 0xE13, 0xE13, 0xE13, 0xE13, 0xE13, 0xE13, 0xE13, 0x013, # s13: [";] and spaces
|
|
0xE14, 0x801, 0xE14, 0xE14, 0xE14, 0xE14, 0xE14, 0xE14, 0xE14, 0x014 # s14: [",] and spaces
|
|
]
|
|
|
|
type
|
|
HttpCode* = enum
|
|
## HTTP error codes
|
|
Http100 = "100 Continue",
|
|
Http101 = "101 Switching Protocols",
|
|
Http200 = "200 OK",
|
|
Http201 = "201 Created",
|
|
Http202 = "202 Accepted",
|
|
Http203 = "203 Non-Authoritative Information",
|
|
Http204 = "204 No Content",
|
|
Http205 = "205 Reset Content",
|
|
Http206 = "206 Partial Content",
|
|
Http300 = "300 Multiple Choices",
|
|
Http301 = "301 Moved Permanently",
|
|
Http302 = "302 Found",
|
|
Http303 = "303 See Other",
|
|
Http304 = "304 Not Modified",
|
|
Http305 = "305 Use Proxy",
|
|
Http307 = "307 Temporary Redirect",
|
|
Http400 = "400 Bad Request",
|
|
Http401 = "401 Unauthorized",
|
|
Http403 = "403 Forbidden",
|
|
Http404 = "404 Not Found",
|
|
Http405 = "405 Method Not Allowed",
|
|
Http406 = "406 Not Acceptable",
|
|
Http407 = "407 Proxy Authentication Required",
|
|
Http408 = "408 Request Timeout",
|
|
Http409 = "409 Conflict",
|
|
Http410 = "410 Gone",
|
|
Http411 = "411 Length Required",
|
|
Http412 = "412 Precondition Failed",
|
|
Http413 = "413 Request Entity Too Large",
|
|
Http414 = "414 Request-URI Too Long",
|
|
Http415 = "415 Unsupported Media Type",
|
|
Http416 = "416 Requested Range Not Satisfiable",
|
|
Http417 = "417 Expectation Failed",
|
|
Http418 = "418 I'm a teapot",
|
|
Http421 = "421 Misdirected Request",
|
|
Http422 = "422 Unprocessable Entity",
|
|
Http426 = "426 Upgrade Required",
|
|
Http428 = "428 Precondition Required",
|
|
Http429 = "429 Too Many Requests",
|
|
Http431 = "431 Request Header Fields Too Large",
|
|
Http451 = "451 Unavailable For Legal Reasons",
|
|
Http500 = "500 Internal Server Error",
|
|
Http501 = "501 Not Implemented",
|
|
Http502 = "502 Bad Gateway",
|
|
Http503 = "503 Service Unavailable",
|
|
Http504 = "504 Gateway Timeout",
|
|
Http505 = "505 HTTP Version Not Supported"
|
|
|
|
HttpVersion* = enum
|
|
## HTTP version
|
|
HttpVersion09
|
|
HttpVersion11,
|
|
HttpVersion10,
|
|
HttpVersion20,
|
|
HttpVersionError
|
|
|
|
HttpMethod* = enum
|
|
## HTTP methods
|
|
MethodGet,
|
|
MethodPost,
|
|
MethodHead,
|
|
MethodPut,
|
|
MethodDelete,
|
|
MethodTrace,
|
|
MethodOptions,
|
|
MethodConnect,
|
|
MethodPatch,
|
|
MethodError
|
|
|
|
HttpStatus* = enum
|
|
## HTTP parser status type
|
|
Success, Failure
|
|
|
|
HttpHeaderPart* = object
|
|
## HTTP offset representation object
|
|
s*: int ## Start offset
|
|
e*: int ## End offset
|
|
|
|
HttpHeader* = object
|
|
## HTTP header representation object
|
|
name*: HttpHeaderPart ## Header name
|
|
value*: HttpHeaderPart ## Header value
|
|
|
|
HttpRequestHeader* = object
|
|
## HTTP request header
|
|
data: seq[byte] ## Data blob
|
|
meth*: HttpMethod ## HTTP request method
|
|
version*: HttpVersion ## HTTP version
|
|
status*: HttpStatus ## HTTP headers processing status
|
|
url: HttpHeaderPart
|
|
state*: int
|
|
hdrs: seq[HttpHeader]
|
|
length*: int ## HTTP headers length
|
|
|
|
HttpHeadersList* = object
|
|
## HTTP request headers list
|
|
data: seq[byte]
|
|
state*: int
|
|
status*: HttpStatus
|
|
hdrs: seq[HttpHeader]
|
|
length*: int
|
|
|
|
HttpResponseHeader* = object
|
|
## HTTP response header
|
|
data: seq[byte] ## Data blob
|
|
version*: HttpVersion ## HTTP version
|
|
code*: int ## HTTP result code
|
|
status*: HttpStatus ## HTTP headers processing status
|
|
rsn: HttpHeaderPart
|
|
state*: int
|
|
hdrs: seq[HttpHeader]
|
|
length*: int ## HTTP headers length
|
|
|
|
ContentDispositionHeader* = object
|
|
## `Content-Disposition` header
|
|
data: seq[byte]
|
|
state*: int
|
|
status*: HttpStatus
|
|
disptype: HttpHeaderPart
|
|
flds: seq[HttpHeader]
|
|
|
|
AcceptMediaType* = object
|
|
mtype*: HttpHeaderPart
|
|
stype*: HttpHeaderPart
|
|
params*: seq[HttpHeader]
|
|
|
|
AcceptHeader* = object
|
|
data: seq[byte]
|
|
state*: int
|
|
status*: HttpStatus
|
|
mediaTypes*: seq[AcceptMediaType]
|
|
|
|
MediaType* = object
|
|
media*: string
|
|
subtype*: string
|
|
|
|
AcceptMediaItem* = object
|
|
mediaType*: MediaType
|
|
qvalue*: float
|
|
params*: seq[tuple[name: string, value: string]]
|
|
|
|
AcceptInfo* = object
|
|
data*: seq[AcceptMediaItem]
|
|
|
|
ContentTypeData* = object
|
|
## `Content-Type` header data
|
|
state*: int
|
|
status*: HttpStatus
|
|
mediaType*: MediaType
|
|
params*: seq[tuple[name: string, value: string]]
|
|
|
|
HttpReqRespHeader* = HttpRequestHeader | HttpResponseHeader | HttpHeadersList
|
|
|
|
BChar* = byte | char
|
|
|
|
proc toString[T: BChar](data: openArray[T], start, stop: int): string
|
|
|
|
template processHeaders(sm: untyped, state: var int, ch: char): int =
|
|
let code =
|
|
case ch
|
|
of ALPHA:
|
|
1
|
|
of NUM:
|
|
2
|
|
of TOKENURI:
|
|
3
|
|
of TOKENONLY:
|
|
4
|
|
of URIONLY:
|
|
5
|
|
of CR:
|
|
6
|
|
of LF:
|
|
7
|
|
of COLON:
|
|
8
|
|
of SLASH:
|
|
9
|
|
of DOT:
|
|
10
|
|
of SPACE:
|
|
11
|
|
else:
|
|
0
|
|
let newstate = sm[(state shl 4) + code]
|
|
state = newstate and 0x0F
|
|
newstate
|
|
|
|
template processDisposition(sm: untyped, state: var int, ch: char): int =
|
|
let code =
|
|
case ch
|
|
of LTOKEN:
|
|
1
|
|
of LSEP:
|
|
2
|
|
of BSLASH:
|
|
3
|
|
of DQUOTE:
|
|
4
|
|
of EQUALS:
|
|
5
|
|
of SEMCOL:
|
|
6
|
|
of SPACEO:
|
|
7
|
|
of HOTAB:
|
|
8
|
|
else:
|
|
0
|
|
let newstate = sm[(state shl 3) + state + code]
|
|
state = newstate and 0x0F
|
|
newstate
|
|
|
|
template processAcceptHeader(sm: untyped, state: var int, ch: char): int =
|
|
let code =
|
|
case ch
|
|
of LTOKEN:
|
|
1
|
|
of LSEP2:
|
|
2
|
|
of FSLASH:
|
|
3
|
|
of BSLASH:
|
|
4
|
|
of COMMA:
|
|
5
|
|
of SEMCOL:
|
|
6
|
|
of DQUOTE:
|
|
7
|
|
of EQUALS:
|
|
8
|
|
of SPACE:
|
|
9
|
|
else:
|
|
0
|
|
let newstate = sm[(state shl 3) + (state shl 1) + code]
|
|
state = newstate and 0xFF
|
|
newstate
|
|
|
|
template processContentType(sm: untyped, state: var int, ch: char): int =
|
|
let code =
|
|
case ch
|
|
of CTL2:
|
|
1
|
|
of LTOKEN:
|
|
2
|
|
of LSEP3:
|
|
3
|
|
of FSLASH:
|
|
4
|
|
of BSLASH:
|
|
5
|
|
of SEMCOL:
|
|
6
|
|
of DQUOTE:
|
|
7
|
|
of EQUALS:
|
|
8
|
|
of SPACEO:
|
|
9
|
|
else:
|
|
0
|
|
let newstate = sm[(state shl 3) + (state shl 1) + code]
|
|
state = newstate and 0xFF
|
|
newstate
|
|
|
|
proc processMethod[T: BChar](data: openArray[T], s, e: int): HttpMethod =
|
|
let length = e - s + 1
|
|
case char(data[s])
|
|
of 'G':
|
|
if length == 3:
|
|
if char(data[s + 1]) == 'E' and char(data[s + 2]) == 'T':
|
|
return MethodGet
|
|
of 'H':
|
|
if length == 4:
|
|
if char(data[s + 1]) == 'E' and char(data[s + 2]) == 'A' and
|
|
char(data[s + 3]) == 'D':
|
|
return MethodHead
|
|
of 'P':
|
|
if length == 3:
|
|
if char(data[s + 1]) == 'U' and char(data[s + 2]) == 'T':
|
|
return MethodPut
|
|
elif length == 4:
|
|
if char(data[s + 1]) == 'O' and char(data[s + 2]) == 'S' and
|
|
char(data[s + 3]) == 'T':
|
|
return MethodPost
|
|
elif length == 5:
|
|
if char(data[s + 1]) == 'A' and char(data[s + 2]) == 'T' and
|
|
char(data[s + 3]) == 'C' and char(data[s + 4]) == 'H':
|
|
return MethodPatch
|
|
of 'D':
|
|
if length == 6:
|
|
if char(data[s + 1]) == 'E' and char(data[s + 2]) == 'L' and
|
|
char(data[s + 3]) == 'E' and char(data[s + 4]) == 'T' and
|
|
char(data[s + 5]) == 'E':
|
|
return MethodDelete
|
|
of 'T':
|
|
if length == 5:
|
|
if char(data[s + 1]) == 'R' and char(data[s + 2]) == 'A' and
|
|
char(data[s + 3]) == 'C' and char(data[s + 4]) == 'E':
|
|
return MethodTrace
|
|
of 'O':
|
|
if length == 7:
|
|
if char(data[s + 1]) == 'P' and char(data[s + 2]) == 'T' and
|
|
char(data[s + 3]) == 'I' and char(data[s + 4]) == 'O' and
|
|
char(data[s + 5]) == 'N' and char(data[s + 6]) == 'S':
|
|
return MethodOptions
|
|
of 'C':
|
|
if length == 7:
|
|
if char(data[s + 1]) == 'O' and char(data[s + 2]) == 'N' and
|
|
char(data[s + 3]) == 'N' and char(data[s + 4]) == 'E' and
|
|
char(data[s + 5]) == 'C' and char(data[s + 6]) == 'T':
|
|
return MethodConnect
|
|
else:
|
|
discard
|
|
|
|
return HttpMethod.MethodError
|
|
|
|
proc processVersion[T: BChar](data: openArray[T], s, e: int): HttpVersion =
|
|
let length = e - s + 1
|
|
if length == 8:
|
|
if char(data[s]) == 'H' and char(data[s + 1]) == 'T' and
|
|
char(data[s + 2]) == 'T' and char(data[s + 3]) == 'P' and
|
|
char(data[s + 4]) == '/':
|
|
if char(data[s + 5]) == '1' and char(data[s + 6]) == '.':
|
|
if char(data[s + 7]) == '0':
|
|
return HttpVersion10
|
|
elif char(data[s + 7]) == '1':
|
|
return HttpVersion11
|
|
elif char(data[s + 5]) == '0' and char(data[s + 6]) == '.':
|
|
if char(data[s + 7]) == '9':
|
|
return HttpVersion09
|
|
elif char(data[s + 5]) == '2' and char(data[s + 6]) == '.':
|
|
if char(data[s + 7]) == '0':
|
|
return HttpVersion20
|
|
return HttpVersionError
|
|
|
|
proc processCode[T: BChar](data: openArray[T], s, e: int): int =
|
|
var res = -1
|
|
let length = e - s + 1
|
|
if length == 3:
|
|
res = (ord(data[s]) - ord('0')) * 100 +
|
|
(ord(data[s + 1]) - ord('0')) * 10 +
|
|
ord(data[s + 2]) - ord('0')
|
|
res
|
|
|
|
proc parseRequest*[T: BChar](data: openArray[T],
|
|
makeCopy: bool): HttpRequestHeader =
|
|
## Parse sequence of characters or bytes ``data`` as HTTP request header.
|
|
##
|
|
## If `makeCopy` flag is ``true``, procedure will create a copy of ``data``
|
|
## in result.
|
|
##
|
|
## Returns `HttpRequestHeader` instance.
|
|
var
|
|
index = 0
|
|
state = 0
|
|
start = -1
|
|
finish = 0
|
|
hdr: HttpHeader
|
|
|
|
var res = HttpRequestHeader(
|
|
status: HttpStatus.Failure,
|
|
version: HttpVersionError,
|
|
meth: MethodError,
|
|
hdrs: newSeq[HttpHeader]()
|
|
)
|
|
|
|
if len(data) == 0:
|
|
return res
|
|
|
|
if makeCopy:
|
|
# Make copy of ``data`` sequence in our result object.
|
|
res.data = newSeq[byte](len(data))
|
|
copyMem(addr res.data[0], unsafeAddr data[0], len(data))
|
|
|
|
while index < len(data):
|
|
let ps = requestSM.processHeaders(state, char(data[index]))
|
|
res.state = ps
|
|
case ps
|
|
of 0x81:
|
|
start = index
|
|
of 0x82:
|
|
if start == -1:
|
|
break
|
|
finish = index - 1
|
|
let m = processMethod(data, start, finish)
|
|
if m == HttpMethod.MethodError:
|
|
break
|
|
res.meth = m
|
|
start = -1
|
|
of 0x83:
|
|
start = index
|
|
of 0x84:
|
|
if start == -1:
|
|
break
|
|
finish = index - 1
|
|
res.url = HttpHeaderPart(s: start, e: finish)
|
|
start = -1
|
|
of 0x85:
|
|
start = index
|
|
of 0x86:
|
|
if start == -1:
|
|
break
|
|
finish = index - 1
|
|
let m = processVersion(data, start, finish)
|
|
if m == HttpVersion.HttpVersionError:
|
|
break
|
|
res.version = m
|
|
start = -1
|
|
of 0x87, 0x8A, 0x8D:
|
|
discard
|
|
of 0x88:
|
|
start = index
|
|
of 0x89:
|
|
if start == -1:
|
|
break
|
|
finish = index - 1
|
|
hdr.name = HttpHeaderPart(s: start, e: finish)
|
|
start = -1
|
|
of 0x8B:
|
|
start = index
|
|
of 0x8C:
|
|
if start == -1:
|
|
# empty header
|
|
hdr.value = HttpHeaderPart(s: -1, e: -1)
|
|
else:
|
|
finish = index - 1
|
|
hdr.value = HttpHeaderPart(s: start, e: finish)
|
|
res.hdrs.add(hdr)
|
|
start = -1
|
|
of 0x8E:
|
|
res.length = index + 1
|
|
res.status = HttpStatus.Success
|
|
break
|
|
of 0xC0..0xCF:
|
|
# error
|
|
break
|
|
of 0x00..0x0F:
|
|
# data processing
|
|
discard
|
|
else:
|
|
# must not be happened
|
|
break
|
|
inc(index)
|
|
|
|
res
|
|
|
|
proc parseRequest*[T: BChar](data: sink seq[T]): HttpRequestHeader =
|
|
## Parse sequence of characters or bytes as HTTP request header.
|
|
##
|
|
## Note: to prevent unnecessary allocations source array ``data`` will be
|
|
## be shallow copied to result and all parsed fields will have references to
|
|
## this buffer. If you plan to change contents of ``data`` while parsing
|
|
## request and/or processing headers, please make a real copy of ``data`` and
|
|
## pass copy to ``parseRequest(data)``.
|
|
##
|
|
## Returns `HttpRequestHeader` instance.
|
|
var res = parseRequest(data, false)
|
|
when declared(shallowCopy):
|
|
shallowCopy(res.data, cast[seq[byte]](data))
|
|
else:
|
|
res.data = cast[seq[byte]](data)
|
|
res
|
|
|
|
proc parseHeaders*[T: BChar](data: openArray[T],
|
|
makeCopy: bool): HttpHeadersList =
|
|
## Parse sequence of characters or bytes ``data`` as HTTP headers list.
|
|
##
|
|
## If `makeCopy` flag is ``true``, procedure will create a copy of ``data``
|
|
## in result.
|
|
##
|
|
## Returns `HttpHeadersList` instance.
|
|
var
|
|
index = 0
|
|
state = 8
|
|
start = 0
|
|
finish = 0
|
|
hdr: HttpHeader
|
|
|
|
var res = HttpHeadersList(
|
|
status: HttpStatus.Failure,
|
|
hdrs: newSeq[HttpHeader]()
|
|
)
|
|
|
|
if len(data) == 0:
|
|
return res
|
|
|
|
if makeCopy:
|
|
# Make copy of ``data`` sequence in our result object.
|
|
res.data = newSeq[byte](len(data))
|
|
copyMem(addr res.data[0], unsafeAddr data[0], len(data))
|
|
|
|
while index < len(data):
|
|
let ps = requestSM.processHeaders(state, char(data[index]))
|
|
res.state = ps
|
|
case ps
|
|
of 0x87, 0x8A, 0x8D:
|
|
discard
|
|
of 0x88:
|
|
start = index
|
|
of 0x89:
|
|
if start == -1:
|
|
break
|
|
finish = index - 1
|
|
hdr.name = HttpHeaderPart(s: start, e: finish)
|
|
start = -1
|
|
of 0x8B:
|
|
start = index
|
|
of 0x8C:
|
|
if start == -1:
|
|
# empty header
|
|
hdr.value = HttpHeaderPart(s: -1, e: -1)
|
|
else:
|
|
finish = index - 1
|
|
hdr.value = HttpHeaderPart(s: start, e: finish)
|
|
res.hdrs.add(hdr)
|
|
start = -1
|
|
of 0x8E:
|
|
res.length = index + 1
|
|
res.status = HttpStatus.Success
|
|
break
|
|
of 0xC0..0xCF:
|
|
# error
|
|
break
|
|
of 0x00..0x0F:
|
|
# data processing
|
|
discard
|
|
else:
|
|
# must not be happened
|
|
break
|
|
inc(index)
|
|
res
|
|
|
|
proc parseHeaders*[T: BChar](data: sink seq[T]): HttpHeadersList =
|
|
## Parse sequence of characters or bytes as HTTP headers list.
|
|
##
|
|
## Note: to prevent unnecessary allocations source array ``data`` will be
|
|
## be shallow copied to result and all parsed fields will have references to
|
|
## this buffer. If you plan to change contents of ``data`` while parsing
|
|
## request and/or processing headers, please make a real copy of ``data`` and
|
|
## pass copy to ``parseHeaders(data)``.
|
|
##
|
|
## Returns `HttpHeadersList` instance.
|
|
var res = parseHeaders(data, false)
|
|
when declared(shallowCopy):
|
|
shallowCopy(res.data, cast[seq[byte]](data))
|
|
else:
|
|
res.data = cast[seq[byte]](data)
|
|
res
|
|
|
|
proc parseResponse*[T: BChar](data: openArray[T],
|
|
makeCopy: bool): HttpResponseHeader =
|
|
## Parse sequence of characters or bytes as HTTP response header.
|
|
##
|
|
## If `makeCopy` flag is ``true``, procedure will create a copy of ``data``
|
|
## in result.
|
|
##
|
|
## Returns `HttpResponseHeader` instance.
|
|
var
|
|
index = 0
|
|
state = 0
|
|
start = -1
|
|
finish = 0
|
|
hdr: HttpHeader
|
|
|
|
var res = HttpResponseHeader(
|
|
status: HttpStatus.Failure,
|
|
version: HttpVersionError,
|
|
code: -1,
|
|
hdrs: newSeq[HttpHeader]()
|
|
)
|
|
|
|
if len(data) == 0:
|
|
return res
|
|
|
|
if makeCopy:
|
|
# Make copy of ``data`` sequence in our result object.
|
|
res.data = newSeq[byte](len(data))
|
|
copyMem(addr res.data[0], unsafeAddr data[0], len(data))
|
|
|
|
while index < len(data):
|
|
let ps = responseSM.processHeaders(state, char(data[index]))
|
|
res.state = ps
|
|
case ps
|
|
of 0x81:
|
|
start = index
|
|
of 0x82:
|
|
if start == -1:
|
|
break
|
|
finish = index - 1
|
|
let m = processVersion(data, start, finish)
|
|
if m == HttpVersion.HttpVersionError:
|
|
break
|
|
res.version = m
|
|
start = -1
|
|
of 0x83:
|
|
start = index
|
|
of 0x84, 0x85:
|
|
if start == -1:
|
|
break
|
|
finish = index - 1
|
|
let m = processCode(data, start, finish)
|
|
if m == -1:
|
|
break
|
|
res.code = m
|
|
if ps == 0x84:
|
|
res.rsn = HttpHeaderPart(s: -1, e: -1)
|
|
start = -1
|
|
of 0x86:
|
|
start = index
|
|
of 0x87:
|
|
if start == -1:
|
|
res.rsn = HttpHeaderPart(s: -1, e: -1)
|
|
else:
|
|
finish = index - 1
|
|
res.rsn = HttpHeaderPart(s: start, e: finish)
|
|
start = -1
|
|
of 0x88, 0x89, 0x8C, 0x8F:
|
|
discard
|
|
of 0x8A:
|
|
start = index
|
|
of 0x8B:
|
|
if start == -1:
|
|
break
|
|
finish = index - 1
|
|
hdr.name = HttpHeaderPart(s: start, e: finish)
|
|
start = -1
|
|
of 0x8D:
|
|
start = index
|
|
of 0x8E:
|
|
if start == -1:
|
|
# empty header
|
|
hdr.value = HttpHeaderPart(s: -1, e: -1)
|
|
else:
|
|
finish = index - 1
|
|
hdr.value = HttpHeaderPart(s: start, e: finish)
|
|
res.hdrs.add(hdr)
|
|
start = -1
|
|
of 0x9F:
|
|
res.length = index + 1
|
|
res.status = HttpStatus.Success
|
|
break
|
|
of 0xC0..0xCF:
|
|
# error
|
|
break
|
|
of 0x00..0x0F:
|
|
# data processing
|
|
discard
|
|
else:
|
|
# must not be happened
|
|
break
|
|
inc(index)
|
|
res
|
|
|
|
proc parseResponse*[T: BChar](data: sink seq[T]): HttpResponseHeader =
|
|
## Parse sequence of characters or bytes as HTTP response header.
|
|
##
|
|
## Note: to prevent unnecessary allocations source array ``data`` will be
|
|
## be shallow copied to result and all parsed fields will have references to
|
|
## this buffer. If you plan to change contents of ``data`` while parsing
|
|
## request and/or processing headers, please make a real copy of ``data`` and
|
|
## pass copy to ``parseResponse(data)``.
|
|
##
|
|
## Returns `HttpResponseHeader` instance.
|
|
var res = parseResponse(data, false)
|
|
when declared(shallowCopy):
|
|
shallowCopy(res.data, cast[seq[byte]](data))
|
|
else:
|
|
res.data = cast[seq[byte]](data)
|
|
res
|
|
|
|
proc parseDisposition*[T: BChar](data: openArray[T],
|
|
makeCopy: bool): ContentDispositionHeader =
|
|
## Parse sequence of characters or bytes of HTTP ``Content-Disposition``
|
|
## header according to RFC6266.
|
|
##
|
|
## TODO: Support extended `<fieldname>*` values.
|
|
##
|
|
## If `makeCopy` flag is ``true``, procedure will create a copy of ``data``
|
|
## in result.
|
|
##
|
|
## Returns `ContentDispositionHeader` instance.
|
|
var
|
|
index = 0
|
|
state = 0
|
|
start = -1
|
|
finish = 0
|
|
hdr: HttpHeader
|
|
|
|
var res = ContentDispositionHeader(
|
|
status: HttpStatus.Failure,
|
|
disptype: HttpHeaderPart(),
|
|
flds: newSeq[HttpHeader]()
|
|
)
|
|
|
|
if len(data) == 0:
|
|
return res
|
|
|
|
if makeCopy:
|
|
# Make copy of ``data`` sequence in our result object.
|
|
res.data = newSeq[byte](len(data))
|
|
copyMem(addr res.data[0], unsafeAddr data[0], len(data))
|
|
|
|
while index < len(data):
|
|
let ps = contdispSM.processDisposition(state, char(data[index]))
|
|
res.state = ps
|
|
case ps
|
|
of 0x81:
|
|
start = index
|
|
of 0x82:
|
|
if start == -1:
|
|
break
|
|
finish = index - 1
|
|
res.disptype = HttpHeaderPart(s: start, e: finish)
|
|
start = -1
|
|
of 0x83:
|
|
start = index
|
|
of 0x84:
|
|
if start == -1:
|
|
break
|
|
finish = index - 1
|
|
hdr.name = HttpHeaderPart(s: start, e: finish)
|
|
start = -1
|
|
of 0x85:
|
|
start = index
|
|
of 0x86:
|
|
if start == -1:
|
|
break
|
|
finish = index - 1
|
|
hdr.value = HttpHeaderPart(s: start, e: finish)
|
|
res.flds.add(hdr)
|
|
start = -1
|
|
of 0x87:
|
|
start = index + 1
|
|
of 0x88:
|
|
start = index
|
|
of 0x89, 0x8A:
|
|
discard
|
|
of 0x8B:
|
|
if start == -1:
|
|
break
|
|
finish = index - 1
|
|
hdr.value = HttpHeaderPart(s: start, e: finish)
|
|
res.flds.add(hdr)
|
|
of 0x8C:
|
|
discard
|
|
of 0x8D:
|
|
hdr.value = HttpHeaderPart(s: index, e: index - 1)
|
|
res.flds.add(hdr)
|
|
of 0x00..0x0D:
|
|
discard
|
|
of 0xC0..0xCD:
|
|
break
|
|
else:
|
|
break
|
|
inc(index)
|
|
|
|
res.status =
|
|
case res.state
|
|
of 0x01, 0x81:
|
|
if start == -1:
|
|
HttpStatus.Failure
|
|
else:
|
|
res.disptype = HttpHeaderPart(s: start, e: index - 1)
|
|
HttpStatus.Success
|
|
of 0x84:
|
|
hdr.value = HttpHeaderPart(s: index, e: index - 1)
|
|
res.flds.add(hdr)
|
|
HttpStatus.Success
|
|
of 0x05, 0x85:
|
|
if start == -1:
|
|
HttpStatus.Failure
|
|
else:
|
|
hdr.value = HttpHeaderPart(s: start, e: index - 1)
|
|
res.flds.add(hdr)
|
|
HttpStatus.Success
|
|
of 0x0B, 0x8B:
|
|
HttpStatus.Success
|
|
else:
|
|
HttpStatus.Failure
|
|
|
|
if res.status == HttpStatus.Success:
|
|
res
|
|
else:
|
|
ContentDispositionHeader(status: HttpStatus.Failure)
|
|
|
|
proc parseDisposition*[T: BChar](data: sink seq[T]): ContentDispositionHeader =
|
|
## Parse sequence of characters or bytes of HTTP ``Content-Disposition``
|
|
## header according to RFC6266.
|
|
##
|
|
## Note: to prevent unnecessary allocations source array ``data`` will be
|
|
## be shallow copied to result and all parsed fields will have references to
|
|
## this buffer. If you plan to change contents of ``data`` while parsing
|
|
## request and/or processing headers, please make a real copy of ``data`` and
|
|
## pass copy to ``parseDisposition(data)``.
|
|
##
|
|
## Returns `ContentDispositionHeader` instance.
|
|
var res = parseDisposition(data, false)
|
|
when declared(shallowCopy):
|
|
shallowCopy(res.data, cast[seq[byte]](data))
|
|
else:
|
|
res.data = cast[seq[byte]](data)
|
|
res
|
|
|
|
proc parseContentType*[T: BChar](data: openArray[T]): ContentTypeData =
|
|
## Parse sequence of characters or bytes of HTTP ``Content-Type``
|
|
## header according to RFC7231 (section 3.1.1.5).
|
|
##
|
|
## Returns `ContentTypeData` instance.
|
|
var
|
|
index = 0
|
|
state = 0
|
|
start = -1
|
|
finish = 0
|
|
name = ""
|
|
value = ""
|
|
|
|
var res = ContentTypeData(status: HttpStatus.Failure)
|
|
|
|
if len(data) == 0:
|
|
return res
|
|
|
|
while index < len(data):
|
|
let ps = contentTypeSM.processContentType(state, char(data[index]))
|
|
res.state = ps
|
|
case ps
|
|
of 0x801:
|
|
# media-type start
|
|
start = index
|
|
of 0x802:
|
|
# media-type finish
|
|
if start == -1:
|
|
break
|
|
finish = index - 1
|
|
res.mediaType.media = data.toString(start, finish)
|
|
start = -1
|
|
of 0x803:
|
|
# sub-media-type start
|
|
start = index
|
|
of 0x804, 0x805:
|
|
# sub-media-type finish
|
|
if start == -1:
|
|
break
|
|
finish = index - 1
|
|
res.mediaType.subtype = data.toString(start, finish)
|
|
start = -1
|
|
of 0x806:
|
|
# parameter name start
|
|
start = index
|
|
of 0x807:
|
|
# parameter name finish
|
|
if start == -1:
|
|
break
|
|
finish = index - 1
|
|
name = data.toString(start, finish)
|
|
start = -1
|
|
of 0x808:
|
|
start = index
|
|
of 0x809, 0x80A:
|
|
if start == -1:
|
|
break
|
|
finish = index - 1
|
|
value = data.toString(start, finish)
|
|
res.params.add((name: name, value: value))
|
|
start = -1
|
|
of 0x80C:
|
|
value.reset()
|
|
value.add(data[index])
|
|
of 0x00C, 0x80E:
|
|
value.add(data[index])
|
|
of 0x80F:
|
|
res.params.add((name: name, value: value))
|
|
of 0x811:
|
|
res.params.add((name: name, value: ""))
|
|
of 0x80B, 0x80D, 0x810:
|
|
discard
|
|
of 0x000..0x00B, 0x00D..0x011:
|
|
discard
|
|
of 0xE00..0xE10:
|
|
break
|
|
else:
|
|
break
|
|
inc(index)
|
|
|
|
res.status =
|
|
case res.state
|
|
of 0x003, 0x803:
|
|
if start == -1:
|
|
HttpStatus.Failure
|
|
else:
|
|
res.mediaType.subtype = data.toString(start, index - 1)
|
|
HttpStatus.Success
|
|
of 0x807:
|
|
res.params.add((name: name, value: ""))
|
|
HttpStatus.Success
|
|
of 0x808, 0x008:
|
|
if start == -1:
|
|
HttpStatus.Failure
|
|
else:
|
|
res.params.add((name: name, value: data.toString(start, index - 1)))
|
|
HttpStatus.Success
|
|
of 0x80F:
|
|
HttpStatus.Success
|
|
else:
|
|
HttpStatus.Failure
|
|
|
|
if res.status == HttpStatus.Success:
|
|
res
|
|
else:
|
|
ContentTypeData(status: HttpStatus.Failure)
|
|
|
|
proc parseAcceptHeader*[T: BChar](data: openArray[T],
|
|
makeCopy: bool): AcceptHeader =
|
|
var
|
|
index = 0
|
|
state = 0
|
|
start = -1
|
|
finish = 0
|
|
hdr: HttpHeader
|
|
mtype: AcceptMediaType
|
|
|
|
var res = AcceptHeader(status: HttpStatus.Failure)
|
|
|
|
if len(data) == 0:
|
|
return res
|
|
|
|
if makeCopy:
|
|
# Make copy of ``data`` sequence in our result object.
|
|
res.data = newSeq[byte](len(data))
|
|
copyMem(addr res.data[0], unsafeAddr data[0], len(data))
|
|
|
|
while index < len(data):
|
|
let ps = acceptSM.processAcceptHeader(state, char(data[index]))
|
|
res.state = ps
|
|
case ps
|
|
of 0x801:
|
|
mtype = AcceptMediaType()
|
|
start = index
|
|
of 0x802:
|
|
if start == -1:
|
|
mtype.mtype = HttpHeaderPart(s: -1, e: -1)
|
|
break
|
|
finish = index - 1
|
|
mtype.mtype = HttpHeaderPart(s: start, e: finish)
|
|
start = -1
|
|
of 0x803:
|
|
start = index
|
|
of 0x804, 0x805, 0x806:
|
|
if start == -1:
|
|
mtype.stype = HttpHeaderPart(s: -1, e: -1)
|
|
break
|
|
finish = index - 1
|
|
mtype.stype = HttpHeaderPart(s: start, e: finish)
|
|
start = -1
|
|
if ps == 0x805:
|
|
res.mediaTypes.add(mtype)
|
|
of 0x807:
|
|
hdr = HttpHeader()
|
|
start = index
|
|
of 0x808:
|
|
if start == -1:
|
|
hdr.name = HttpHeaderPart(s: -1, e: -1)
|
|
break
|
|
finish = index - 1
|
|
hdr.name = HttpHeaderPart(s: start, e: finish)
|
|
start = -1
|
|
of 0x809:
|
|
discard
|
|
of 0x80A:
|
|
start = index
|
|
of 0x80B, 0x80C:
|
|
if start == -1:
|
|
hdr.value = HttpHeaderPart(s: -1, e: -1)
|
|
break
|
|
finish = index - 1
|
|
hdr.value = HttpHeaderPart(s: start, e: finish)
|
|
mtype.params.add(hdr)
|
|
start = -1
|
|
if ps == 0x80C:
|
|
res.mediaTypes.add(mtype)
|
|
of 0x80D:
|
|
hdr.value = HttpHeaderPart(s: index, e: index - 1)
|
|
mtype.params.add(hdr)
|
|
start = -1
|
|
of 0x80E:
|
|
discard
|
|
of 0x80F:
|
|
start = index
|
|
of 0x810, 0x811:
|
|
discard
|
|
of 0x812:
|
|
if start == -1:
|
|
hdr.value = HttpHeaderPart(s: -1, e: -1)
|
|
break
|
|
finish = index - 1
|
|
hdr.value = HttpHeaderPart(s: start, e: finish)
|
|
mtype.params.add(hdr)
|
|
start = -1
|
|
of 0x814:
|
|
res.mediaTypes.add(mtype)
|
|
of 0x000 .. 0x014:
|
|
discard
|
|
else:
|
|
break
|
|
inc(index)
|
|
|
|
res.status =
|
|
case res.state
|
|
of 0x803, 0x003:
|
|
if start == -1:
|
|
HttpStatus.Failure
|
|
else:
|
|
finish = index - 1
|
|
mtype.stype = HttpHeaderPart(s: start, e: finish)
|
|
res.mediaTypes.add(mtype)
|
|
HttpStatus.Success
|
|
of 0x808, 0x008:
|
|
hdr.value = HttpHeaderPart(s: index, e: index - 1)
|
|
mtype.params.add(hdr)
|
|
res.mediaTypes.add(mtype)
|
|
HttpStatus.Success
|
|
of 0x80A, 0x00A:
|
|
if start == -1:
|
|
HttpStatus.Failure
|
|
else:
|
|
finish = index - 1
|
|
hdr.value = HttpHeaderPart(s: start, e: finish)
|
|
mtype.params.add(hdr)
|
|
res.mediaTypes.add(mtype)
|
|
HttpStatus.Success
|
|
of 0x812:
|
|
res.mediaTypes.add(mtype)
|
|
HttpStatus.Success
|
|
else:
|
|
HttpStatus.Failure
|
|
|
|
if res.status == HttpStatus.Success:
|
|
res
|
|
else:
|
|
AcceptHeader(status: HttpStatus.Failure)
|
|
|
|
template success*(reqresp: HttpReqRespHeader | ContentDispositionHeader |
|
|
AcceptHeader | ContentTypeData): bool =
|
|
## Returns ``true`` is ``reqresp`` was successfully parsed.
|
|
reqresp.status == HttpStatus.Success
|
|
|
|
template failed*(reqresp: HttpReqRespHeader | ContentDispositionHeader |
|
|
AcceptHeader | ContentTypeData): bool =
|
|
## Returns ``true`` if ``reqresp`` parsing was failed.
|
|
reqresp.status == HttpStatus.Failure
|
|
|
|
proc compare(data: openArray[byte], header: HttpHeader, key: string): int =
|
|
## Case-insensitive comparison function.
|
|
let length = header.name.e - header.name.s + 1
|
|
var res = length - len(key)
|
|
if res == 0:
|
|
var idx = 0
|
|
for i in header.name.s..header.name.e:
|
|
res = ord(toLowerAscii(char(data[i]))) - ord(toLowerAscii(key[idx]))
|
|
if res != 0:
|
|
return res
|
|
inc(idx)
|
|
res
|
|
|
|
proc contains*(reqresp: HttpReqRespHeader, header: string): bool =
|
|
## Return ``true``, if header with key ``header`` exists in `reqresp` object.
|
|
if reqresp.success():
|
|
for item in reqresp.hdrs:
|
|
if reqresp.data.compare(item, header) == 0:
|
|
return true
|
|
return false
|
|
|
|
proc toString[T: BChar](data: openArray[T], start, stop: int): string =
|
|
## Slice a raw data blob into a string
|
|
## This is an inclusive slice
|
|
## The output string is null-terminated for raw C-compat
|
|
let length = stop - start + 1
|
|
var res = newString(length)
|
|
if length > 0:
|
|
copyMem(addr res[0], unsafeAddr data[start], length)
|
|
res
|
|
|
|
proc `[]`*(reqresp: HttpReqRespHeader, header: string): string =
|
|
## Retrieve HTTP header value from ``reqresp`` with key ``header``.
|
|
if reqresp.success():
|
|
for item in reqresp.hdrs:
|
|
if reqresp.data.compare(item, header) == 0:
|
|
if item.value.s == -1 and item.value.e == -1:
|
|
return ""
|
|
else:
|
|
return reqresp.data.toString(item.value.s, item.value.e)
|
|
""
|
|
|
|
iterator headers*(reqresp: HttpReqRespHeader,
|
|
key: string = ""): tuple[name: string, value: string] =
|
|
## Iterates over all or specific headers in ``reqresp`` headers object.
|
|
## You can specify ``key` string to iterate only over headers which has key
|
|
## equal to ``key`` string.
|
|
if reqresp.success():
|
|
for item in reqresp.hdrs:
|
|
if len(key) == 0:
|
|
let name = reqresp.data.toString(item.name.s, item.name.e)
|
|
let value =
|
|
if item.value.s == -1 and item.value.e == -1:
|
|
""
|
|
else:
|
|
reqresp.data.toString(item.value.s, item.value.e)
|
|
yield (name, value)
|
|
else:
|
|
if reqresp.data.compare(item, key) == 0:
|
|
let name = key
|
|
let value =
|
|
if item.value.s == -1 and item.value.e == -1:
|
|
""
|
|
else:
|
|
reqresp.data.toString(item.value.s, item.value.e)
|
|
yield (name, value)
|
|
|
|
iterator headers*(reqresp: HttpReqRespHeader,
|
|
buffer: openArray[byte],
|
|
key: string = ""): tuple[name: string, value: string] =
|
|
## Iterates over all or specific headers in ``reqresp`` headers object.
|
|
## You can specify ``key` string to iterate only over headers which has key
|
|
## equal to ``key`` string.
|
|
if reqresp.success():
|
|
for item in reqresp.hdrs:
|
|
if len(key) == 0:
|
|
let name = buffer.toString(item.name.s, item.name.e)
|
|
let value =
|
|
if item.value.s == -1 and item.value.e == -1:
|
|
""
|
|
else:
|
|
buffer.toString(item.value.s, item.value.e)
|
|
yield (name, value)
|
|
else:
|
|
if buffer.compare(item, key) == 0:
|
|
let name = key
|
|
let value =
|
|
if item.value.s == -1 and item.value.e == -1:
|
|
""
|
|
else:
|
|
buffer.toString(item.value.s, item.value.e)
|
|
yield (name, value)
|
|
|
|
iterator fields*(header: ContentDispositionHeader): tuple[name: string,
|
|
value: string] =
|
|
if header.success():
|
|
for item in header.flds:
|
|
var name = header.data.toString(item.name.s, item.name.e)
|
|
var value =
|
|
if item.value.s == -1 and item.value.e == -1:
|
|
""
|
|
else:
|
|
header.data.toString(item.value.s, item.value.e)
|
|
yield(name, value.replace("\\\"", "\""))
|
|
|
|
iterator fields*(header: ContentDispositionHeader,
|
|
buffer: openArray[byte]): tuple[name: string, value: string] =
|
|
if header.success():
|
|
for item in header.flds:
|
|
var name = buffer.toString(item.name.s, item.name.e)
|
|
var value =
|
|
if item.value.s == -1 and item.value.e == -1:
|
|
""
|
|
else:
|
|
buffer.toString(item.value.s, item.value.e)
|
|
yield(name, value.replace("\\\"", "\""))
|
|
|
|
proc init*(t: typedesc[MediaType], s: string): MediaType =
|
|
let parts = s.split("/", 1)
|
|
case len(parts)
|
|
of 1:
|
|
MediaType(media: parts[0], subtype: "*")
|
|
of 2:
|
|
MediaType(media: parts[0], subtype: parts[1])
|
|
else:
|
|
MediaType(media: "*", subtype: "*")
|
|
|
|
proc init*(t: typedesc[MediaType], media: string, subtype: string): MediaType =
|
|
MediaType(media: media, subtype: subtype)
|
|
|
|
proc `$`*(s: MediaType): string =
|
|
s.media & "/" & s.subtype
|
|
|
|
proc cmp*(x, y: MediaType): int =
|
|
if x.media != "*" and y.media != "*":
|
|
let res = cmpIgnoreCase(x.media, y.media)
|
|
if res == 0:
|
|
if x.subtype != "*" and y.subtype != "*":
|
|
cmpIgnoreCase(x.subtype, y.subtype)
|
|
else:
|
|
0
|
|
else:
|
|
res
|
|
else:
|
|
if x.subtype != "*" and y.subtype != "*":
|
|
cmpIgnoreCase(x.subtype, y.subtype)
|
|
else:
|
|
0
|
|
|
|
proc `==`*(x, y: MediaType): bool =
|
|
cmp(x, y) == 0
|
|
|
|
proc isValid*(x: MediaType): bool =
|
|
## Returns ``true`` if MediaType ``x`` is not empty and do not have illegal
|
|
## characters.
|
|
if (len(x.media) == 0) or (len(x.subtype) == 0):
|
|
false
|
|
else:
|
|
for ch in x.media:
|
|
if ch notin LTOKEN:
|
|
return false
|
|
for ch in x.subtype:
|
|
if ch notin LTOKEN:
|
|
return false
|
|
true
|
|
|
|
proc isWildCard*(x: MediaType): bool =
|
|
## Returns ``true`` if media type ``x`` is not empty and do not have wildcard
|
|
## values in ``media`` or ``subtype``.
|
|
##
|
|
## Values like `*/*` or `application/*` are wildcard types.
|
|
if (len(x.media) == 0) or (len(x.subtype) == 0):
|
|
false
|
|
else:
|
|
(x.media == "*") or (x.subtype == "*")
|
|
|
|
proc `$`*(s: AcceptMediaItem): string =
|
|
## Returns string representation of AcceptMediaItem object.
|
|
if len(s.params) > 0:
|
|
let params = s.params.mapIt(it.name & "=" & it.value).join(";")
|
|
$s.mediaType & ";" & params
|
|
else:
|
|
$s.mediaType
|
|
|
|
proc `$`*(s: AcceptInfo): string =
|
|
## Returns string representation of AcceptInfo object.
|
|
s.data.mapIt($it).join(",")
|
|
|
|
proc mediaType*(s: AcceptMediaType, buffer: openArray[byte]): string =
|
|
## Returns media type/subtype as string.
|
|
buffer.toString(s.mtype.s, s.mtype.e) & "/" &
|
|
buffer.toString(s.stype.s, s.stype.e)
|
|
|
|
proc parameters*(s: AcceptMediaItem): string =
|
|
## Returns media type parameters as single string, if media type do not have
|
|
## any parameters specified, empty string will be returned.
|
|
if len(s.params) > 0:
|
|
s.params.mapIt(it.name & "=" & it.value).join(";")
|
|
else:
|
|
""
|
|
|
|
iterator types*(header: AcceptHeader): string =
|
|
## Iterate over AcceptHeader media type/subtypes.
|
|
if header.success():
|
|
for item in header.mediaTypes:
|
|
yield item.mediaType(header.data)
|
|
|
|
iterator types*(header: AcceptHeader, buffer: openArray[byte]): string =
|
|
## Iterate over AcceptHeader media type/subtypes.
|
|
if header.success():
|
|
for item in header.mediaTypes:
|
|
yield item.mediaType(buffer)
|
|
|
|
proc cmp*(x, y: AcceptMediaItem): int {.procvar.} =
|
|
if x.qvalue == y.qvalue: return 0
|
|
if x.qvalue < y.qvalue: return -1
|
|
return 1
|
|
|
|
proc getQvalue*(value: string): Result[float, cstring] =
|
|
## Parse quality value string and returns floating point number. If quality
|
|
## value string format is not acceptable error will be returned.
|
|
## https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.9
|
|
const
|
|
IncorrectQValue = cstring"Incorrect q-value"
|
|
IncorrectQValueLength = cstring"Incorrect q-value length"
|
|
case len(value)
|
|
of 0:
|
|
ok(1.0)
|
|
of 1:
|
|
case value[0]
|
|
of '0':
|
|
ok(0.0)
|
|
of '1':
|
|
ok(1.0)
|
|
else:
|
|
err(IncorrectQValue)
|
|
of 2:
|
|
if value[1] != '.':
|
|
return err(IncorrectQValue)
|
|
case value[0]
|
|
of '0':
|
|
ok(0.0)
|
|
of '1':
|
|
ok(1.0)
|
|
else:
|
|
err(IncorrectQValue)
|
|
of 3:
|
|
if value[1] != '.':
|
|
return err(IncorrectQValue)
|
|
case value[0]
|
|
of '0':
|
|
if isDigit(value[2]):
|
|
return ok(parseFloat(value))
|
|
err(IncorrectQValue)
|
|
of '1':
|
|
if value[2] == '0':
|
|
return ok(1.0)
|
|
err(IncorrectQValue)
|
|
else:
|
|
err(IncorrectQValue)
|
|
of 4:
|
|
if value[1] != '.':
|
|
return err(IncorrectQValue)
|
|
case value[0]
|
|
of '0':
|
|
if isDigit(value[2]) and isDigit(value[3]):
|
|
return ok(parseFloat(value))
|
|
err(IncorrectQValue)
|
|
of '1':
|
|
if (value[2] == '0') and (value[3] == '0'):
|
|
return ok(1.0)
|
|
err(IncorrectQValue)
|
|
else:
|
|
err(IncorrectQValue)
|
|
of 5:
|
|
if value[1] != '.':
|
|
return err(IncorrectQValue)
|
|
case value[0]
|
|
of '0':
|
|
if isDigit(value[2]) and isDigit(value[3]) and isDigit(value[4]):
|
|
return ok(parseFloat(value))
|
|
err(IncorrectQValue)
|
|
of '1':
|
|
if (value[2] == '0') and (value[3] == '0') and (value[4] == '0'):
|
|
return ok(1.0)
|
|
err(IncorrectQValue)
|
|
else:
|
|
err(IncorrectQValue)
|
|
else:
|
|
err(IncorrectQValueLength)
|
|
|
|
iterator mediaItems*(header: AcceptHeader): AcceptMediaItem =
|
|
## Iterates over all media types and its parameters.
|
|
##
|
|
## If `q-value` has invalid format - `0.0` will be returned, if `q-value`
|
|
## appears multiple times, last value will be used, if last value is invalid
|
|
## `0.0` will be returned.
|
|
if header.success():
|
|
for item in header.mediaTypes:
|
|
let mtype = header.data.toString(item.mtype.s, item.mtype.e)
|
|
let stype = header.data.toString(item.stype.s, item.stype.e)
|
|
if len(item.params) == 0:
|
|
yield AcceptMediaItem(mediaType: MediaType.init(mtype, stype),
|
|
qvalue: 1.0)
|
|
else:
|
|
var qvalue: float = 1.0
|
|
let params =
|
|
block:
|
|
var res: seq[tuple[name: string, value: string]]
|
|
for param in item.params:
|
|
let name = header.data.toString(param.name.s, param.name.e)
|
|
let value = header.data.toString(param.value.s, param.value.e)
|
|
if name == "q":
|
|
qvalue =
|
|
block:
|
|
let res = getQvalue(value)
|
|
if res.isErr():
|
|
0.0
|
|
else:
|
|
res.get()
|
|
res.add((name, value))
|
|
res
|
|
yield AcceptMediaItem(mediaType: MediaType.init(mtype, stype),
|
|
qvalue: qvalue, params: params)
|
|
|
|
iterator mediaItems*(header: AcceptHeader,
|
|
buffer: openArray[byte]): AcceptMediaItem =
|
|
## Iterates over all media types and its parameters.
|
|
##
|
|
## If `q-value` has invalid format - `0.0` will be returned, if `q-value`
|
|
## appears multiple times, last value will be used, if last value is invalid
|
|
## `0.0` will be returned.
|
|
if header.success():
|
|
for item in header.mediaTypes:
|
|
let mtype = buffer.toString(item.mtype.s, item.mtype.e)
|
|
let stype = buffer.toString(item.stype.s, item.stype.e)
|
|
if len(item.params) == 0:
|
|
yield AcceptMediaItem(mediaType: MediaType.init(mtype, stype),
|
|
qvalue: 1.0)
|
|
else:
|
|
var qvalue: float = 1.0
|
|
let params =
|
|
block:
|
|
var res: seq[tuple[name: string, value: string]]
|
|
for param in item.params:
|
|
let name = buffer.toString(param.name.s, param.name.e)
|
|
let value = buffer.toString(param.value.s, param.value.e)
|
|
if name == "q":
|
|
qvalue =
|
|
block:
|
|
let res = getQvalue(value)
|
|
if res.isErr():
|
|
0.0
|
|
else:
|
|
res.get()
|
|
res.add((name, value))
|
|
res
|
|
yield AcceptMediaItem(mediaType: MediaType.init(mtype, stype),
|
|
qvalue: qvalue, params: params)
|
|
|
|
proc getAcceptInfo*(value: string): Result[AcceptInfo, cstring] =
|
|
var res: seq[AcceptMediaItem]
|
|
var toparse =
|
|
if len(value) == 0:
|
|
"*/*"
|
|
else:
|
|
value
|
|
let header = parseAcceptHeader(toparse, false)
|
|
if header.success():
|
|
for item in header.mediaItems(toparse.toOpenArrayByte(0, len(toparse) - 1)):
|
|
res.add(item)
|
|
res.sort(cmp, SortOrder.Descending)
|
|
ok(AcceptInfo(data: res))
|
|
else:
|
|
err("Invalid Accept header format")
|
|
|
|
proc getContentType*(value: string): Result[ContentTypeData, cstring] =
|
|
let header = parseContentType(value)
|
|
if header.success():
|
|
ok(header)
|
|
else:
|
|
err("Invalid Content-Type header format")
|
|
|
|
proc `==`*(a: ContentTypeData, b: MediaType): bool =
|
|
if a.status != HttpStatus.Success:
|
|
false
|
|
else:
|
|
a.mediaType == b
|
|
|
|
proc `==`*(a: MediaType, b: ContentTypeData): bool =
|
|
`==`(b, a)
|
|
|
|
proc `==`*(a, b: ContentTypeData): bool =
|
|
if (a.status != HttpStatus.Success) or (b.status != HttpStatus.Success):
|
|
return false
|
|
if a.mediaType != b.mediaType:
|
|
return false
|
|
if len(a.params) != len(b.params):
|
|
return false
|
|
if len(a.params) > 0:
|
|
proc cmpTuple(a, b: tuple[name: string, value: string]): int =
|
|
let res = cmpIgnoreCase(a.name, b.name)
|
|
if res != 0:
|
|
return res
|
|
cmpIgnoreCase(a.value, b.value)
|
|
let aparams = a.params.sorted(cmpTuple, SortOrder.Ascending)
|
|
let bparams = b.params.sorted(cmpTuple, SortOrder.Ascending)
|
|
for index, item in aparams.pairs():
|
|
if cmpTuple(item, bparams[index]) != 0:
|
|
return false
|
|
true
|
|
|
|
proc `$`*(a: ContentTypeData): string =
|
|
if a.status != HttpStatus.Success:
|
|
"<incomplete>"
|
|
else:
|
|
let params = a.params.mapIt(it.name & "=\"" & it.value & "\"").join("; ")
|
|
if len(params) > 0:
|
|
$a.mediaType & "; " & params
|
|
else:
|
|
$a.mediaType
|
|
|
|
proc uri*(request: HttpRequestHeader): string =
|
|
## Returns HTTP request URI as string from ``request``.
|
|
if request.success():
|
|
if request.url.s == -1 and request.url.e == -1:
|
|
""
|
|
else:
|
|
request.data.toString(request.url.s, request.url.e)
|
|
else:
|
|
""
|
|
|
|
proc uri*(request: HttpRequestHeader, buffer: openArray[byte]): string =
|
|
## Returns HTTP request URI as string from ``request``.
|
|
if request.success():
|
|
if request.url.s == -1 and request.url.e == -1:
|
|
""
|
|
else:
|
|
buffer.toString(request.url.s, request.url.e)
|
|
else:
|
|
""
|
|
|
|
proc reason*(response: HttpResponseHeader): string =
|
|
## Returns HTTP reason string from ``response``.
|
|
if response.success():
|
|
if response.rsn.s == -1 and response.rsn.e == -1:
|
|
""
|
|
else:
|
|
response.data.toString(response.rsn.s, response.rsn.e)
|
|
else:
|
|
""
|
|
|
|
proc reason*(response: HttpResponseHeader, buffer: openArray[byte]): string =
|
|
## Returns HTTP reason string from ``response``.
|
|
if response.success():
|
|
if response.rsn.s == -1 and response.rsn.e == -1:
|
|
""
|
|
else:
|
|
buffer.toString(response.rsn.s, response.rsn.e)
|
|
else:
|
|
""
|
|
|
|
proc dispositionType*(header: ContentDispositionHeader): string =
|
|
## Returns disposition type of ``Content-Disposition`` header.
|
|
if header.success():
|
|
if header.disptype.s == -1 and header.disptype.e == -1:
|
|
""
|
|
else:
|
|
header.data.toString(header.disptype.s, header.disptype.e)
|
|
else:
|
|
""
|
|
|
|
proc dispositionType*(header: ContentDispositionHeader,
|
|
buffer: openArray[byte]): string =
|
|
## Returns disposition type of ``Content-Disposition`` header.
|
|
if header.success():
|
|
if header.disptype.s == -1 and header.disptype.e == -1:
|
|
""
|
|
else:
|
|
buffer.toString(header.disptype.s, header.disptype.e)
|
|
else:
|
|
""
|
|
|
|
proc len*(reqresp: HttpReqRespHeader): int =
|
|
## Returns number of headers in ``reqresp``.
|
|
if reqresp.success():
|
|
len(reqresp.hdrs)
|
|
else:
|
|
0
|
|
|
|
proc size*(reqresp: HttpReqRespHeader): int =
|
|
## Returns size of HTTP headers in octets (bytes).
|
|
if reqresp.success():
|
|
reqresp.length
|
|
else:
|
|
0
|
|
|
|
proc `$`*(version: HttpVersion): string =
|
|
## Return string representation of HTTP version ``version``.
|
|
case version
|
|
of HttpVersion09:
|
|
"HTTP/0.9"
|
|
of HttpVersion10:
|
|
"HTTP/1.0"
|
|
of HttpVersion11:
|
|
"HTTP/1.1"
|
|
of HttpVersion20:
|
|
"HTTP/2.0"
|
|
else:
|
|
"HTTP/1.0"
|
|
|
|
{.push overflowChecks: off.}
|
|
proc contentLength*(reqresp: HttpReqRespHeader): int =
|
|
## Returns "Content-Length" header value as positive integer.
|
|
##
|
|
## If header is not present, ``0`` value will be returned, if value of header
|
|
## has non-integer value ``-1`` will be returned.
|
|
const
|
|
MaxValue = high(int) div 10
|
|
MaxNumber = high(int) mod 10
|
|
if reqresp.success():
|
|
let nstr = reqresp["Content-Length"]
|
|
if len(nstr) == 0:
|
|
0
|
|
else:
|
|
let vstr = strip(nstr)
|
|
var res = 0
|
|
for i in 0..<len(vstr):
|
|
let ch = vstr[i]
|
|
if ch notin NUM:
|
|
return -1
|
|
let digit = ord(ch) - ord('0')
|
|
if (res > MaxValue) or (res == MaxValue and digit > MaxNumber):
|
|
# overflow
|
|
return -1
|
|
res = res * 10 + digit
|
|
res
|
|
else:
|
|
-1
|
|
{.pop.}
|
|
|
|
proc httpDate*(datetime: DateTime): string =
|
|
## Returns ``datetime`` formated as HTTP full date (RFC-822).
|
|
## ``Note``: ``datetime`` must be in UTC/GMT zone.
|
|
var res = datetime.format("ddd, dd MMM yyyy HH:mm:ss")
|
|
res.add(" GMT")
|
|
res
|
|
|
|
proc httpDate*(): string {.inline.} =
|
|
## Returns current datetime formatted as HTTP full date (RFC-822).
|
|
utc(now()).httpDate()
|
|
|
|
proc `$`*(m: HttpMethod): string =
|
|
## Returns string representation of HTTP method ``m``.
|
|
case m
|
|
of MethodGet: "GET"
|
|
of MethodPost: "POST"
|
|
of MethodHead: "HEAD"
|
|
of MethodPut: "PUT"
|
|
of MethodDelete: "DELETE"
|
|
of MethodTrace: "TRACE"
|
|
of MethodOptions: "OPTIONS"
|
|
of MethodConnect: "CONNECT"
|
|
of MethodPatch: "PATCH"
|
|
of MethodError: "ERROR"
|
|
|
|
proc checkHeaderName*(value: string): bool =
|
|
## Validates ``value`` as `header field name` string and returns ``true``
|
|
## on success.
|
|
if len(value) == 0:
|
|
false
|
|
else:
|
|
var res = true
|
|
for ch in value:
|
|
if ch notin HEADERNAME:
|
|
res = false
|
|
break
|
|
res
|
|
|
|
proc checkHeaderValue*(value: string): bool =
|
|
## Validates ``value`` as `header field value` string and returns ``true``
|
|
## on success.
|
|
var res = true
|
|
for ch in value:
|
|
if (ch == CR) or (ch == LF):
|
|
res = false
|
|
break
|
|
res
|
|
|
|
proc toInt*(code: HttpCode): int =
|
|
## Returns ``code`` as integer value.
|
|
case code
|
|
of Http100: 100
|
|
of Http101: 101
|
|
of Http200: 200
|
|
of Http201: 201
|
|
of Http202: 202
|
|
of Http203: 203
|
|
of Http204: 204
|
|
of Http205: 205
|
|
of Http206: 206
|
|
of Http300: 300
|
|
of Http301: 301
|
|
of Http302: 302
|
|
of Http303: 303
|
|
of Http304: 304
|
|
of Http305: 305
|
|
of Http307: 307
|
|
of Http400: 400
|
|
of Http401: 401
|
|
of Http403: 403
|
|
of Http404: 404
|
|
of Http405: 405
|
|
of Http406: 406
|
|
of Http407: 407
|
|
of Http408: 408
|
|
of Http409: 409
|
|
of Http410: 410
|
|
of Http411: 411
|
|
of Http412: 412
|
|
of Http413: 413
|
|
of Http414: 414
|
|
of Http415: 415
|
|
of Http416: 416
|
|
of Http417: 417
|
|
of Http418: 418
|
|
of Http421: 421
|
|
of Http422: 422
|
|
of Http426: 426
|
|
of Http428: 428
|
|
of Http429: 429
|
|
of Http431: 431
|
|
of Http451: 451
|
|
of Http500: 500
|
|
of Http501: 501
|
|
of Http502: 502
|
|
of Http503: 503
|
|
of Http504: 504
|
|
of Http505: 505
|
|
|
|
const wildCardMediaType* = MediaType.init("*", "*")
|
|
|
|
proc improveMatch(initialAcceptHeadersMatchIndex: int,
|
|
initialAppPreferencesMatchIndex: int,
|
|
matchQ: float,
|
|
sortedAcceptHeaders: seq[AcceptMediaItem],
|
|
sortedAppPreferences: varargs[MediaType]): MediaType =
|
|
var bestAppPreferencesIndex = initialAppPreferencesMatchIndex
|
|
|
|
for acceptIndex in initialAcceptHeadersMatchIndex .. len(sortedAcceptHeaders) - 1:
|
|
if sortedAcceptHeaders[acceptIndex].qvalue < matchQ:
|
|
# If 'q' parameter change, no point continuing
|
|
break
|
|
# Searching for better match between lower indexes of application preferences
|
|
# than current match
|
|
for offerIndex in 0 ..< bestAppPreferencesIndex:
|
|
if sortedAcceptHeaders[acceptIndex].mediaType == sortedAppPreferences[offerIndex]:
|
|
bestAppPreferencesIndex = offerIndex
|
|
# The next element will higher index => worse match, so no point in continuing:
|
|
break
|
|
sortedAppPreferences[bestAppPreferencesIndex]
|
|
|
|
proc selectContentType*(
|
|
sortedAcceptHeaders: seq[AcceptMediaItem],
|
|
sortedAppPreferences: varargs[MediaType]): Result[MediaType, cstring] =
|
|
# Step 1: Find first accept header that matches:
|
|
for i, accept in sortedAcceptHeaders.pairs:
|
|
for j, offer in sortedAppPreferences.pairs:
|
|
if accept.mediaType == offer:
|
|
let matchQ = accept.qvalue
|
|
# Step 2: Try to find a better match in application preferenced content
|
|
# types with the same 'q' parameter
|
|
return ok(improveMatch(i,
|
|
j,
|
|
matchQ,
|
|
sortedAcceptHeaders,
|
|
sortedAppPreferences))
|
|
# If there is no match:
|
|
return err("Preferred content type not found")
|