diff options
author | Adam Strzelecki <ono@java.pl> | 2015-09-29 22:34:57 +0200 |
---|---|---|
committer | Adam Strzelecki <ono@java.pl> | 2015-09-30 12:26:25 +0200 |
commit | 144dc8f8ad209ebcb6591e0adc1bc4cb73528f3a (patch) | |
tree | 92a7e34e176cb56e48780b13dce30eb6aed8c09a /lib/deprecated/pure | |
parent | 4071219e201d76d75a95ce712027aac1d2ee5281 (diff) | |
download | Nim-144dc8f8ad209ebcb6591e0adc1bc4cb73528f3a.tar.gz |
Move deprecated modules into lib/deprecated/
This gives clear indication what modules are now deprecated and reduce clutter in non-deprecated module directories.
Diffstat (limited to 'lib/deprecated/pure')
-rw-r--r-- | lib/deprecated/pure/actors.nim | 241 | ||||
-rw-r--r-- | lib/deprecated/pure/actors.nim.cfg | 3 | ||||
-rw-r--r-- | lib/deprecated/pure/asyncio.nim | 712 | ||||
-rw-r--r-- | lib/deprecated/pure/ftpclient.nim | 675 | ||||
-rw-r--r-- | lib/deprecated/pure/parseopt.nim | 178 | ||||
-rw-r--r-- | lib/deprecated/pure/parseurl.nim | 114 | ||||
-rw-r--r-- | lib/deprecated/pure/sockets.nim | 1738 |
7 files changed, 3661 insertions, 0 deletions
diff --git a/lib/deprecated/pure/actors.nim b/lib/deprecated/pure/actors.nim new file mode 100644 index 000000000..f0791f954 --- /dev/null +++ b/lib/deprecated/pure/actors.nim @@ -0,0 +1,241 @@ +# +# +# Nim's Runtime Library +# (c) Copyright 2012 Andreas Rumpf +# +# See the file "copying.txt", included in this +# distribution, for details about the copyright. +# + +## `Actor`:idx: support for Nim. An actor is implemented as a thread with +## a channel as its inbox. This module requires the ``--threads:on`` +## command line switch. +## +## Example: +## +## .. code-block:: nim +## +## var +## a: ActorPool[int, void] +## createActorPool(a) +## for i in 0 .. < 300: +## a.spawn(i, proc (x: int) {.thread.} = echo x) +## a.join() +## +## **Note**: This whole module is deprecated. Use `threadpool` and ``spawn`` +## instead. + +{.deprecated.} + +from os import sleep + +type + Task*[In, Out] = object{.pure, final.} ## a task + when Out isnot void: + receiver*: ptr Channel[Out] ## the receiver channel of the response + action*: proc (x: In): Out {.thread.} ## action to execute; + ## sometimes useful + shutDown*: bool ## set to tell an actor to shut-down + data*: In ## the data to process + + Actor[In, Out] = object{.pure, final.} + i: Channel[Task[In, Out]] + t: TThread[ptr Actor[In, Out]] + + PActor*[In, Out] = ptr Actor[In, Out] ## an actor +{.deprecated: [TTask: Task, TActor: Actor].} + +proc spawn*[In, Out](action: proc( + self: PActor[In, Out]){.thread.}): PActor[In, Out] = + ## creates an actor; that is a thread with an inbox. The caller MUST call + ## ``join`` because that also frees the actor's associated resources. + result = cast[PActor[In, Out]](allocShared0(sizeof(result[]))) + open(result.i) + createThread(result.t, action, result) + +proc inbox*[In, Out](self: PActor[In, Out]): ptr Channel[In] = + ## gets a pointer to the associated inbox of the actor `self`. + result = addr(self.i) + +proc running*[In, Out](a: PActor[In, Out]): bool = + ## returns true if the actor `a` is running. + result = running(a.t) + +proc ready*[In, Out](a: PActor[In, Out]): bool = + ## returns true if the actor `a` is ready to process new messages. + result = ready(a.i) + +proc join*[In, Out](a: PActor[In, Out]) = + ## joins an actor. + joinThread(a.t) + close(a.i) + deallocShared(a) + +proc recv*[In, Out](a: PActor[In, Out]): Task[In, Out] = + ## receives a task from `a`'s inbox. + result = recv(a.i) + +proc send*[In, Out, X, Y](receiver: PActor[In, Out], msg: In, + sender: PActor[X, Y]) = + ## sends a message to `a`'s inbox. + var t: Task[In, Out] + t.receiver = addr(sender.i) + shallowCopy(t.data, msg) + send(receiver.i, t) + +proc send*[In, Out](receiver: PActor[In, Out], msg: In, + sender: ptr Channel[Out] = nil) = + ## sends a message to `receiver`'s inbox. + var t: Task[In, Out] + t.receiver = sender + shallowCopy(t.data, msg) + send(receiver.i, t) + +proc sendShutdown*[In, Out](receiver: PActor[In, Out]) = + ## send a shutdown message to `receiver`. + var t: Task[In, Out] + t.shutdown = true + send(receiver.i, t) + +proc reply*[In, Out](t: Task[In, Out], m: Out) = + ## sends a message to io's output message box. + when Out is void: + {.error: "you cannot reply to a void outbox".} + assert t.receiver != nil + send(t.receiver[], m) + + +# ----------------- actor pools ---------------------------------------------- + +type + ActorPool*[In, Out] = object{.pure, final.} ## an actor pool + actors: seq[PActor[In, Out]] + when Out isnot void: + outputs: Channel[Out] +{.deprecated: [TActorPool: ActorPool].} + +proc `^`*[T](f: ptr Channel[T]): T = + ## alias for 'recv'. + result = recv(f[]) + +proc poolWorker[In, Out](self: PActor[In, Out]) {.thread.} = + while true: + var m = self.recv + if m.shutDown: break + when Out is void: + m.action(m.data) + else: + send(m.receiver[], m.action(m.data)) + #self.reply() + +proc createActorPool*[In, Out](a: var ActorPool[In, Out], poolSize = 4) = + ## creates an actor pool. + newSeq(a.actors, poolSize) + when Out isnot void: + open(a.outputs) + for i in 0 .. < a.actors.len: + a.actors[i] = spawn(poolWorker[In, Out]) + +proc sync*[In, Out](a: var ActorPool[In, Out], polling=50) = + ## waits for every actor of `a` to finish with its work. Currently this is + ## implemented as polling every `polling` ms and has a slight chance + ## of failing since we check for every actor to be in `ready` state and not + ## for messages still in ether. This will change in a later + ## version, however. + var allReadyCount = 0 + while true: + var wait = false + for i in 0..high(a.actors): + if not a.actors[i].i.ready: + wait = true + allReadyCount = 0 + break + if not wait: + # it's possible that some actor sent a message to some other actor but + # both appeared to be non-working as the message takes some time to + # arrive. We assume that this won't take longer than `polling` and + # simply attempt a second time and declare victory then. ;-) + inc allReadyCount + if allReadyCount > 1: break + sleep(polling) + +proc terminate*[In, Out](a: var ActorPool[In, Out]) = + ## terminates each actor in the actor pool `a` and frees the + ## resources attached to `a`. + var t: Task[In, Out] + t.shutdown = true + for i in 0.. <a.actors.len: send(a.actors[i].i, t) + for i in 0.. <a.actors.len: join(a.actors[i]) + when Out isnot void: + close(a.outputs) + a.actors = nil + +proc join*[In, Out](a: var ActorPool[In, Out]) = + ## short-cut for `sync` and then `terminate`. + sync(a) + terminate(a) + +template setupTask = + t.action = action + shallowCopy(t.data, input) + +template schedule = + # extremely simple scheduler: We always try the first thread first, so that + # it remains 'hot' ;-). Round-robin hurts for keeping threads hot. + for i in 0..high(p.actors): + if p.actors[i].i.ready: + p.actors[i].i.send(t) + return + # no thread ready :-( --> send message to the thread which has the least + # messages pending: + var minIdx = -1 + var minVal = high(int) + for i in 0..high(p.actors): + var curr = p.actors[i].i.peek + if curr == 0: + # ok, is ready now: + p.actors[i].i.send(t) + return + if curr < minVal and curr >= 0: + minVal = curr + minIdx = i + if minIdx >= 0: + p.actors[minIdx].i.send(t) + else: + raise newException(DeadThreadError, "cannot send message; thread died") + +proc spawn*[In, Out](p: var ActorPool[In, Out], input: In, + action: proc (input: In): Out {.thread.} + ): ptr Channel[Out] = + ## uses the actor pool to run ``action(input)`` concurrently. + ## `spawn` is guaranteed to not block. + var t: Task[In, Out] + setupTask() + result = addr(p.outputs) + t.receiver = result + schedule() + +proc spawn*[In](p: var ActorPool[In, void], input: In, + action: proc (input: In) {.thread.}) = + ## uses the actor pool to run ``action(input)`` concurrently. + ## `spawn` is guaranteed to not block. + var t: Task[In, void] + setupTask() + schedule() + +when not defined(testing) and isMainModule: + var + a: ActorPool[int, void] + createActorPool(a) + for i in 0 .. < 300: + a.spawn(i, proc (x: int) {.thread.} = echo x) + + when false: + proc treeDepth(n: PNode): int {.thread.} = + var x = a.spawn(treeDepth, n.le) + var y = a.spawn(treeDepth, n.ri) + result = max(^x, ^y) + 1 + + a.join() + + diff --git a/lib/deprecated/pure/actors.nim.cfg b/lib/deprecated/pure/actors.nim.cfg new file mode 100644 index 000000000..c6bb9c545 --- /dev/null +++ b/lib/deprecated/pure/actors.nim.cfg @@ -0,0 +1,3 @@ +# to shut up the tester: +--threads:on + diff --git a/lib/deprecated/pure/asyncio.nim b/lib/deprecated/pure/asyncio.nim new file mode 100644 index 000000000..5fd45b215 --- /dev/null +++ b/lib/deprecated/pure/asyncio.nim @@ -0,0 +1,712 @@ +# +# +# Nim's Runtime Library +# (c) Copyright 2012 Andreas Rumpf, Dominik Picheta +# See the file "copying.txt", included in this +# distribution, for details about the copyright. +# + +include "system/inclrtl" + +import sockets, os + +## +## **Warning:** This module is deprecated since version 0.10.2. +## Use the brand new `asyncdispatch <asyncdispatch.html>`_ module together +## with the `asyncnet <asyncnet.html>`_ module. + +## This module implements an asynchronous event loop together with asynchronous +## sockets which use this event loop. +## It is akin to Python's asyncore module. Many modules that use sockets +## have an implementation for this module, those modules should all have a +## ``register`` function which you should use to add the desired objects to a +## dispatcher which you created so +## that you can receive the events associated with that module's object. +## +## Once everything is registered in a dispatcher, you need to call the ``poll`` +## function in a while loop. +## +## **Note:** Most modules have tasks which need to be ran regularly, this is +## why you should not call ``poll`` with a infinite timeout, or even a +## very long one. In most cases the default timeout is fine. +## +## **Note:** This module currently only supports select(), this is limited by +## FD_SETSIZE, which is usually 1024. So you may only be able to use 1024 +## sockets at a time. +## +## Most (if not all) modules that use asyncio provide a userArg which is passed +## on with the events. The type that you set userArg to must be inheriting from +## ``RootObj``! +## +## **Note:** If you want to provide async ability to your module please do not +## use the ``Delegate`` object, instead use ``AsyncSocket``. It is possible +## that in the future this type's fields will not be exported therefore breaking +## your code. +## +## **Warning:** The API of this module is unstable, and therefore is subject +## to change. +## +## Asynchronous sockets +## ==================== +## +## For most purposes you do not need to worry about the ``Delegate`` type. The +## ``AsyncSocket`` is what you are after. It's a reference to +## the ``AsyncSocketObj`` object. This object defines events which you should +## overwrite by your own procedures. +## +## For server sockets the only event you need to worry about is the ``handleAccept`` +## event, in your handleAccept proc you should call ``accept`` on the server +## socket which will give you the client which is connecting. You should then +## set any events that you want to use on that client and add it to your dispatcher +## using the ``register`` procedure. +## +## An example ``handleAccept`` follows: +## +## .. code-block:: nim +## +## var disp = newDispatcher() +## ... +## proc handleAccept(s: AsyncSocket) = +## echo("Accepted client.") +## var client: AsyncSocket +## new(client) +## s.accept(client) +## client.handleRead = ... +## disp.register(client) +## ... +## +## For client sockets you should only be interested in the ``handleRead`` and +## ``handleConnect`` events. The former gets called whenever the socket has +## received messages and can be read from and the latter gets called whenever +## the socket has established a connection to a server socket; from that point +## it can be safely written to. +## +## Getting a blocking client from an AsyncSocket +## ============================================= +## +## If you need a asynchronous server socket but you wish to process the clients +## synchronously then you can use the ``getSocket`` converter to get +## a ``Socket`` from the ``AsyncSocket`` object, this can then be combined +## with ``accept`` like so: +## +## .. code-block:: nim +## +## proc handleAccept(s: AsyncSocket) = +## var client: Socket +## getSocket(s).accept(client) + +{.deprecated.} + +when defined(windows): + from winlean import TimeVal, SocketHandle, FD_SET, FD_ZERO, TFdSet, + FD_ISSET, select +else: + from posix import TimeVal, SocketHandle, FD_SET, FD_ZERO, TFdSet, + FD_ISSET, select + +type + DelegateObj* = object + fd*: SocketHandle + deleVal*: RootRef + + handleRead*: proc (h: RootRef) {.nimcall, gcsafe.} + handleWrite*: proc (h: RootRef) {.nimcall, gcsafe.} + handleError*: proc (h: RootRef) {.nimcall, gcsafe.} + hasDataBuffered*: proc (h: RootRef): bool {.nimcall, gcsafe.} + + open*: bool + task*: proc (h: RootRef) {.nimcall, gcsafe.} + mode*: FileMode + + Delegate* = ref DelegateObj + + Dispatcher* = ref DispatcherObj + DispatcherObj = object + delegates: seq[Delegate] + + AsyncSocket* = ref AsyncSocketObj + AsyncSocketObj* = object of RootObj + socket: Socket + info: SocketStatus + + handleRead*: proc (s: AsyncSocket) {.closure, gcsafe.} + handleWrite: proc (s: AsyncSocket) {.closure, gcsafe.} + handleConnect*: proc (s: AsyncSocket) {.closure, gcsafe.} + + handleAccept*: proc (s: AsyncSocket) {.closure, gcsafe.} + + handleTask*: proc (s: AsyncSocket) {.closure, gcsafe.} + + lineBuffer: TaintedString ## Temporary storage for ``readLine`` + sendBuffer: string ## Temporary storage for ``send`` + sslNeedAccept: bool + proto: Protocol + deleg: Delegate + + SocketStatus* = enum + SockIdle, SockConnecting, SockConnected, SockListening, SockClosed, + SockUDPBound + +{.deprecated: [TDelegate: DelegateObj, PDelegate: Delegate, + TInfo: SocketStatus, PAsyncSocket: AsyncSocket, TAsyncSocket: AsyncSocketObj, + TDispatcher: DispatcherObj, PDispatcher: Dispatcher, + ].} + + +proc newDelegate*(): Delegate = + ## Creates a new delegate. + new(result) + result.handleRead = (proc (h: RootRef) = discard) + result.handleWrite = (proc (h: RootRef) = discard) + result.handleError = (proc (h: RootRef) = discard) + result.hasDataBuffered = (proc (h: RootRef): bool = return false) + result.task = (proc (h: RootRef) = discard) + result.mode = fmRead + +proc newAsyncSocket(): AsyncSocket = + new(result) + result.info = SockIdle + + result.handleRead = (proc (s: AsyncSocket) = discard) + result.handleWrite = nil + result.handleConnect = (proc (s: AsyncSocket) = discard) + result.handleAccept = (proc (s: AsyncSocket) = discard) + result.handleTask = (proc (s: AsyncSocket) = discard) + + result.lineBuffer = "".TaintedString + result.sendBuffer = "" + +proc asyncSocket*(domain: Domain = AF_INET, typ: SockType = SOCK_STREAM, + protocol: Protocol = IPPROTO_TCP, + buffered = true): AsyncSocket = + ## Initialises an AsyncSocket object. If a socket cannot be initialised + ## EOS is raised. + result = newAsyncSocket() + result.socket = socket(domain, typ, protocol, buffered) + result.proto = protocol + if result.socket == invalidSocket: raiseOSError(osLastError()) + result.socket.setBlocking(false) + +proc toAsyncSocket*(sock: Socket, state: SocketStatus = SockConnected): AsyncSocket = + ## Wraps an already initialized ``Socket`` into a AsyncSocket. + ## This is useful if you want to use an already connected Socket as an + ## asynchronous AsyncSocket in asyncio's event loop. + ## + ## ``state`` may be overriden, i.e. if ``sock`` is not connected it should be + ## adjusted properly. By default it will be assumed that the socket is + ## connected. Please note this is only applicable to TCP client sockets, if + ## ``sock`` is a different type of socket ``state`` needs to be adjusted!!! + ## + ## ================ ================================================================ + ## Value Meaning + ## ================ ================================================================ + ## SockIdle Socket has only just been initialised, not connected or closed. + ## SockConnected Socket is connected to a server. + ## SockConnecting Socket is in the process of connecting to a server. + ## SockListening Socket is a server socket and is listening for connections. + ## SockClosed Socket has been closed. + ## SockUDPBound Socket is a UDP socket which is listening for data. + ## ================ ================================================================ + ## + ## **Warning**: If ``state`` is set incorrectly the resulting ``AsyncSocket`` + ## object may not work properly. + ## + ## **Note**: This will set ``sock`` to be non-blocking. + result = newAsyncSocket() + result.socket = sock + result.proto = if state == SockUDPBound: IPPROTO_UDP else: IPPROTO_TCP + result.socket.setBlocking(false) + result.info = state + +proc asyncSockHandleRead(h: RootRef) = + when defined(ssl): + if AsyncSocket(h).socket.isSSL and not + AsyncSocket(h).socket.gotHandshake: + return + + if AsyncSocket(h).info != SockListening: + if AsyncSocket(h).info != SockConnecting: + AsyncSocket(h).handleRead(AsyncSocket(h)) + else: + AsyncSocket(h).handleAccept(AsyncSocket(h)) + +proc close*(sock: AsyncSocket) {.gcsafe.} +proc asyncSockHandleWrite(h: RootRef) = + when defined(ssl): + if AsyncSocket(h).socket.isSSL and not + AsyncSocket(h).socket.gotHandshake: + return + + if AsyncSocket(h).info == SockConnecting: + AsyncSocket(h).handleConnect(AsyncSocket(h)) + AsyncSocket(h).info = SockConnected + # Stop receiving write events if there is no handleWrite event. + if AsyncSocket(h).handleWrite == nil: + AsyncSocket(h).deleg.mode = fmRead + else: + AsyncSocket(h).deleg.mode = fmReadWrite + else: + if AsyncSocket(h).sendBuffer != "": + let sock = AsyncSocket(h) + try: + let bytesSent = sock.socket.sendAsync(sock.sendBuffer) + if bytesSent == 0: + # Apparently the socket cannot be written to. Even though select + # just told us that it can be... This used to be an assert. Just + # do nothing instead. + discard + elif bytesSent != sock.sendBuffer.len: + sock.sendBuffer = sock.sendBuffer[bytesSent .. ^1] + elif bytesSent == sock.sendBuffer.len: + sock.sendBuffer = "" + + if AsyncSocket(h).handleWrite != nil: + AsyncSocket(h).handleWrite(AsyncSocket(h)) + except OSError: + # Most likely the socket closed before the full buffer could be sent to it. + sock.close() # TODO: Provide a handleError for users? + else: + if AsyncSocket(h).handleWrite != nil: + AsyncSocket(h).handleWrite(AsyncSocket(h)) + else: + AsyncSocket(h).deleg.mode = fmRead + +when defined(ssl): + proc asyncSockDoHandshake(h: PObject) {.gcsafe.} = + if AsyncSocket(h).socket.isSSL and not + AsyncSocket(h).socket.gotHandshake: + if AsyncSocket(h).sslNeedAccept: + var d = "" + let ret = AsyncSocket(h).socket.acceptAddrSSL(AsyncSocket(h).socket, d) + assert ret != AcceptNoClient + if ret == AcceptSuccess: + AsyncSocket(h).info = SockConnected + else: + # handshake will set socket's ``sslNoHandshake`` field. + discard AsyncSocket(h).socket.handshake() + + +proc asyncSockTask(h: RootRef) = + when defined(ssl): + h.asyncSockDoHandshake() + + AsyncSocket(h).handleTask(AsyncSocket(h)) + +proc toDelegate(sock: AsyncSocket): Delegate = + result = newDelegate() + result.deleVal = sock + result.fd = getFD(sock.socket) + # We need this to get write events, just to know when the socket connects. + result.mode = fmReadWrite + result.handleRead = asyncSockHandleRead + result.handleWrite = asyncSockHandleWrite + result.task = asyncSockTask + # TODO: Errors? + #result.handleError = (proc (h: PObject) = assert(false)) + + result.hasDataBuffered = + proc (h: RootRef): bool {.nimcall.} = + return AsyncSocket(h).socket.hasDataBuffered() + + sock.deleg = result + if sock.info notin {SockIdle, SockClosed}: + sock.deleg.open = true + else: + sock.deleg.open = false + +proc connect*(sock: AsyncSocket, name: string, port = Port(0), + af: Domain = AF_INET) = + ## Begins connecting ``sock`` to ``name``:``port``. + sock.socket.connectAsync(name, port, af) + sock.info = SockConnecting + if sock.deleg != nil: + sock.deleg.open = true + +proc close*(sock: AsyncSocket) = + ## Closes ``sock``. Terminates any current connections. + sock.socket.close() + sock.info = SockClosed + if sock.deleg != nil: + sock.deleg.open = false + +proc bindAddr*(sock: AsyncSocket, port = Port(0), address = "") = + ## Equivalent to ``sockets.bindAddr``. + sock.socket.bindAddr(port, address) + if sock.proto == IPPROTO_UDP: + sock.info = SockUDPBound + if sock.deleg != nil: + sock.deleg.open = true + +proc listen*(sock: AsyncSocket) = + ## Equivalent to ``sockets.listen``. + sock.socket.listen() + sock.info = SockListening + if sock.deleg != nil: + sock.deleg.open = true + +proc acceptAddr*(server: AsyncSocket, client: var AsyncSocket, + address: var string) = + ## Equivalent to ``sockets.acceptAddr``. This procedure should be called in + ## a ``handleAccept`` event handler **only** once. + ## + ## **Note**: ``client`` needs to be initialised. + assert(client != nil) + client = newAsyncSocket() + var c: Socket + new(c) + when defined(ssl): + if server.socket.isSSL: + var ret = server.socket.acceptAddrSSL(c, address) + # The following shouldn't happen because when this function is called + # it is guaranteed that there is a client waiting. + # (This should be called in handleAccept) + assert(ret != AcceptNoClient) + if ret == AcceptNoHandshake: + client.sslNeedAccept = true + else: + client.sslNeedAccept = false + client.info = SockConnected + else: + server.socket.acceptAddr(c, address) + client.sslNeedAccept = false + client.info = SockConnected + else: + server.socket.acceptAddr(c, address) + client.sslNeedAccept = false + client.info = SockConnected + + if c == invalidSocket: raiseSocketError(server.socket) + c.setBlocking(false) # TODO: Needs to be tested. + + # deleg.open is set in ``toDelegate``. + + client.socket = c + client.lineBuffer = "".TaintedString + client.sendBuffer = "" + client.info = SockConnected + +proc accept*(server: AsyncSocket, client: var AsyncSocket) = + ## Equivalent to ``sockets.accept``. + var dummyAddr = "" + server.acceptAddr(client, dummyAddr) + +proc acceptAddr*(server: AsyncSocket): tuple[sock: AsyncSocket, + address: string] {.deprecated.} = + ## Equivalent to ``sockets.acceptAddr``. + ## + ## **Deprecated since version 0.9.0:** Please use the function above. + var client = newAsyncSocket() + var address: string = "" + acceptAddr(server, client, address) + return (client, address) + +proc accept*(server: AsyncSocket): AsyncSocket {.deprecated.} = + ## Equivalent to ``sockets.accept``. + ## + ## **Deprecated since version 0.9.0:** Please use the function above. + new(result) + var address = "" + server.acceptAddr(result, address) + +proc newDispatcher*(): Dispatcher = + new(result) + result.delegates = @[] + +proc register*(d: Dispatcher, deleg: Delegate) = + ## Registers delegate ``deleg`` with dispatcher ``d``. + d.delegates.add(deleg) + +proc register*(d: Dispatcher, sock: AsyncSocket): Delegate {.discardable.} = + ## Registers async socket ``sock`` with dispatcher ``d``. + result = sock.toDelegate() + d.register(result) + +proc unregister*(d: Dispatcher, deleg: Delegate) = + ## Unregisters deleg ``deleg`` from dispatcher ``d``. + for i in 0..len(d.delegates)-1: + if d.delegates[i] == deleg: + d.delegates.del(i) + return + raise newException(IndexError, "Could not find delegate.") + +proc isWriteable*(s: AsyncSocket): bool = + ## Determines whether socket ``s`` is ready to be written to. + var writeSock = @[s.socket] + return selectWrite(writeSock, 1) != 0 and s.socket notin writeSock + +converter getSocket*(s: AsyncSocket): Socket = + return s.socket + +proc isConnected*(s: AsyncSocket): bool = + ## Determines whether ``s`` is connected. + return s.info == SockConnected +proc isListening*(s: AsyncSocket): bool = + ## Determines whether ``s`` is listening for incoming connections. + return s.info == SockListening +proc isConnecting*(s: AsyncSocket): bool = + ## Determines whether ``s`` is connecting. + return s.info == SockConnecting +proc isClosed*(s: AsyncSocket): bool = + ## Determines whether ``s`` has been closed. + return s.info == SockClosed +proc isSendDataBuffered*(s: AsyncSocket): bool = + ## Determines whether ``s`` has data waiting to be sent, i.e. whether this + ## socket's sendBuffer contains data. + return s.sendBuffer.len != 0 + +proc setHandleWrite*(s: AsyncSocket, + handleWrite: proc (s: AsyncSocket) {.closure, gcsafe.}) = + ## Setter for the ``handleWrite`` event. + ## + ## To remove this event you should use the ``delHandleWrite`` function. + ## It is advised to use that function instead of just setting the event to + ## ``proc (s: AsyncSocket) = nil`` as that would mean that that function + ## would be called constantly. + s.deleg.mode = fmReadWrite + s.handleWrite = handleWrite + +proc delHandleWrite*(s: AsyncSocket) = + ## Removes the ``handleWrite`` event handler on ``s``. + s.handleWrite = nil + +{.push warning[deprecated]: off.} +proc recvLine*(s: AsyncSocket, line: var TaintedString): bool {.deprecated.} = + ## Behaves similar to ``sockets.recvLine``, however it handles non-blocking + ## sockets properly. This function guarantees that ``line`` is a full line, + ## if this function can only retrieve some data; it will save this data and + ## add it to the result when a full line is retrieved. + ## + ## Unlike ``sockets.recvLine`` this function will raise an EOS or ESSL + ## exception if an error occurs. + ## + ## **Deprecated since version 0.9.2**: This function has been deprecated in + ## favour of readLine. + setLen(line.string, 0) + var dataReceived = "".TaintedString + var ret = s.socket.recvLineAsync(dataReceived) + case ret + of RecvFullLine: + if s.lineBuffer.len > 0: + string(line).add(s.lineBuffer.string) + setLen(s.lineBuffer.string, 0) + string(line).add(dataReceived.string) + if string(line) == "": + line = "\c\L".TaintedString + result = true + of RecvPartialLine: + string(s.lineBuffer).add(dataReceived.string) + result = false + of RecvDisconnected: + result = true + of RecvFail: + s.raiseSocketError(async = true) + result = false +{.pop.} + +proc readLine*(s: AsyncSocket, line: var TaintedString): bool = + ## Behaves similar to ``sockets.readLine``, however it handles non-blocking + ## sockets properly. This function guarantees that ``line`` is a full line, + ## if this function can only retrieve some data; it will save this data and + ## add it to the result when a full line is retrieved, when this happens + ## False will be returned. True will only be returned if a full line has been + ## retrieved or the socket has been disconnected in which case ``line`` will + ## be set to "". + ## + ## This function will raise an EOS exception when a socket error occurs. + setLen(line.string, 0) + var dataReceived = "".TaintedString + var ret = s.socket.readLineAsync(dataReceived) + case ret + of ReadFullLine: + if s.lineBuffer.len > 0: + string(line).add(s.lineBuffer.string) + setLen(s.lineBuffer.string, 0) + string(line).add(dataReceived.string) + if string(line) == "": + line = "\c\L".TaintedString + result = true + of ReadPartialLine: + string(s.lineBuffer).add(dataReceived.string) + result = false + of ReadNone: + result = false + of ReadDisconnected: + result = true + +proc send*(sock: AsyncSocket, data: string) = + ## Sends ``data`` to socket ``sock``. This is basically a nicer implementation + ## of ``sockets.sendAsync``. + ## + ## If ``data`` cannot be sent immediately it will be buffered and sent + ## when ``sock`` becomes writeable (during the ``handleWrite`` event). + ## It's possible that only a part of ``data`` will be sent immediately, while + ## the rest of it will be buffered and sent later. + if sock.sendBuffer.len != 0: + sock.sendBuffer.add(data) + return + let bytesSent = sock.socket.sendAsync(data) + assert bytesSent >= 0 + if bytesSent == 0: + sock.sendBuffer.add(data) + sock.deleg.mode = fmReadWrite + elif bytesSent != data.len: + sock.sendBuffer.add(data[bytesSent .. ^1]) + sock.deleg.mode = fmReadWrite + +proc timeValFromMilliseconds(timeout = 500): Timeval = + if timeout != -1: + var seconds = timeout div 1000 + result.tv_sec = seconds.int32 + result.tv_usec = ((timeout - seconds * 1000) * 1000).int32 + +proc createFdSet(fd: var TFdSet, s: seq[Delegate], m: var int) = + FD_ZERO(fd) + for i in items(s): + m = max(m, int(i.fd)) + FD_SET(i.fd, fd) + +proc pruneSocketSet(s: var seq[Delegate], fd: var TFdSet) = + var i = 0 + var L = s.len + while i < L: + if FD_ISSET(s[i].fd, fd) != 0'i32: + s[i] = s[L-1] + dec(L) + else: + inc(i) + setLen(s, L) + +proc select(readfds, writefds, exceptfds: var seq[Delegate], + timeout = 500): int = + var tv {.noInit.}: Timeval = timeValFromMilliseconds(timeout) + + var rd, wr, ex: TFdSet + var m = 0 + createFdSet(rd, readfds, m) + createFdSet(wr, writefds, m) + createFdSet(ex, exceptfds, m) + + if timeout != -1: + result = int(select(cint(m+1), addr(rd), addr(wr), addr(ex), addr(tv))) + else: + result = int(select(cint(m+1), addr(rd), addr(wr), addr(ex), nil)) + + pruneSocketSet(readfds, (rd)) + pruneSocketSet(writefds, (wr)) + pruneSocketSet(exceptfds, (ex)) + +proc poll*(d: Dispatcher, timeout: int = 500): bool = + ## This function checks for events on all the delegates in the `PDispatcher`. + ## It then proceeds to call the correct event handler. + ## + ## This function returns ``True`` if there are file descriptors that are still + ## open, otherwise ``False``. File descriptors that have been + ## closed are immediately removed from the dispatcher automatically. + ## + ## **Note:** Each delegate has a task associated with it. This gets called + ## after each select() call, if you set timeout to ``-1`` the tasks will + ## only be executed after one or more file descriptors becomes readable or + ## writeable. + result = true + var readDg, writeDg, errorDg: seq[Delegate] = @[] + var len = d.delegates.len + var dc = 0 + + while dc < len: + let deleg = d.delegates[dc] + if (deleg.mode != fmWrite or deleg.mode != fmAppend) and deleg.open: + readDg.add(deleg) + if (deleg.mode != fmRead) and deleg.open: + writeDg.add(deleg) + if deleg.open: + errorDg.add(deleg) + inc dc + else: + # File/socket has been closed. Remove it from dispatcher. + d.delegates[dc] = d.delegates[len-1] + dec len + + d.delegates.setLen(len) + + var hasDataBufferedCount = 0 + for d in d.delegates: + if d.hasDataBuffered(d.deleVal): + hasDataBufferedCount.inc() + d.handleRead(d.deleVal) + if hasDataBufferedCount > 0: return true + + if readDg.len() == 0 and writeDg.len() == 0: + ## TODO: Perhaps this shouldn't return if errorDg has something? + return false + + if select(readDg, writeDg, errorDg, timeout) != 0: + for i in 0..len(d.delegates)-1: + if i > len(d.delegates)-1: break # One delegate might've been removed. + let deleg = d.delegates[i] + if not deleg.open: continue # This delegate might've been closed. + if (deleg.mode != fmWrite or deleg.mode != fmAppend) and + deleg notin readDg: + deleg.handleRead(deleg.deleVal) + if (deleg.mode != fmRead) and deleg notin writeDg: + deleg.handleWrite(deleg.deleVal) + if deleg notin errorDg: + deleg.handleError(deleg.deleVal) + + # Execute tasks + for i in items(d.delegates): + i.task(i.deleVal) + +proc len*(disp: Dispatcher): int = + ## Retrieves the amount of delegates in ``disp``. + return disp.delegates.len + +when not defined(testing) and isMainModule: + + proc testConnect(s: AsyncSocket, no: int) = + echo("Connected! " & $no) + + proc testRead(s: AsyncSocket, no: int) = + echo("Reading! " & $no) + var data = "" + if not s.readLine(data): return + if data == "": + echo("Closing connection. " & $no) + s.close() + echo(data) + echo("Finished reading! " & $no) + + proc testAccept(s: AsyncSocket, disp: Dispatcher, no: int) = + echo("Accepting client! " & $no) + var client: AsyncSocket + new(client) + var address = "" + s.acceptAddr(client, address) + echo("Accepted ", address) + client.handleRead = + proc (s: AsyncSocket) = + testRead(s, 2) + disp.register(client) + + proc main = + var d = newDispatcher() + + var s = asyncSocket() + s.connect("amber.tenthbit.net", Port(6667)) + s.handleConnect = + proc (s: AsyncSocket) = + testConnect(s, 1) + s.handleRead = + proc (s: AsyncSocket) = + testRead(s, 1) + d.register(s) + + var server = asyncSocket() + server.handleAccept = + proc (s: AsyncSocket) = + testAccept(s, d, 78) + server.bindAddr(Port(5555)) + server.listen() + d.register(server) + + while d.poll(-1): discard + main() diff --git a/lib/deprecated/pure/ftpclient.nim b/lib/deprecated/pure/ftpclient.nim new file mode 100644 index 000000000..229fe4b51 --- /dev/null +++ b/lib/deprecated/pure/ftpclient.nim @@ -0,0 +1,675 @@ +# +# +# Nim's Runtime Library +# (c) Copyright 2015 Dominik Picheta +# See the file "copying.txt", included in this +# distribution, for details about the copyright. +# + +include "system/inclrtl" + +import sockets, strutils, parseutils, times, os, asyncio + +from asyncnet import nil +from rawsockets import nil +from asyncdispatch import PFuture +## **Note**: This module is deprecated since version 0.11.3. +## You should use the async version of this module +## `asyncftpclient <asyncftpclient.html>`_. +## +## ---- +## +## This module **partially** implements an FTP client as specified +## by `RFC 959 <http://tools.ietf.org/html/rfc959>`_. +## +## This module provides both a synchronous and asynchronous implementation. +## The asynchronous implementation requires you to use the ``asyncFTPClient`` +## function. You are then required to register the ``AsyncFTPClient`` with a +## asyncio dispatcher using the ``register`` function. Take a look at the +## asyncio module documentation for more information. +## +## **Note**: The asynchronous implementation is only asynchronous for long +## file transfers, calls to functions which use the command socket will block. +## +## Here is some example usage of this module: +## +## .. code-block:: Nim +## var ftp = ftpClient("example.org", user = "user", pass = "pass") +## ftp.connect() +## ftp.retrFile("file.ext", "file.ext") +## +## **Warning:** The API of this module is unstable, and therefore is subject +## to change. + +{.deprecated.} + +type + FtpBase*[SockType] = ref FtpBaseObj[SockType] + FtpBaseObj*[SockType] = object + csock*: SockType + dsock*: SockType + when SockType is asyncio.AsyncSocket: + handleEvent*: proc (ftp: AsyncFTPClient, ev: FTPEvent){.closure,gcsafe.} + disp: Dispatcher + asyncDSockID: Delegate + user*, pass*: string + address*: string + when SockType is asyncnet.AsyncSocket: + port*: rawsockets.Port + else: + port*: Port + + jobInProgress*: bool + job*: FTPJob[SockType] + + dsockConnected*: bool + + FTPJobType* = enum + JRetrText, JRetr, JStore + + FtpJob[T] = ref FtpJobObj[T] + FTPJobObj[T] = object + prc: proc (ftp: FTPBase[T], async: bool): bool {.nimcall, gcsafe.} + case typ*: FTPJobType + of JRetrText: + lines: string + of JRetr, JStore: + file: File + filename: string + total: BiggestInt # In bytes. + progress: BiggestInt # In bytes. + oneSecond: BiggestInt # Bytes transferred in one second. + lastProgressReport: float # Time + toStore: string # Data left to upload (Only used with async) + else: nil + + FtpClientObj* = FtpBaseObj[Socket] + FtpClient* = ref FtpClientObj + + AsyncFtpClient* = ref AsyncFtpClientObj ## Async alternative to TFTPClient. + AsyncFtpClientObj* = FtpBaseObj[asyncio.AsyncSocket] + + FTPEventType* = enum + EvTransferProgress, EvLines, EvRetr, EvStore + + FTPEvent* = object ## Event + filename*: string + case typ*: FTPEventType + of EvLines: + lines*: string ## Lines that have been transferred. + of EvRetr, EvStore: ## Retr/Store operation finished. + nil + of EvTransferProgress: + bytesTotal*: BiggestInt ## Bytes total. + bytesFinished*: BiggestInt ## Bytes transferred. + speed*: BiggestInt ## Speed in bytes/s + currentJob*: FTPJobType ## The current job being performed. + + ReplyError* = object of IOError + FTPError* = object of IOError + +{.deprecated: [ + TFTPClient: FTPClientObj, TFTPJob: FTPJob, PAsyncFTPClient: AsyncFTPClient, + TAsyncFTPClient: AsyncFTPClientObj, TFTPEvent: FTPEvent, + EInvalidReply: ReplyError, EFTP: FTPError +].} + +const multiLineLimit = 10000 + +proc ftpClient*(address: string, port = Port(21), + user, pass = ""): FtpClient = + ## Create a ``FtpClient`` object. + new(result) + result.user = user + result.pass = pass + result.address = address + result.port = port + + result.dsockConnected = false + result.csock = socket() + if result.csock == invalidSocket: raiseOSError(osLastError()) + +template blockingOperation(sock: Socket, body: stmt) {.immediate.} = + body + +template blockingOperation(sock: asyncio.AsyncSocket, body: stmt) {.immediate.} = + sock.setBlocking(true) + body + sock.setBlocking(false) + +proc expectReply[T](ftp: FtpBase[T]): TaintedString = + result = TaintedString"" + blockingOperation(ftp.csock): + when T is Socket: + ftp.csock.readLine(result) + else: + discard ftp.csock.readLine(result) + var count = 0 + while result[3] == '-': + ## Multi-line reply. + var line = TaintedString"" + when T is Socket: + ftp.csock.readLine(line) + else: + discard ftp.csock.readLine(line) + result.add("\n" & line) + count.inc() + if count >= multiLineLimit: + raise newException(ReplyError, "Reached maximum multi-line reply count.") + +proc send*[T](ftp: FtpBase[T], m: string): TaintedString = + ## Send a message to the server, and wait for a primary reply. + ## ``\c\L`` is added for you. + ## + ## **Note:** The server may return multiple lines of coded replies. + blockingOperation(ftp.csock): + ftp.csock.send(m & "\c\L") + return ftp.expectReply() + +proc assertReply(received: TaintedString, expected: string) = + if not received.string.startsWith(expected): + raise newException(ReplyError, + "Expected reply '$1' got: $2" % [ + expected, received.string]) + +proc assertReply(received: TaintedString, expected: varargs[string]) = + for i in items(expected): + if received.string.startsWith(i): return + raise newException(ReplyError, + "Expected reply '$1' got: $2" % + [expected.join("' or '"), received.string]) + +proc createJob[T](ftp: FtpBase[T], + prc: proc (ftp: FtpBase[T], async: bool): bool {. + nimcall,gcsafe.}, + cmd: FTPJobType) = + if ftp.jobInProgress: + raise newException(FTPError, "Unable to do two jobs at once.") + ftp.jobInProgress = true + new(ftp.job) + ftp.job.prc = prc + ftp.job.typ = cmd + case cmd + of JRetrText: + ftp.job.lines = "" + of JRetr, JStore: + ftp.job.toStore = "" + +proc deleteJob[T](ftp: FtpBase[T]) = + assert ftp.jobInProgress + ftp.jobInProgress = false + case ftp.job.typ + of JRetrText: + ftp.job.lines = "" + of JRetr, JStore: + ftp.job.file.close() + ftp.dsock.close() + +proc handleTask(s: AsyncSocket, ftp: AsyncFTPClient) = + if ftp.jobInProgress: + if ftp.job.typ in {JRetr, JStore}: + if epochTime() - ftp.job.lastProgressReport >= 1.0: + var r: FTPEvent + ftp.job.lastProgressReport = epochTime() + r.typ = EvTransferProgress + r.bytesTotal = ftp.job.total + r.bytesFinished = ftp.job.progress + r.speed = ftp.job.oneSecond + r.filename = ftp.job.filename + r.currentJob = ftp.job.typ + ftp.job.oneSecond = 0 + ftp.handleEvent(ftp, r) + +proc handleWrite(s: AsyncSocket, ftp: AsyncFTPClient) = + if ftp.jobInProgress: + if ftp.job.typ == JStore: + assert (not ftp.job.prc(ftp, true)) + +proc handleConnect(s: AsyncSocket, ftp: AsyncFTPClient) = + ftp.dsockConnected = true + assert(ftp.jobInProgress) + if ftp.job.typ == JStore: + s.setHandleWrite(proc (s: AsyncSocket) = handleWrite(s, ftp)) + else: + s.delHandleWrite() + +proc handleRead(s: AsyncSocket, ftp: AsyncFTPClient) = + assert ftp.jobInProgress + assert ftp.job.typ != JStore + # This can never return true, because it shouldn't check for code + # 226 from csock. + assert(not ftp.job.prc(ftp, true)) + +proc pasv[T](ftp: FtpBase[T]) = + ## Negotiate a data connection. + when T is Socket: + ftp.dsock = socket() + if ftp.dsock == invalidSocket: raiseOSError(osLastError()) + elif T is AsyncSocket: + ftp.dsock = asyncSocket() + ftp.dsock.handleRead = + proc (s: AsyncSocket) = + handleRead(s, ftp) + ftp.dsock.handleConnect = + proc (s: AsyncSocket) = + handleConnect(s, ftp) + ftp.dsock.handleTask = + proc (s: AsyncSocket) = + handleTask(s, ftp) + ftp.disp.register(ftp.dsock) + else: + {.fatal: "Incorrect socket instantiation".} + + var pasvMsg = ftp.send("PASV").string.strip.TaintedString + assertReply(pasvMsg, "227") + var betweenParens = captureBetween(pasvMsg.string, '(', ')') + var nums = betweenParens.split(',') + var ip = nums[0.. ^3] + var port = nums[^2.. ^1] + var properPort = port[0].parseInt()*256+port[1].parseInt() + ftp.dsock.connect(ip.join("."), Port(properPort.toU16)) + when T is AsyncSocket: + ftp.dsockConnected = false + else: + ftp.dsockConnected = true + +proc normalizePathSep(path: string): string = + return replace(path, '\\', '/') + +proc connect*[T](ftp: FtpBase[T]) = + ## Connect to the FTP server specified by ``ftp``. + when T is AsyncSocket: + blockingOperation(ftp.csock): + ftp.csock.connect(ftp.address, ftp.port) + elif T is Socket: + ftp.csock.connect(ftp.address, ftp.port) + else: + {.fatal: "Incorrect socket instantiation".} + + var reply = ftp.expectReply() + if reply.startsWith("120"): + # 120 Service ready in nnn minutes. + # We wait until we receive 220. + reply = ftp.expectReply() + + # Handle 220 messages from the server + assertReply ftp.expectReply(), "220" + + if ftp.user != "": + assertReply(ftp.send("USER " & ftp.user), "230", "331") + + if ftp.pass != "": + assertReply ftp.send("PASS " & ftp.pass), "230" + +proc pwd*[T](ftp: FtpBase[T]): string = + ## Returns the current working directory. + var wd = ftp.send("PWD") + assertReply wd, "257" + return wd.string.captureBetween('"') # " + +proc cd*[T](ftp: FtpBase[T], dir: string) = + ## Changes the current directory on the remote FTP server to ``dir``. + assertReply ftp.send("CWD " & dir.normalizePathSep), "250" + +proc cdup*[T](ftp: FtpBase[T]) = + ## Changes the current directory to the parent of the current directory. + assertReply ftp.send("CDUP"), "200" + +proc getLines[T](ftp: FtpBase[T], async: bool = false): bool = + ## Downloads text data in ASCII mode + ## Returns true if the download is complete. + ## It doesn't if `async` is true, because it doesn't check for 226 then. + if ftp.dsockConnected: + var r = TaintedString"" + when T is AsyncSocket: + if ftp.asyncDSock.readLine(r): + if r.string == "": + ftp.dsockConnected = false + else: + ftp.job.lines.add(r.string & "\n") + elif T is Socket: + assert(not async) + ftp.dsock.readLine(r) + if r.string == "": + ftp.dsockConnected = false + else: + ftp.job.lines.add(r.string & "\n") + else: + {.fatal: "Incorrect socket instantiation".} + + if not async: + var readSocks: seq[Socket] = @[ftp.csock] + # This is only needed here. Asyncio gets this socket... + blockingOperation(ftp.csock): + if readSocks.select(1) != 0 and ftp.csock in readSocks: + assertReply ftp.expectReply(), "226" + return true + +proc listDirs*[T](ftp: FtpBase[T], dir: string = "", + async = false): seq[string] = + ## Returns a list of filenames in the given directory. If ``dir`` is "", + ## the current directory is used. If ``async`` is true, this + ## function will return immediately and it will be your job to + ## use asyncio's ``poll`` to progress this operation. + + ftp.createJob(getLines[T], JRetrText) + ftp.pasv() + + assertReply ftp.send("NLST " & dir.normalizePathSep), ["125", "150"] + + if not async: + while not ftp.job.prc(ftp, false): discard + result = splitLines(ftp.job.lines) + ftp.deleteJob() + else: return @[] + +proc fileExists*(ftp: FtpClient, file: string): bool {.deprecated.} = + ## **Deprecated since version 0.9.0:** Please use ``existsFile``. + ## + ## Determines whether ``file`` exists. + ## + ## Warning: This function may block. Especially on directories with many + ## files, because a full list of file names must be retrieved. + var files = ftp.listDirs() + for f in items(files): + if f.normalizePathSep == file.normalizePathSep: return true + +proc existsFile*(ftp: FtpClient, file: string): bool = + ## Determines whether ``file`` exists. + ## + ## Warning: This function may block. Especially on directories with many + ## files, because a full list of file names must be retrieved. + var files = ftp.listDirs() + for f in items(files): + if f.normalizePathSep == file.normalizePathSep: return true + +proc createDir*[T](ftp: FtpBase[T], dir: string, recursive: bool = false) = + ## Creates a directory ``dir``. If ``recursive`` is true, the topmost + ## subdirectory of ``dir`` will be created first, following the secondmost... + ## etc. this allows you to give a full path as the ``dir`` without worrying + ## about subdirectories not existing. + if not recursive: + assertReply ftp.send("MKD " & dir.normalizePathSep), "257" + else: + var reply = TaintedString"" + var previousDirs = "" + for p in split(dir, {os.DirSep, os.AltSep}): + if p != "": + previousDirs.add(p) + reply = ftp.send("MKD " & previousDirs) + previousDirs.add('/') + assertReply reply, "257" + +proc chmod*[T](ftp: FtpBase[T], path: string, + permissions: set[FilePermission]) = + ## Changes permission of ``path`` to ``permissions``. + var userOctal = 0 + var groupOctal = 0 + var otherOctal = 0 + for i in items(permissions): + case i + of fpUserExec: userOctal.inc(1) + of fpUserWrite: userOctal.inc(2) + of fpUserRead: userOctal.inc(4) + of fpGroupExec: groupOctal.inc(1) + of fpGroupWrite: groupOctal.inc(2) + of fpGroupRead: groupOctal.inc(4) + of fpOthersExec: otherOctal.inc(1) + of fpOthersWrite: otherOctal.inc(2) + of fpOthersRead: otherOctal.inc(4) + + var perm = $userOctal & $groupOctal & $otherOctal + assertReply ftp.send("SITE CHMOD " & perm & + " " & path.normalizePathSep), "200" + +proc list*[T](ftp: FtpBase[T], dir: string = "", async = false): string = + ## Lists all files in ``dir``. If ``dir`` is ``""``, uses the current + ## working directory. If ``async`` is true, this function will return + ## immediately and it will be your job to call asyncio's + ## ``poll`` to progress this operation. + ftp.createJob(getLines[T], JRetrText) + ftp.pasv() + + assertReply(ftp.send("LIST" & " " & dir.normalizePathSep), ["125", "150"]) + + if not async: + while not ftp.job.prc(ftp, false): discard + result = ftp.job.lines + ftp.deleteJob() + else: + return "" + +proc retrText*[T](ftp: FtpBase[T], file: string, async = false): string = + ## Retrieves ``file``. File must be ASCII text. + ## If ``async`` is true, this function will return immediately and + ## it will be your job to call asyncio's ``poll`` to progress this operation. + ftp.createJob(getLines[T], JRetrText) + ftp.pasv() + assertReply ftp.send("RETR " & file.normalizePathSep), ["125", "150"] + + if not async: + while not ftp.job.prc(ftp, false): discard + result = ftp.job.lines + ftp.deleteJob() + else: + return "" + +proc getFile[T](ftp: FtpBase[T], async = false): bool = + if ftp.dsockConnected: + var r = "".TaintedString + var bytesRead = 0 + var returned = false + if async: + when T is Socket: + raise newException(FTPError, "FTPClient must be async.") + else: + bytesRead = ftp.dsock.recvAsync(r, BufferSize) + returned = bytesRead != -1 + else: + bytesRead = ftp.dsock.recv(r, BufferSize) + returned = true + let r2 = r.string + if r2 != "": + ftp.job.progress.inc(r2.len) + ftp.job.oneSecond.inc(r2.len) + ftp.job.file.write(r2) + elif returned and r2 == "": + ftp.dsockConnected = false + + when T is Socket: + if not async: + var readSocks: seq[Socket] = @[ftp.csock] + blockingOperation(ftp.csock): + if readSocks.select(1) != 0 and ftp.csock in readSocks: + assertReply ftp.expectReply(), "226" + return true + +proc retrFile*[T](ftp: FtpBase[T], file, dest: string, async = false) = + ## Downloads ``file`` and saves it to ``dest``. Usage of this function + ## asynchronously is recommended to view the progress of the download. + ## The ``EvRetr`` event is passed to the specified ``handleEvent`` function + ## when the download is finished, and the ``filename`` field will be equal + ## to ``file``. + ftp.createJob(getFile[T], JRetr) + ftp.job.file = open(dest, mode = fmWrite) + ftp.pasv() + var reply = ftp.send("RETR " & file.normalizePathSep) + assertReply reply, ["125", "150"] + if {'(', ')'} notin reply.string: + raise newException(ReplyError, "Reply has no file size.") + var fileSize: BiggestInt + if reply.string.captureBetween('(', ')').parseBiggestInt(fileSize) == 0: + raise newException(ReplyError, "Reply has no file size.") + + ftp.job.total = fileSize + ftp.job.lastProgressReport = epochTime() + ftp.job.filename = file.normalizePathSep + + if not async: + while not ftp.job.prc(ftp, false): discard + ftp.deleteJob() + +proc doUpload[T](ftp: FtpBase[T], async = false): bool = + if ftp.dsockConnected: + if ftp.job.toStore.len() > 0: + assert(async) + let bytesSent = ftp.dsock.sendAsync(ftp.job.toStore) + if bytesSent == ftp.job.toStore.len: + ftp.job.toStore = "" + elif bytesSent != ftp.job.toStore.len and bytesSent != 0: + ftp.job.toStore = ftp.job.toStore[bytesSent .. ^1] + ftp.job.progress.inc(bytesSent) + ftp.job.oneSecond.inc(bytesSent) + else: + var s = newStringOfCap(4000) + var len = ftp.job.file.readBuffer(addr(s[0]), 4000) + setLen(s, len) + if len == 0: + # File finished uploading. + ftp.dsock.close() + ftp.dsockConnected = false + + if not async: + assertReply ftp.expectReply(), "226" + return true + return false + + if not async: + ftp.dsock.send(s) + else: + let bytesSent = ftp.dsock.sendAsync(s) + if bytesSent == 0: + ftp.job.toStore.add(s) + elif bytesSent != s.len: + ftp.job.toStore.add(s[bytesSent .. ^1]) + len = bytesSent + + ftp.job.progress.inc(len) + ftp.job.oneSecond.inc(len) + +proc store*[T](ftp: FtpBase[T], file, dest: string, async = false) = + ## Uploads ``file`` to ``dest`` on the remote FTP server. Usage of this + ## function asynchronously is recommended to view the progress of + ## the download. + ## The ``EvStore`` event is passed to the specified ``handleEvent`` function + ## when the upload is finished, and the ``filename`` field will be + ## equal to ``file``. + ftp.createJob(doUpload[T], JStore) + ftp.job.file = open(file) + ftp.job.total = ftp.job.file.getFileSize() + ftp.job.lastProgressReport = epochTime() + ftp.job.filename = file + ftp.pasv() + + assertReply ftp.send("STOR " & dest.normalizePathSep), ["125", "150"] + + if not async: + while not ftp.job.prc(ftp, false): discard + ftp.deleteJob() + +proc close*[T](ftp: FtpBase[T]) = + ## Terminates the connection to the server. + assertReply ftp.send("QUIT"), "221" + if ftp.jobInProgress: ftp.deleteJob() + ftp.csock.close() + ftp.dsock.close() + +proc csockHandleRead(s: AsyncSocket, ftp: AsyncFTPClient) = + if ftp.jobInProgress: + assertReply ftp.expectReply(), "226" # Make sure the transfer completed. + var r: FTPEvent + case ftp.job.typ + of JRetrText: + r.typ = EvLines + r.lines = ftp.job.lines + of JRetr: + r.typ = EvRetr + r.filename = ftp.job.filename + if ftp.job.progress != ftp.job.total: + raise newException(FTPError, "Didn't download full file.") + of JStore: + r.typ = EvStore + r.filename = ftp.job.filename + if ftp.job.progress != ftp.job.total: + raise newException(FTPError, "Didn't upload full file.") + ftp.deleteJob() + + ftp.handleEvent(ftp, r) + +proc asyncFTPClient*(address: string, port = Port(21), + user, pass = "", + handleEvent: proc (ftp: AsyncFTPClient, ev: FTPEvent) {.closure,gcsafe.} = + (proc (ftp: AsyncFTPClient, ev: FTPEvent) = discard)): AsyncFTPClient = + ## Create a ``AsyncFTPClient`` object. + ## + ## Use this if you want to use asyncio's dispatcher. + var dres: AsyncFtpClient + new(dres) + dres.user = user + dres.pass = pass + dres.address = address + dres.port = port + dres.dsockConnected = false + dres.handleEvent = handleEvent + dres.csock = asyncSocket() + dres.csock.handleRead = + proc (s: AsyncSocket) = + csockHandleRead(s, dres) + result = dres + +proc register*(d: Dispatcher, ftp: AsyncFTPClient): Delegate {.discardable.} = + ## Registers ``ftp`` with dispatcher ``d``. + ftp.disp = d + return ftp.disp.register(ftp.csock) + +when not defined(testing) and isMainModule: + proc main = + var d = newDispatcher() + let hev = + proc (ftp: AsyncFTPClient, event: FTPEvent) = + case event.typ + of EvStore: + echo("Upload finished!") + ftp.retrFile("payload.jpg", "payload2.jpg", async = true) + of EvTransferProgress: + var time: int64 = -1 + if event.speed != 0: + time = (event.bytesTotal - event.bytesFinished) div event.speed + echo(event.currentJob) + echo(event.speed div 1000, " kb/s. - ", + event.bytesFinished, "/", event.bytesTotal, + " - ", time, " seconds") + echo(d.len) + of EvRetr: + echo("Download finished!") + ftp.close() + echo d.len + else: assert(false) + var ftp = asyncFTPClient("example.com", user = "foo", pass = "bar", handleEvent = hev) + + d.register(ftp) + d.len.echo() + ftp.connect() + echo "connected" + ftp.store("payload.jpg", "payload.jpg", async = true) + d.len.echo() + echo "uploading..." + while true: + if not d.poll(): break + main() + +when not defined(testing) and isMainModule: + var ftp = ftpClient("example.com", user = "foo", pass = "bar") + ftp.connect() + echo ftp.pwd() + echo ftp.list() + echo("uploading") + ftp.store("payload.jpg", "payload.jpg", async = false) + + echo("Upload complete") + ftp.retrFile("payload.jpg", "payload2.jpg", async = false) + + echo("Download complete") + sleep(5000) + ftp.close() + sleep(200) diff --git a/lib/deprecated/pure/parseopt.nim b/lib/deprecated/pure/parseopt.nim new file mode 100644 index 000000000..218f5ab81 --- /dev/null +++ b/lib/deprecated/pure/parseopt.nim @@ -0,0 +1,178 @@ +# +# +# Nim's Runtime Library +# (c) Copyright 2015 Andreas Rumpf +# +# See the file "copying.txt", included in this +# distribution, for details about the copyright. +# + +## This module provides the standard Nim command line parser. +## It supports one convenience iterator over all command line options and some +## lower-level features. +## +## Supported syntax: +## +## 1. short options - ``-abcd``, where a, b, c, d are names +## 2. long option - ``--foo:bar``, ``--foo=bar`` or ``--foo`` +## 3. argument - everything else + +{.push debugger: off.} + +include "system/inclrtl" + +import + os, strutils + +type + CmdLineKind* = enum ## the detected command line token + cmdEnd, ## end of command line reached + cmdArgument, ## argument detected + cmdLongOption, ## a long option ``--option`` detected + cmdShortOption ## a short option ``-c`` detected + OptParser* = + object of RootObj ## this object implements the command line parser + cmd: string + pos: int + inShortState: bool + kind*: CmdLineKind ## the dected command line token + key*, val*: TaintedString ## key and value pair; ``key`` is the option + ## or the argument, ``value`` is not "" if + ## the option was given a value + +{.deprecated: [TCmdLineKind: CmdLineKind, TOptParser: OptParser].} + +proc parseWord(s: string, i: int, w: var string, + delim: set[char] = {'\x09', ' ', '\0'}): int = + result = i + if s[result] == '\"': + inc(result) + while not (s[result] in {'\0', '\"'}): + add(w, s[result]) + inc(result) + if s[result] == '\"': inc(result) + else: + while not (s[result] in delim): + add(w, s[result]) + inc(result) + +when declared(os.paramCount): + proc quote(s: string): string = + if find(s, {' ', '\t'}) >= 0 and s[0] != '"': + if s[0] == '-': + result = newStringOfCap(s.len) + var i = parseWord(s, 0, result, {'\0', ' ', '\x09', ':', '='}) + if s[i] in {':','='}: + result.add s[i] + inc i + result.add '"' + while i < s.len: + result.add s[i] + inc i + result.add '"' + else: + result = '"' & s & '"' + else: + result = s + + # we cannot provide this for NimRtl creation on Posix, because we can't + # access the command line arguments then! + + proc initOptParser*(cmdline = ""): OptParser = + ## inits the option parser. If ``cmdline == ""``, the real command line + ## (as provided by the ``OS`` module) is taken. + result.pos = 0 + result.inShortState = false + if cmdline != "": + result.cmd = cmdline + else: + result.cmd = "" + for i in countup(1, paramCount()): + result.cmd.add quote(paramStr(i).string) + result.cmd.add ' ' + result.kind = cmdEnd + result.key = TaintedString"" + result.val = TaintedString"" + +proc handleShortOption(p: var OptParser) = + var i = p.pos + p.kind = cmdShortOption + add(p.key.string, p.cmd[i]) + inc(i) + p.inShortState = true + while p.cmd[i] in {'\x09', ' '}: + inc(i) + p.inShortState = false + if p.cmd[i] in {':', '='}: + inc(i) + p.inShortState = false + while p.cmd[i] in {'\x09', ' '}: inc(i) + i = parseWord(p.cmd, i, p.val.string) + if p.cmd[i] == '\0': p.inShortState = false + p.pos = i + +proc next*(p: var OptParser) {.rtl, extern: "npo$1".} = + ## parses the first or next option; ``p.kind`` describes what token has been + ## parsed. ``p.key`` and ``p.val`` are set accordingly. + var i = p.pos + while p.cmd[i] in {'\x09', ' '}: inc(i) + p.pos = i + setLen(p.key.string, 0) + setLen(p.val.string, 0) + if p.inShortState: + handleShortOption(p) + return + case p.cmd[i] + of '\0': + p.kind = cmdEnd + of '-': + inc(i) + if p.cmd[i] == '-': + p.kind = cmdLongoption + inc(i) + i = parseWord(p.cmd, i, p.key.string, {'\0', ' ', '\x09', ':', '='}) + while p.cmd[i] in {'\x09', ' '}: inc(i) + if p.cmd[i] in {':', '='}: + inc(i) + while p.cmd[i] in {'\x09', ' '}: inc(i) + p.pos = parseWord(p.cmd, i, p.val.string) + else: + p.pos = i + else: + p.pos = i + handleShortOption(p) + else: + p.kind = cmdArgument + p.pos = parseWord(p.cmd, i, p.key.string) + +proc cmdLineRest*(p: OptParser): TaintedString {.rtl, extern: "npo$1".} = + ## retrieves the rest of the command line that has not been parsed yet. + result = strip(substr(p.cmd, p.pos, len(p.cmd) - 1)).TaintedString + +when declared(initOptParser): + iterator getopt*(): tuple[kind: CmdLineKind, key, val: TaintedString] = + ## This is an convenience iterator for iterating over the command line. + ## This uses the OptParser object. Example: + ## + ## .. code-block:: nim + ## var + ## filename = "" + ## for kind, key, val in getopt(): + ## case kind + ## of cmdArgument: + ## filename = key + ## of cmdLongOption, cmdShortOption: + ## case key + ## of "help", "h": writeHelp() + ## of "version", "v": writeVersion() + ## of cmdEnd: assert(false) # cannot happen + ## if filename == "": + ## # no filename has been given, so we show the help: + ## writeHelp() + var p = initOptParser() + while true: + next(p) + if p.kind == cmdEnd: break + yield (p.kind, p.key, p.val) + +{.pop.} diff --git a/lib/deprecated/pure/parseurl.nim b/lib/deprecated/pure/parseurl.nim new file mode 100644 index 000000000..6d58e8a73 --- /dev/null +++ b/lib/deprecated/pure/parseurl.nim @@ -0,0 +1,114 @@ +# +# +# Nim's Runtime Library +# (c) Copyright 2015 Dominik Picheta +# +# See the file "copying.txt", included in this +# distribution, for details about the copyright. +# + +## **Warnings:** This module is deprecated since version 0.10.2. +## Use the `uri <uri.html>`_ module instead. +## +## Parses & constructs URLs. + +{.deprecated.} + +import strutils + +type + Url* = tuple[ ## represents a *Uniform Resource Locator* (URL) + ## any optional component is "" if it does not exist + scheme, username, password, + hostname, port, path, query, anchor: string] + +{.deprecated: [TUrl: Url].} + +proc parseUrl*(url: string): Url {.deprecated.} = + var i = 0 + + var scheme, username, password: string = "" + var hostname, port, path, query, anchor: string = "" + + var temp = "" + + if url[i] != '/': # url isn't a relative path + while true: + # Scheme + if url[i] == ':': + if url[i+1] == '/' and url[i+2] == '/': + scheme = temp + temp.setLen(0) + inc(i, 3) # Skip the // + # Authority(username, password) + if url[i] == '@': + username = temp + let colon = username.find(':') + if colon >= 0: + password = username.substr(colon+1) + username = username.substr(0, colon-1) + temp.setLen(0) + inc(i) #Skip the @ + # hostname(subdomain, domain, port) + if url[i] == '/' or url[i] == '\0': + hostname = temp + let colon = hostname.find(':') + if colon >= 0: + port = hostname.substr(colon+1) + hostname = hostname.substr(0, colon-1) + + temp.setLen(0) + break + + temp.add(url[i]) + inc(i) + + if url[i] == '/': inc(i) # Skip the '/' + # Path + while true: + if url[i] == '?': + path = temp + temp.setLen(0) + if url[i] == '#': + if temp[0] == '?': + query = temp + else: + path = temp + temp.setLen(0) + + if url[i] == '\0': + if temp[0] == '?': + query = temp + elif temp[0] == '#': + anchor = temp + else: + path = temp + break + + temp.add(url[i]) + inc(i) + + return (scheme, username, password, hostname, port, path, query, anchor) + +proc `$`*(u: Url): string {.deprecated.} = + ## turns the URL `u` into its string representation. + result = "" + if u.scheme.len > 0: + result.add(u.scheme) + result.add("://") + if u.username.len > 0: + result.add(u.username) + if u.password.len > 0: + result.add(":") + result.add(u.password) + result.add("@") + result.add(u.hostname) + if u.port.len > 0: + result.add(":") + result.add(u.port) + if u.path.len > 0: + result.add("/") + result.add(u.path) + result.add(u.query) + result.add(u.anchor) + diff --git a/lib/deprecated/pure/sockets.nim b/lib/deprecated/pure/sockets.nim new file mode 100644 index 000000000..8fa69256b --- /dev/null +++ b/lib/deprecated/pure/sockets.nim @@ -0,0 +1,1738 @@ +# +# +# Nim's Runtime Library +# (c) Copyright 2013 Andreas Rumpf, Dominik Picheta +# +# See the file "copying.txt", included in this +# distribution, for details about the copyright. +# + +## **Warning:** Since version 0.10.2 this module is deprecated. +## Use the `net <net.html>`_ or the +## `rawsockets <rawsockets.html>`_ module instead. +## +## This module implements portable sockets, it supports a mix of different types +## of sockets. Sockets are buffered by default meaning that data will be +## received in ``BufferSize`` (4000) sized chunks, buffering +## behaviour can be disabled by setting the ``buffered`` parameter when calling +## the ``socket`` function to `false`. Be aware that some functions may not yet +## support buffered sockets (mainly the recvFrom function). +## +## Most procedures raise OSError on error, but some may return ``-1`` or a +## boolean ``false``. +## +## SSL is supported through the OpenSSL library. This support can be activated +## by compiling with the ``-d:ssl`` switch. When an SSL socket is used it will +## raise ESSL exceptions when SSL errors occur. +## +## Asynchronous sockets are supported, however a better alternative is to use +## the `asyncio <asyncio.html>`_ module. + +{.deprecated.} + +include "system/inclrtl" + +{.deadCodeElim: on.} + +when hostOS == "solaris": + {.passl: "-lsocket -lnsl".} + +import os, parseutils +from times import epochTime +import unsigned + +when defined(ssl): + import openssl + +when defined(Windows): + import winlean +else: + import posix + +# Note: The enumerations are mapped to Window's constants. + +when defined(ssl): + + type + SSLError* = object of Exception + + SSLCVerifyMode* = enum + CVerifyNone, CVerifyPeer + + SSLProtVersion* = enum + protSSLv2, protSSLv3, protTLSv1, protSSLv23 + + SSLContext* = distinct SSLCTX + + SSLAcceptResult* = enum + AcceptNoClient = 0, AcceptNoHandshake, AcceptSuccess + + {.deprecated: [ESSL: SSLError, TSSLCVerifyMode: SSLCVerifyMode, + TSSLProtVersion: SSLProtVersion, PSSLContext: SSLContext, + TSSLAcceptResult: SSLAcceptResult].} + +const + BufferSize*: int = 4000 ## size of a buffered socket's buffer + +type + SocketImpl = object ## socket type + fd: SocketHandle + case isBuffered: bool # determines whether this socket is buffered. + of true: + buffer: array[0..BufferSize, char] + currPos: int # current index in buffer + bufLen: int # current length of buffer + of false: nil + when defined(ssl): + case isSsl: bool + of true: + sslHandle: SSLPtr + sslContext: SSLContext + sslNoHandshake: bool # True if needs handshake. + sslHasPeekChar: bool + sslPeekChar: char + of false: nil + nonblocking: bool + + Socket* = ref SocketImpl + + Port* = distinct uint16 ## port type + + Domain* = enum ## domain, which specifies the protocol family of the + ## created socket. Other domains than those that are listed + ## here are unsupported. + AF_UNIX, ## for local socket (using a file). Unsupported on Windows. + AF_INET = 2, ## for network protocol IPv4 or + AF_INET6 = 23 ## for network protocol IPv6. + + SockType* = enum ## second argument to `socket` proc + SOCK_STREAM = 1, ## reliable stream-oriented service or Stream Sockets + SOCK_DGRAM = 2, ## datagram service or Datagram Sockets + SOCK_RAW = 3, ## raw protocols atop the network layer. + SOCK_SEQPACKET = 5 ## reliable sequenced packet service + + Protocol* = enum ## third argument to `socket` proc + IPPROTO_TCP = 6, ## Transmission control protocol. + IPPROTO_UDP = 17, ## User datagram protocol. + IPPROTO_IP, ## Internet protocol. Unsupported on Windows. + IPPROTO_IPV6, ## Internet Protocol Version 6. Unsupported on Windows. + IPPROTO_RAW, ## Raw IP Packets Protocol. Unsupported on Windows. + IPPROTO_ICMP ## Control message protocol. Unsupported on Windows. + + Servent* = object ## information about a service + name*: string + aliases*: seq[string] + port*: Port + proto*: string + + Hostent* = object ## information about a given host + name*: string + aliases*: seq[string] + addrtype*: Domain + length*: int + addrList*: seq[string] + + SOBool* = enum ## Boolean socket options. + OptAcceptConn, OptBroadcast, OptDebug, OptDontRoute, OptKeepAlive, + OptOOBInline, OptReuseAddr + + RecvLineResult* = enum ## result for recvLineAsync + RecvFullLine, RecvPartialLine, RecvDisconnected, RecvFail + + ReadLineResult* = enum ## result for readLineAsync + ReadFullLine, ReadPartialLine, ReadDisconnected, ReadNone + + TimeoutError* = object of Exception + +{.deprecated: [TSocket: Socket, TType: SockType, TPort: Port, TDomain: Domain, + TProtocol: Protocol, TServent: Servent, THostent: Hostent, + TSOBool: SOBool, TRecvLineResult: RecvLineResult, + TReadLineResult: ReadLineResult, ETimeout: TimeoutError, + TSocketImpl: SocketImpl].} + +when defined(booting): + let invalidSocket*: Socket = nil ## invalid socket +else: + const invalidSocket*: Socket = nil ## invalid socket + +when defined(windows): + let + osInvalidSocket = winlean.INVALID_SOCKET +else: + let + osInvalidSocket = posix.INVALID_SOCKET + +proc newTSocket(fd: SocketHandle, isBuff: bool): Socket = + if fd == osInvalidSocket: + return nil + new(result) + result.fd = fd + result.isBuffered = isBuff + if isBuff: + result.currPos = 0 + result.nonblocking = false + +proc `==`*(a, b: Port): bool {.borrow.} + ## ``==`` for ports. + +proc `$`*(p: Port): string {.borrow.} + ## returns the port number as a string + +proc ntohl*(x: int32): int32 = + ## Converts 32-bit integers from network to host byte order. + ## On machines where the host byte order is the same as network byte order, + ## this is a no-op; otherwise, it performs a 4-byte swap operation. + when cpuEndian == bigEndian: result = x + else: result = (x shr 24'i32) or + (x shr 8'i32 and 0xff00'i32) or + (x shl 8'i32 and 0xff0000'i32) or + (x shl 24'i32) + +proc ntohs*(x: int16): int16 = + ## Converts 16-bit integers from network to host byte order. On machines + ## where the host byte order is the same as network byte order, this is + ## a no-op; otherwise, it performs a 2-byte swap operation. + when cpuEndian == bigEndian: result = x + else: result = (x shr 8'i16) or (x shl 8'i16) + +proc htonl*(x: int32): int32 = + ## Converts 32-bit integers from host to network byte order. On machines + ## where the host byte order is the same as network byte order, this is + ## a no-op; otherwise, it performs a 4-byte swap operation. + result = sockets.ntohl(x) + +proc htons*(x: int16): int16 = + ## Converts 16-bit positive integers from host to network byte order. + ## On machines where the host byte order is the same as network byte + ## order, this is a no-op; otherwise, it performs a 2-byte swap operation. + result = sockets.ntohs(x) + +when defined(Posix): + proc toInt(domain: Domain): cint = + case domain + of AF_UNIX: result = posix.AF_UNIX + of AF_INET: result = posix.AF_INET + of AF_INET6: result = posix.AF_INET6 + else: discard + + proc toInt(typ: SockType): cint = + case typ + of SOCK_STREAM: result = posix.SOCK_STREAM + of SOCK_DGRAM: result = posix.SOCK_DGRAM + of SOCK_SEQPACKET: result = posix.SOCK_SEQPACKET + of SOCK_RAW: result = posix.SOCK_RAW + else: discard + + proc toInt(p: Protocol): cint = + case p + of IPPROTO_TCP: result = posix.IPPROTO_TCP + of IPPROTO_UDP: result = posix.IPPROTO_UDP + of IPPROTO_IP: result = posix.IPPROTO_IP + of IPPROTO_IPV6: result = posix.IPPROTO_IPV6 + of IPPROTO_RAW: result = posix.IPPROTO_RAW + of IPPROTO_ICMP: result = posix.IPPROTO_ICMP + else: discard + +else: + proc toInt(domain: Domain): cint = + result = toU16(ord(domain)) + + proc toInt(typ: SockType): cint = + result = cint(ord(typ)) + + proc toInt(p: Protocol): cint = + result = cint(ord(p)) + +proc socket*(domain: Domain = AF_INET, typ: SockType = SOCK_STREAM, + protocol: Protocol = IPPROTO_TCP, buffered = true): Socket = + ## Creates a new socket; returns `InvalidSocket` if an error occurs. + + # TODO: Perhaps this should just raise EOS when an error occurs. + when defined(Windows): + result = newTSocket(winlean.socket(ord(domain), ord(typ), ord(protocol)), buffered) + else: + result = newTSocket(posix.socket(toInt(domain), toInt(typ), toInt(protocol)), buffered) + +when defined(ssl): + CRYPTO_malloc_init() + SslLibraryInit() + SslLoadErrorStrings() + ErrLoadBioStrings() + OpenSSL_add_all_algorithms() + + proc raiseSSLError(s = "") = + if s != "": + raise newException(SSLError, s) + let err = ErrPeekLastError() + if err == 0: + raise newException(SSLError, "No error reported.") + if err == -1: + raiseOSError(osLastError()) + var errStr = ErrErrorString(err, nil) + raise newException(SSLError, $errStr) + + # http://simplestcodings.blogspot.co.uk/2010/08/secure-server-client-using-openssl-in-c.html + proc loadCertificates(ctx: SSL_CTX, certFile, keyFile: string) = + if certFile != "" and not existsFile(certFile): + raise newException(system.IOError, "Certificate file could not be found: " & certFile) + if keyFile != "" and not existsFile(keyFile): + raise newException(system.IOError, "Key file could not be found: " & keyFile) + + if certFile != "": + var ret = SSLCTXUseCertificateChainFile(ctx, certFile) + if ret != 1: + raiseSslError() + + # TODO: Password? www.rtfm.com/openssl-examples/part1.pdf + if keyFile != "": + if SSL_CTX_use_PrivateKey_file(ctx, keyFile, + SSL_FILETYPE_PEM) != 1: + raiseSslError() + + if SSL_CTX_check_private_key(ctx) != 1: + raiseSslError("Verification of private key file failed.") + + proc newContext*(protVersion = protSSLv23, verifyMode = CVerifyPeer, + certFile = "", keyFile = ""): SSLContext = + ## Creates an SSL context. + ## + ## Protocol version specifies the protocol to use. SSLv2, SSLv3, TLSv1 are + ## are available with the addition of ``ProtSSLv23`` which allows for + ## compatibility with all of them. + ## + ## There are currently only two options for verify mode; + ## one is ``CVerifyNone`` and with it certificates will not be verified + ## the other is ``CVerifyPeer`` and certificates will be verified for + ## it, ``CVerifyPeer`` is the safest choice. + ## + ## The last two parameters specify the certificate file path and the key file + ## path, a server socket will most likely not work without these. + ## Certificates can be generated using the following command: + ## ``openssl req -x509 -nodes -days 365 -newkey rsa:1024 -keyout mycert.pem -out mycert.pem``. + var newCTX: SSL_CTX + case protVersion + of protSSLv23: + newCTX = SSL_CTX_new(SSLv23_method()) # SSlv2,3 and TLS1 support. + of protSSLv2: + raiseSslError("SSLv2 is no longer secure and has been deprecated, use protSSLv3") + of protSSLv3: + newCTX = SSL_CTX_new(SSLv3_method()) + of protTLSv1: + newCTX = SSL_CTX_new(TLSv1_method()) + + if newCTX.SSLCTXSetCipherList("ALL") != 1: + raiseSslError() + case verifyMode + of CVerifyPeer: + newCTX.SSLCTXSetVerify(SSLVerifyPeer, nil) + of CVerifyNone: + newCTX.SSLCTXSetVerify(SSLVerifyNone, nil) + if newCTX == nil: + raiseSslError() + + discard newCTX.SSLCTXSetMode(SSL_MODE_AUTO_RETRY) + newCTX.loadCertificates(certFile, keyFile) + return SSLContext(newCTX) + + proc wrapSocket*(ctx: SSLContext, socket: Socket) = + ## Wraps a socket in an SSL context. This function effectively turns + ## ``socket`` into an SSL socket. + ## + ## **Disclaimer**: This code is not well tested, may be very unsafe and + ## prone to security vulnerabilities. + + socket.isSSL = true + socket.sslContext = ctx + socket.sslHandle = SSLNew(SSLCTX(socket.sslContext)) + socket.sslNoHandshake = false + socket.sslHasPeekChar = false + if socket.sslHandle == nil: + raiseSslError() + + if SSLSetFd(socket.sslHandle, socket.fd) != 1: + raiseSslError() + +proc raiseSocketError*(socket: Socket, err: int = -1, async = false) = + ## Raises proper errors based on return values of ``recv`` functions. + ## + ## If ``async`` is ``True`` no error will be thrown in the case when the + ## error was caused by no data being available to be read. + ## + ## If ``err`` is not lower than 0 no exception will be raised. + when defined(ssl): + if socket.isSSL: + if err <= 0: + var ret = SSLGetError(socket.sslHandle, err.cint) + case ret + of SSL_ERROR_ZERO_RETURN: + raiseSslError("TLS/SSL connection failed to initiate, socket closed prematurely.") + of SSL_ERROR_WANT_CONNECT, SSL_ERROR_WANT_ACCEPT: + if async: + return + else: raiseSslError("Not enough data on socket.") + of SSL_ERROR_WANT_WRITE, SSL_ERROR_WANT_READ: + if async: + return + else: raiseSslError("Not enough data on socket.") + of SSL_ERROR_WANT_X509_LOOKUP: + raiseSslError("Function for x509 lookup has been called.") + of SSL_ERROR_SYSCALL, SSL_ERROR_SSL: + raiseSslError() + else: raiseSslError("Unknown Error") + + if err == -1 and not (when defined(ssl): socket.isSSL else: false): + let lastError = osLastError() + if async: + when defined(windows): + if lastError.int32 == WSAEWOULDBLOCK: + return + else: raiseOSError(lastError) + else: + if lastError.int32 == EAGAIN or lastError.int32 == EWOULDBLOCK: + return + else: raiseOSError(lastError) + else: raiseOSError(lastError) + +proc listen*(socket: Socket, backlog = SOMAXCONN) {.tags: [ReadIOEffect].} = + ## Marks ``socket`` as accepting connections. + ## ``Backlog`` specifies the maximum length of the + ## queue of pending connections. + if listen(socket.fd, cint(backlog)) < 0'i32: raiseOSError(osLastError()) + +proc invalidIp4(s: string) {.noreturn, noinline.} = + raise newException(ValueError, "invalid ip4 address: " & s) + +proc parseIp4*(s: string): BiggestInt = + ## parses an IP version 4 in dotted decimal form like "a.b.c.d". + ## + ## This is equivalent to `inet_ntoa`:idx:. + ## + ## Raises EInvalidValue in case of an error. + var a, b, c, d: int + var i = 0 + var j = parseInt(s, a, i) + if j <= 0: invalidIp4(s) + inc(i, j) + if s[i] == '.': inc(i) + else: invalidIp4(s) + j = parseInt(s, b, i) + if j <= 0: invalidIp4(s) + inc(i, j) + if s[i] == '.': inc(i) + else: invalidIp4(s) + j = parseInt(s, c, i) + if j <= 0: invalidIp4(s) + inc(i, j) + if s[i] == '.': inc(i) + else: invalidIp4(s) + j = parseInt(s, d, i) + if j <= 0: invalidIp4(s) + inc(i, j) + if s[i] != '\0': invalidIp4(s) + result = BiggestInt(a shl 24 or b shl 16 or c shl 8 or d) + +template gaiNim(a, p, h, list: expr): stmt = + block: + var gaiResult = getaddrinfo(a, $p, addr(h), list) + if gaiResult != 0'i32: + when defined(windows): + raiseOSError(osLastError()) + else: + raiseOSError(osLastError(), $gai_strerror(gaiResult)) + +proc bindAddr*(socket: Socket, port = Port(0), address = "") {. + tags: [ReadIOEffect].} = + ## binds an address/port number to a socket. + ## Use address string in dotted decimal form like "a.b.c.d" + ## or leave "" for any address. + + if address == "": + var name: Sockaddr_in + when defined(Windows): + name.sin_family = int16(ord(AF_INET)) + else: + name.sin_family = posix.AF_INET + name.sin_port = sockets.htons(int16(port)) + name.sin_addr.s_addr = sockets.htonl(INADDR_ANY) + if bindSocket(socket.fd, cast[ptr SockAddr](addr(name)), + sizeof(name).SockLen) < 0'i32: + raiseOSError(osLastError()) + else: + var hints: AddrInfo + var aiList: ptr AddrInfo = nil + hints.ai_family = toInt(AF_INET) + hints.ai_socktype = toInt(SOCK_STREAM) + hints.ai_protocol = toInt(IPPROTO_TCP) + gaiNim(address, port, hints, aiList) + if bindSocket(socket.fd, aiList.ai_addr, aiList.ai_addrlen.SockLen) < 0'i32: + raiseOSError(osLastError()) + +proc getSockName*(socket: Socket): Port = + ## returns the socket's associated port number. + var name: Sockaddr_in + when defined(Windows): + name.sin_family = int16(ord(AF_INET)) + else: + name.sin_family = posix.AF_INET + #name.sin_port = htons(cint16(port)) + #name.sin_addr.s_addr = htonl(INADDR_ANY) + var namelen = sizeof(name).SockLen + if getsockname(socket.fd, cast[ptr SockAddr](addr(name)), + addr(namelen)) == -1'i32: + raiseOSError(osLastError()) + result = Port(sockets.ntohs(name.sin_port)) + +template acceptAddrPlain(noClientRet, successRet: expr, + sslImplementation: stmt): stmt {.immediate.} = + assert(client != nil) + var sockAddress: Sockaddr_in + var addrLen = sizeof(sockAddress).SockLen + var sock = accept(server.fd, cast[ptr SockAddr](addr(sockAddress)), + addr(addrLen)) + + if sock == osInvalidSocket: + let err = osLastError() + when defined(windows): + if err.int32 == WSAEINPROGRESS: + client = invalidSocket + address = "" + when noClientRet.int == -1: + return + else: + return noClientRet + else: raiseOSError(err) + else: + if err.int32 == EAGAIN or err.int32 == EWOULDBLOCK: + client = invalidSocket + address = "" + when noClientRet.int == -1: + return + else: + return noClientRet + else: raiseOSError(err) + else: + client.fd = sock + client.isBuffered = server.isBuffered + sslImplementation + # Client socket is set above. + address = $inet_ntoa(sockAddress.sin_addr) + when successRet.int == -1: + return + else: + return successRet + +proc acceptAddr*(server: Socket, client: var Socket, address: var string) {. + tags: [ReadIOEffect].} = + ## Blocks until a connection is being made from a client. When a connection + ## is made sets ``client`` to the client socket and ``address`` to the address + ## of the connecting client. + ## If ``server`` is non-blocking then this function returns immediately, and + ## if there are no connections queued the returned socket will be + ## ``InvalidSocket``. + ## This function will raise EOS if an error occurs. + ## + ## The resulting client will inherit any properties of the server socket. For + ## example: whether the socket is buffered or not. + ## + ## **Note**: ``client`` must be initialised (with ``new``), this function + ## makes no effort to initialise the ``client`` variable. + ## + ## **Warning:** When using SSL with non-blocking sockets, it is best to use + ## the acceptAddrSSL procedure as this procedure will most likely block. + acceptAddrPlain(-1, -1): + when defined(ssl): + if server.isSSL: + # We must wrap the client sock in a ssl context. + + server.sslContext.wrapSocket(client) + let ret = SSLAccept(client.sslHandle) + while ret <= 0: + let err = SSLGetError(client.sslHandle, ret) + if err != SSL_ERROR_WANT_ACCEPT: + case err + of SSL_ERROR_ZERO_RETURN: + raiseSslError("TLS/SSL connection failed to initiate, socket closed prematurely.") + of SSL_ERROR_WANT_READ, SSL_ERROR_WANT_WRITE, + SSL_ERROR_WANT_CONNECT, SSL_ERROR_WANT_ACCEPT: + raiseSslError("acceptAddrSSL should be used for non-blocking SSL sockets.") + of SSL_ERROR_WANT_X509_LOOKUP: + raiseSslError("Function for x509 lookup has been called.") + of SSL_ERROR_SYSCALL, SSL_ERROR_SSL: + raiseSslError() + else: + raiseSslError("Unknown error") + +proc setBlocking*(s: Socket, blocking: bool) {.tags: [], gcsafe.} + ## Sets blocking mode on socket + +when defined(ssl): + proc acceptAddrSSL*(server: Socket, client: var Socket, + address: var string): SSLAcceptResult {. + tags: [ReadIOEffect].} = + ## This procedure should only be used for non-blocking **SSL** sockets. + ## It will immediately return with one of the following values: + ## + ## ``AcceptSuccess`` will be returned when a client has been successfully + ## accepted and the handshake has been successfully performed between + ## ``server`` and the newly connected client. + ## + ## ``AcceptNoHandshake`` will be returned when a client has been accepted + ## but no handshake could be performed. This can happen when the client + ## connects but does not yet initiate a handshake. In this case + ## ``acceptAddrSSL`` should be called again with the same parameters. + ## + ## ``AcceptNoClient`` will be returned when no client is currently attempting + ## to connect. + template doHandshake(): stmt = + when defined(ssl): + if server.isSSL: + client.setBlocking(false) + # We must wrap the client sock in a ssl context. + + if not client.isSSL or client.sslHandle == nil: + server.sslContext.wrapSocket(client) + let ret = SSLAccept(client.sslHandle) + while ret <= 0: + let err = SSLGetError(client.sslHandle, ret) + if err != SSL_ERROR_WANT_ACCEPT: + case err + of SSL_ERROR_ZERO_RETURN: + raiseSslError("TLS/SSL connection failed to initiate, socket closed prematurely.") + of SSL_ERROR_WANT_READ, SSL_ERROR_WANT_WRITE, + SSL_ERROR_WANT_CONNECT, SSL_ERROR_WANT_ACCEPT: + client.sslNoHandshake = true + return AcceptNoHandshake + of SSL_ERROR_WANT_X509_LOOKUP: + raiseSslError("Function for x509 lookup has been called.") + of SSL_ERROR_SYSCALL, SSL_ERROR_SSL: + raiseSslError() + else: + raiseSslError("Unknown error") + client.sslNoHandshake = false + + if client.isSSL and client.sslNoHandshake: + doHandshake() + return AcceptSuccess + else: + acceptAddrPlain(AcceptNoClient, AcceptSuccess): + doHandshake() + +proc accept*(server: Socket, client: var Socket) {.tags: [ReadIOEffect].} = + ## Equivalent to ``acceptAddr`` but doesn't return the address, only the + ## socket. + ## + ## **Note**: ``client`` must be initialised (with ``new``), this function + ## makes no effort to initialise the ``client`` variable. + + var addrDummy = "" + acceptAddr(server, client, addrDummy) + +proc acceptAddr*(server: Socket): tuple[client: Socket, address: string] {. + deprecated, tags: [ReadIOEffect].} = + ## Slightly different version of ``acceptAddr``. + ## + ## **Deprecated since version 0.9.0:** Please use the function above. + var client: Socket + new(client) + var address = "" + acceptAddr(server, client, address) + return (client, address) + +proc accept*(server: Socket): Socket {.deprecated, tags: [ReadIOEffect].} = + ## **Deprecated since version 0.9.0:** Please use the function above. + new(result) + var address = "" + acceptAddr(server, result, address) + +proc close*(socket: Socket) = + ## closes a socket. + when defined(windows): + discard winlean.closesocket(socket.fd) + else: + discard posix.close(socket.fd) + # TODO: These values should not be discarded. An EOS should be raised. + # http://stackoverflow.com/questions/12463473/what-happens-if-you-call-close-on-a-bsd-socket-multiple-times + when defined(ssl): + if socket.isSSL: + discard SSLShutdown(socket.sslHandle) + SSLFree(socket.sslHandle) + socket.sslHandle = nil + +proc getServByName*(name, proto: string): Servent {.tags: [ReadIOEffect].} = + ## Searches the database from the beginning and finds the first entry for + ## which the service name specified by ``name`` matches the s_name member + ## and the protocol name specified by ``proto`` matches the s_proto member. + ## + ## On posix this will search through the ``/etc/services`` file. + when defined(Windows): + var s = winlean.getservbyname(name, proto) + else: + var s = posix.getservbyname(name, proto) + if s == nil: raiseOSError(osLastError(), "Service not found.") + result.name = $s.s_name + result.aliases = cstringArrayToSeq(s.s_aliases) + result.port = Port(s.s_port) + result.proto = $s.s_proto + +proc getServByPort*(port: Port, proto: string): Servent {.tags: [ReadIOEffect].} = + ## Searches the database from the beginning and finds the first entry for + ## which the port specified by ``port`` matches the s_port member and the + ## protocol name specified by ``proto`` matches the s_proto member. + ## + ## On posix this will search through the ``/etc/services`` file. + when defined(Windows): + var s = winlean.getservbyport(ze(int16(port)).cint, proto) + else: + var s = posix.getservbyport(ze(int16(port)).cint, proto) + if s == nil: raiseOSError(osLastError(), "Service not found.") + result.name = $s.s_name + result.aliases = cstringArrayToSeq(s.s_aliases) + result.port = Port(s.s_port) + result.proto = $s.s_proto + +proc getHostByAddr*(ip: string): Hostent {.tags: [ReadIOEffect].} = + ## This function will lookup the hostname of an IP Address. + var myaddr: InAddr + myaddr.s_addr = inet_addr(ip) + + when defined(windows): + var s = winlean.gethostbyaddr(addr(myaddr), sizeof(myaddr).cuint, + cint(sockets.AF_INET)) + if s == nil: raiseOSError(osLastError()) + else: + var s = posix.gethostbyaddr(addr(myaddr), sizeof(myaddr).Socklen, + cint(posix.AF_INET)) + if s == nil: + raiseOSError(osLastError(), $hstrerror(h_errno)) + + result.name = $s.h_name + result.aliases = cstringArrayToSeq(s.h_aliases) + when defined(windows): + result.addrtype = Domain(s.h_addrtype) + else: + if s.h_addrtype == posix.AF_INET: + result.addrtype = AF_INET + elif s.h_addrtype == posix.AF_INET6: + result.addrtype = AF_INET6 + else: + raiseOSError(osLastError(), "unknown h_addrtype") + result.addrList = cstringArrayToSeq(s.h_addr_list) + result.length = int(s.h_length) + +proc getHostByName*(name: string): Hostent {.tags: [ReadIOEffect].} = + ## This function will lookup the IP address of a hostname. + when defined(Windows): + var s = winlean.gethostbyname(name) + else: + var s = posix.gethostbyname(name) + if s == nil: raiseOSError(osLastError()) + result.name = $s.h_name + result.aliases = cstringArrayToSeq(s.h_aliases) + when defined(windows): + result.addrtype = Domain(s.h_addrtype) + else: + if s.h_addrtype == posix.AF_INET: + result.addrtype = AF_INET + elif s.h_addrtype == posix.AF_INET6: + result.addrtype = AF_INET6 + else: + raiseOSError(osLastError(), "unknown h_addrtype") + result.addrList = cstringArrayToSeq(s.h_addr_list) + result.length = int(s.h_length) + +proc getSockOptInt*(socket: Socket, level, optname: int): int {. + tags: [ReadIOEffect].} = + ## getsockopt for integer options. + var res: cint + var size = sizeof(res).SockLen + if getsockopt(socket.fd, cint(level), cint(optname), + addr(res), addr(size)) < 0'i32: + raiseOSError(osLastError()) + result = int(res) + +proc setSockOptInt*(socket: Socket, level, optname, optval: int) {. + tags: [WriteIOEffect].} = + ## setsockopt for integer options. + var value = cint(optval) + if setsockopt(socket.fd, cint(level), cint(optname), addr(value), + sizeof(value).SockLen) < 0'i32: + raiseOSError(osLastError()) + +proc toCInt(opt: SOBool): cint = + case opt + of OptAcceptConn: SO_ACCEPTCONN + of OptBroadcast: SO_BROADCAST + of OptDebug: SO_DEBUG + of OptDontRoute: SO_DONTROUTE + of OptKeepAlive: SO_KEEPALIVE + of OptOOBInline: SO_OOBINLINE + of OptReuseAddr: SO_REUSEADDR + +proc getSockOpt*(socket: Socket, opt: SOBool, level = SOL_SOCKET): bool {. + tags: [ReadIOEffect].} = + ## Retrieves option ``opt`` as a boolean value. + var res: cint + var size = sizeof(res).SockLen + if getsockopt(socket.fd, cint(level), toCInt(opt), + addr(res), addr(size)) < 0'i32: + raiseOSError(osLastError()) + result = res != 0 + +proc setSockOpt*(socket: Socket, 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) + if setsockopt(socket.fd, cint(level), toCInt(opt), addr(valuei), + sizeof(valuei).SockLen) < 0'i32: + raiseOSError(osLastError()) + +proc connect*(socket: Socket, address: string, port = Port(0), + af: Domain = AF_INET) {.tags: [ReadIOEffect].} = + ## Connects socket to ``address``:``port``. ``Address`` can be an IP address or a + ## host name. If ``address`` is a host name, this function will try each IP + ## of that host name. ``htons`` is already performed on ``port`` so you must + ## not do it. + ## + ## If ``socket`` is an SSL socket a handshake will be automatically performed. + var hints: AddrInfo + var aiList: ptr AddrInfo = nil + hints.ai_family = toInt(af) + hints.ai_socktype = toInt(SOCK_STREAM) + hints.ai_protocol = toInt(IPPROTO_TCP) + gaiNim(address, port, hints, aiList) + # try all possibilities: + var success = false + var lastError: OSErrorCode + var it = aiList + while it != nil: + if connect(socket.fd, it.ai_addr, it.ai_addrlen.SockLen) == 0'i32: + success = true + break + else: lastError = osLastError() + it = it.ai_next + + freeaddrinfo(aiList) + if not success: raiseOSError(lastError) + + when defined(ssl): + if socket.isSSL: + let ret = SSLConnect(socket.sslHandle) + if ret <= 0: + let err = SSLGetError(socket.sslHandle, ret) + case err + of SSL_ERROR_ZERO_RETURN: + raiseSslError("TLS/SSL connection failed to initiate, socket closed prematurely.") + of SSL_ERROR_WANT_READ, SSL_ERROR_WANT_WRITE, SSL_ERROR_WANT_CONNECT, + SSL_ERROR_WANT_ACCEPT: + raiseSslError("The operation did not complete. Perhaps you should use connectAsync?") + of SSL_ERROR_WANT_X509_LOOKUP: + raiseSslError("Function for x509 lookup has been called.") + of SSL_ERROR_SYSCALL, SSL_ERROR_SSL: + raiseSslError() + else: + raiseSslError("Unknown error") + + when false: + var s: TSockAddrIn + s.sin_addr.s_addr = inet_addr(address) + s.sin_port = sockets.htons(int16(port)) + when defined(windows): + s.sin_family = toU16(ord(af)) + else: + case af + of AF_UNIX: s.sin_family = posix.AF_UNIX + of AF_INET: s.sin_family = posix.AF_INET + of AF_INET6: s.sin_family = posix.AF_INET6 + else: nil + if connect(socket.fd, cast[ptr TSockAddr](addr(s)), sizeof(s).cint) < 0'i32: + OSError() + +proc connectAsync*(socket: Socket, name: string, port = Port(0), + af: Domain = AF_INET) {.tags: [ReadIOEffect].} = + ## A variant of ``connect`` for non-blocking sockets. + ## + ## This procedure will immediately return, it will not block until a connection + ## is made. It is up to the caller to make sure the connection has been established + ## by checking (using ``select``) whether the socket is writeable. + ## + ## **Note**: For SSL sockets, the ``handshake`` procedure must be called + ## whenever the socket successfully connects to a server. + var hints: AddrInfo + var aiList: ptr AddrInfo = nil + hints.ai_family = toInt(af) + hints.ai_socktype = toInt(SOCK_STREAM) + hints.ai_protocol = toInt(IPPROTO_TCP) + gaiNim(name, port, hints, aiList) + # try all possibilities: + var success = false + var lastError: OSErrorCode + var it = aiList + while it != nil: + var ret = connect(socket.fd, it.ai_addr, it.ai_addrlen.SockLen) + if ret == 0'i32: + success = true + break + else: + lastError = osLastError() + when defined(windows): + # Windows EINTR doesn't behave same as POSIX. + if lastError.int32 == WSAEWOULDBLOCK: + success = true + break + else: + if lastError.int32 == EINTR or lastError.int32 == EINPROGRESS: + success = true + break + + it = it.ai_next + + freeaddrinfo(aiList) + if not success: raiseOSError(lastError) + when defined(ssl): + if socket.isSSL: + socket.sslNoHandshake = true + +when defined(ssl): + proc handshake*(socket: Socket): bool {.tags: [ReadIOEffect, WriteIOEffect].} = + ## This proc needs to be called on a socket after it connects. This is + ## only applicable when using ``connectAsync``. + ## This proc performs the SSL handshake. + ## + ## Returns ``False`` whenever the socket is not yet ready for a handshake, + ## ``True`` whenever handshake completed successfully. + ## + ## A ESSL error is raised on any other errors. + result = true + if socket.isSSL: + var ret = SSLConnect(socket.sslHandle) + if ret <= 0: + var errret = SSLGetError(socket.sslHandle, ret) + case errret + of SSL_ERROR_ZERO_RETURN: + raiseSslError("TLS/SSL connection failed to initiate, socket closed prematurely.") + of SSL_ERROR_WANT_CONNECT, SSL_ERROR_WANT_ACCEPT, + SSL_ERROR_WANT_READ, SSL_ERROR_WANT_WRITE: + return false + of SSL_ERROR_WANT_X509_LOOKUP: + raiseSslError("Function for x509 lookup has been called.") + of SSL_ERROR_SYSCALL, SSL_ERROR_SSL: + raiseSslError() + else: + raiseSslError("Unknown Error") + socket.sslNoHandshake = false + else: + raiseSslError("Socket is not an SSL socket.") + + proc gotHandshake*(socket: Socket): bool = + ## Determines whether a handshake has occurred between a client (``socket``) + ## and the server that ``socket`` is connected to. + ## + ## Throws ESSL if ``socket`` is not an SSL socket. + if socket.isSSL: + return not socket.sslNoHandshake + else: + raiseSslError("Socket is not an SSL socket.") + +proc timeValFromMilliseconds(timeout = 500): Timeval = + if timeout != -1: + var seconds = timeout div 1000 + result.tv_sec = seconds.int32 + result.tv_usec = ((timeout - seconds * 1000) * 1000).int32 + +proc createFdSet(fd: var TFdSet, s: seq[Socket], m: var int) = + FD_ZERO(fd) + for i in items(s): + m = max(m, int(i.fd)) + FD_SET(i.fd, fd) + +proc pruneSocketSet(s: var seq[Socket], fd: var TFdSet) = + var i = 0 + var L = s.len + while i < L: + if FD_ISSET(s[i].fd, fd) == 0'i32: + # not set. + s[i] = s[L-1] + dec(L) + else: + inc(i) + setLen(s, L) + +proc hasDataBuffered*(s: Socket): bool = + ## Determines whether a socket has data buffered. + result = false + if s.isBuffered: + result = s.bufLen > 0 and s.currPos != s.bufLen + + when defined(ssl): + if s.isSSL and not result: + result = s.sslHasPeekChar + +proc checkBuffer(readfds: var seq[Socket]): int = + ## Checks the buffer of each socket in ``readfds`` to see whether there is data. + ## Removes the sockets from ``readfds`` and returns the count of removed sockets. + var res: seq[Socket] = @[] + result = 0 + for s in readfds: + if hasDataBuffered(s): + inc(result) + res.add(s) + if result > 0: + readfds = res + +proc select*(readfds, writefds, exceptfds: var seq[Socket], + timeout = 500): int {.tags: [ReadIOEffect].} = + ## Traditional select function. This function will return the number of + ## sockets that are ready to be read from, written to, or which have errors. + ## If there are none; 0 is returned. + ## ``Timeout`` is in milliseconds and -1 can be specified for no timeout. + ## + ## Sockets which are **not** ready for reading, writing or which don't have + ## errors waiting on them are removed from the ``readfds``, ``writefds``, + ## ``exceptfds`` sequences respectively. + let buffersFilled = checkBuffer(readfds) + if buffersFilled > 0: + return buffersFilled + + var tv {.noInit.}: Timeval = timeValFromMilliseconds(timeout) + + var rd, wr, ex: TFdSet + var m = 0 + createFdSet((rd), readfds, m) + createFdSet((wr), writefds, m) + createFdSet((ex), exceptfds, m) + + if timeout != -1: + result = int(select(cint(m+1), addr(rd), addr(wr), addr(ex), addr(tv))) + else: + result = int(select(cint(m+1), addr(rd), addr(wr), addr(ex), nil)) + + pruneSocketSet(readfds, (rd)) + pruneSocketSet(writefds, (wr)) + pruneSocketSet(exceptfds, (ex)) + +proc select*(readfds, writefds: var seq[Socket], + timeout = 500): int {.tags: [ReadIOEffect].} = + ## Variant of select with only a read and write list. + let buffersFilled = checkBuffer(readfds) + if buffersFilled > 0: + return buffersFilled + var tv {.noInit.}: Timeval = timeValFromMilliseconds(timeout) + + var rd, wr: TFdSet + var m = 0 + createFdSet((rd), readfds, m) + createFdSet((wr), writefds, m) + + if timeout != -1: + result = int(select(cint(m+1), addr(rd), addr(wr), nil, addr(tv))) + else: + result = int(select(cint(m+1), addr(rd), addr(wr), nil, nil)) + + pruneSocketSet(readfds, (rd)) + pruneSocketSet(writefds, (wr)) + +proc selectWrite*(writefds: var seq[Socket], + timeout = 500): int {.tags: [ReadIOEffect].} = + ## When a socket in ``writefds`` is ready to be written to then a non-zero + ## value will be returned specifying the count of the sockets which can be + ## written to. The sockets which **cannot** be written to will also be removed + ## from ``writefds``. + ## + ## ``timeout`` is specified in milliseconds and ``-1`` can be specified for + ## an unlimited time. + var tv {.noInit.}: Timeval = timeValFromMilliseconds(timeout) + + var wr: TFdSet + var m = 0 + createFdSet((wr), writefds, m) + + if timeout != -1: + result = int(select(cint(m+1), nil, addr(wr), nil, addr(tv))) + else: + result = int(select(cint(m+1), nil, addr(wr), nil, nil)) + + pruneSocketSet(writefds, (wr)) + +proc select*(readfds: var seq[Socket], timeout = 500): int = + ## variant of select with a read list only + let buffersFilled = checkBuffer(readfds) + if buffersFilled > 0: + return buffersFilled + var tv {.noInit.}: Timeval = timeValFromMilliseconds(timeout) + + var rd: TFdSet + var m = 0 + createFdSet((rd), readfds, m) + + if timeout != -1: + result = int(select(cint(m+1), addr(rd), nil, nil, addr(tv))) + else: + result = int(select(cint(m+1), addr(rd), nil, nil, nil)) + + pruneSocketSet(readfds, (rd)) + +proc readIntoBuf(socket: Socket, flags: int32): int = + result = 0 + when defined(ssl): + if socket.isSSL: + result = SSLRead(socket.sslHandle, addr(socket.buffer), int(socket.buffer.high)) + else: + result = recv(socket.fd, addr(socket.buffer), cint(socket.buffer.high), flags) + else: + result = recv(socket.fd, addr(socket.buffer), cint(socket.buffer.high), flags) + if result <= 0: + socket.bufLen = 0 + socket.currPos = 0 + return result + socket.bufLen = result + socket.currPos = 0 + +template retRead(flags, readBytes: int) {.dirty.} = + let res = socket.readIntoBuf(flags.int32) + if res <= 0: + if readBytes > 0: + return readBytes + else: + return res + +proc recv*(socket: Socket, data: pointer, size: int): int {.tags: [ReadIOEffect].} = + ## Receives data from a socket. + ## + ## **Note**: This is a low-level function, you may be interested in the higher + ## level versions of this function which are also named ``recv``. + if size == 0: return + if socket.isBuffered: + if socket.bufLen == 0: + retRead(0'i32, 0) + + var read = 0 + while read < size: + if socket.currPos >= socket.bufLen: + retRead(0'i32, read) + + let chunk = min(socket.bufLen-socket.currPos, size-read) + var d = cast[cstring](data) + copyMem(addr(d[read]), addr(socket.buffer[socket.currPos]), chunk) + read.inc(chunk) + socket.currPos.inc(chunk) + + result = read + else: + when defined(ssl): + if socket.isSSL: + if socket.sslHasPeekChar: + copyMem(data, addr(socket.sslPeekChar), 1) + socket.sslHasPeekChar = false + if size-1 > 0: + var d = cast[cstring](data) + result = SSLRead(socket.sslHandle, addr(d[1]), size-1) + 1 + else: + result = 1 + else: + result = SSLRead(socket.sslHandle, data, size) + else: + result = recv(socket.fd, data, size.cint, 0'i32) + else: + result = recv(socket.fd, data, size.cint, 0'i32) + +proc waitFor(socket: Socket, waited: var float, timeout, size: int, + funcName: string): int {.tags: [TimeEffect].} = + ## determines the amount of characters that can be read. Result will never + ## be larger than ``size``. For unbuffered sockets this will be ``1``. + ## For buffered sockets it can be as big as ``BufferSize``. + ## + ## If this function does not determine that there is data on the socket + ## within ``timeout`` ms, an ETimeout error will be raised. + result = 1 + if size <= 0: assert false + if timeout == -1: return size + if socket.isBuffered and socket.bufLen != 0 and socket.bufLen != socket.currPos: + result = socket.bufLen - socket.currPos + result = min(result, size) + else: + if timeout - int(waited * 1000.0) < 1: + raise newException(TimeoutError, "Call to '" & funcName & "' timed out.") + + when defined(ssl): + if socket.isSSL: + if socket.hasDataBuffered: + # sslPeekChar is present. + return 1 + let sslPending = SSLPending(socket.sslHandle) + if sslPending != 0: + return sslPending + + var s = @[socket] + var startTime = epochTime() + let selRet = select(s, timeout - int(waited * 1000.0)) + if selRet < 0: raiseOSError(osLastError()) + if selRet != 1: + raise newException(TimeoutError, "Call to '" & funcName & "' timed out.") + waited += (epochTime() - startTime) + +proc recv*(socket: Socket, data: pointer, size: int, timeout: int): int {. + tags: [ReadIOEffect, TimeEffect].} = + ## overload with a ``timeout`` parameter in milliseconds. + var waited = 0.0 # number of seconds already waited + + var read = 0 + while read < size: + let avail = waitFor(socket, waited, timeout, size-read, "recv") + var d = cast[cstring](data) + result = recv(socket, addr(d[read]), avail) + if result == 0: break + if result < 0: + return result + inc(read, result) + + result = read + +proc recv*(socket: Socket, data: var string, size: int, timeout = -1): int = + ## Higher-level version of ``recv``. + ## + ## When 0 is returned the socket's connection has been closed. + ## + ## This function will throw an EOS exception when an error occurs. A value + ## lower than 0 is never returned. + ## + ## A timeout may be specified in milliseconds, if enough data is not received + ## within the time specified an ETimeout exception will be raised. + ## + ## **Note**: ``data`` must be initialised. + data.setLen(size) + result = recv(socket, cstring(data), size, timeout) + if result < 0: + data.setLen(0) + socket.raiseSocketError(result) + data.setLen(result) + +proc recvAsync*(socket: Socket, data: var string, size: int): int = + ## Async version of ``recv``. + ## + ## When socket is non-blocking and no data is available on the socket, + ## ``-1`` will be returned and ``data`` will be ``""``. + ## + ## **Note**: ``data`` must be initialised. + data.setLen(size) + result = recv(socket, cstring(data), size) + if result < 0: + data.setLen(0) + socket.raiseSocketError(async = true) + result = -1 + data.setLen(result) + +proc peekChar(socket: Socket, c: var char): int {.tags: [ReadIOEffect].} = + if socket.isBuffered: + result = 1 + if socket.bufLen == 0 or socket.currPos > socket.bufLen-1: + var res = socket.readIntoBuf(0'i32) + if res <= 0: + result = res + + c = socket.buffer[socket.currPos] + else: + when defined(ssl): + if socket.isSSL: + if not socket.sslHasPeekChar: + result = SSLRead(socket.sslHandle, addr(socket.sslPeekChar), 1) + socket.sslHasPeekChar = true + + c = socket.sslPeekChar + return + result = recv(socket.fd, addr(c), 1, MSG_PEEK) + +proc recvLine*(socket: Socket, line: var TaintedString, timeout = -1): bool {. + tags: [ReadIOEffect, TimeEffect], deprecated.} = + ## Receive a line of data from ``socket``. + ## + ## If a full line is received ``\r\L`` is not + ## added to ``line``, however if solely ``\r\L`` is received then ``line`` + ## will be set to it. + ## + ## ``True`` is returned if data is available. ``False`` suggests an + ## error, EOS exceptions are not raised and ``False`` is simply returned + ## instead. + ## + ## If the socket is disconnected, ``line`` will be set to ``""`` and ``True`` + ## will be returned. + ## + ## A timeout can be specified in milliseconds, if data is not received within + ## the specified time an ETimeout exception will be raised. + ## + ## **Deprecated since version 0.9.2**: This function has been deprecated in + ## favour of readLine. + + template addNLIfEmpty(): stmt = + if line.len == 0: + line.add("\c\L") + + var waited = 0.0 + + setLen(line.string, 0) + while true: + var c: char + discard waitFor(socket, waited, timeout, 1, "recvLine") + var n = recv(socket, addr(c), 1) + if n < 0: return + elif n == 0: return true + if c == '\r': + discard waitFor(socket, waited, timeout, 1, "recvLine") + n = peekChar(socket, c) + if n > 0 and c == '\L': + discard recv(socket, addr(c), 1) + elif n <= 0: return false + addNLIfEmpty() + return true + elif c == '\L': + addNLIfEmpty() + return true + add(line.string, c) + +proc readLine*(socket: Socket, line: var TaintedString, timeout = -1) {. + tags: [ReadIOEffect, TimeEffect].} = + ## Reads a line of data from ``socket``. + ## + ## If a full line is read ``\r\L`` is not + ## added to ``line``, however if solely ``\r\L`` is read then ``line`` + ## will be set to it. + ## + ## If the socket is disconnected, ``line`` will be set to ``""``. + ## + ## An EOS exception will be raised in the case of a socket error. + ## + ## A timeout can be specified in milliseconds, if data is not received within + ## the specified time an ETimeout exception will be raised. + + template addNLIfEmpty(): stmt = + if line.len == 0: + line.add("\c\L") + + var waited = 0.0 + + setLen(line.string, 0) + while true: + var c: char + discard waitFor(socket, waited, timeout, 1, "readLine") + var n = recv(socket, addr(c), 1) + if n < 0: socket.raiseSocketError() + elif n == 0: return + if c == '\r': + discard waitFor(socket, waited, timeout, 1, "readLine") + n = peekChar(socket, c) + if n > 0 and c == '\L': + discard recv(socket, addr(c), 1) + elif n <= 0: socket.raiseSocketError() + addNLIfEmpty() + return + elif c == '\L': + addNLIfEmpty() + return + add(line.string, c) + +proc recvLineAsync*(socket: Socket, + line: var TaintedString): RecvLineResult {.tags: [ReadIOEffect], deprecated.} = + ## Similar to ``recvLine`` but designed for non-blocking sockets. + ## + ## The values of the returned enum should be pretty self explanatory: + ## + ## * If a full line has been retrieved; ``RecvFullLine`` is returned. + ## * If some data has been retrieved; ``RecvPartialLine`` is returned. + ## * If the socket has been disconnected; ``RecvDisconnected`` is returned. + ## * If call to ``recv`` failed; ``RecvFail`` is returned. + ## + ## **Deprecated since version 0.9.2**: This function has been deprecated in + ## favour of readLineAsync. + + setLen(line.string, 0) + while true: + var c: char + var n = recv(socket, addr(c), 1) + if n < 0: + return (if line.len == 0: RecvFail else: RecvPartialLine) + elif n == 0: + return (if line.len == 0: RecvDisconnected else: RecvPartialLine) + if c == '\r': + n = peekChar(socket, c) + if n > 0 and c == '\L': + discard recv(socket, addr(c), 1) + elif n <= 0: + return (if line.len == 0: RecvFail else: RecvPartialLine) + return RecvFullLine + elif c == '\L': return RecvFullLine + add(line.string, c) + +proc readLineAsync*(socket: Socket, + line: var TaintedString): ReadLineResult {.tags: [ReadIOEffect].} = + ## Similar to ``recvLine`` but designed for non-blocking sockets. + ## + ## The values of the returned enum should be pretty self explanatory: + ## + ## * If a full line has been retrieved; ``ReadFullLine`` is returned. + ## * If some data has been retrieved; ``ReadPartialLine`` is returned. + ## * If the socket has been disconnected; ``ReadDisconnected`` is returned. + ## * If no data could be retrieved; ``ReadNone`` is returned. + ## * If call to ``recv`` failed; **an EOS exception is raised.** + setLen(line.string, 0) + + template errorOrNone = + socket.raiseSocketError(async = true) + return ReadNone + + while true: + var c: char + var n = recv(socket, addr(c), 1) + #echo(n) + if n < 0: + if line.len == 0: errorOrNone else: return ReadPartialLine + elif n == 0: + return (if line.len == 0: ReadDisconnected else: ReadPartialLine) + if c == '\r': + n = peekChar(socket, c) + if n > 0 and c == '\L': + discard recv(socket, addr(c), 1) + elif n <= 0: + if line.len == 0: errorOrNone else: return ReadPartialLine + return ReadFullLine + elif c == '\L': return ReadFullLine + add(line.string, c) + +proc recv*(socket: Socket): TaintedString {.tags: [ReadIOEffect], deprecated.} = + ## receives all the available data from the socket. + ## Socket errors will result in an ``EOS`` error. + ## If socket is not a connectionless socket and socket is not connected + ## ``""`` will be returned. + ## + ## **Deprecated since version 0.9.2**: This function is not safe for use. + const bufSize = 4000 + result = newStringOfCap(bufSize).TaintedString + var pos = 0 + while true: + var bytesRead = recv(socket, addr(string(result)[pos]), bufSize-1) + if bytesRead == -1: raiseOSError(osLastError()) + setLen(result.string, pos + bytesRead) + if bytesRead != bufSize-1: break + # increase capacity: + setLen(result.string, result.string.len + bufSize) + inc(pos, bytesRead) + when false: + var buf = newString(bufSize) + result = TaintedString"" + while true: + var bytesRead = recv(socket, cstring(buf), bufSize-1) + # Error + if bytesRead == -1: OSError(osLastError()) + + buf[bytesRead] = '\0' # might not be necessary + setLen(buf, bytesRead) + add(result.string, buf) + if bytesRead != bufSize-1: break + +{.push warning[deprecated]: off.} +proc recvTimeout*(socket: Socket, timeout: int): TaintedString {. + tags: [ReadIOEffect], deprecated.} = + ## overloaded variant to support a ``timeout`` parameter, the ``timeout`` + ## parameter specifies the amount of milliseconds to wait for data on the + ## socket. + ## + ## **Deprecated since version 0.9.2**: This function is not safe for use. + if socket.bufLen == 0: + var s = @[socket] + if s.select(timeout) != 1: + raise newException(TimeoutError, "Call to recv() timed out.") + + return socket.recv +{.pop.} + +proc recvAsync*(socket: Socket, s: var TaintedString): bool {. + tags: [ReadIOEffect], deprecated.} = + ## receives all the data from a non-blocking socket. If socket is non-blocking + ## and there are no messages available, `False` will be returned. + ## Other socket errors will result in an ``EOS`` error. + ## If socket is not a connectionless socket and socket is not connected + ## ``s`` will be set to ``""``. + ## + ## **Deprecated since version 0.9.2**: This function is not safe for use. + const bufSize = 1000 + # ensure bufSize capacity: + setLen(s.string, bufSize) + setLen(s.string, 0) + var pos = 0 + while true: + var bytesRead = recv(socket, addr(string(s)[pos]), bufSize-1) + when defined(ssl): + if socket.isSSL: + if bytesRead <= 0: + var ret = SSLGetError(socket.sslHandle, bytesRead.cint) + case ret + of SSL_ERROR_ZERO_RETURN: + raiseSslError("TLS/SSL connection failed to initiate, socket closed prematurely.") + of SSL_ERROR_WANT_CONNECT, SSL_ERROR_WANT_ACCEPT: + raiseSslError("Unexpected error occurred.") # This should just not happen. + of SSL_ERROR_WANT_WRITE, SSL_ERROR_WANT_READ: + return false + of SSL_ERROR_WANT_X509_LOOKUP: + raiseSslError("Function for x509 lookup has been called.") + of SSL_ERROR_SYSCALL, SSL_ERROR_SSL: + raiseSslError() + else: raiseSslError("Unknown Error") + + if bytesRead == -1 and not (when defined(ssl): socket.isSSL else: false): + let err = osLastError() + when defined(windows): + if err.int32 == WSAEWOULDBLOCK: + return false + else: raiseOSError(err) + else: + if err.int32 == EAGAIN or err.int32 == EWOULDBLOCK: + return false + else: raiseOSError(err) + + setLen(s.string, pos + bytesRead) + if bytesRead != bufSize-1: break + # increase capacity: + setLen(s.string, s.string.len + bufSize) + inc(pos, bytesRead) + result = true + +proc recvFrom*(socket: Socket, data: var string, length: int, + address: var string, port: var Port, flags = 0'i32): int {. + tags: [ReadIOEffect].} = + ## Receives data from ``socket``. This function should normally be used with + ## connection-less sockets (UDP sockets). + ## + ## If an error occurs the return value will be ``-1``. Otherwise the return + ## value will be the length of data received. + ## + ## **Warning:** This function does not yet have a buffered implementation, + ## so when ``socket`` is buffered the non-buffered implementation will be + ## used. Therefore if ``socket`` contains something in its buffer this + ## function will make no effort to return it. + + # TODO: Buffered sockets + data.setLen(length) + var sockAddress: Sockaddr_in + var addrLen = sizeof(sockAddress).SockLen + result = recvfrom(socket.fd, cstring(data), length.cint, flags.cint, + cast[ptr SockAddr](addr(sockAddress)), addr(addrLen)) + + if result != -1: + data.setLen(result) + address = $inet_ntoa(sockAddress.sin_addr) + port = ntohs(sockAddress.sin_port).Port + +proc recvFromAsync*(socket: Socket, data: var string, length: int, + address: var string, port: var Port, + flags = 0'i32): bool {.tags: [ReadIOEffect].} = + ## Variant of ``recvFrom`` for non-blocking sockets. Unlike ``recvFrom``, + ## this function will raise an EOS error whenever a socket error occurs. + ## + ## If there is no data to be read from the socket ``False`` will be returned. + result = true + var callRes = recvFrom(socket, data, length, address, port, flags) + if callRes < 0: + let err = osLastError() + when defined(windows): + if err.int32 == WSAEWOULDBLOCK: + return false + else: raiseOSError(err) + else: + if err.int32 == EAGAIN or err.int32 == EWOULDBLOCK: + return false + else: raiseOSError(err) + +proc skip*(socket: Socket) {.tags: [ReadIOEffect], deprecated.} = + ## skips all the data that is pending for the socket + ## + ## **Deprecated since version 0.9.2**: This function is not safe for use. + const bufSize = 1000 + var buf = alloc(bufSize) + while recv(socket, buf, bufSize) == bufSize: discard + dealloc(buf) + +proc skip*(socket: Socket, size: int, timeout = -1) = + ## Skips ``size`` amount of bytes. + ## + ## An optional timeout can be specified in milliseconds, if skipping the + ## bytes takes longer than specified an ETimeout exception will be raised. + ## + ## Returns the number of skipped bytes. + var waited = 0.0 + var dummy = alloc(size) + var bytesSkipped = 0 + while bytesSkipped != size: + let avail = waitFor(socket, waited, timeout, size-bytesSkipped, "skip") + bytesSkipped += recv(socket, dummy, avail) + dealloc(dummy) + +proc send*(socket: Socket, data: pointer, size: int): int {. + tags: [WriteIOEffect].} = + ## sends data to a socket. + when defined(ssl): + if socket.isSSL: + return SSLWrite(socket.sslHandle, cast[cstring](data), size) + + when defined(windows) or defined(macosx): + result = send(socket.fd, data, size.cint, 0'i32) + else: + when defined(solaris): + const MSG_NOSIGNAL = 0 + result = send(socket.fd, data, size, int32(MSG_NOSIGNAL)) + +proc send*(socket: Socket, data: string) {.tags: [WriteIOEffect].} = + ## sends data to a socket. + if socket.nonblocking: + raise newException(ValueError, "This function cannot be used on non-blocking sockets.") + let sent = send(socket, cstring(data), data.len) + if sent < 0: + when defined(ssl): + if socket.isSSL: + raiseSslError() + + raiseOSError(osLastError()) + + if sent != data.len: + raiseOSError(osLastError(), "Could not send all data.") + +proc sendAsync*(socket: Socket, data: string): int {.tags: [WriteIOEffect].} = + ## sends data to a non-blocking socket. + ## Returns ``0`` if no data could be sent, if data has been sent + ## returns the amount of bytes of ``data`` that was successfully sent. This + ## number may not always be the length of ``data`` but typically is. + ## + ## An EOS (or ESSL if socket is an SSL socket) exception is raised if an error + ## occurs. + result = send(socket, cstring(data), data.len) + when defined(ssl): + if socket.isSSL: + if result <= 0: + let ret = SSLGetError(socket.sslHandle, result.cint) + case ret + of SSL_ERROR_ZERO_RETURN: + raiseSslError("TLS/SSL connection failed to initiate, socket closed prematurely.") + of SSL_ERROR_WANT_CONNECT, SSL_ERROR_WANT_ACCEPT: + raiseSslError("Unexpected error occurred.") # This should just not happen. + of SSL_ERROR_WANT_WRITE, SSL_ERROR_WANT_READ: + return 0 + of SSL_ERROR_WANT_X509_LOOKUP: + raiseSslError("Function for x509 lookup has been called.") + of SSL_ERROR_SYSCALL, SSL_ERROR_SSL: + raiseSslError() + else: raiseSslError("Unknown Error") + else: + return + if result == -1: + let err = osLastError() + when defined(windows): + if err.int32 == WSAEINPROGRESS: + return 0 + else: raiseOSError(err) + else: + if err.int32 == EAGAIN or err.int32 == EWOULDBLOCK: + return 0 + else: raiseOSError(err) + + +proc trySend*(socket: Socket, data: string): bool {.tags: [WriteIOEffect].} = + ## safe alternative to ``send``. Does not raise an EOS when an error occurs, + ## and instead returns ``false`` on failure. + result = send(socket, cstring(data), data.len) == data.len + +proc sendTo*(socket: Socket, address: string, port: Port, data: pointer, + size: int, af: Domain = AF_INET, flags = 0'i32): int {. + tags: [WriteIOEffect].} = + ## low-level sendTo proc. This proc sends ``data`` to the specified ``address``, + ## which may be an IP address or a hostname, if a hostname is specified + ## this function will try each IP of that hostname. + ## + ## **Note:** This proc is not available for SSL sockets. + var hints: AddrInfo + var aiList: ptr AddrInfo = nil + hints.ai_family = toInt(af) + hints.ai_socktype = toInt(SOCK_STREAM) + hints.ai_protocol = toInt(IPPROTO_TCP) + gaiNim(address, port, hints, aiList) + + # try all possibilities: + var success = false + var it = aiList + while it != nil: + result = sendto(socket.fd, data, size.cint, flags.cint, it.ai_addr, + it.ai_addrlen.SockLen) + if result != -1'i32: + success = true + break + it = it.ai_next + + freeaddrinfo(aiList) + +proc sendTo*(socket: Socket, address: string, port: Port, + data: string): int {.tags: [WriteIOEffect].} = + ## Friendlier version of the low-level ``sendTo``. + result = socket.sendTo(address, port, cstring(data), data.len) + +when defined(Windows): + const + IOCPARM_MASK = 127 + IOC_IN = int(-2147483648) + FIONBIO = IOC_IN.int32 or ((sizeof(int32) and IOCPARM_MASK) shl 16) or + (102 shl 8) or 126 + + proc ioctlsocket(s: SocketHandle, cmd: clong, + argptr: ptr clong): cint {. + stdcall, importc:"ioctlsocket", dynlib: "ws2_32.dll".} + +proc setBlocking(s: Socket, blocking: bool) = + when defined(Windows): + var mode = clong(ord(not blocking)) # 1 for non-blocking, 0 for blocking + if ioctlsocket(s.fd, FIONBIO, addr(mode)) == -1: + raiseOSError(osLastError()) + else: # BSD sockets + var x: int = fcntl(s.fd, F_GETFL, 0) + if x == -1: + raiseOSError(osLastError()) + else: + var mode = if blocking: x and not O_NONBLOCK else: x or O_NONBLOCK + if fcntl(s.fd, F_SETFL, mode) == -1: + raiseOSError(osLastError()) + s.nonblocking = not blocking + +discard """ proc setReuseAddr*(s: Socket) = + var blah: int = 1 + var mode = SO_REUSEADDR + if setsockopt(s.fd, SOL_SOCKET, mode, addr blah, TSOcklen(sizeof(int))) == -1: + raiseOSError(osLastError()) """ + +proc connect*(socket: Socket, address: string, port = Port(0), timeout: int, + af: Domain = AF_INET) {.tags: [ReadIOEffect, WriteIOEffect].} = + ## Connects to server as specified by ``address`` on port specified by ``port``. + ## + ## The ``timeout`` paremeter specifies the time in milliseconds to allow for + ## the connection to the server to be made. + let originalStatus = not socket.nonblocking + socket.setBlocking(false) + + socket.connectAsync(address, port, af) + var s: seq[Socket] = @[socket] + if selectWrite(s, timeout) != 1: + raise newException(TimeoutError, "Call to 'connect' timed out.") + else: + when defined(ssl): + if socket.isSSL: + socket.setBlocking(true) + doAssert socket.handshake() + socket.setBlocking(originalStatus) + +proc isSSL*(socket: Socket): bool = return socket.isSSL + ## Determines whether ``socket`` is a SSL socket. + +proc getFD*(socket: Socket): SocketHandle = return socket.fd + ## Returns the socket's file descriptor + +proc isBlocking*(socket: Socket): bool = not socket.nonblocking + ## Determines whether ``socket`` is blocking. + +when defined(Windows): + var wsa: WSAData + if wsaStartup(0x0101'i16, addr wsa) != 0: raiseOSError(osLastError()) + + |