ctucx.git: fritzbox-exporter

prometheus exporter for lte fritzboxes [used @ fbexporter.ctu.cx]

commit c9329772b7d5427289ddb534b35bb2321f4bb50d
parent 05c65de8e553773ddd61438d9b77955b8669eee5
Author: ctucx <c@ctu.cx>
Date: Mon, 20 Apr 2020 02:51:40 +0200

foo
2 files changed, 355 insertions(+), 149 deletions(-)
A
src/asyncHttpServer.nim
|
219
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
M
src/fb_exporter.nim
|
285
++++++++++++++++++++++++++++++++++++++-----------------------------------------
diff --git a/src/asyncHttpServer.nim b/src/asyncHttpServer.nim
@@ -0,0 +1,218 @@
+import asyncdispatch, asynchttpserver, macros
+import strutils, parseutils
+import cookies, uri, times, os, re, json
+import tables
+
+export asyncdispatch, asynchttpserver, cookies, uri
+export strutils, re
+export tables, json
+
+
+type
+  ServerRef* = ref object
+    port*: uint16
+    address*: string
+    server*: AsyncHttpServer
+
+proc newServer* (address: string = "0.0.0.0", port: uint16 = 5000): ServerRef =
+  return ServerRef(
+    address: address,
+    port: port,
+    server: newAsyncHttpServer()
+  )
+
+proc cookies*(req: Request): Table[string, string] =
+  result = initTable[string, string]()
+
+  if (let cookie = req.headers.getOrDefault("Cookie"); cookie != ""):
+    var i = 0
+    while true:
+      i += skipWhile(cookie, {' ', '\t'}, i)
+      var keystart = i
+      i += skipUntil(cookie, {'='}, i)
+      var keyend = i-1
+      if i >= len(cookie): break
+      inc(i) # skip '='
+      var valstart = i
+      i += skipUntil(cookie, {';'}, i)
+      result[substr(cookie, keystart, keyend)] = substr(cookie, valstart, i-1)
+      if i >= len(cookie): break
+      inc(i) # skip ';'
+
+proc parseQuery*(data: string): Table[string, string] =
+  result = initTable[string, string]()
+
+  let data = data.split("&")
+  for i in data:
+    let timed = i.split("=")
+    if timed.len > 1:
+      result.add(decodeUrl(timed[0]), decodeUrl(timed[1]))
+
+proc daysForward*(days: int): DateTime =
+  return getTime().utc + initInterval(days = days)
+
+template setCookie* (key: string, value: string, time: DateTime) =
+  headers.add("Set-Cookie", setCookie(key, value, time, request.headers["host"], noName=true))
+
+proc redirect* (request: Request, url: string) {.async.}=
+  await request.respond(Http303, "", newHttpHeaders([("Location", url)]))
+
+proc respondJson* (request: Request, httpCode: HttpCode, status: string, message: string, data: JsonNode) {.async.} = 
+  let response = %* {
+      "status": status,
+      "msg":    message,
+      "data":   data
+    }
+
+  await request.respond(httpCode, $response, newHttpHeaders([("Content-Type","application/json")]))
+
+macro pages*(server: ServerRef, body: untyped): untyped =
+  ## This macro provides convenient page adding.
+  ##
+  ## `body` should be StmtList.
+  ## page type can be:
+  ## -   ``equals``
+  ## -   ``startsWith``
+  ## -   ``endsWith``
+  ## -   ``regex``
+  ## -   ``notfound`` - this page uses without URL argument.
+  ##
+  ## When a new request to the server is received, variables are automatically created:
+  ## -   ``request`` - new Request.
+  ## -   ``url`` - matched URL.
+  ##     -   ``equals`` - URL is request.url.path
+  ##     -   ``startsWith`` - URL is text after `startswith`.
+  ##     -   ``endsWith`` - URL is text before `endswith`.
+  ##     -   ``regex`` - `url` param not created.
+  ##     -   ``notfound`` - `url` param not created.
+  ## -   ``decoded_url`` - URL always is request.url.path
+  var
+    stmtlist = newStmtList()
+    notfound_declaration = false
+
+  stmtlist.add(
+    newNimNode(nnkLetSection).add(  # let urlParams: JsonNode = await parseQuery(request)
+      newNimNode(nnkIdentDefs).add(  # let decode_url: string = decodeUrl(request.url.path)
+        ident("decoded_url"),
+        ident("string"),
+        newCall(
+          "decodeUrl",
+          newNimNode(nnkDotExpr).add(
+            newNimNode(nnkDotExpr).add(
+              ident("request"), ident("url")
+            ),
+            ident("path")
+          )
+        )
+      )
+    )
+  )
+  stmtlist.add(newNimNode(nnkIfStmt))
+  var ifstmtlist = stmtlist[1]
+
+  for i in body:  # for each page in statment list.
+    let
+      current = if i.len == 3: $i[0] else: "equals"
+      path = if i.len == 3: i[1] else: i[0]
+      slist = if i.len == 3: i[2] else: i[1]
+    if (i.kind == nnkCall and
+        (path.kind == nnkStrLit or path.kind == nnkCallStrLit or path.kind == nnkEmpty) and
+        slist.kind == nnkStmtList):
+      if current == "equals":
+        slist.insert(0,  # let url: string = `path`
+          newNimNode(nnkLetSection).add(
+            newNimNode(nnkIdentDefs).add(
+              ident("url"), ident("string"), path
+            )
+          )
+        )
+        ifstmtlist.add(  # decoded_url == `path`
+          newNimNode(nnkElifBranch).add(
+            newCall("==", path, ident("decoded_url")),
+            slist
+          )
+        )
+      elif current == "startsWith":
+        slist.insert(0,  # let url = decoded_url[`path`.len..^1]
+          newNimNode(nnkLetSection).add(
+            newNimNode(nnkIdentDefs).add(
+              ident("url"),
+              ident("string"),
+              newCall(
+                "[]",
+                ident("decoded_url"),
+                newCall("..^", newCall("len", path), newLit(1))
+              )
+            )
+          )
+        )
+        ifstmtlist.add(  # decode_url.startsWith(`path`)
+          newNimNode(nnkElifBranch).add(
+            newCall("startsWith", ident("decoded_url"), path),
+            slist
+            )
+          )
+      elif current == "endsWith":
+        slist.insert(0,  # let url: string = decoded_url[0..^`path`.len]
+          newNimNode(nnkLetSection).add(
+            newNimNode(nnkIdentDefs).add(
+              ident("url"),
+              ident("string"),
+              newCall(
+                "[]",
+                ident("decoded_url"),
+                newCall(
+                  "..^", newLit(0), newCall("+", newLit(1), newCall("len", path))
+                )
+              )
+            )
+          )
+        )
+        ifstmtlist.add(  # decode_url.endsWith(`path`)
+          newNimNode(nnkElifBranch).add(
+            newCall("endsWith", ident("decoded_url"), path),
+            slist
+          )
+        )
+      elif current == "regex":
+        ifstmtlist.add(  # decode_url.match(`path`)
+          newNimNode(nnkElifBranch).add(
+            newCall("match", ident("decoded_url"), path),
+            slist))
+      elif current == "notfound":
+        notfound_declaration = true
+        ifstmtlist.add(newNimNode(nnkElse).add(slist))
+
+  if not notfound_declaration:
+    ifstmtlist.add(
+      newNimNode(nnkElse).add(
+        newCall(  # await request.respond(Http404, "Not found")
+          "await",
+          newCall("respond", ident("request"), ident("Http404"), newLit("Not found"))
+        )
+      )
+    )
+
+  result = newNimNode(nnkProcDef).add(
+    ident("receivepages"),  # procedure name.
+    newEmptyNode(),  # for template and macros
+    newEmptyNode(),  # generics
+    newNimNode(nnkFormalParams).add(  # proc params
+      newEmptyNode(),  # return type
+      newNimNode(nnkIdentDefs).add(  # param
+        ident("request"),  # param name
+        ident("Request"),  # param type
+        newEmptyNode()  # param default value
+      )
+    ),
+    newNimNode(nnkPragma).add(  # pragma declaration
+      ident("async"),
+      ident("gcsafe")
+    ),
+    newEmptyNode(),
+    stmtlist)
+
+macro start*(server: ServerRef): untyped =
+  result = quote do:
+    echo "Server starts on http://", `server`.address, ":", `server`.port
+    waitFor `server`.server.serve(Port(`server`.port), receivepages, `server`.address)+
\ No newline at end of file
diff --git a/src/fb_exporter.nim b/src/fb_exporter.nim
@@ -1,38 +1,25 @@
-import asynchttpserver
-import asyncdispatch
-import json
-import strutils
-import times
+import times, posix
+import asyncHttpServer
 
