ctucx.git: smartied

[nimlang] smarthome server

commit 822a7f347a07e47f904d5a99ed69787aadfaff9f
parent e6e7f78bf6b3d7a968f9a179f8d32565f4fdc30b
Author: Milan Pässler <me@pbb.lc>
Date: Sat, 27 Jul 2019 21:53:45 +0200

add accessToken
8 files changed, 86 insertions(+), 70 deletions(-)
M
config.json
|
9
+++++----
M
src/frontend_http.nim
|
11
++++++++---
M
src/frontend_tcp.nim
|
32
+++++++++-----------------------
M
src/frontend_ws.nim
|
26
+++++---------------------
M
src/smartied.nim
|
1
-
M
src/types.nim
|
11
++++++++++-
M
src/util.nim
|
61
+++++++++++++++++++++++++++++++++++++++++++++++--------------
M
src/vars.nim
|
5
++---
diff --git a/config.json b/config.json
@@ -96,9 +96,10 @@
 	"httpPort": 5000,
 	"tcpPort": 5001,
 	"wsPort": 5002,
-	"modbusAddr": "127.0.0.1",
-	"modbusPort": 5502,
-	"lacrosseAddr": "127.0.0.1",
+	"modbusAddr": "192.168.1.1",
+	"modbusPort": 502,
+	"lacrosseAddr": "192.168.1.1",
 	"lacrossePort": 2342,
-	"powermeterUpdateIntervalSec": 20
+	"powermeterUpdateIntervalSec": 20,
+	"accessToken": "passwort"
 }
diff --git a/src/frontend_http.nim b/src/frontend_http.nim
@@ -6,11 +6,16 @@ import sequtils
 import json
 import vars
 
-proc processHttpClient(req: Request) {.async.} =
+proc processHttpClient(req: Request) {.async,gcsafe.} =
   if req.reqMethod == HttpGet:
-    await req.respond(Http200, $(%* server.state))
+    if req.headers.hasKey("Authorization") and req.headers["Authorization"] == "Bearer " & server.config.accessToken:
+      await req.respond(Http200, $(%* server.state))
+    else:
+      await req.respond(Http401, "401 Unauthorized")
   elif req.reqMethod == HttpPost:
-    await req.respond(Http200, await tryHandleRequest(req.body))
+    let client = Client(id: lastClientId, sendProc: proc (msg: string) {.async.} = await req.respond(Http200, msg))
+    lastClientId += 1
+    await client.tryHandleRequest(req.body)
   else:
     await req.respond(Http405, "405 Method Not Allowed")
 
diff --git a/src/frontend_tcp.nim b/src/frontend_tcp.nim
@@ -6,39 +6,25 @@ import sequtils
 import json
 import vars
 
-proc broadcast(msg: string) {.locks: true.}=
-  for client in tcpClients:
-    try:
-      asyncCheck client.send(msg)
-    except:
-      client.close()
-      tcpClients.keepIf(proc(x: AsyncSocket): bool = x != client)
-
-proc processTcpClient(client: AsyncSocket) {.async.} =
+proc processTcpClient(sock: AsyncSocket) {.async.} =
+  let client = Client(id: lastClientId, sendProc: proc (msg: string) {.async.} = await sock.send(msg & '\n'))
+  lastClientId += 1
   try:
-    await client.send($(%*server.state))
     while true:
-      let req = await client.recvLine()
+      let req = await sock.recvLine()
       if req == "":
-        client.close()
+        sock.close()
         break
-
-      let resp = await tryHandleRequest(req)
-      await client.send(resp & '\n')
+      await client.tryHandleRequest(req)
   except:
-    client.close()
-    tcpClients.keepIf(proc(x: AsyncSocket): bool = x != client)
+    sock.close()
 
 proc serveTcp*() {.async.} =
-  registerBroadcastHandler(broadcast)
-  tcpClients = @[]
-
   var socket = newAsyncSocket()
   socket.setSockOpt(OptReuseAddr, true)
   socket.bindAddr(Port(server.config.tcpPort))
   socket.listen()
 
   while true:
