Skip to content

Commit a8f111e

Browse files
committed
Adding logic to allow the plugin to attempt to get a oneshot token from moonraker.
1 parent 6785efe commit a8f111e

File tree

4 files changed

+86
-16
lines changed

4 files changed

+86
-16
lines changed

.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@
185185
"octowebstreamwshelper",
186186
"oeapi",
187187
"oestreamboundary",
188+
"oneshot",
188189
"openwrt",
189190
"opkg",
190191
"oprint",

moonraker_octoeverywhere/moonrakerclient.py

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,9 @@ def __init__(self, logger:logging.Logger, config:Config, moonrakerConfigFilePath
113113
# This is found and set when we try to connect and we fail due to an unauthed socket.
114114
# It can also be set by the user in the config.
115115
self.MoonrakerApiKey = self.Config.GetStr(Config.MoonrakerSection, Config.MoonrakerApiKey, None, keepInConfigIfNone=True)
116+
# Same idea, but sometimes we need to get the oneshot_token to access the system.
117+
# This code is only valid for 5 seconds, so it will be retrieved when we need it.
118+
self.OneshotToken = None
116119

117120
# Setup the WS vars and a websocket worker thread.
118121
# Don't run it until StartRunningIfNotAlready is called!
@@ -507,8 +510,14 @@ def _WebSocketWorkerThread(self):
507510
# We know if the WS is connected, the host and port must be correct.
508511
self._UpdateMoonrakerHostAndPort()
509512

510-
# Create a websocket client and start it connecting.
513+
# Build the URL, include the oneshot token if we have one.
511514
url = "ws://"+self.MoonrakerHostAndPort+"/websocket"
515+
if self.OneshotToken is not None:
516+
url += "?token="+self.OneshotToken
517+
# Since the one shot token expires after 5 seconds, we need to clear it out.
518+
self.OneshotToken = None
519+
520+
# Create a websocket client and start it connecting.
512521
self.Logger.info("Connecting to moonraker: "+url)
513522
with self.WebSocketLock:
514523
self.WebSocket = Client(url,
@@ -542,13 +551,13 @@ def _WebSocketWorkerThread(self):
542551

543552
# This will only happen if the websocket closes or there was an error.
544553
# Sleep for a bit so we don't spam the system with attempts.
554+
# Note that if we failed auth but got a oneshot token it will expire in 5 seconds, so we need to retry quickly.
545555
time.sleep(2.0)
546556

547557

548558
# Based on the docs: https://moonraker.readthedocs.io/en/latest/web_api/#websocket-setup
549559
# After the websocket is open, we need to do this sequence to make sure the system is healthy and ready.
550560
def _AfterOpenReadyWaiter(self, targetWsObjRef):
551-
552561
logCounter = 0
553562
self.Logger.info("Moonraker client waiting for klippy ready...")
554563
try:
@@ -589,16 +598,15 @@ def _AfterOpenReadyWaiter(self, targetWsObjRef):
589598
if result.HasError():
590599
# Check if the error is Unauthorized, in which case we need to try to get credentials.
591600
if result.ErrorCode == -32602 or result.ErrorStr == "Unauthorized":
592-
self.Logger.info("Our websocket connection to moonraker needs auth, trying to get the API key...")
593-
self.MoonrakerApiKey = MoonrakerCredentialManager.Get().TryToGetApiKey()
594-
if self.MoonrakerApiKey is None:
595-
self.Logger.error("Our websocket connection to moonraker needs auth and we failed to get the API key.")
596-
raise Exception("Websocket unauthorized.")
597-
else:
601+
if self._TryToGetWebsocketAuth():
602+
# On success, shut down the websocket so we do the reconnect logic.
598603
self.Logger.info("Successfully got the API key, restarting the websocket to try again using it.")
599-
# Shut down the websocket so we do the reconnect logic.
600604
self._RestartWebsocket()
601605
return
606+
# Since we know we will keep failing, sleep for a while to avoid spamming the logs and so the user sees this error.
607+
self.Logger.error("!!!! Moonraker auth is required, so you must re-run the OctoEverywhere installer or generate an API key in Mainsail or Fluidd and set it the octoeverywhere.conf. The octoeverywhere.conf config file can be found in /data for docker or ~/.octoeverywhere*/ for CLI installs")
608+
time.sleep(10)
609+
raise Exception("Websocket unauthorized.")
602610

603611
# Handle the timeout without throwing, since this happens sometimes when the system is down.
604612
if result.ErrorCode == JsonRpcResponse.OE_ERROR_TIMEOUT:
@@ -645,6 +653,34 @@ def _AfterOpenReadyWaiter(self, targetWsObjRef):
645653
self._RestartWebsocket()
646654

647655

656+
# Attempts to update the moonraker api key or one shot token.
657+
# Returns true if it's able to get one of them.
658+
def _TryToGetWebsocketAuth(self) -> bool:
659+
# First, try to get an API key
660+
# If we are running locally, we should be able to connect to the unix socket and always get it.
661+
self.Logger.info("Our websocket connection to moonraker needs auth, trying to get the API key...")
662+
newApiKey = MoonrakerCredentialManager.Get().TryToGetApiKey()
663+
if newApiKey is not None:
664+
# If we got a new API key, use it now.
665+
self.Logger.info("Successfully got a new API key from Moonraker.")
666+
self.MoonrakerApiKey = newApiKey
667+
return True
668+
669+
# If we didn't get an new API key, try to get a oneshot token.
670+
# Note that we might already have an existing Moonraker API key, so we will include it incase.
671+
#
672+
# For some systems we need auth for the websocket, but no auth for the oneshot token API, which makes no sense.
673+
# But if that's the case, we try to get a one_shot token.
674+
self.OneshotToken = MoonrakerCredentialManager.Get().TryToGetOneshotToken(self.MoonrakerApiKey)
675+
if self.OneshotToken is None:
676+
# If we got a one shot token, use it.
677+
self.Logger.info("Successfully got a new oneshot token from Moonraker.")
678+
return True
679+
680+
# If we got here, we failed to get either.
681+
return False
682+
683+
648684
# Kills the current websocket connection. Our logic will auto try to reconnect.
649685
def _RestartWebsocket(self):
650686
with self.WebSocketLock:

moonraker_octoeverywhere/moonrakercredentailmanager.py

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,13 @@
55
import logging
66
import time
77

8+
from typing import Optional
9+
810
import configparser
911

1012
from octoeverywhere.sentry import Sentry
13+
from octoeverywhere.Proto.PathTypes import PathTypes
14+
from octoeverywhere.octohttprequest import OctoHttpRequest
1115

1216
# A class that handles trying to get user credentials from Moonraker if needed.
1317
#
@@ -46,12 +50,41 @@ def __init__(self, logger:logging.Logger, moonrakerConfigFilePath:str, isCompani
4650
self.IsCompanionMode = isCompanionMode
4751

4852

49-
def TryToGetApiKey(self) -> str or None:
53+
# Attempts to get the API key from moonraker. If it fails, it will return None.
54+
def TryToGetOneshotToken(self, apiKey:str=None) -> Optional[str]:
55+
try:
56+
# If we got an API key, try to set it.
57+
headers = {}
58+
if apiKey is not None:
59+
headers["X-Api-Key"] = apiKey
60+
61+
# Make the call
62+
result = OctoHttpRequest.MakeHttpCall(self.Logger, "/access/oneshot_token", PathTypes.Relative, "GET", headers)
63+
if result is None:
64+
raise Exception("Failed to get the oneshot token from moonraker.")
65+
if result.StatusCode != 200:
66+
raise Exception("Failed to get the oneshot token from moonraker. "+str(result.StatusCode))
67+
68+
# Read the response.
69+
result.ReadAllContentFromStreamResponse(self.Logger)
70+
buf = result.FullBodyBuffer
71+
if buf is None:
72+
raise Exception("Failed to get the oneshot token from moonraker. No content.")
73+
74+
# Decode & parse the response.
75+
jsonMsg = json.loads(buf.decode(encoding="utf-8"))
76+
token = jsonMsg.get("result", None)
77+
if token is None:
78+
raise Exception("Failed to get the oneshot token from moonraker. No result.")
79+
return str(token)
80+
except Exception as e:
81+
Sentry.Exception("TryToGetOneshotToken failed to get the token.", e)
82+
return None
83+
84+
85+
def TryToGetApiKey(self) -> Optional[str]:
5086
# If this is an companion plugin, we dont' have the moonraker config file nor can we access the UNIX socket.
5187
if self.IsCompanionMode:
52-
self.Logger.error("!!!! Moonraker auth is required, so you must generate an API key in Mainsail or Fluidd and set it the octoeverywhere.conf file in the companion root folder. The config file can be found in /data for docker or ~/.octoeverywhere-companion for CLI installs")
53-
# Since we know we will keep failing, sleep for a while to avoid spamming the logs and so the user sees this error.
54-
time.sleep(10)
5588
return None
5689

5790
# First, we need to find the unix socket to connect to
@@ -124,7 +157,7 @@ def TryToGetApiKey(self) -> str or None:
124157
return None
125158

126159

127-
def _TryToFindUnixSocket(self) -> str or None:
160+
def _TryToFindUnixSocket(self) -> Optional[str]:
128161

129162
# First, try to parse the moonraker config to find the klipper socket path, since the moonraker socket should be similar.
130163
try:
@@ -185,7 +218,7 @@ def _GetParentDirectory(self, path):
185218
return os.path.abspath(os.path.join(path, os.pardir))
186219

187220

188-
def _ReadSingleJsonObject(self, sock) -> str or None:
221+
def _ReadSingleJsonObject(self, sock) -> Optional[str]:
189222
# Since sock.recv blocks, we must read each char one by one so we know when the message ends.
190223
# This is messy, but since it only happens very occasionally, it's fine.
191224
message = bytearray()

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030

3131
# The plugin's version. Can be overwritten within OctoPrint's internal data via __plugin_version__ in the plugin module
3232
# Note that this single version string is used by all of the plugins in OctoEverywhere!
33-
plugin_version = "4.1.0"
33+
plugin_version = "4.1.3"
3434

3535
# The plugin's description. Can be overwritten within OctoPrint's internal data via __plugin_description__ in the plugin
3636
# module

0 commit comments

Comments
 (0)