-# config
-var httpPort {.threadvar.}: uint16
-var authToken {.threadvar.}: string
+proc CtrlCHook() {.noconv.} =
+  echo "Ctrl+C fired! \nStopping Server now!"
+  quit()
 
-# state
-var mobiled_lastUpdated:int64 = 0
-var mobiled_data {.threadvar.}: JsonNode
-
-var inetstat_lastUpdated:int64 = 0
-var inetstat_data {.threadvar.}: JsonNode
-
-
-# main
-proc isInt*(s: string): bool =
+proc isInt* (s: string): bool =
   try:
     discard s.parseInt()
     result = true
   except:
     discard
 
-proc isFloat*(s: string): bool =
+proc isFloat* (s: string): bool =
   try:
     discard s.parseFloat()
     result = true
   except:
     discard
 
-
-proc parse_ctlmgr(n: string): JsonNode =
+proc parseCtlMgr* (n: string): JsonNode =
   var input = n.split("\n")
 
   input.del(0)

@@ -75,136 +62,135 @@ proc parse_ctlmgr(n: string): JsonNode =
         data[last_group].add(entry[0], %(value))
   return data
 
-proc processHttpClient(req: Request) {.async.} =
-  if req.reqMethod == HttpGet:
-
-    if inetstat_data != nil or mobiled_data != nil:
-      if req.url.path == "/metrics":
-        var res = ""
-
-        var total_transmit   = 0
-        var total_receive    = 0
-        var month_transmit   = 0
-        var month_receive    = 0
-        var today_transmit   = 0
-        var today_receive    = 0
-        
-        var upstream         = 0
-        var downstream       = 0
-        
-        var cell0_technology = ""
-        var cell0_quality    = 0
-        var cell0_band       = 0
-        var cell0_distance   = 0
-
-        var cell1_technology = ""
-        var cell1_quality    = 0
-        var cell1_band       = 0
-        var cell1_distance   = 0
-
-        if inetstat_lastUpdated != 0:
-          if inetstat_data.hasKey("Total0"):
-            #total
-            if inetstat_data["Total0"].hasKey("BytesSentHigh") and inetstat_data["Total0"].hasKey("BytesSentLow"):
-              total_transmit = inetstat_data["Total0"]["BytesSentHigh"].getInt() shl 32 + inetstat_data["Total0"]["BytesSentLow"].getInt()
-            if inetstat_data["Total0"].hasKey("BytesReceivedHigh") and inetstat_data["Total0"].hasKey("BytesReceivedLow"):
-              total_receive = inetstat_data["Total0"]["BytesReceivedHigh"].getInt() shl 32 + inetstat_data["Total0"]["BytesReceivedLow"].getInt()
-
-            #month
-            if inetstat_data["ThisMonth0"].hasKey("BytesSentHigh") and inetstat_data["ThisMonth0"].hasKey("BytesSentLow"):
-              month_transmit = inetstat_data["ThisMonth0"]["BytesSentHigh"].getInt() shl 32 + inetstat_data["ThisMonth0"]["BytesSentLow"].getInt()
-            if inetstat_data["ThisMonth0"].hasKey("BytesReceivedHigh") and inetstat_data["ThisMonth0"].hasKey("BytesReceivedLow"):
-              month_receive = inetstat_data["ThisMonth0"]["BytesReceivedHigh"].getInt() shl 32 + inetstat_data["ThisMonth0"]["BytesReceivedLow"].getInt()
-                         
-            #today
-            if inetstat_data["Today0"].hasKey("BytesSentHigh") and inetstat_data["Today0"].hasKey("BytesSentLow"):
-              today_transmit = inetstat_data["Today0"]["BytesSentHigh"].getInt() shl 32 + inetstat_data["Today0"]["BytesSentLow"].getInt()
-            if inetstat_data["Today0"].hasKey("BytesReceivedHigh") and inetstat_data["Today0"].hasKey("BytesReceivedLow"):
-              today_receive = inetstat_data["Today0"]["BytesReceivedHigh"].getInt() shl 32 + inetstat_data["Today0"]["BytesReceivedLow"].getInt()
-
-            #total     
-            res &= "fritzbox_network_transmit_bytes_total " & $(total_transmit) & " " & $(inetstat_lastUpdated * 1000) & "\n"
-            res &= "fritzbox_network_receive_bytes_total " & $(total_receive) & " " & $(inetstat_lastUpdated * 1000) & "\n"
-
-            #month
-            res &= "fritzbox_network_transmit_bytes_month " & $(month_transmit) & " " & $(inetstat_lastUpdated * 1000) & "\n"
-            res &= "fritzbox_network_receive_bytes_month " & $(month_receive) & " " & $(inetstat_lastUpdated * 1000) & "\n"
-
-            #today
-            res &= "fritzbox_network_transmit_bytes_today " & $(today_transmit) & " " & $(inetstat_lastUpdated * 1000) & "\n"
-            res &= "fritzbox_network_receive_bytes_today " & $(today_receive) & " " & $(inetstat_lastUpdated * 1000) & "\n"
-
-        if mobiled_lastUpdated != 0:
-          if mobiled_data.hasKey("ue0"):
-            if mobiled_data["ue0"].hasKey("conn_rate_rx"): downstream = mobiled_data["ue0"]["conn_rate_rx"].getInt()
-            if mobiled_data["ue0"].hasKey("conn_rate_tx"): upstream = mobiled_data["ue0"]["conn_rate_tx"].getInt()
-             
-            if mobiled_data["ue0"].hasKey("conn_cell"):
-              let cells = mobiled_data["ue0"]["conn_cell"].getStr.split(",")
-
-              var cell0 = ""
-              var cell1 = ""
-              
-              if cells[0] != "" and mobiled_data.hasKey("cell"&cells[0]): cell0 = "cell"&cells[0]
-              if cells[1] != "" and mobiled_data.hasKey("cell"&cells[1]): cell1 = "cell"&cells[1]
-                
-              if cell0 != "":
-                if mobiled_data[cell0].hasKey("technology"): cell0_technology = mobiled_data[cell0]["technology"].getStr()
-                if mobiled_data[cell0].hasKey("quality"): cell0_quality = mobiled_data[cell0]["quality"].getInt()
-                if mobiled_data[cell0].hasKey("band"): cell0_band = mobiled_data[cell0]["band"].getInt()
-                if mobiled_data[cell0].hasKey("distance"): cell0_distance = mobiled_data[cell0]["distance"].getInt()
-
-              if cell1 != "":
-                if mobiled_data[cell1].hasKey("technology"): cell1_technology = mobiled_data[cell1]["technology"].getStr()
-                if mobiled_data[cell1].hasKey("quality"): cell1_quality = mobiled_data[cell1]["quality"].getInt()
-                if mobiled_data[cell1].hasKey("band"): cell1_band = mobiled_data[cell1]["band"].getInt()
-                if mobiled_data[cell1].hasKey("distance"): cell1_distance = mobiled_data[cell1]["distance"].getInt()
-
-                
-
-          res &= "fritzbox_network_downstram " & $(downstream) & " " & $(mobiled_lastUpdated * 1000) & "\n"
-          res &= "fritzbox_network_upstream " & $(upstream) & " " & $(mobiled_lastUpdated * 1000) & "\n"
-
-          if cell0_band != 0: res &= "fritzbox_network_band{cell=\"0\"} " & $(cell0_band) & " " & $(mobiled_lastUpdated * 1000) & "\n"
-          if cell0_quality != 0: res &= "fritzbox_network_quality{cell=\"0\"} " & $(cell0_quality) & " " & $(mobiled_lastUpdated * 1000) & "\n"
-          if cell0_distance != 0: res &= "fritzbox_network_distance{cell=\"0\"} " & $(cell0_distance/1000) & " " & $(mobiled_lastUpdated * 1000) & "\n"
-
-          if cell1_band != 0: res &= "fritzbox_network_band{cell=\"1\"} " & $(cell1_band) & " " & $(mobiled_lastUpdated * 1000) & "\n"
-          if cell1_quality != 0: res &= "fritzbox_network_quality{cell=\"1\"} " & $(cell1_quality) & " " & $(mobiled_lastUpdated * 1000) & "\n"
-          if cell1_distance != 0: res &= "fritzbox_network_distance{cell=\"1\"} " & $(cell1_distance/1000) & " " & $(mobiled_lastUpdated * 1000) & "\n"
-
-          await req.respond(Http200, res)
-      elif req.url.path == "/inetstat.json":
-        await req.respond(Http200, $(%* inetstat_data))
-      elif req.url.path == "/mobiled.json":
-        await req.respond(Http200, $(%* mobiled_data))
+proc getTxTraffic (data: JsonNode, mode: string): int =
+  if data[mode].hasKey("BytesSentHigh") and data[mode].hasKey("BytesSentLow"):
+    return data[mode]["BytesSentHigh"].getInt shl 32 + data[mode]["BytesSentLow"].getInt
+
+proc getRxTraffic (data: JsonNode, mode: string): int =
+  if data[mode].hasKey("BytesReceivedHigh") and data[mode].hasKey("BytesReceivedLow"):
+    return data[mode]["BytesReceivedHigh"].getInt shl 32 + data[mode]["BytesReceivedLow"].getInt
+
+proc prometheusResponse* (request: Request, state: JsonNode) {.async.} = 
+  var res = ""
+
+  if state["inetstat"]["lastUpdated"].getInt == 0 and state["mobiled"]["lastUpdated"].getInt == 0:
+    await request.respond(Http500, "500 No data yet")
+
+  if state["inetstat"]["lastUpdated"].getInt != 0:
+    let data        = state["inetstat"]["data"]
+    let lastUpdated = state["inetstat"]["lastUpdated"].getInt
+
+    if data.hasKey("Total0"):
+      #total
+      res &= "fritzbox_network_transmit_bytes_total " & $(data.getTxTraffic("Total0")) & " " & $(lastUpdated * 1000) & "\n"
+      res &= "fritzbox_network_receive_bytes_total " & $(data.getRxTraffic("Total0")) & " " & $(lastUpdated * 1000) & "\n"
+
+      #month
+      res &= "fritzbox_network_transmit_bytes_month " & $(data.getTxTraffic("ThisMonth0")) & " " & $(lastUpdated * 1000) & "\n"
+      res &= "fritzbox_network_receive_bytes_month " & $(data.getRxTraffic("ThisMonth0")) & " " & $(lastUpdated * 1000) & "\n"
+
+      #today
+      res &= "fritzbox_network_transmit_bytes_today " & $(data.getTxTraffic("Today0")) & " " & $(lastUpdated * 1000) & "\n"
+      res &= "fritzbox_network_receive_bytes_today " & $(data.getRxTraffic("Today0")) & " " & $(lastUpdated * 1000) & "\n"
+
+
+  if state["mobiled"]["lastUpdated"].getInt != 0:
+    let data        = state["mobiled"]["data"]
+    let lastUpdated = state["mobiled"]["lastUpdated"].getInt()
+
+    if data.hasKey("ue0"):
+      if data["ue0"].hasKey("conn_rate_rx"):
+        let downstream = data["ue0"]["conn_rate_rx"].getInt()
+        res &= "fritzbox_network_downstram " & $(downstream) & " " & $(lastUpdated * 1000) & "\n"
+
+
+      if data["ue0"].hasKey("conn_rate_tx"):
+        let upstream = data["ue0"]["conn_rate_tx"].getInt()
+        res &= "fritzbox_network_upstream " & $(upstream) & " " & $(lastUpdated * 1000) & "\n"
+
+ 
+      if data["ue0"].hasKey("conn_cell"):
+        template parseCell(data: JsonNode, num: int) = 
+          let cells = data["ue0"]["conn_cell"].getStr.split(",")
+
+          if cells[num] != "" and data.hasKey("cell"&cells[num]):
+            let cell = "cell"&cells[num]
+
+            if cell != "":
+              if data[cell].hasKey("technology"):
+                let cell_technology = data[cell]["technology"].getStr
+
+              if data[cell].hasKey("quality"):
+                let cell_quality = data[cell]["quality"].getInt
+                res &= "fritzbox_network_quality{cell=\"" & $num & "\"} " & $(cell_quality) & " " & $(lastUpdated * 1000) & "\n"
+
+              if data[cell].hasKey("band"):
+                let cell_band = data[cell]["band"].getInt
+                res &= "fritzbox_network_band{cell=\"" & $num & "\"} " & $(cell_band) & " " & $(lastUpdated * 1000) & "\n"
+
+              if data[cell].hasKey("distance"):
+                let cell_distance = data[cell]["distance"].getInt
+                res &= "fritzbox_network_distance{cell=\"" & $num & "\"} " & $(cell_distance/1000) & " " & $(lastUpdated * 1000) & "\n"
+
+        data.parseCell(0) 
+        data.parseCell(1)
+
+  await request.respond(Http200, res)
+
+proc main =  # for gcsafe
+  setControlCHook(CtrlCHook)
+
+  onSignal(SIGTERM):
+    echo "Got SIGTERM! \nStopping Server now!"
+    quit()
+
+  let authToken = "penis123"
+  var server    = newServer()  # launch on http://localhost:5000
+  var state     = %* {
+      "mobiled": {
+          "data": {},
+          "lastUpdated": 0,
+        },
+      "mobiled": {
+          "data": {},
+          "lastUpdated": 0,
+        }
+    }
+
+  server.pages:
+    equals("/metrics"):
+      await request.prometheusResponse(state)
+
+    equals("/inetstat.json"):
+      if state["inetstat"]["lastUpdated"].getInt != 0:
+        await request.respondJson(Http200, "success", "", state["inetstat"]["data"])
       else:
-        await req.respond(Http404, "404 Not found")
-    else:
-      await req.respond(Http500, "500 No data yet")
-
-  elif req.reqMethod == HttpPost:
-    if req.url.query == authToken:
-      if req.url.path == "/update/inetstat":
-        inetstat_data = parse_ctlmgr(req.body)
-        inetstat_lastUpdated = toUnix(getTime())
-        await req.respond(Http200, "Noted, thanks")
-      elif req.url.path == "/update/mobiled":
-        mobiled_data = parse_ctlmgr(req.body)
-        mobiled_lastUpdated = toUnix(getTime())
-        await req.respond(Http200, "Noted, thanks")
+        await request.respondJson(Http404, "error", "no data yet", newJObject())
+
+    equals("/mobiled.json"):
+      if state["mobiled"]["lastUpdated"].getInt != 0:
+        await request.respondJson(Http200, "success", "", state["mobiled"]["data"])
       else:
-        await req.respond(Http404, "404 Not found")
-    else:
-      await req.respond(Http401, "401 Unauthorized")
+        await request.respondJson(Http404, "error", "no data yet", newJObject())
+
+    startsWith("/update/"):
+      if request.reqMethod == HttpPost:
+        if request.url.query == authToken:
+          if url == "inetstat":
+            state["inetstat"]["data"]        =  parseCtlMgr(request.body)
+            state["inetstat"]["lastUpdated"] = %toUnix(getTime())
+            await request.respond(Http200, "Noted, thanks")
+
+          elif url == "mobiled":
+            state["mobiled"]["data"]         =  parseCtlMgr(request.body)
+            state["mobiled"]["lastUpdated"]  = %toUnix(getTime())
+            await request.respond(Http200, "Noted, thanks")
 
-  else:
-    await req.respond(Http405, "405 Method Not Allowed")
+          else:
+            await request.respond(Http404, "404 Not found")
+        else:
+          await request.respond(Http401, "401 Unauthorized")
 
-proc serveHttp*() {.async.} =
-  httpPort = 1234
-  authToken = "penis123"
-  var httpServer = newAsyncHttpServer()
-  await httpServer.serve(Port(httpPort), processHttpClient)
+  server.start()
 
-waitFor serveHttp()
+main()+
\ No newline at end of file