Merge branch 'devel' of https://github.com/Araq/Nimrod into devel

This commit is contained in:
Araq 2014-11-08 13:11:46 +01:00
commit cb976c9c9a
4 changed files with 64 additions and 46 deletions

View file

@ -37,6 +37,7 @@ type
AsyncHttpServer* = ref object AsyncHttpServer* = ref object
socket: PAsyncSocket socket: PAsyncSocket
reuseAddr: bool
HttpCode* = enum HttpCode* = enum
Http200 = "200 OK", Http200 = "200 OK",
@ -64,9 +65,10 @@ proc `==`*(protocol: tuple[orig: string, major, minor: int],
of HttpVer10: 0 of HttpVer10: 0
result = protocol.major == major and protocol.minor == minor result = protocol.major == major and protocol.minor == minor
proc newAsyncHttpServer*(): PAsyncHttpServer = proc newAsyncHttpServer*(reuseAddr = true): PAsyncHttpServer =
## Creates a new ``AsyncHttpServer`` instance. ## Creates a new ``AsyncHttpServer`` instance.
new result new result
result.reuseAddr = reuseAddr
proc addHeaders(msg: var string, headers: PStringTable) = proc addHeaders(msg: var string, headers: PStringTable) =
for k, v in headers: for k, v in headers:
@ -210,6 +212,8 @@ proc serve*(server: PAsyncHttpServer, port: Port,
## ##
## When a request is made by a client the specified callback will be called. ## When a request is made by a client the specified callback will be called.
server.socket = newAsyncSocket() server.socket = newAsyncSocket()
if server.reuseAddr:
server.socket.setSockOpt(OptReuseAddr, true)
server.socket.bindAddr(port, address) server.socket.bindAddr(port, address)
server.socket.listen() server.socket.listen()

View file

@ -60,6 +60,8 @@ import rawsockets
import net import net
import os import os
export SOBool
when defined(ssl): when defined(ssl):
import openssl import openssl
@ -425,6 +427,17 @@ when defined(ssl):
socket.bioOut = bioNew(bio_s_mem()) socket.bioOut = bioNew(bio_s_mem())
sslSetBio(socket.sslHandle, socket.bioIn, socket.bioOut) sslSetBio(socket.sslHandle, socket.bioIn, socket.bioOut)
proc getSockOpt*(socket: AsyncSocket, opt: SOBool, level = SOL_SOCKET): bool {.
tags: [ReadIOEffect].} =
## Retrieves option ``opt`` as a boolean value.
var res = getSockOptInt(socket.fd, cint(level), toCInt(opt))
result = res != 0
proc setSockOpt*(socket: AsyncSocket, opt: SOBool, value: bool,
level = SOL_SOCKET) {.tags: [WriteIOEffect].} =
## Sets option ``opt`` to a boolean value specified by ``value``.
var valuei = cint(if value: 1 else: 0)
setSockOptInt(socket.fd, cint(level), toCInt(opt), valuei)
when isMainModule: when isMainModule:
type type

View file

@ -17,27 +17,27 @@
## ##
## Retrieving a website ## Retrieving a website
## ==================== ## ====================
## ##
## This example uses HTTP GET to retrieve ## This example uses HTTP GET to retrieve
## ``http://google.com`` ## ``http://google.com``
## ##
## .. code-block:: Nim ## .. code-block:: Nim
## echo(getContent("http://google.com")) ## echo(getContent("http://google.com"))
## ##
## Using HTTP POST ## Using HTTP POST
## =============== ## ===============
## ##
## This example demonstrates the usage of the W3 HTML Validator, it ## This example demonstrates the usage of the W3 HTML Validator, it
## uses ``multipart/form-data`` as the ``Content-Type`` to send the HTML to ## uses ``multipart/form-data`` as the ``Content-Type`` to send the HTML to
## the server. ## the server.
## ##
## .. code-block:: Nim ## .. code-block:: Nim
## var headers: string = "Content-Type: multipart/form-data; boundary=xyz\c\L" ## var headers: string = "Content-Type: multipart/form-data; boundary=xyz\c\L"
## var body: string = "--xyz\c\L" ## var body: string = "--xyz\c\L"
## # soap 1.2 output ## # soap 1.2 output
## body.add("Content-Disposition: form-data; name=\"output\"\c\L") ## body.add("Content-Disposition: form-data; name=\"output\"\c\L")
## body.add("\c\Lsoap12\c\L") ## body.add("\c\Lsoap12\c\L")
## ##
## # html ## # html
## body.add("--xyz\c\L") ## body.add("--xyz\c\L")
## body.add("Content-Disposition: form-data; name=\"uploaded_file\";" & ## body.add("Content-Disposition: form-data; name=\"uploaded_file\";" &
@ -45,7 +45,7 @@
## body.add("Content-Type: text/html\c\L") ## body.add("Content-Type: text/html\c\L")
## body.add("\c\L<html><head></head><body><p>test</p></body></html>\c\L") ## body.add("\c\L<html><head></head><body><p>test</p></body></html>\c\L")
## body.add("--xyz--") ## body.add("--xyz--")
## ##
## echo(postContent("http://validator.w3.org/check", headers, body)) ## echo(postContent("http://validator.w3.org/check", headers, body))
## ##
## Asynchronous HTTP requests ## Asynchronous HTTP requests
@ -95,8 +95,8 @@ from net import nil
type type
Response* = tuple[ Response* = tuple[
version: string, version: string,
status: string, status: string,
headers: StringTableRef, headers: StringTableRef,
body: string] body: string]
@ -108,7 +108,7 @@ type
## does not conform to the implemented ## does not conform to the implemented
## protocol ## protocol
HttpRequestError* = object of IOError ## Thrown in the ``getContent`` proc HttpRequestError* = object of IOError ## Thrown in the ``getContent`` proc
## and ``postContent`` proc, ## and ``postContent`` proc,
## when the server returns an error ## when the server returns an error
@ -123,7 +123,7 @@ proc httpError(msg: string) =
new(e) new(e)
e.msg = msg e.msg = msg
raise e raise e
proc fileError(msg: string) = proc fileError(msg: string) =
var e: ref IOError var e: ref IOError
new(e) new(e)
@ -167,7 +167,7 @@ proc parseChunks(s: TSocket, timeout: int): string =
s.skip(2, timeout) # Skip \c\L s.skip(2, timeout) # Skip \c\L
# Trailer headers will only be sent if the request specifies that we want # Trailer headers will only be sent if the request specifies that we want
# them: http://tools.ietf.org/html/rfc2616#section-3.6.1 # them: http://tools.ietf.org/html/rfc2616#section-3.6.1
proc parseBody(s: TSocket, headers: PStringTable, timeout: int): string = proc parseBody(s: TSocket, headers: PStringTable, timeout: int): string =
result = "" result = ""
if headers["Transfer-Encoding"] == "chunked": if headers["Transfer-Encoding"] == "chunked":
@ -191,7 +191,7 @@ proc parseBody(s: TSocket, headers: PStringTable, timeout: int): string =
" got: " & $received) " got: " & $received)
else: else:
# (http://tools.ietf.org/html/rfc2616#section-4.4) NR.4 TODO # (http://tools.ietf.org/html/rfc2616#section-4.4) NR.4 TODO
# -REGION- Connection: Close # -REGION- Connection: Close
# (http://tools.ietf.org/html/rfc2616#section-4.4) NR.5 # (http://tools.ietf.org/html/rfc2616#section-4.4) NR.5
if headers["Connection"] == "close": if headers["Connection"] == "close":
@ -241,7 +241,7 @@ proc parseResponse(s: TSocket, getBody: bool, timeout: int): TResponse =
inc(linei, le) inc(linei, le)
if line[linei] != ':': httpError("invalid headers") if line[linei] != ':': httpError("invalid headers")
inc(linei) # Skip : inc(linei) # Skip :
result.headers[name] = line[linei.. -1].strip() result.headers[name] = line[linei.. -1].strip()
if not fullyRead: if not fullyRead:
httpError("Connection was closed before full request has been made") httpError("Connection was closed before full request has been made")
@ -256,17 +256,17 @@ type
## correspond to a GET request, but without the response ## correspond to a GET request, but without the response
## body. ## body.
httpGET, ## Retrieves the specified resource. httpGET, ## Retrieves the specified resource.
httpPOST, ## Submits data to be processed to the identified httpPOST, ## Submits data to be processed to the identified
## resource. The data is included in the body of the ## resource. The data is included in the body of the
## request. ## request.
httpPUT, ## Uploads a representation of the specified resource. httpPUT, ## Uploads a representation of the specified resource.
httpDELETE, ## Deletes the specified resource. httpDELETE, ## Deletes the specified resource.
httpTRACE, ## Echoes back the received request, so that a client httpTRACE, ## Echoes back the received request, so that a client
## can see what intermediate servers are adding or ## can see what intermediate servers are adding or
## changing in the request. ## changing in the request.
httpOPTIONS, ## Returns the HTTP methods that the server supports httpOPTIONS, ## Returns the HTTP methods that the server supports
## for specified address. ## for specified address.
httpCONNECT ## Converts the request connection to a transparent httpCONNECT ## Converts the request connection to a transparent
## TCP/IP tunnel, usually used for proxies. ## TCP/IP tunnel, usually used for proxies.
{.deprecated: [THttpMethod: HttpMethod].} {.deprecated: [THttpMethod: HttpMethod].}
@ -281,7 +281,7 @@ proc newProxy*(url: string, auth = ""): PProxy =
## Constructs a new ``TProxy`` object. ## Constructs a new ``TProxy`` object.
result = PProxy(url: parseUrl(url), auth: auth) result = PProxy(url: parseUrl(url), auth: auth)
proc request*(url: string, httpMethod = httpGET, extraHeaders = "", proc request*(url: string, httpMethod = httpGET, extraHeaders = "",
body = "", body = "",
sslContext: PSSLContext = defaultSSLContext, sslContext: PSSLContext = defaultSSLContext,
timeout = -1, userAgent = defUserAgent, timeout = -1, userAgent = defUserAgent,
@ -298,7 +298,7 @@ proc request*(url: string, httpMethod = httpGET, extraHeaders = "",
headers.add(" " & url) headers.add(" " & url)
headers.add(" HTTP/1.1\c\L") headers.add(" HTTP/1.1\c\L")
add(headers, "Host: " & r.hostname & "\c\L") add(headers, "Host: " & r.hostname & "\c\L")
if userAgent != "": if userAgent != "":
add(headers, "User-Agent: " & userAgent & "\c\L") add(headers, "User-Agent: " & userAgent & "\c\L")
@ -307,7 +307,7 @@ proc request*(url: string, httpMethod = httpGET, extraHeaders = "",
add(headers, "Proxy-Authorization: basic " & auth & "\c\L") add(headers, "Proxy-Authorization: basic " & auth & "\c\L")
add(headers, extraHeaders) add(headers, extraHeaders)
add(headers, "\c\L") add(headers, "\c\L")
var s = socket() var s = socket()
if s == invalidSocket: raiseOSError(osLastError()) if s == invalidSocket: raiseOSError(osLastError())
var port = sockets.TPort(80) var port = sockets.TPort(80)
@ -320,7 +320,7 @@ proc request*(url: string, httpMethod = httpGET, extraHeaders = "",
"SSL support is not available. Cannot connect over SSL.") "SSL support is not available. Cannot connect over SSL.")
if r.port != "": if r.port != "":
port = sockets.TPort(r.port.parseInt) port = sockets.TPort(r.port.parseInt)
if timeout == -1: if timeout == -1:
s.connect(r.hostname, port) s.connect(r.hostname, port)
else: else:
@ -328,10 +328,10 @@ proc request*(url: string, httpMethod = httpGET, extraHeaders = "",
s.send(headers) s.send(headers)
if body != "": if body != "":
s.send(body) s.send(body)
result = parseResponse(s, httpMethod != httpHEAD, timeout) result = parseResponse(s, httpMethod != httpHEAD, timeout)
s.close() s.close()
proc redirection(status: string): bool = proc redirection(status: string): bool =
const redirectionNRs = ["301", "302", "303", "307"] const redirectionNRs = ["301", "302", "303", "307"]
for i in items(redirectionNRs): for i in items(redirectionNRs):
@ -346,7 +346,7 @@ proc getNewLocation(lastUrl: string, headers: PStringTable): string =
if r.hostname == "" and r.path != "": if r.hostname == "" and r.path != "":
let origParsed = parseURL(lastUrl) let origParsed = parseURL(lastUrl)
result = origParsed.hostname & "/" & r.path result = origParsed.hostname & "/" & r.path
proc get*(url: string, extraHeaders = "", maxRedirects = 5, proc get*(url: string, extraHeaders = "", maxRedirects = 5,
sslContext: PSSLContext = defaultSSLContext, sslContext: PSSLContext = defaultSSLContext,
timeout = -1, userAgent = defUserAgent, timeout = -1, userAgent = defUserAgent,
@ -365,7 +365,7 @@ proc get*(url: string, extraHeaders = "", maxRedirects = 5,
result = request(redirectTo, httpGET, extraHeaders, "", sslContext, result = request(redirectTo, httpGET, extraHeaders, "", sslContext,
timeout, userAgent, proxy) timeout, userAgent, proxy)
lastUrl = redirectTo lastUrl = redirectTo
proc getContent*(url: string, extraHeaders = "", maxRedirects = 5, proc getContent*(url: string, extraHeaders = "", maxRedirects = 5,
sslContext: PSSLContext = defaultSSLContext, sslContext: PSSLContext = defaultSSLContext,
timeout = -1, userAgent = defUserAgent, timeout = -1, userAgent = defUserAgent,
@ -381,7 +381,7 @@ proc getContent*(url: string, extraHeaders = "", maxRedirects = 5,
raise newException(EHTTPRequestErr, r.status) raise newException(EHTTPRequestErr, r.status)
else: else:
return r.body return r.body
proc post*(url: string, extraHeaders = "", body = "", proc post*(url: string, extraHeaders = "", body = "",
maxRedirects = 5, maxRedirects = 5,
sslContext: PSSLContext = defaultSSLContext, sslContext: PSSLContext = defaultSSLContext,
@ -404,7 +404,7 @@ proc post*(url: string, extraHeaders = "", body = "",
result = request(redirectTo, meth, xh, body, sslContext, timeout, result = request(redirectTo, meth, xh, body, sslContext, timeout,
userAgent, proxy) userAgent, proxy)
lastUrl = redirectTo lastUrl = redirectTo
proc postContent*(url: string, extraHeaders = "", body = "", proc postContent*(url: string, extraHeaders = "", body = "",
maxRedirects = 5, maxRedirects = 5,
sslContext: PSSLContext = defaultSSLContext, sslContext: PSSLContext = defaultSSLContext,
@ -421,7 +421,7 @@ proc postContent*(url: string, extraHeaders = "", body = "",
raise newException(EHTTPRequestErr, r.status) raise newException(EHTTPRequestErr, r.status)
else: else:
return r.body return r.body
proc downloadFile*(url: string, outputFilename: string, proc downloadFile*(url: string, outputFilename: string,
sslContext: PSSLContext = defaultSSLContext, sslContext: PSSLContext = defaultSSLContext,
timeout = -1, userAgent = defUserAgent, timeout = -1, userAgent = defUserAgent,
@ -456,7 +456,7 @@ type
socket: AsyncSocket socket: AsyncSocket
connected: bool connected: bool
currentURL: TURL ## Where we are currently connected. currentURL: TURL ## Where we are currently connected.
headers: StringTableRef headers*: StringTableRef
maxRedirects: int maxRedirects: int
userAgent: string userAgent: string
when defined(ssl): when defined(ssl):
@ -528,7 +528,7 @@ proc parseChunks(client: PAsyncHttpClient): Future[string] {.async.} =
discard await recvFull(client.socket, 2) # Skip \c\L discard await recvFull(client.socket, 2) # Skip \c\L
# Trailer headers will only be sent if the request specifies that we want # Trailer headers will only be sent if the request specifies that we want
# them: http://tools.ietf.org/html/rfc2616#section-3.6.1 # them: http://tools.ietf.org/html/rfc2616#section-3.6.1
proc parseBody(client: PAsyncHttpClient, proc parseBody(client: PAsyncHttpClient,
headers: PStringTable): Future[string] {.async.} = headers: PStringTable): Future[string] {.async.} =
result = "" result = ""
@ -549,7 +549,7 @@ proc parseBody(client: PAsyncHttpClient,
$length & " got " & $result.len) $length & " got " & $result.len)
else: else:
# (http://tools.ietf.org/html/rfc2616#section-4.4) NR.4 TODO # (http://tools.ietf.org/html/rfc2616#section-4.4) NR.4 TODO
# -REGION- Connection: Close # -REGION- Connection: Close
# (http://tools.ietf.org/html/rfc2616#section-4.4) NR.5 # (http://tools.ietf.org/html/rfc2616#section-4.4) NR.5
if headers["Connection"] == "close": if headers["Connection"] == "close":
@ -598,7 +598,7 @@ proc parseResponse(client: PAsyncHttpClient,
inc(linei, le) inc(linei, le)
if line[linei] != ':': httpError("invalid headers") if line[linei] != ':': httpError("invalid headers")
inc(linei) # Skip : inc(linei) # Skip :
result.headers[name] = line[linei.. -1].strip() result.headers[name] = line[linei.. -1].strip()
if not fullyRead: if not fullyRead:
httpError("Connection was closed before full request has been made") httpError("Connection was closed before full request has been made")
@ -621,14 +621,14 @@ proc newConnection(client: PAsyncHttpClient, url: TURL) {.async.} =
else: else:
rawsockets.TPort(80) rawsockets.TPort(80)
else: rawsockets.TPort(url.port.parseInt) else: rawsockets.TPort(url.port.parseInt)
if url.scheme.toLower() == "https": if url.scheme.toLower() == "https":
when defined(ssl): when defined(ssl):
client.sslContext.wrapSocket(client.socket) client.sslContext.wrapSocket(client.socket)
else: else:
raise newException(EHttpRequestErr, raise newException(EHttpRequestErr,
"SSL support is not available. Cannot connect over SSL.") "SSL support is not available. Cannot connect over SSL.")
await client.socket.connect(url.hostname, port) await client.socket.connect(url.hostname, port)
client.currentURL = url client.currentURL = url
client.connected = true client.connected = true
@ -648,13 +648,13 @@ proc request*(client: PAsyncHttpClient, url: string, httpMethod = httpGET,
if not client.headers.hasKey("user-agent") and client.userAgent != "": if not client.headers.hasKey("user-agent") and client.userAgent != "":
client.headers["User-Agent"] = client.userAgent client.headers["User-Agent"] = client.userAgent
var headers = generateHeaders(r, httpMethod, client.headers) var headers = generateHeaders(r, httpMethod, client.headers)
await client.socket.send(headers) await client.socket.send(headers)
if body != "": if body != "":
await client.socket.send(body) await client.socket.send(body)
result = await parseResponse(client, httpMethod != httpHEAD) result = await parseResponse(client, httpMethod != httpHEAD)
proc get*(client: PAsyncHttpClient, url: string): Future[TResponse] {.async.} = proc get*(client: PAsyncHttpClient, url: string): Future[TResponse] {.async.} =
@ -676,7 +676,7 @@ when isMainModule:
proc main() {.async.} = proc main() {.async.} =
var client = newAsyncHttpClient() var client = newAsyncHttpClient()
var resp = await client.request("http://picheta.me") var resp = await client.request("http://picheta.me")
echo("Got response: ", resp.status) echo("Got response: ", resp.status)
echo("Body:\n") echo("Body:\n")
echo(resp.body) echo(resp.body)
@ -703,13 +703,13 @@ when isMainModule:
#var r = get("http://validator.w3.org/check?uri=http%3A%2F%2Fgoogle.com& #var r = get("http://validator.w3.org/check?uri=http%3A%2F%2Fgoogle.com&
# charset=%28detect+automatically%29&doctype=Inline&group=0") # charset=%28detect+automatically%29&doctype=Inline&group=0")
var headers: string = "Content-Type: multipart/form-data; boundary=xyz\c\L" var headers: string = "Content-Type: multipart/form-data; boundary=xyz\c\L"
var body: string = "--xyz\c\L" var body: string = "--xyz\c\L"
# soap 1.2 output # soap 1.2 output
body.add("Content-Disposition: form-data; name=\"output\"\c\L") body.add("Content-Disposition: form-data; name=\"output\"\c\L")
body.add("\c\Lsoap12\c\L") body.add("\c\Lsoap12\c\L")
# html # html
body.add("--xyz\c\L") body.add("--xyz\c\L")
body.add("Content-Disposition: form-data; name=\"uploaded_file\";" & body.add("Content-Disposition: form-data; name=\"uploaded_file\";" &

View file

@ -428,7 +428,8 @@ proc close*(socket: Socket) =
elif res != 1: elif res != 1:
socketError(socket) socketError(socket)
proc toCInt(opt: SOBool): cint = proc toCInt*(opt: SOBool): cint =
## Converts a ``SOBool`` into its Socket Option cint representation.
case opt case opt
of OptAcceptConn: SO_ACCEPTCONN of OptAcceptConn: SO_ACCEPTCONN
of OptBroadcast: SO_BROADCAST of OptBroadcast: SO_BROADCAST