-    let client = await socket.accept()
-    tcpClients.add(client)
-    asyncCheck processTcpClient(client)
+    let sock = await socket.accept()
+    asyncCheck processTcpClient(sock)
diff --git a/src/frontend_ws.nim b/src/frontend_ws.nim
@@ -7,20 +7,13 @@ import sequtils
 import json
 import vars
 
-proc broadcast(msg: string) {.locks: true.} =
-  for client in wsClients:
-    try:
-      asyncCheck client.send(msg)
-    except:
-      client.close()
-      wsClients.keepIf(proc(x: WebSocket): bool = x != client)
-
-proc processWsClient(req: Request) {.async.} =
+proc processWsClient(req: Request) {.async,gcsafe.} =
   var ws: WebSocket
+  var client: Client
   try:
     ws = await newWebsocket(req)
-    wsClients.add(ws)
-    await ws.send($(%*server.state))
+    lastClientId += 1
+    client = Client(id: lastClientId, sendProc: proc (msg: string) {.async.} = await ws.send(msg))
   except:
     asyncCheck req.respond(Http404, "404")
     return

@@ -28,19 +21,10 @@ proc processWsClient(req: Request) {.async.} =
   try:
     while true:
       let req = await ws.receiveStrPacket()
-      let resp = await tryHandleRequest(req)
-      await ws.send(resp)
+      await client.tryHandleRequest(req)
   except:
     ws.close()
-    wsClients.keepIf(proc(x: WebSocket): bool = x != ws)
 
 proc serveWs*() {.async.} =
-  registerBroadcastHandler(broadcast)
-  wsClients = @[]
-
-  addTimer(1000, false, proc (fd: AsyncFD): bool {.gcsafe.} =
-      broadcast("")
-    )
-
   var httpServer = newAsyncHttpServer()
   await httpServer.serve(Port(server.config.wsPort), processWsClient)
diff --git a/src/smartied.nim b/src/smartied.nim
@@ -13,7 +13,6 @@ import util
 import tables
 
 server = Server(config: parseJson(readFile("./config.json")).to(Config))
-echo server.state
 
 initUtil()
 initModbus()
diff --git a/src/types.nim b/src/types.nim
@@ -1,4 +1,5 @@
 import asyncnet
+import asyncdispatch
 import json
 import tables
 import sequtils

@@ -30,6 +31,7 @@ type Config* = object
   powermeterUpdateIntervalSec*: uint
   devices*: Table[string, DeviceConfig]
   clientConfigs*: Table[string, JsonNode]
+  accessToken*: string
 
 type DeviceState* = object
   case type*: DeviceType

@@ -56,9 +58,11 @@ type Server* = object
 type ActionType* = enum
   SetRelayAction,
   ToggleRelayAction,
-  GetClientConfigAction
+  GetClientConfigAction,
+  SetSubscriptionStateAction
 
 type Action* = object
+  accessToken*: string
   case type*: ActionType
   of SetRelayAction:
     setRelayBoard*: string

@@ -69,6 +73,8 @@ type Action* = object
     toggleRelay*: uint8
   of GetClientConfigAction:
     configName*: string
+  of SetSubscriptionStateAction:
+    subscribed*: bool
 
 type ResponseStatus* = enum
   Err,

@@ -86,3 +92,6 @@ type LacrosseMessage* = object
 
 type modbus* = pointer
 
+type Client* = object
+  sendProc*: proc (msg: string): Future[void] {.gcsafe.}
+  id*: int
diff --git a/src/util.nim b/src/util.nim
@@ -5,21 +5,39 @@ import types
 import tables
 import backend_relayboard
 import vars
+import sequtils
 
