Total refactoring of auth logic

- Still need major test updates
- Still needs more thorough testing
- Want to add an option to override login endpoint
This commit is contained in:
Kevin Fronczak
2020-05-24 04:09:23 +00:00
parent 384c2f372b
commit af279b337d
7 changed files with 397 additions and 452 deletions
+17 -42
View File
@@ -2,8 +2,7 @@
import logging
from json import dumps
import blinkpy.helpers.errors as ERROR
from blinkpy.helpers.util import http_req, get_time, BlinkException, Throttle
from blinkpy.helpers.util import get_time, Throttle
from blinkpy.helpers.constants import DEFAULT_URL
_LOGGER = logging.getLogger(__name__)
@@ -12,45 +11,31 @@ MIN_THROTTLE_TIME = 2
def request_login(
blink,
url,
username,
password,
notification_key,
uid,
is_retry=False,
device_id="Blinkpy",
auth, url, login_data, is_retry=False,
):
"""
Login request.
:param blink: Blink instance.
:param auth: Auth instance.
:param url: Login url.
:param username: Blink username.
:param password: Blink password.
:param notification_key: Randomly genereated key.
:param uid: Randomly generated unique id key.
:param is_retry: Is this part of a re-authorization attempt?
:param device_id: Name of application to send at login.
:login_data: Dictionary containing blink login data.
"""
headers = {"Host": DEFAULT_URL, "Content-Type": "application/json"}
data = dumps(
{
"email": username,
"password": password,
"notification_key": notification_key,
"unique_id": uid,
"email": login_data["username"],
"password": login_data["password"],
"notification_key": login_data["notification_key"],
"unique_id": login_data["uid"],
"app_version": "6.0.7 (520300) #afb0be72a",
"device_identifier": login_data["device_id"],
"client_name": "Computer",
"client_type": "android",
"device_identifier": device_id,
"device_name": "Blinkpy",
"os_version": "5.1.1",
"reauth": "true",
}
)
return http_req(
blink,
return auth.query(
url=url,
headers=headers,
data=data,
@@ -60,19 +45,14 @@ def request_login(
)
def request_verify(blink, verify_key):
def request_verify(auth, blink, verify_key):
"""Send verification key to blink servers."""
url = "{}/api/v4/account/{}/client/{}/pin/verify".format(
blink.urls.base_url, blink.account_id, blink.client_id
)
data = dumps({"pin": verify_key})
return http_req(
blink,
url=url,
headers=blink.auth_header,
data=data,
json_resp=False,
reqtype="post",
return auth.query(
url=url, headers=auth.header, data=data, json_resp=False, reqtype="post",
)
@@ -288,13 +268,10 @@ def http_get(blink, url, stream=False, json=True, is_retry=False):
:param json: Return json response? TRUE/False
:param is_retry: Is this part of a re-auth attempt?
"""
if blink.auth_header is None:
raise BlinkException(ERROR.AUTH_TOKEN)
_LOGGER.debug("Making GET request to %s", url)
return http_req(
blink,
return blink.auth.query(
url=url,
headers=blink.auth_header,
headers=blink.auth.header,
reqtype="get",
stream=stream,
json_resp=json,
@@ -309,9 +286,7 @@ def http_post(blink, url, is_retry=False):
:param url: URL to perfom post request.
:param is_retry: Is this part of a re-auth attempt?
"""
if blink.auth_header is None:
raise BlinkException(ERROR.AUTH_TOKEN)
_LOGGER.debug("Making POST request to %s", url)
return http_req(
blink, url=url, headers=blink.auth_header, reqtype="post", is_retry=is_retry
return blink.auth.query(
url=url, headers=blink.auth.header, reqtype="post", is_retry=is_retry
)
+189
View File
@@ -0,0 +1,189 @@
"""Login handler for blink."""
import logging
from functools import partial
from requests import Request, Session, exceptions
from blinkpy import api
from blinkpy.helpers import util
from blinkpy.helpers.constants import BLINK_URL, LOGIN_ENDPOINT
from blinkpy.helpers import errors as ERROR
_LOGGER = logging.getLogger(__name__)
class Auth:
"""Class to handle login communication."""
def __init__(self, login_data=None, no_prompt=False):
"""
Initialize auth handler.
:param login_data: dictionary for login data
can contain the following:
- username
- password
- device_id
- uid
- notification_key
:param no_prompt: Should any user input prompts
be supressed? True/FALSE
"""
self.data = login_data
self.token = None
self.host = None
self.region_id = None
self.login_response = None
self.no_prompt = no_prompt
self.session = self.create_session()
self.validate_login()
@property
def login_attributes(self):
"""Return a dictionary of login attributes."""
attributes = {
"token": self.token,
"host": self.host,
"region_id": self.region_id,
}
return util.merge_dicts(attributes, self.data)
@property
def header(self):
"""Return authorization header."""
if self.token is None:
return None
return {"Host": self.host, "TOKEN_AUTH": self.token}
def create_session(self):
"""Create a session for blink communication."""
sess = Session()
sess.get = partial(sess.get, timeout=10)
return sess
def prepare_request(self, url, headers, data, reqtype):
"""Prepare a request."""
req = Request(reqtype.upper(), url, headers=headers, data=data)
return req.prepare()
def validate_login(self):
"""Check login information and prompt if not available."""
if "username" not in self.data:
self.data["username"] = None
if "password" not in self.data:
self.data["password"] = None
if not self.no_prompt:
self.data = util.prompt_login_data(self.data)
self.data = util.validate_login_data(self.data)
def login(self, login_url=LOGIN_ENDPOINT):
"""Attempt login to blink servers."""
_LOGGER.info("Attempting login with %s", login_url)
response = api.request_login(self, login_url, self.data, is_retry=False,)
try:
if response.status_code == 200:
return response.json()
raise LoginError
except AttributeError:
raise LoginError
def refresh_token(self):
"""Refresh auth token."""
try:
_LOGGER.info("Token expired, attempting automatic refresh.")
self.login_response = self.login()
region_ids = (*self.login_response["region"],)
self.region_id = region_ids[0]
self.host = f"{self.region_id}.{BLINK_URL}"
self.token = self.login_response["authtoken"]["authtoken"]
except KeyError:
_LOGGER.error("Malformed login response: %s", self.login_response)
raise TokenRefreshFailed
return True
def validate_response(self, response, json_resp):
"""Check for valid response."""
if not json_resp:
return response
json_data = response.json()
try:
if json_data["code"] in ERROR.BLINK_ERRORS:
raise exceptions.ConnectionError
except KeyError:
pass
return json_data
def query(
self,
url=None,
data=None,
headers=None,
reqtype="get",
stream=False,
json_resp=True,
is_retry=False,
):
"""
Perform server requests.
:param url: URL to perform request
:param data: Data to send
:param headers: Headers to send
:param reqtype: Can be 'get' or 'post' (default: 'get')
:param stream: Stream response? True/FALSE
:param json_resp: Return JSON response? TRUE/False
:param is_retry: Is this a retry attempt? True/FALSE
"""
req = self.prepare_request(url, headers, data, reqtype)
try:
response = self.session.send(req, stream=stream)
return self.validate_response(response, json_resp)
except (exceptions.ConnectionError, exceptions.Timeout, TokenRefreshFailed):
try:
if not is_retry:
self.refresh_token()
return self.query(
url=url,
data=data,
headers=headers,
reqtype=reqtype,
stream=stream,
json_resp=json_resp,
is_retry=True,
)
except (TokenRefreshFailed, LoginError):
_LOGGER.error("Endpoint %s failed. Unable to refresh login tokens", url)
_LOGGER.error("Endpoint %s failed", url)
return None
def send_auth_key(self, blink, key):
"""Send 2FA key to blink servers."""
if key is not None:
response = api.request_verify(self, blink, key)
try:
json_resp = response.json()
blink.available = json_resp["valid"]
except (KeyError, TypeError):
_LOGGER.error("Did not receive valid response from server.")
return False
return True
def check_key_required(self):
"""Check if 2FA key is required."""
try:
if self.login_response["client"]["verification_required"]:
return True
except KeyError:
pass
return False
class TokenRefreshFailed(Exception):
"""Class to throw failed refresh exception."""
class LoginError(Exception):
"""Class to throw failed login exception."""
+145 -152
View File
@@ -24,22 +24,15 @@ from slugify import slugify
from blinkpy import api
from blinkpy.sync_module import BlinkSyncModule
from blinkpy.helpers.util import (
create_session,
merge_dicts,
get_time,
BlinkURLHandler,
Throttle,
)
from blinkpy.helpers import util
from blinkpy.helpers.constants import (
BLINK_URL,
DEFAULT_MOTION_INTERVAL,
DEFAULT_REFRESH,
DEVICE_ID,
MIN_THROTTLE_TIME,
LOGIN_URLS,
)
from blinkpy.helpers.constants import __version__
from blinkpy.login_handler import LoginHandler
from blinkpy.auth import Auth, TokenRefreshFailed, LoginError
_LOGGER = logging.getLogger(__name__)
@@ -50,25 +43,22 @@ class Blink:
def __init__(
self,
data=None,
username=None,
password=None,
cred_file=None,
refresh_rate=DEFAULT_REFRESH,
motion_interval=DEFAULT_MOTION_INTERVAL,
legacy_subdomain=False,
no_prompt=False,
persist_key=None,
device_id="Blinkpy",
device_id=DEVICE_ID,
):
"""
Initialize Blink system.
:param data: Blink login data. Ignores username, password,
and device_id if supplied.
:param username: Blink username (usually email address)
:param password: Blink password
:param cred_file: JSON formatted file to store credentials.
If username and password are given, file
is ignored. Otherwise, username and password
are loaded from file.
:param refresh_rate: Refresh rate of blink information.
Defaults to 15 (seconds)
:param motion_interval: How far back to register motion in minutes.
@@ -86,166 +76,161 @@ class Blink:
fit the implementation (ie. "Home Assistant" in a
Home Assistant integration).
"""
self.login_handler = LoginHandler(
username=username,
password=password,
cred_file=cred_file,
persist_key=persist_key,
device_id=device_id,
)
self._token = None
self._auth_header = None
self._host = None
if data is None:
data = {}
data["username"] = username
data["password"] = password
data["device_id"] = device_id
self.auth = Auth(login_data=data, no_prompt=no_prompt)
self.account_id = None
self.client_id = None
self.network_ids = []
self.urls = None
self.sync = CaseInsensitiveDict({})
self.region = None
self.region_id = None
self.last_refresh = None
self.refresh_rate = refresh_rate
self.session = create_session()
self.networks = []
self.cameras = CaseInsensitiveDict({})
self.video_list = CaseInsensitiveDict({})
self.login_url = LOGIN_URLS[0]
self.login_urls = []
self.motion_interval = motion_interval
self.version = __version__
self.legacy = legacy_subdomain
self.no_prompt = no_prompt
self.available = False
self.key_required = False
self.login_response = {}
@property
def auth_header(self):
"""Return the authentication header."""
return self._auth_header
def start(self):
"""
Perform full system setup.
Method logs in and sets auth token, urls, and ids for future requests.
Essentially this is just a wrapper function for ease of use.
"""
if not self.available:
self.get_auth_token()
if self.key_required and not self.no_prompt:
email = self.login_handler.data["username"]
key = input("Enter code sent to {}: ".format(email))
result = self.login_handler.send_auth_key(self, key)
self.key_required = not result
self.setup_post_verify()
elif not self.key_required:
self.setup_post_verify()
def setup_post_verify(self):
"""Initialize blink system after verification."""
camera_list = self.get_cameras()
networks = self.get_ids()
for network_name, network_id in networks.items():
if network_id not in camera_list.keys():
camera_list[network_id] = {}
_LOGGER.warning("No cameras found for %s", network_name)
sync_module = BlinkSyncModule(
self, network_name, network_id, camera_list[network_id]
)
sync_module.start()
self.sync[network_name] = sync_module
self.cameras = self.merge_cameras()
self.available = self.refresh()
self.key_required = False
def login(self):
"""Perform server login. DEPRECATED."""
_LOGGER.warning(
"Method is deprecated and will be removed in a future version. Please use the LoginHandler.login() method instead."
)
return self.login_handler.login(self)
def get_auth_token(self, is_retry=False):
"""Retrieve the authentication token from Blink."""
self.login_response = self.login_handler.login(self)
if not self.login_response:
self.available = False
return False
self.setup_params(self.login_response)
if self.login_handler.check_key_required(self):
self.key_required = True
return self._auth_header
def setup_params(self, response):
"""Retrieve blink parameters from login response."""
self.login_url = self.login_handler.login_url
((self.region_id, self.region),) = response["region"].items()
self._host = "{}.{}".format(self.region_id, BLINK_URL)
self._token = response["authtoken"]["authtoken"]
self._auth_header = {"Host": self._host, "TOKEN_AUTH": self._token}
self.urls = BlinkURLHandler(self.region_id, legacy=self.legacy)
self.networks = self.get_networks()
self.client_id = response["client"]["id"]
self.account_id = response["account"]["id"]
def get_networks(self):
"""Get network information."""
response = api.request_networks(self)
try:
return response["summary"]
except KeyError:
return None
def get_ids(self):
"""Set the network ID and Account ID."""
all_networks = []
network_dict = {}
for network, status in self.networks.items():
if status["onboarded"]:
all_networks.append("{}".format(network))
network_dict[status["name"]] = network
self.network_ids = all_networks
return network_dict
def get_cameras(self):
"""Retrieve a camera list for each onboarded network."""
response = api.request_homescreen(self)
try:
all_cameras = {}
for camera in response["cameras"]:
camera_network = str(camera["network_id"])
camera_name = camera["name"]
camera_id = camera["id"]
camera_info = {"name": camera_name, "id": camera_id}
if camera_network not in all_cameras:
all_cameras[camera_network] = []
all_cameras[camera_network].append(camera_info)
return all_cameras
except KeyError:
_LOGGER.error("Initialization failue. Could not retrieve cameras.")
return {}
@Throttle(seconds=MIN_THROTTLE_TIME)
def refresh(self, force_cache=False):
@util.Throttle(seconds=MIN_THROTTLE_TIME)
def refresh(self, force=False):
"""
Perform a system refresh.
:param force_cache: Force an update of the camera cache
:param force: Force an update of the camera data
"""
if self.check_if_ok_to_update() or force_cache:
if self.check_if_ok_to_update() or force:
if not self.available:
self.auth.refresh_token()
self.setup_post_verify()
for sync_name, sync_module in self.sync.items():
_LOGGER.debug("Attempting refresh of sync %s", sync_name)
sync_module.refresh(force_cache=force_cache)
if not force_cache:
sync_module.refresh(force_cache=force)
if not force:
# Prevents rapid clearing of motion detect property
self.last_refresh = int(time.time())
return True
return False
def start(self):
"""Perform full system setup."""
try:
self.auth.refresh_token()
self.setup_login_ids()
self.setup_urls()
except (LoginError, TokenRefreshFailed, BlinkSetupError):
_LOGGER.error("Cannot setup Blink platform.")
self.available = False
return False
self.key_required = self.auth.check_key_required()
if self.key_required:
if self.no_prompt:
return True
self.setup_prompt_2fa()
return self.setup_post_verify()
def setup_prompt_2fa(self):
"""Prompt for 2FA."""
email = self.auth.data["username"]
pin = input(f"Enter code sent to {email}: ")
result = self.auth.send_auth_key(self, pin)
self.key_required = not result
def setup_post_verify(self):
"""Initialize blink system after verification."""
try:
self.setup_networks()
networks = self.setup_network_ids()
cameras = self.setup_camera_list()
except BlinkSetupError:
self.available = False
return False
for name, network_id in networks.items():
sync_cameras = cameras.get(network_id, {})
self.setup_sync_module(name, network_id, sync_cameras)
self.cameras = self.merge_cameras()
self.available = True
self.key_required = False
return True
def setup_sync_module(self, name, network_id, cameras):
"""Initialize a sync module."""
self.sync[name] = BlinkSyncModule(self, name, network_id, cameras)
self.sync[name].start()
def setup_camera_list(self):
"""Create camera list for onboarded networks."""
all_cameras = {}
response = api.request_homescreen(self)
try:
for camera in response["cameras"]:
camera_network = str(camera["network_id"])
if camera_network not in all_cameras:
all_cameras[camera_network] = []
all_cameras[camera_network].append(
{"name": camera["name"], "id": camera["id"]}
)
return all_cameras
except KeyError:
_LOGGER.error("Unable to retrieve cameras from response %s", response)
raise BlinkSetupError
def setup_login_ids(self):
"""Retrieve login id numbers from login response."""
try:
self.client_id = self.auth.login_response["client"]["id"]
self.account_id = self.auth.login_response["account"]["id"]
except KeyError:
_LOGGER.error("Malformed login response of %s", self.auth.login_response)
raise BlinkSetupError
def setup_urls(self):
"""Create urls for api."""
try:
self.urls = util.BlinkURLHandler(self.auth.region_id)
except TypeError:
_LOGGER.error(
"Unable to extract region is from response %s", self.auth.login_response
)
raise BlinkSetupError
def setup_networks(self):
"""Get network information."""
response = api.request_networks(self)
try:
self.networks = response["summary"]
except KeyError:
raise BlinkSetupError
def setup_network_ids(self):
"""Create the network ids for onboarded networks."""
all_networks = []
network_dict = {}
try:
for network, status in self.networks.items():
if status["onboarded"]:
all_networks.append(f"{network}")
network_dict[status["name"]] = network
except AttributeError:
_LOGGER.error(
"Unable to retrieve network information from %s", self.networks
)
raise BlinkSetupError
self.network_ids = all_networks
return network_dict
def check_if_ok_to_update(self):
"""Check if it is ok to perform an http request."""
current_time = int(time.time())
@@ -260,9 +245,13 @@ class Blink:
"""Merge all sync camera dicts into one."""
combined = CaseInsensitiveDict({})
for sync in self.sync:
combined = merge_dicts(combined, self.sync[sync].cameras)
combined = util.merge_dicts(combined, self.sync[sync].cameras)
return combined
def save(self, file_name):
"""Save login data to file."""
util.json_save(self.auth.login_attributes, file_name)
def download_videos(self, path, since=None, camera="all", stop=10, debug=False):
"""
Download all videos from server since specified time.
@@ -283,7 +272,7 @@ class Blink:
parsed_datetime = parse(since, fuzzy=True)
since_epochs = parsed_datetime.timestamp()
formatted_date = get_time(time_to_convert=since_epochs)
formatted_date = util.get_time(time_to_convert=since_epochs)
_LOGGER.info("Retrieving videos since %s", formatted_date)
if not isinstance(camera, list):
@@ -343,3 +332,7 @@ class Blink:
camera_name, created_at, address, filename
)
)
class BlinkSetupError(Exception):
"""Class to handle setup errors."""
+2 -1
View File
@@ -48,10 +48,10 @@ URLS
BLINK_URL = "immedia-semi.com"
DEFAULT_URL = "{}.{}".format("rest-prod", BLINK_URL)
BASE_URL = "https://{}".format(DEFAULT_URL)
LOGIN_ENDPOINT = "{}/api/v3/login".format(BASE_URL)
LOGIN_URLS = [
"{}/api/v4/login".format(BASE_URL),
"{}/api/v3/login".format(BASE_URL),
"{}/api/v2/login".format(BASE_URL),
]
"""
@@ -62,6 +62,7 @@ ONLINE = {"online": True, "offline": False}
"""
OTHER
"""
DEVICE_ID = "Blinkpy"
TIMESTAMP_FORMAT = "%Y-%m-%dT%H:%M:%S%z"
DEFAULT_MOTION_INTERVAL = 1
DEFAULT_REFRESH = 30
+43 -99
View File
@@ -1,19 +1,38 @@
"""Useful functions for blinkpy."""
import json
import logging
import time
import secrets
from calendar import timegm
from functools import partial, wraps
from requests import Request, Session, exceptions
from functools import wraps
from getpass import getpass
import dateutil.parser
from blinkpy.helpers.constants import BLINK_URL, TIMESTAMP_FORMAT
import blinkpy.helpers.errors as ERROR
from blinkpy.helpers import constants as const
_LOGGER = logging.getLogger(__name__)
def json_load(file_name):
"""Load json credentials from file."""
try:
with open(file_name, "r") as json_file:
data = json.load(json_file)
return data
except FileNotFoundError:
_LOGGER.error("Could not find %s", file_name)
except json.decoder.JSONDecodeError:
_LOGGER.error("File %s has improperly formatted json", file_name)
return None
def json_save(data, file_name):
"""Save data to file location."""
with open(file_name, "w") as json_file:
json.dump(data, json_file, indent=4)
def gen_uid(size):
"""Create a random sring."""
full_token = secrets.token_hex(size)
@@ -34,7 +53,7 @@ def get_time(time_to_convert=None):
"""Create blink-compatible timestamp."""
if time_to_convert is None:
time_to_convert = time.time()
return time.strftime(TIMESTAMP_FORMAT, time.gmtime(time_to_convert))
return time.strftime(const.TIMESTAMP_FORMAT, time.gmtime(time_to_convert))
def merge_dicts(dict_a, dict_b):
@@ -48,103 +67,28 @@ def merge_dicts(dict_a, dict_b):
return {**dict_a, **dict_b}
def create_session():
"""
Create a session for blink communication.
def prompt_login_data(data):
"""Prompt user for username and password."""
if data["username"] is None:
data["username"] = input("Username:")
if data["password"] is None:
data["password"] = getpass("Password:")
From @ericfrederich via
https://github.com/kennethreitz/requests/issues/2011
"""
sess = Session()
sess.get = partial(sess.get, timeout=5)
return sess
return data
def attempt_reauthorization(blink):
"""Attempt to refresh auth token and links."""
_LOGGER.info("Auth token expired, attempting reauthorization.")
headers = blink.get_auth_token(is_retry=True)
return headers
def validate_login_data(data):
"""Check for missing keys."""
valid_keys = {
"uid": gen_uid(const.SIZE_UID),
"notification_key": gen_uid(const.SIZE_NOTIFICATION_KEY),
"device_id": const.DEVICE_ID,
}
for key in valid_keys:
if key not in data:
data[key] = valid_keys[key]
def http_req(
blink,
url="http://example.com",
data=None,
headers=None,
reqtype="get",
stream=False,
json_resp=True,
is_retry=False,
):
"""
Perform server requests and check if reauthorization neccessary.
:param blink: Blink instance
:param url: URL to perform request
:param data: Data to send (default: None)
:param headers: Headers to send (default: None)
:param reqtype: Can be 'get' or 'post' (default: 'get')
:param stream: Stream response? True/FALSE
:param json_resp: Return JSON response? TRUE/False
:param is_retry: Is this a retry attempt? True/FALSE
"""
if reqtype == "post":
req = Request("POST", url, headers=headers, data=data)
elif reqtype == "get":
req = Request("GET", url, headers=headers)
else:
_LOGGER.error("Invalid request type: %s", reqtype)
raise BlinkException(ERROR.REQUEST)
prepped = req.prepare()
try:
response = blink.session.send(prepped, stream=stream)
if json_resp and "code" in response.json():
resp_dict = response.json()
code = resp_dict["code"]
message = resp_dict["message"]
if is_retry and code in ERROR.BLINK_ERRORS:
_LOGGER.error("Cannot obtain new token for server auth.")
return None
elif code in ERROR.BLINK_ERRORS:
headers = attempt_reauthorization(blink)
if not headers:
raise exceptions.ConnectionError
return http_req(
blink,
url=url,
data=data,
headers=headers,
reqtype=reqtype,
stream=stream,
json_resp=json_resp,
is_retry=True,
)
_LOGGER.warning("Response from server: %s - %s", code, message)
except (exceptions.ConnectionError, exceptions.Timeout):
_LOGGER.info("Cannot connect to server with url %s.", url)
if not is_retry:
headers = attempt_reauthorization(blink)
return http_req(
blink,
url=url,
data=data,
headers=headers,
reqtype=reqtype,
stream=stream,
json_resp=json_resp,
is_retry=True,
)
_LOGGER.error("Endpoint %s failed. Possible issue with Blink servers.", url)
return None
if json_resp:
return response.json()
return response
return data
class BlinkException(Exception):
@@ -169,7 +113,7 @@ class BlinkURLHandler:
self.subdomain = "rest-{}".format(region_id)
if legacy:
self.subdomain = "rest.{}".format(region_id)
self.base_url = "https://{}.{}".format(self.subdomain, BLINK_URL)
self.base_url = "https://{}.{}".format(self.subdomain, const.BLINK_URL)
self.home_url = "{}/homescreen".format(self.base_url)
self.event_url = "{}/events/network".format(self.base_url)
self.network_url = "{}/network".format(self.base_url)
-154
View File
@@ -1,154 +0,0 @@
"""Login handler for blink."""
import json
import logging
from os.path import isfile
from getpass import getpass
from blinkpy import api
from blinkpy.helpers import util
from blinkpy.helpers import constants as const
_LOGGER = logging.getLogger(__name__)
class LoginHandler:
"""Class to handle login communication."""
def __init__(
self,
username=None,
password=None,
cred_file=None,
persist_key=None,
device_id="Blinkpy",
):
"""
Initialize login handler.
:param username: Blink username
:param password: Blink password
:param cred_file: JSON formatted credential file.
:param persist_key: File location of persistant key.
:param device_id: Name of application to send at login.
"""
self.login_url = None
self.login_urls = const.LOGIN_URLS
self.cred_file = cred_file
self.persist_key = persist_key
self.device_id = device_id
self.data = {
"username": username,
"password": password,
"uid": None,
"notification_key": None,
}
self.check_keys()
def check_keys(self):
"""Check if uid exists, if not create."""
uid = util.gen_uid(const.SIZE_UID)
notification_key = util.gen_uid(const.SIZE_NOTIFICATION_KEY)
data = {"uid": uid, "notification_key": notification_key}
if self.persist_key is None:
return data
if not isfile(self.persist_key):
with open(self.persist_key, "w") as json_file:
json.dump(data, json_file)
else:
with open(self.persist_key, "r") as json_file:
data = json.load(json_file)
return data
def check_cred_file(self):
"""Check if credential file supplied and use if so."""
if isfile(self.cred_file):
try:
with open(self.cred_file, "r") as json_file:
creds = json.load(json_file)
self.data["username"] = creds["username"]
self.data["password"] = creds["password"]
except ValueError:
_LOGGER.error(
"Improperly formatted json file %s.", self.cred_file, exc_info=True
)
return False
except KeyError:
_LOGGER.error("JSON file information incomplete %s.", exc_info=True)
return False
return True
return False
def check_login(self):
"""Check login information and prompt if not available."""
if self.data["username"] is None:
self.data["username"] = input("Username:")
if self.data["password"] is None:
self.data["password"] = getpass("Password:")
if self.data["username"] and self.data["password"]:
return True
return False
def validate_response(self, url, response):
"""Validate response from login endpoint."""
try:
if response.status_code != 200:
return False
except AttributeError:
_LOGGER.error(
"Response for %s did not return a status code. Deprecated endpoint?",
url,
)
return False
return True
def login(self, blink):
"""Attempt login to blink servers."""
if self.cred_file is not None:
self.check_cred_file()
if not self.check_login():
_LOGGER.error("Cannot login with username %s", self.data["username"])
return False
for url in self.login_urls:
_LOGGER.info("Attempting login with %s", url)
response = api.request_login(
blink,
url,
self.data["username"],
self.data["password"],
self.data["notification_key"],
self.data["uid"],
is_retry=False,
device_id=self.device_id,
)
if self.validate_response(url, response):
self.login_url = url
return response.json()
_LOGGER.error("Failed to login to Blink servers. Last response: %s", response)
return False
def send_auth_key(self, blink, key):
"""Send 2FA key to blink servers."""
if key is not None:
response = api.request_verify(blink, key)
try:
json_resp = response.json()
blink.available = json_resp["valid"]
except (KeyError, TypeError):
_LOGGER.error("Did not receive valid response from server.")
return False
return True
def check_key_required(self, blink):
"""Check if 2FA key is required."""
try:
if blink.login_response["client"]["verification_required"]:
return True
except KeyError:
pass
return False
+1 -4
View File
@@ -21,10 +21,8 @@ class BlinkSyncModule:
:param blink: Blink class instantiation
"""
self.blink = blink
self._auth_header = blink.auth_header
self.network_id = network_id
self.region = blink.region
self.region_id = blink.region_id
self.region_id = blink.auth.region_id
self.name = network_name
self.serial = None
self.status = None
@@ -49,7 +47,6 @@ class BlinkSyncModule:
"network_id": self.network_id,
"serial": self.serial,
"status": self.status,
"region": self.region,
"region_id": self.region_id,
}
return attr