Skip to content

Commit

Permalink
ONVIF authentication with wb and API key #1355
Browse files Browse the repository at this point in the history
  • Loading branch information
mrlt8 committed Oct 1, 2024
1 parent bd4ebd8 commit ccb41b3
Show file tree
Hide file tree
Showing 3 changed files with 71 additions and 23 deletions.
2 changes: 1 addition & 1 deletion app/frontend.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ def index():
@app.route("/onvif/device_service", methods=["POST"])
@app.route("/onvif/media_service", methods=["POST"])
def onvif_service():
response = onvif.onvif_resp(wb.streams)
response = onvif.service_resp(wb.streams)
return Response(response, content_type="application/soap+xml")

@app.route("/api/sse_status")
Expand Down
20 changes: 18 additions & 2 deletions app/wyzebridge/auth.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import os
from base64 import urlsafe_b64encode
from hashlib import sha256
from base64 import b64decode, b64encode, urlsafe_b64encode
from hashlib import sha1, sha256
from typing import Optional

from werkzeug.security import generate_password_hash
Expand Down Expand Up @@ -82,6 +82,22 @@ def _update_credentials(cls, email: str, force: bool = False) -> None:

cls.api = get_credential("wb_api") or gen_api_key(email)

@classmethod
def auth_onvif(cls, creds: Optional[dict]) -> bool:
if creds and creds.get("username") == "wb":
hashed = onvif_hash(creds["nonce"], creds["created"], cls.api)
return hashed == creds.get("password")

return cls.enabled is False


def onvif_hash(nonce, created, password) -> str:
if not nonce or not created or not password:
return ""

sha1_hash = sha1(b64decode(nonce) + created.encode() + password.encode())
return b64encode(sha1_hash.digest()).decode()


def redact_password(password: Optional[str]):
return f"{password[0]}{'*' * (len(password) - 1)}" if password else "NOT SET"
Expand Down
72 changes: 52 additions & 20 deletions app/wyzebridge/onvif.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@

from flask import request
from wyzebridge import config
from wyzebridge.auth import WbAuth
from wyzebridge.bridge_utils import env_bool
from wyzebridge.logging import logger

NAMESPACES = {
"s": "http://www.w3.org/2003/05/soap-envelope",
"wsdl": "http://www.onvif.org/ver10/media/wsdl",
"wsse": "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd",
"wsu": "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd",
}


Expand Down Expand Up @@ -72,35 +75,41 @@ def ws_discovery(server):
sock.sendto(response.encode("utf-8"), addr)


def parse_action(xml_request):
onvif_path = os.path.basename(request.path)
def parse_request(xml_request):
service = os.path.basename(request.path)
try:
root = ElementTree.fromstring(xml_request)
namespace = {"s": NAMESPACES["s"]}
body = root.find(".//s:Body", namespace)
if body is not None and len(body):
action_element = body[0]
action = action_element.tag.rsplit("}", 1)[-1]
token = action_element.find(".//wsdl:ProfileToken", NAMESPACES)
profile = token.text if token is not None else None
logger.debug(f"{onvif_path=}, {action=}, {profile=}, {xml_request=}")
return action, profile
except ElementTree.ParseError as e:
logger.error(f"XML parsing error: {e}")
return None, None


def onvif_resp(streams):
action, profile_token = parse_action(request.data)
creds = None
if auth := root.find(".//wsse:UsernameToken", NAMESPACES):
creds = {
"username": auth.findtext(".//wsse:Username", None, NAMESPACES),
"password": auth.findtext(".//wsse:Password", None, NAMESPACES),
"nonce": auth.findtext(".//wsse:Nonce", None, NAMESPACES),
"created": auth.findtext(".//wsu:Created", None, NAMESPACES),
}

if (body := root.find(".//s:Body", NAMESPACES)) and len(body) > 0:
action = body[0].tag.rsplit("}", 1)[-1]
profile = body[0].findtext(".//wsdl:ProfileToken", None, NAMESPACES)
logger.info(f"{service=}, {action=}, {profile=}")
return action, profile, creds
except Exception as ex:
logger.error(f"[ONVIF] error parsing XML request: {ex}")

return None, None, None


def service_resp(streams):
action, profile, creds = parse_request(request.data)

if action == "GetProfiles":
resp = get_profiles(streams.streams)
elif action == "GetVideoSources":
resp = get_video_sources()
elif action == "GetStreamUri":
resp = get_stream_uri(profile_token)
resp = get_stream_uri(profile)
elif action == "GetSnapshotUri":
resp = get_snapshot_uri(profile_token)
resp = get_snapshot_uri(profile)
elif action == "GetSystemDateAndTime":
resp = get_system_date_and_time()
elif action == "GetServices":
Expand Down Expand Up @@ -128,6 +137,10 @@ def onvif_resp(streams):
else:
resp = unknown_request()

if not WbAuth.auth_onvif(creds):
logger.error("Onvif auth failed")
resp = unauthorized()

return f"""<?xml version="1.0" encoding="utf-8"?>
<soapenv:Envelope xmlns:soapenv="http://www.w3.org/2003/05/soap-envelope"
xmlns:tds="http://www.onvif.org/ver10/device/wsdl"
Expand Down Expand Up @@ -442,3 +455,22 @@ def unknown_request():
<soapenv:Text xml:lang="en">The requested command is not supported by this device.</soapenv:Text>
</soapenv:Reason>
</soapenv:Fault>"""


def unauthorized():
return """<soap:Fault>
<soap:Code>
<soap:Value>soap:Sender</soap:Value>
<soap:Subcode>
<soap:Value>env:NotAuthorized</soap:Value>
</soap:Subcode>
</soap:Code>
<soap:Reason>
<soap:Text xml:lang="en">Authorization failed: Invalid credentials</soap:Text>
</soap:Reason>
<soap:Detail>
<wsse:FailedAuthentication xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
Security token is invalid or expired
</wsse:FailedAuthentication>
</soap:Detail>
</soap:Fault>"""

0 comments on commit ccb41b3

Please sign in to comment.