-proc initUtil*() =
-  broadcastHandlers = @[]
-
-proc registerBroadcastHandler*(handler: proc (msg: string) {.gcsafe, locks: true.}) =
-  broadcastHandlers.add(handler)
+proc trySend*(client: Client, msg: string) {.async.} =
+  try:
+    await client.sendProc(msg)
+  except:
+    let e = getCurrentException()
+    echo("error while sending data to client: ", e.msg)
+    echo("removing client ", client.id)
+    echo clients.map(proc(x: Client): int = x.id)
+    clients.keepIf(proc(x: Client): bool = x.id != client.id)
+    echo clients.map(proc(x: Client): int = x.id)
 
 proc broadcast*(msg: string) =
-  for broadcastHandler in broadcastHandlers:
-    broadcastHandler(msg)
+  for client in clients.filter(proc (x: Client): bool = true):
+    asyncCheck client.trySend(msg)
 
-proc handleRequest*(req: string): Future[JsonNode] {.async.} =
+proc initUtil*() =
+  addTimer(1000, false, proc (fd: AsyncFD): bool {.gcsafe.} =
+      broadcast("")
+    )
+
+  lastClientId = 1
+  clients = @[]
+
+proc handleRequest*(client: Client, req: string): Future[JsonNode] {.async.} =
   let action = parseJson(req).to(Action)
 
-  if action.type == SetRelayAction:
+  if action.accessToken != server.config.accessToken:
+    raise newException(OsError, "invalid accessToken")
+
+  case action.type
+  of SetRelayAction:
     let config = server.config.devices[action.setRelayBoard]
 
     server.state[action.setRelayBoard].relays[action.setRelay] = action.setValue

@@ -27,7 +45,7 @@ proc handleRequest*(req: string): Future[JsonNode] {.async.} =
 
     broadcast($(%*server.state))
     return JsonNode()
-  elif action.type == ToggleRelayAction:
+  of ToggleRelayAction:
     let config = server.config.devices[action.toggleRelayBoard]
 
     server.state[action.toggleRelayBoard].relays[action.toggleRelay] = not server.state[action.toggleRelayBoard].relays[action.toggleRelay]

@@ -35,16 +53,31 @@ proc handleRequest*(req: string): Future[JsonNode] {.async.} =
 
     broadcast($(%*server.state))
     return JsonNode()
-  elif action.type == GetClientConfigAction:
+  of GetClientConfigAction:
     let clientConfig: JsonNode = server.config.clientConfigs[action.configName]
     return clientConfig
+  of SetSubscriptionStateAction:
+    if action.subscribed:
+      echo("adding client ", client.id)
+      echo clients.map(proc(x: Client): int = x.id)
+      clients.keepIf(proc(x: Client): bool = x.id != client.id)
+      echo clients.map(proc(x: Client): int = x.id)
+      clients.add(client)
+      echo clients.map(proc(x: Client): int = x.id)
+      await client.sendProc($(%*server.state))
+    else:
+      echo("removing client ", client.id)
+      echo clients.map(proc(x: Client): int = x.id)
+      clients.keepIf(proc(x: Client): bool = x.id != client.id)
+      echo clients.map(proc(x: Client): int = x.id)
+    return JsonNode()
 
-proc tryHandleRequest*(req: string): Future[string] {.async.} =
+proc tryHandleRequest*(client: Client, req: string): Future[void] {.async.} =
   GC_fullCollect()
   var resp: Response
   try:
-    resp = Response(status: Ok, data: await handleRequest(req))
+    resp = Response(status: Ok, data: await client.handleRequest(req))
   except:
     let e = getCurrentException()
     resp = Response(status: Err, data: newJString(e.msg))
-  return $(%*resp)
+  await client.sendProc($(%*resp))
diff --git a/src/vars.nim b/src/vars.nim
@@ -2,8 +2,7 @@ import types
 import ws
 import asyncnet
 
-var wsClients* {.threadvar.}: seq[WebSocket]
-var tcpClients* {.threadvar.}: seq[AsyncSocket]
+var clients* {.threadvar.}: seq[Client]
 var server* {.threadvar.}: Server
 var mb* {.threadvar.}: modbus
-var broadcastHandlers* {.threadvar.}: seq[proc (msg: string) {.gcsafe.}]
+var lastClientId* {.threadvar.}: int