Fix queryParams() to not produce empty values.

Fix part() cancellation.
Add requestInfo() procedure.
Fix request.scheme.
Add MultiPart.isEmpty()
Fix MultiPart counter.
Add isEmpty() for HttpTable.
Add some documentation in HttpTable.
This commit is contained in:
cheatfate 2021-02-02 12:48:04 +02:00 committed by zah
parent a310a5620a
commit 8c20b369b7
3 changed files with 124 additions and 32 deletions

View File

@ -44,6 +44,7 @@ iterator queryParams*(query: string): tuple[key: string, value: 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: ""
yield (decodeUrl(k), decodeUrl(v))

View File

@ -51,8 +51,10 @@ type
instance*: StreamServer
# semaphore*: AsyncSemaphore
maxConnections*: int
backlogSize: int
baseUri*: Uri
flags*: set[HttpServerFlags]
socketFlags*: set[ServerFlags]
connections*: Table[string, Future[void]]
acceptLoop*: Future[void]
lifetime*: Future[void]
@ -130,7 +132,9 @@ proc new*(htype: typedesc[HttpServerRef],
maxHeadersSize: maxHeadersSize,
maxRequestBodySize: maxRequestBodySize,
processCallback: processCallback,
flags: serverFlags
backLogSize: backLogSize,
flags: serverFlags,
socketFlags: socketFlags
)
res.baseUri =
@ -191,6 +195,12 @@ proc prepareRequest(conn: HttpConnectionRef,
if req.version notin {HttpVersion10, HttpVersion11}:
return err(Http505)
request.scheme =
if HttpServerFlags.Secure in conn.server.flags:
"https"
else:
"http"
request.version = req.version
request.meth = req.meth
@ -641,16 +651,19 @@ proc post*(req: HttpRequestRef): Future[HttpTable] {.async.} =
# We must handle `Expect` first.
try:
await req.handleExpect()
except CancelledError as exc:
await mpreader.close()
raise exc
except HttpCriticalError as exc:
await mpreader.close()
raise exc
# Reading multipart/form-data parts.
var runLoop = true
var loopError: ref MultipartError
while runLoop:
var part: MultiPart
try:
let part = await mpreader.readPart()
part = await mpreader.readPart()
var value = await part.getBody()
## TODO (cheatfate) double copy here.
var strvalue = newString(len(value))
@ -660,14 +673,17 @@ proc post*(req: HttpRequestRef): Future[HttpTable] {.async.} =
await part.close()
except MultiPartEoM:
runLoop = false
except MultipartError as exc:
# We preserve error to avoid Nim's exception transformation bug.
runLoop = false
loopError = exc
except CancelledError as exc:
if not(part.isEmpty()):
await part.close()
await mpreader.close()
raise exc
except MultipartError as exc:
if not(part.isEmpty()):
await part.close()
await mpreader.close()
raise exc
await mpreader.close()
if not(isNil(loopError)):
raise loopError
req.postTable = some(table)
return table
else:
@ -883,24 +899,95 @@ proc finish*(resp: HttpResponseRef) {.async.} =
resp.state = HttpResponseState.Failed
raise newHttpCriticalError("Unable to send response")
when isMainModule:
proc process(req: RequestFence[HttpRequestRef]): Future[HttpResponseRef] {.
async.} =
if req.isOk():
let request = req.get()
let post = await request.post()
let response = request.getResponse()
await response.sendBody("Got [" & $request.meth & "] request")
return response
proc requestInfo*(req: HttpRequestRef, contentType = "text/text"): string =
proc h(t: string): string =
case contentType
of "text/text":
"\r\n" & t & " ===\r\n"
of "text/html":
"<h3>" & t & "</h3>"
else:
return dumbResponse()
t
proc kv(k, v: string): string =
case contentType
of "text/text":
k & ": " & v & "\r\n"
of "text/html":
"<div><code><b>" & k & "</b></code><code>" & v & "</code></div>"
else:
k & ": " & v
let res = HttpServerRef.new(initTAddress("127.0.0.1:30080"), process,
maxConnections = 1)
if res.isOk():
let server = res.get()
server.start()
echo "HTTP server was started"
waitFor server.join()
let header =
case contentType
of "text/html":
"<html><head><title>Request Information</title>" &
"<style>code {padding-left: 30px;}</style>" &
"</head><body>"
else:
echo "Failed to start server: ", res.error
""
let footer =
case contentType
of "text/html":
"</body></html>"
else:
""
var res = h("Request information")
res.add(kv("request.scheme", $req.scheme))
res.add(kv("request.method", $req.meth))
res.add(kv("request.version", $req.version))
res.add(kv("request.uri", $req.uri))
res.add(kv("request.flags", $req.requestFlags))
res.add(kv("request.TransferEncoding", $req.transferEncoding))
res.add(kv("request.ContentEncoding", $req.contentEncoding))
let body =
if req.hasBody():
if req.contentLength == 0:
"present, size not available"
else:
"present, size = " & $req.contentLength
else:
"not available"
res.add(kv("request.body", body))
if not(req.queryTable.isEmpty()):
res.add(h("Query arguments"))
for k, v in req.queryTable.stringItems():
res.add(kv(k, v))
if not(req.headersTable.isEmpty()):
res.add(h("HTTP headers"))
for k, v in req.headersTable.stringItems(true):
res.add(kv(k, v))
if req.meth in PostMethods:
if req.postTable.isSome():
let postTable = req.postTable.get()
if not(postTable.isEmpty()):
res.add(h("POST arguments"))
for k, v in postTable.stringItems():
res.add(kv(k, v))
res.add(h("Connection information"))
res.add(kv("local.address", $req.connection.transp.localAddress()))
res.add(kv("remote.address", $req.connection.transp.remoteAddress()))
res.add(h("Server configuration"))
let maxConn =
if req.connection.server.maxConnections < 0:
"unlimited"
else:
$req.connection.server.maxConnections
res.add(kv("server.maxConnections", $maxConn))
res.add(kv("server.maxHeadersSize", $req.connection.server.maxHeadersSize))
res.add(kv("server.maxRequestBodySize",
$req.connection.server.maxRequestBodySize))
res.add(kv("server.backlog", $req.connection.server.backLogSize))
res.add(kv("server.headersTimeout", $req.connection.server.headersTimeout))
res.add(kv("server.bodyTimeout", $req.connection.server.bodyTimeout))
res.add(kv("server.baseUri", $req.connection.server.baseUri))
res.add(kv("server.flags", $req.connection.server.flags))
res.add(kv("server.socket.flags", $req.connection.server.socketFlags))
header & res & footer

View File

@ -173,13 +173,13 @@ proc readPart*(mpr: MultiPartReaderRef): Future[MultiPart] {.async.} =
if headersList.failed():
raise newException(MultiPartIncorrectError,
"Incorrect part headers found")
inc(mpr.counter)
var part = MultiPart(
kind: MultiPartSource.Stream,
headers: HttpTable.init(),
stream: newBoundedStreamReader(mpr.stream, -1, mpr.boundary),
counter: mpr.counter
)
inc(mpr.counter)
for k, v in headersList.headers(mpr.buffer.toOpenArray(0, res - 1)):
part.headers.add(k, v)
@ -355,13 +355,13 @@ proc getPart*(mpr: var MultiPartReader): Result[MultiPart, string] =
# We set reader's offset to the place right after <CR><LF>
mpr.offset = start + pos2 + 2
inc(mpr.counter)
var part = MultiPart(
kind: MultiPartSource.Buffer,
headers: HttpTable.init(),
buffer: @(mpr.buffer.toOpenArray(start, start + pos2 - 1)),
counter: mpr.counter
)
inc(mpr.counter)
for k, v in headersList.headers(mpr.buffer.toOpenArray(hstart, hfinish)):
part.headers.add(k, v)
@ -374,6 +374,10 @@ proc getPart*(mpr: var MultiPartReader): Result[MultiPart, string] =
else:
err("Incorrect multipart form")
func isEmpty*(mp: MultiPart): bool =
## Returns ``true`` is multipart ``mp`` is not initialized/filled yet.
mp.counter == 0
func getMultipartBoundary*(ch: openarray[string]): HttpResult[string] =
## Returns ``multipart/form-data`` boundary value from ``Content-Type``
## header.