parent
e1ed34627f
commit
fd350de580
1 changed files with 145 additions and 125 deletions
|
|
@ -58,15 +58,18 @@ type
|
||||||
socket: AsyncSocket
|
socket: AsyncSocket
|
||||||
reuseAddr: bool
|
reuseAddr: bool
|
||||||
reusePort: bool
|
reusePort: bool
|
||||||
|
maxBody: int ## The maximum content-length that will be read for the body.
|
||||||
|
|
||||||
{.deprecated: [TRequest: Request, PAsyncHttpServer: AsyncHttpServer,
|
{.deprecated: [TRequest: Request, PAsyncHttpServer: AsyncHttpServer,
|
||||||
THttpCode: HttpCode, THttpVersion: HttpVersion].}
|
THttpCode: HttpCode, THttpVersion: HttpVersion].}
|
||||||
|
|
||||||
proc newAsyncHttpServer*(reuseAddr = true, reusePort = false): AsyncHttpServer =
|
proc newAsyncHttpServer*(reuseAddr = true, reusePort = false,
|
||||||
|
maxBody = 8388608): AsyncHttpServer =
|
||||||
## Creates a new ``AsyncHttpServer`` instance.
|
## Creates a new ``AsyncHttpServer`` instance.
|
||||||
new result
|
new result
|
||||||
result.reuseAddr = reuseAddr
|
result.reuseAddr = reuseAddr
|
||||||
result.reusePort = reusePort
|
result.reusePort = reusePort
|
||||||
|
result.maxBody = maxBody
|
||||||
|
|
||||||
proc addHeaders(msg: var string, headers: HttpHeaders) =
|
proc addHeaders(msg: var string, headers: HttpHeaders) =
|
||||||
for k, v in headers:
|
for k, v in headers:
|
||||||
|
|
@ -129,138 +132,155 @@ proc parseProtocol(protocol: string): tuple[orig: string, major, minor: int] =
|
||||||
proc sendStatus(client: AsyncSocket, status: string): Future[void] =
|
proc sendStatus(client: AsyncSocket, status: string): Future[void] =
|
||||||
client.send("HTTP/1.1 " & status & "\c\L\c\L")
|
client.send("HTTP/1.1 " & status & "\c\L\c\L")
|
||||||
|
|
||||||
proc processClient(client: AsyncSocket, address: string,
|
proc processRequest(server: AsyncHttpServer, req: FutureVar[Request],
|
||||||
callback: proc (request: Request):
|
client: AsyncSocket,
|
||||||
|
address: string, lineFut: FutureVar[string],
|
||||||
|
callback: proc (request: Request):
|
||||||
Future[void] {.closure, gcsafe.}) {.async.} =
|
Future[void] {.closure, gcsafe.}) {.async.} =
|
||||||
var request: Request
|
|
||||||
request.url = initUri()
|
|
||||||
request.headers = newHttpHeaders()
|
|
||||||
var lineFut = newFutureVar[string]("asynchttpserver.processClient")
|
|
||||||
lineFut.mget() = newStringOfCap(80)
|
|
||||||
var key, value = ""
|
|
||||||
|
|
||||||
while not client.isClosed:
|
# Alias `request` to `req.mget()` so we don't have to write `mget` everywhere.
|
||||||
# GET /path HTTP/1.1
|
template request(): Request =
|
||||||
# Header: val
|
req.mget()
|
||||||
# \n
|
|
||||||
request.headers.clear()
|
|
||||||
request.body = ""
|
|
||||||
request.hostname.shallowCopy(address)
|
|
||||||
assert client != nil
|
|
||||||
request.client = client
|
|
||||||
|
|
||||||
# We should skip at least one empty line before the request
|
# GET /path HTTP/1.1
|
||||||
# https://tools.ietf.org/html/rfc7230#section-3.5
|
# Header: val
|
||||||
for i in 0..1:
|
# \n
|
||||||
lineFut.mget().setLen(0)
|
request.headers.clear()
|
||||||
lineFut.clean()
|
request.body = ""
|
||||||
await client.recvLineInto(lineFut, maxLength=maxLine) # TODO: Timeouts.
|
request.hostname.shallowCopy(address)
|
||||||
|
assert client != nil
|
||||||
|
request.client = client
|
||||||
|
|
||||||
if lineFut.mget == "":
|
# We should skip at least one empty line before the request
|
||||||
client.close()
|
# https://tools.ietf.org/html/rfc7230#section-3.5
|
||||||
return
|
for i in 0..1:
|
||||||
|
lineFut.mget().setLen(0)
|
||||||
|
lineFut.clean()
|
||||||
|
await client.recvLineInto(lineFut, maxLength=maxLine) # TODO: Timeouts.
|
||||||
|
|
||||||
if lineFut.mget.len > maxLine:
|
if lineFut.mget == "":
|
||||||
await request.respondError(Http413)
|
client.close()
|
||||||
client.close()
|
|
||||||
return
|
|
||||||
if lineFut.mget != "\c\L":
|
|
||||||
break
|
|
||||||
|
|
||||||
# First line - GET /path HTTP/1.1
|
|
||||||
var i = 0
|
|
||||||
for linePart in lineFut.mget.split(' '):
|
|
||||||
case i
|
|
||||||
of 0:
|
|
||||||
try:
|
|
||||||
# TODO: this is likely slow.
|
|
||||||
request.reqMethod = parseEnum[HttpMethod]("http" & linePart)
|
|
||||||
except ValueError:
|
|
||||||
asyncCheck request.respondError(Http400)
|
|
||||||
continue
|
|
||||||
of 1:
|
|
||||||
try:
|
|
||||||
parseUri(linePart, request.url)
|
|
||||||
except ValueError:
|
|
||||||
asyncCheck request.respondError(Http400)
|
|
||||||
continue
|
|
||||||
of 2:
|
|
||||||
try:
|
|
||||||
request.protocol = parseProtocol(linePart)
|
|
||||||
except ValueError:
|
|
||||||
asyncCheck request.respondError(Http400)
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
await request.respondError(Http400)
|
|
||||||
continue
|
|
||||||
inc i
|
|
||||||
|
|
||||||
# Headers
|
|
||||||
while true:
|
|
||||||
i = 0
|
|
||||||
lineFut.mget.setLen(0)
|
|
||||||
lineFut.clean()
|
|
||||||
await client.recvLineInto(lineFut, maxLength=maxLine)
|
|
||||||
|
|
||||||
if lineFut.mget == "":
|
|
||||||
client.close(); return
|
|
||||||
if lineFut.mget.len > maxLine:
|
|
||||||
await request.respondError(Http413)
|
|
||||||
client.close(); return
|
|
||||||
if lineFut.mget == "\c\L": break
|
|
||||||
let (key, value) = parseHeader(lineFut.mget)
|
|
||||||
request.headers[key] = value
|
|
||||||
# Ensure the client isn't trying to DoS us.
|
|
||||||
if request.headers.len > headerLimit:
|
|
||||||
await client.sendStatus("400 Bad Request")
|
|
||||||
request.client.close()
|
|
||||||
return
|
|
||||||
|
|
||||||
if request.reqMethod == HttpPost:
|
|
||||||
# Check for Expect header
|
|
||||||
if request.headers.hasKey("Expect"):
|
|
||||||
if "100-continue" in request.headers["Expect"]:
|
|
||||||
await client.sendStatus("100 Continue")
|
|
||||||
else:
|
|
||||||
await client.sendStatus("417 Expectation Failed")
|
|
||||||
|
|
||||||
# Read the body
|
|
||||||
# - Check for Content-length header
|
|
||||||
if request.headers.hasKey("Content-Length"):
|
|
||||||
var contentLength = 0
|
|
||||||
if parseInt(request.headers["Content-Length"],
|
|
||||||
contentLength) == 0:
|
|
||||||
await request.respond(Http400, "Bad Request. Invalid Content-Length.")
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
request.body = await client.recv(contentLength)
|
|
||||||
if request.body.len != contentLength:
|
|
||||||
await request.respond(Http400, "Bad Request. Content-Length does not match actual.")
|
|
||||||
continue
|
|
||||||
elif request.reqMethod == HttpPost:
|
|
||||||
await request.respond(Http411, "Content-Length required.")
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Call the user's callback.
|
|
||||||
await callback(request)
|
|
||||||
|
|
||||||
if "upgrade" in request.headers.getOrDefault("connection"):
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Persistent connections
|
if lineFut.mget.len > maxLine:
|
||||||
if (request.protocol == HttpVer11 and
|
await request.respondError(Http413)
|
||||||
request.headers.getOrDefault("connection").normalize != "close") or
|
client.close()
|
||||||
(request.protocol == HttpVer10 and
|
return
|
||||||
request.headers.getOrDefault("connection").normalize == "keep-alive"):
|
if lineFut.mget != "\c\L":
|
||||||
# In HTTP 1.1 we assume that connection is persistent. Unless connection
|
|
||||||
# header states otherwise.
|
|
||||||
# In HTTP 1.0 we assume that the connection should not be persistent.
|
|
||||||
# Unless the connection header states otherwise.
|
|
||||||
discard
|
|
||||||
else:
|
|
||||||
request.client.close()
|
|
||||||
break
|
break
|
||||||
|
|
||||||
|
# First line - GET /path HTTP/1.1
|
||||||
|
var i = 0
|
||||||
|
for linePart in lineFut.mget.split(' '):
|
||||||
|
case i
|
||||||
|
of 0:
|
||||||
|
try:
|
||||||
|
# TODO: this is likely slow.
|
||||||
|
request.reqMethod = parseEnum[HttpMethod]("http" & linePart)
|
||||||
|
except ValueError:
|
||||||
|
asyncCheck request.respondError(Http400)
|
||||||
|
return
|
||||||
|
of 1:
|
||||||
|
try:
|
||||||
|
parseUri(linePart, request.url)
|
||||||
|
except ValueError:
|
||||||
|
asyncCheck request.respondError(Http400)
|
||||||
|
return
|
||||||
|
of 2:
|
||||||
|
try:
|
||||||
|
request.protocol = parseProtocol(linePart)
|
||||||
|
except ValueError:
|
||||||
|
asyncCheck request.respondError(Http400)
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
await request.respondError(Http400)
|
||||||
|
return
|
||||||
|
inc i
|
||||||
|
|
||||||
|
# Headers
|
||||||
|
while true:
|
||||||
|
i = 0
|
||||||
|
lineFut.mget.setLen(0)
|
||||||
|
lineFut.clean()
|
||||||
|
await client.recvLineInto(lineFut, maxLength=maxLine)
|
||||||
|
|
||||||
|
if lineFut.mget == "":
|
||||||
|
client.close(); return
|
||||||
|
if lineFut.mget.len > maxLine:
|
||||||
|
await request.respondError(Http413)
|
||||||
|
client.close(); return
|
||||||
|
if lineFut.mget == "\c\L": break
|
||||||
|
let (key, value) = parseHeader(lineFut.mget)
|
||||||
|
request.headers[key] = value
|
||||||
|
# Ensure the client isn't trying to DoS us.
|
||||||
|
if request.headers.len > headerLimit:
|
||||||
|
await client.sendStatus("400 Bad Request")
|
||||||
|
request.client.close()
|
||||||
|
return
|
||||||
|
|
||||||
|
if request.reqMethod == HttpPost:
|
||||||
|
# Check for Expect header
|
||||||
|
if request.headers.hasKey("Expect"):
|
||||||
|
if "100-continue" in request.headers["Expect"]:
|
||||||
|
await client.sendStatus("100 Continue")
|
||||||
|
else:
|
||||||
|
await client.sendStatus("417 Expectation Failed")
|
||||||
|
|
||||||
|
# Read the body
|
||||||
|
# - Check for Content-length header
|
||||||
|
if request.headers.hasKey("Content-Length"):
|
||||||
|
var contentLength = 0
|
||||||
|
if parseInt(request.headers["Content-Length"],
|
||||||
|
contentLength) == 0:
|
||||||
|
await request.respond(Http400, "Bad Request. Invalid Content-Length.")
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
if contentLength > server.maxBody:
|
||||||
|
await request.respondError(Http413)
|
||||||
|
return
|
||||||
|
request.body = await client.recv(contentLength)
|
||||||
|
if request.body.len != contentLength:
|
||||||
|
await request.respond(Http400, "Bad Request. Content-Length does not match actual.")
|
||||||
|
return
|
||||||
|
elif request.reqMethod == HttpPost:
|
||||||
|
await request.respond(Http411, "Content-Length required.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Call the user's callback.
|
||||||
|
await callback(request)
|
||||||
|
|
||||||
|
if "upgrade" in request.headers.getOrDefault("connection"):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Persistent connections
|
||||||
|
if (request.protocol == HttpVer11 and
|
||||||
|
request.headers.getOrDefault("connection").normalize != "close") or
|
||||||
|
(request.protocol == HttpVer10 and
|
||||||
|
request.headers.getOrDefault("connection").normalize == "keep-alive"):
|
||||||
|
# In HTTP 1.1 we assume that connection is persistent. Unless connection
|
||||||
|
# header states otherwise.
|
||||||
|
# In HTTP 1.0 we assume that the connection should not be persistent.
|
||||||
|
# Unless the connection header states otherwise.
|
||||||
|
discard
|
||||||
|
else:
|
||||||
|
request.client.close()
|
||||||
|
return
|
||||||
|
|
||||||
|
proc processClient(server: AsyncHttpServer, client: AsyncSocket, address: string,
|
||||||
|
callback: proc (request: Request):
|
||||||
|
Future[void] {.closure, gcsafe.}) {.async.} =
|
||||||
|
var request = newFutureVar[Request]("asynchttpserver.processClient")
|
||||||
|
request.mget().url = initUri()
|
||||||
|
request.mget().headers = newHttpHeaders()
|
||||||
|
var lineFut = newFutureVar[string]("asynchttpserver.processClient")
|
||||||
|
lineFut.mget() = newStringOfCap(80)
|
||||||
|
|
||||||
|
while not client.isClosed:
|
||||||
|
try:
|
||||||
|
await processRequest(server, request, client, address, lineFut, callback)
|
||||||
|
except:
|
||||||
|
asyncCheck request.mget().respondError(Http500)
|
||||||
|
|
||||||
proc serve*(server: AsyncHttpServer, port: Port,
|
proc serve*(server: AsyncHttpServer, port: Port,
|
||||||
callback: proc (request: Request): Future[void] {.closure,gcsafe.},
|
callback: proc (request: Request): Future[void] {.closure,gcsafe.},
|
||||||
address = "") {.async.} =
|
address = "") {.async.} =
|
||||||
|
|
@ -280,7 +300,7 @@ proc serve*(server: AsyncHttpServer, port: Port,
|
||||||
# TODO: Causes compiler crash.
|
# TODO: Causes compiler crash.
|
||||||
#var (address, client) = await server.socket.acceptAddr()
|
#var (address, client) = await server.socket.acceptAddr()
|
||||||
var fut = await server.socket.acceptAddr()
|
var fut = await server.socket.acceptAddr()
|
||||||
asyncCheck processClient(fut.client, fut.address, callback)
|
asyncCheck processClient(server, fut.client, fut.address, callback)
|
||||||
#echo(f.isNil)
|
#echo(f.isNil)
|
||||||
#echo(f.repr)
|
#echo(f.repr)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue