Skip to content

Commit

Permalink
Merge pull request #29 from rlippmann/master
Browse files Browse the repository at this point in the history
browser fingerprint/doc updates
  • Loading branch information
rsnodgrass authored May 5, 2023
2 parents 111b386 + 474939a commit 3848aca
Show file tree
Hide file tree
Showing 5 changed files with 181 additions and 12 deletions.
58 changes: 47 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ their ADT system wtih their own tools.**

While two or three Python clients to ADT Pulse existed, they generally only provided
arm/disarm support and none provided support for ADT Pulse when multiple sites existed
under a single account. This attempts to provide APIs to both all the zones (motion
under a single account. This attempts to provide APIs to both all the zones (motion
sensors, door sensors, etc) as well as arming and disarming individual sites.

NOTE: Since this interacts with the unofficial ADT Pulse AJAX web service, the
Expand Down Expand Up @@ -46,36 +46,72 @@ that one site (and not accidentally with another site location).

#### Notes

* any changes to the name/count of sites are not automatically updated for existing site objects
- any changes to the name/count of sites are not automatically updated for existing site objects

## Examples

```python
adt = PyADTPulse(username, password)
adt = PyADTPulse(username, password, fingerprint)

for site in adt.sites:
site.status
site.zones

site.disarm()
site.arm_away()
site.arm_away(force=True)
```

Async version (preferred for new integrations):

```python
adt = PyADTPulse(username, password, fingerprint, do_login=false)

await adt.async_login()

for site in adt.sites:
site.status
site.zones

await site.async_disarm()
await site.async_arm_away()
await site.async_arm_away(force=True)
```

See [example-client.py](example-client.py) for a working example.

## Browser Fingerprinting

ADT Pulse requires 2 factor authentication to log into their site. When you perform the 2 factor authentication, you will see an option to save the browser to not have to re-authenticate through it.

Internally, ADT uses some Javascript code to create a browser fingerprint. This (very long) string is used to check that the browser has been saved upon subsequent logins. It is the "fingerprint" parameter required to be passed in to the PyADTPulse object constructor.

### Note:

The browser fingerprint will change with a browser/OS upgrade. For this reason, it is recommended to create a separate username in ADT Pulse just for monitoring.

There are 2 ways to determine this fingerprint:

1. Visit [this link](https://rawcdn.githack.com/rlippmann/pyadtpulse/b3a0e7097e22446623d170f0a971726fbedb6a2d/doc/browser_fingerprint.html) using the same browser you used to authenticate with ADT Pulse. This should determine the correct browser fingerprint

2. Follow the instructions [here](https://github.com/mrjackyliang/homebridge-adt-pulse#configure-2-factor-authentication)

## See Also

* [ADT Pulse Portal](https://portal.adtpulse.com/)
* [Home Assistant ADT Pulse integration](https://github.com/rsnodgrass/hass-adtpulse/)
* [adt-pulse-mqtt](https://github.com/haruny/adt-pulse-mqtt) – MQTT integration with ADT Pulse alarm panels
- [ADT Pulse Portal](https://portal.adtpulse.com/)
- [Home Assistant ADT Pulse integration](https://github.com/rsnodgrass/hass-adtpulse/)
- [adt-pulse-mqtt](https://github.com/haruny/adt-pulse-mqtt) – MQTT integration with ADT Pulse alarm panels

## Future Enhancements

Feature ideas, but no plans to implement:
Feature ideas:

* support OFFLINE status checking
* support multiple sites (premises/locations) under a single ADT account
* implement lightweight status pings to check if cache needs to be invalidated (every 5 seconds) (https://portal.adtpulse.com/myhome/16.0.0-131/Ajax/SyncCheckServ?t=1568950496392)
* alarm history (/ajax/alarmHistory.jsp)
- 2 factor authenciation
- Cameras (via Janus)

Feature ideas, but no plans to implement:

- support OFFLINE status checking
- support multiple sites (premises/locations) under a single ADT account
~~- implement lightweight status pings to check if cache needs to be invalidated (every 5 seconds) (https://portal.adtpulse.com/myhome/16.0.0-131/Ajax/SyncCheckServ?t=1568950496392)~~
- alarm history (/ajax/alarmHistory.jsp)
38 changes: 38 additions & 0 deletions doc/browser_fingerprint.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<html>
<head>
<script type="text/javascript" src="https://auth.pulse-api.io/v2/sso/US/devicefingerprint"></script>
<script type="text/javascript">
function setFingerprint() {
const e = document.getElementById("fingerprint");
if (e && window.secureAuth && window.secureAuth.fingerprint) {
const r = window.secureAuth.fingerprint.getAllResults()
, t = JSON.stringify(r)
, n = btoa(t);
e.value = n
}
};
function copyFingerprintToClipboard() {
const e = document.getElementById("fingerprint");
if(e) {
e.select();
e.setSelectionRange(0, 99999);
navigator.clipboard.writeText(e.value);
}
};
</script>
<title>ADT Pulse Fingerprint</title>
</head>
<body onload="setFingerprint()">
<h1><img src="./adtpulse.png" width="40" height="40"> Pulse Browser Fingerprint Detector</h1>
<h2>For use with:
<p><a href="https://github.com/rsnodgrass/pyadtpulse">pyadtpulse</a></p>
<p><a href="https://github.com/rsnodgrass/hass-adtpulse">ADT Pulse for Home Assistant</a></p>
<p><a href="https://github.com/adt-pulse-mqtt/adt-pulse-mqtt">ADT Pulse for Home Assistant with MQTT</a></p>
<p><a href="https://github.com/mrjackyliang/homebridge-adt-pulse">ADT Pulse for Homebridge</a></p>
<p>Others?</p>
</h2>
<p>Your browser fingerprint is:</p>
<p><textarea id="fingerprint" name="fingerprint" rows="30" cols="80"></textarea></p>
<p><button onclick="copyFingerprintToClipboard()">Copy Text</button></p>
</body>
</html>
57 changes: 57 additions & 0 deletions pyadtpulse/fingerprint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
{
"fingerprint": {
"uaBrowser": {
"name": "Chrome",
"version": "113.0.0.0",
"major": "113"
},
"uaString": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36",
"uaDevice": {
"model": null,
"type": null,
"vendor": null
},
"uaEngine": {
"name": "WebKit",
"version": "537.36"
},
"uaOS": {
"name": "Linux",
"version": "x86_64"
},
"uaCPU": {
"architecture": "amd64"
},
"uaPlatform": "Linux x86_64",
"language": "en-US",
"colorDepth": 24,
"pixelRatio": 1,
"screenResolution": "1600x900",
"availableScreenResolution": "1600x846",
"timezone": "America/New_York",
"timezoneOffset": 240,
"localStorage": true,
"sessionStorage": true,
"indexedDb": true,
"addBehavior": false,
"openDatabase": true,
"cpuClass": null,
"platform": "Linux x86_64",
"doNotTrack": "1",
"plugins": "Portable Document Format.application/pdf::pdf,Portable Document Format.application/pdf::pdf,Portable Document Format.application/pdf::pdf,Portable Document Format.application/pdf::pdf,Portable Document Format.application/pdf::pdf",
"canvas": "-198900869",
"webGl": "1955868989",
"adBlock": true,
"userTamperLanguage": false,
"userTamperScreenResolution": false,
"userTamperOS": false,
"userTamperBrowser": false,
"touchSupport": {
"maxTouchPoints": 0,
"touchEvent": false,
"touchStart": false
},
"cookieSupport": true,
"fonts": "Andale Mono,Arial,Arial Black,Bauhaus 93,Bodoni 72,Bodoni 72 Oldstyle,Bodoni 72 Smallcaps,Bookshelf Symbol 7,Comic Sans MS,Courier,Courier New,English 111 Vivace BT,Georgia,GeoSlab 703 Lt BT,GeoSlab 703 XBd BT,Helvetica,Humanst 521 Cn BT,Impact,Modern No. 20,MS Gothic,MS PGothic,MS PMincho,PMingLiU,SimSun,Times,Times New Roman,Trebuchet MS,Univers CE 55 Medium,Verdana,Wingdings 2,Wingdings 3"
}
}
38 changes: 38 additions & 0 deletions pyadtpulse/util.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
"""Utility functions for pyadtpulse."""
import logging
import string
import sys
from base64 import urlsafe_b64encode
from pathlib import Path
from random import randint
from threading import RLock, current_thread
from typing import Optional

Expand Down Expand Up @@ -71,6 +75,40 @@ async def make_soup(
return BeautifulSoup(body_text, "html.parser")


FINGERPRINT_LENGTH = 2292
ALLOWABLE_CHARACTERS = list(string.ascii_letters + string.digits)
FINGERPRINT_RANGE_LEN = len(ALLOWABLE_CHARACTERS)


def generate_random_fingerprint() -> str:
"""Generate a random browser fingerprint string.
Returns:
str: a fingerprint string
"""
fingerprint = [
ALLOWABLE_CHARACTERS[(randint(0, FINGERPRINT_RANGE_LEN - 1))]
for i in range(FINGERPRINT_LENGTH)
]
return "".join(fingerprint)


def generate_fingerprint_from_browser_json(filename: str) -> str:
"""Generate a browser fingerprint from a JSON file.
Args:
filename (str): JSON file containing fingerprint information
Returns:
str: the fingerprint
"""
data = Path(filename).read_text()
# Pulse just calls JSON.Stringify() and btoa() in javascript, so we need to
# do this to emulate that
data2 = "".join(data.split())
return str(urlsafe_b64encode(data2.encode("utf-8")), "utf-8")


class DebugRLock:
"""Provides a debug lock logging caller who acquired/released."""

Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

setuptools.setup(
name="pyadtpulse",
version="1.0.4",
version="1.0.5",
packages=["pyadtpulse"],
description="Python interface for ADT Pulse security systems",
long_description=long_description,
Expand Down

0 comments on commit 3848aca

Please sign in to comment.