Browse Source

Use HttpRequestManager to acquire new tokens

This is a re-write from a previous attempt. Instead of requests, which doesn't properly use SSL certificates installed on the computer among other things, we'll now use the HttpRequestManager which uses QNetworkManager under the hood and properly uses system settings.
The QNetworkManager is asynchronous which would normally be very nice, but due to the nature of this call we want to make it synchronous so we'll use a lock here.

Contributes to issue CURA-8539.
Ghostkeeper 3 years ago
parent
commit
f1c763ad9f
1 changed files with 56 additions and 29 deletions
  1. 56 29
      cura/OAuth2/AuthorizationHelpers.py

+ 56 - 29
cura/OAuth2/AuthorizationHelpers.py

@@ -1,16 +1,19 @@
 # Copyright (c) 2021 Ultimaker B.V.
 # Cura is released under the terms of the LGPLv3 or higher.
 
+from base64 import b64encode
 from datetime import datetime
-import json
-import secrets
 from hashlib import sha512
-from base64 import b64encode
+from PyQt5.QtNetwork import QNetworkReply
+import secrets
+from threading import Lock
 from typing import Optional
 import requests
+import urllib.parse
 
 from UM.i18n import i18nCatalog
 from UM.Logger import Logger
+from UM.TaskManagement.HttpRequestManager import HttpRequestManager  # To download log-in tokens.
 
 from cura.OAuth2.Models import AuthenticationResponse, UserProfile, OAuth2Settings
 catalog = i18nCatalog("cura")
@@ -23,6 +26,8 @@ class AuthorizationHelpers:
     def __init__(self, settings: "OAuth2Settings") -> None:
         self._settings = settings
         self._token_url = "{}/token".format(self._settings.OAUTH_SERVER_URL)
+        self._request_lock = Lock()
+        self._auth_response = None  # type: Optional[AuthenticationResponse]
 
     @property
     def settings(self) -> "OAuth2Settings":
@@ -46,10 +51,19 @@ class AuthorizationHelpers:
             "code_verifier": verification_code,
             "scope": self._settings.CLIENT_SCOPES if self._settings.CLIENT_SCOPES is not None else "",
             }
-        try:
-            return self.parseTokenResponse(requests.post(self._token_url, data = data))  # type: ignore
-        except requests.exceptions.ConnectionError as connection_error:
-            return AuthenticationResponse(success = False, err_message = f"Unable to connect to remote server: {connection_error}")
+        headers = {"Content-type": "application/x-www-form-urlencoded"}
+        self._request_lock.acquire()
+        HttpRequestManager.getInstance().post(
+            self._token_url,
+            data = urllib.parse.urlencode(data).encode("UTF-8"),
+            headers_dict = headers,
+            callback = self.parseTokenResponse
+        )
+        self._request_lock.acquire(timeout = 60)  # Block until the request is completed. 1 minute timeout.
+        response = self._auth_response
+        self._auth_response = None
+        self._request_lock.release()
+        return response
 
     def getAccessTokenUsingRefreshToken(self, refresh_token: str) -> "AuthenticationResponse":
         """Request the access token from the authorization server using a refresh token.
@@ -66,15 +80,21 @@ class AuthorizationHelpers:
             "refresh_token": refresh_token,
             "scope": self._settings.CLIENT_SCOPES if self._settings.CLIENT_SCOPES is not None else "",
         }
-        try:
-            return self.parseTokenResponse(requests.post(self._token_url, data = data))  # type: ignore
-        except requests.exceptions.ConnectionError:
-            return AuthenticationResponse(success = False, err_message = "Unable to connect to remote server")
-        except OSError as e:
-            return AuthenticationResponse(success = False, err_message = "Operating system is unable to set up a secure connection: {err}".format(err = str(e)))
+        headers = {"Content-type": "application/x-www-form-urlencoded"}
+        self._request_lock.acquire()
+        HttpRequestManager.getInstance().post(
+            self._token_url,
+            data = urllib.parse.urlencode(data).encode("UTF-8"),
+            headers_dict = headers,
+            callback = self.parseTokenResponse
+        )
+        self._request_lock.acquire(timeout = 60)  # Block until the request is completed. 1 minute timeout.
+        response = self._auth_response
+        self._auth_response = None
+        self._request_lock.release()
+        return response
 
-    @staticmethod
-    def parseTokenResponse(token_response: requests.models.Response) -> "AuthenticationResponse":
+    def parseTokenResponse(self, token_response: QNetworkReply) -> None:
         """Parse the token response from the authorization server into an AuthenticationResponse object.
 
         :param token_response: The JSON string data response from the authorization server.
@@ -82,25 +102,32 @@ class AuthorizationHelpers:
         """
 
         token_data = None
+        http = HttpRequestManager.getInstance()
 
         try:
-            token_data = json.loads(token_response.text)
+            token_data = http.readJSON(token_response)
         except ValueError:
-            Logger.log("w", "Could not parse token response data: %s", token_response.text)
+            Logger.log("w", "Could not parse token response data: %s", http.readText(token_response))
 
         if not token_data:
-            return AuthenticationResponse(success = False, err_message = catalog.i18nc("@message", "Could not read response."))
-
-        if token_response.status_code not in (200, 201):
-            return AuthenticationResponse(success = False, err_message = token_data["error_description"])
-
-        return AuthenticationResponse(success=True,
-                                      token_type=token_data["token_type"],
-                                      access_token=token_data["access_token"],
-                                      refresh_token=token_data["refresh_token"],
-                                      expires_in=token_data["expires_in"],
-                                      scope=token_data["scope"],
-                                      received_at=datetime.now().strftime(TOKEN_TIMESTAMP_FORMAT))
+            self._auth_response = AuthenticationResponse(success = False, err_message = catalog.i18nc("@message", "Could not read response."))
+            self._request_lock.release()
+            return
+
+        if token_response.error() != QNetworkReply.NetworkError.NoError:
+            self._auth_response = AuthenticationResponse(success = False, err_message = token_data["error_description"])
+            self._request_lock.release()
+            return
+
+        self._auth_response = AuthenticationResponse(success = True,
+            token_type = token_data["token_type"],
+            access_token = token_data["access_token"],
+            refresh_token = token_data["refresh_token"],
+            expires_in = token_data["expires_in"],
+            scope = token_data["scope"],
+            received_at = datetime.now().strftime(TOKEN_TIMESTAMP_FORMAT))
+        self._request_lock.release()
+        return
 
     def parseJWT(self, access_token: str) -> Optional["UserProfile"]:
         """Calls the authentication API endpoint to get the token data.