2
0
mirror of https://github.com/fhem/fhem-mirror.git synced 2025-04-23 20:52:13 +00:00

49_Arlo.pm: switched to Python login because of Cloudflare anti-bot protection

git-svn-id: https://svn.fhem.de/fhem/trunk@24377 2b470e98-0d58-463d-a4d8-8e2adae1ed80
This commit is contained in:
maluk 2021-05-03 20:14:38 +00:00
parent 970b066912
commit a62b88228c
2 changed files with 292 additions and 244 deletions

View File

@ -10,7 +10,6 @@ use IO::Socket::SSL;
use HTTP::Request;
use HTTP::Cookies;
use LWP::UserAgent;
use Mail::IMAPClient;
use MIME::Base64;
use HttpUtils;
use JSON;
@ -191,12 +190,8 @@ sub Arlo_Set($) {
Arlo_ReadModes($hash);
} elsif ($opt eq 'updateReadings') {
Arlo_UpdateReadings($hash);
} elsif ($opt eq 'loginSecondFactor') {
Arlo_LoginSecondFactor($hash, $value);
} elsif ($opt eq 'checkMail') {
Arlo_Check2FAMail($hash);
} else {
return "Unknown argument $opt, choose one of autocreate:noArg checkMail:noArg loginSecondFactor readModes:noArg reconnect:noArg updateReadings:noArg ";
return "Unknown argument $opt, choose one of autocreate:noArg readModes:noArg reconnect:noArg updateReadings:noArg ";
}
} elsif ($subtype eq 'BASESTATION' || $subtype eq 'ROUTER') {
if (!Arlo_SetBasestationCmd($hash, $opt, $value)) {
@ -422,25 +417,19 @@ sub Arlo_PrepareRequest($$;$$$$) {
my $cookies = $account->{helper}{cookies};
my $serviceHeaders;
if (substr($url, 0, 1) eq '/') { # Request für normale API
$url = 'https://myapi.arlo.com/hmsweb'.$url;
$serviceHeaders = "Auth-Version: 2\r\nschemaVersion: 1";
} else { # bei Requests an ocapi-app.arlo.com muss der Token Base64-encoded werden
$token = encode_base64($token, '') if (defined($token));
$serviceHeaders = 'source: arloCamWeb';
}
my $headers = '';
$url = 'https://myapi.arlo.com/hmsweb'.$url;
my $headers = "Accept: application/json\r\nAuth-Version: 2\r\n";
$headers = $headers."Authorization: $token\r\n" if (defined($token));
$headers = $headers."Cookie: $cookies\r\n" if (defined($cookies));
$headers = $headers."Content-Type: application/json; charset=utf-8\r\nReferer: https://myapi.arlo.com\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:81.0) Gecko/20100101 Firefox/81.0";
$headers = $headers."\r\n".$serviceHeaders;
$headers = $headers."Content-Type: application/json; charset=utf-8\r\nOrigin: https://myapi.arlo.com\r\nReferer: https://myapi.arlo.com\r\nschemaVersion: 1\r\n";
$headers = $headers."User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36";
$headers = $headers."\r\n".$additionalHeader if (defined($additionalHeader));
Log3 $name, 5, "Arlo header: $headers";
Log3 $name, 4, "Arlo URL: $url";
my $request = {url => $url, method => $method, header => $headers, keepalive => 1, httpversion => '1.1'};
my $request = {url => $url, method => $method, header => $headers, keepalive => 1, httpversion => '1.1', loglevel => 4};
if (defined($body)) {
my $bodyJson = encode_json $body;
@ -488,15 +477,13 @@ sub Arlo_HttpRead($) {
my ($rout, $rin) = ('', '');
vec($rin, $hash->{conn}->fileno(), 1) = 1;
Log3 $hash, 4, "Read http response from $hash->{url}";
Log3 $hash, $hash->{loglevel}, "Read http response from $hash->{url}";
my $nfound = select($rout=$rin, undef, undef, 0.1);
while ($nfound > 0) {
my $buf = '';
my $len = sysread($hash->{conn}, $buf, 65536);
if (!defined($len) || $len <= 0) {
Log3 $hash, 4, "Arlo read http ended";
my ($err, $ret, $redirect) = HttpUtils_ParseAnswer($hash);
Log3 $hash, 5, "Arlo data: $ret";
$hash->{callbackArlo}($hash, $err, $ret);
return;
}
@ -505,9 +492,8 @@ sub Arlo_HttpRead($) {
}
if (HttpUtils_DataComplete($hash)) {
Log3 $hash, 4, "Arlo read http DataComplete";
Log3 $hash, $hash->{loglevel}, "Arlo read http DataComplete";
my ($err, $ret, $redirect) = HttpUtils_ParseAnswer($hash);
Log3 $hash, 5, "Arlo data: $ret";
$hash->{callbackArlo}($hash, $err, $ret);
return;
}
@ -571,7 +557,6 @@ sub Arlo_DefaultCallback($$$) {
} elsif ($data->{error} eq '1022' && $data->{reason} eq 'Access token is invalid') {
Log3 $name, 3, "Arlo access token was invalid. Reconnect to Arlo.";
if ($account->{STATE} eq 'active') {
$account->{RETRY} = 1;
Arlo_Login($account);
}
$logLevel = 5;
@ -585,6 +570,9 @@ sub Arlo_DefaultCallback($$$) {
Log3 $name, 3, 'Invalid Arlo callback response: '.$jsonData;
}
return $response;
} else {
Log3 $name, 2, 'Arlo callback response code '.$hash->{code};
Log3 $name, 4, 'Arlo callback response header '.$hash->{header};
}
}
@ -1128,145 +1116,76 @@ sub Arlo_Login($) {
return;
}
$hash->{STATE} = 'login';
$hash->{STATE} = 'python-login';
delete $hash->{EXPIRY};
delete $hash->{helper}{followUpRequest};
my $password = encode_base64($hash->{helper}{password}, '');
my $postData = {email => $hash->{helper}{username}, password => $password, EnvSource => 'prod', language => 'de'};
delete $hash->{helper}{token};
delete $hash->{helper}{cookies};
Arlo_Request($hash, "https://ocapi-app.arlo.com/api/auth", 'POST', $postData, undef, \&Arlo_LoginCallback);
}
sub Arlo_DefaultAuthCallback($$$;$) {
my ($hash, $err, $jsonData, $tryReconnect) = @_;
my $account = $modules{$MODULE}{defptr}{"account"};
my $name = $account->{NAME};
if ($err) {
Log3 $name, 2, "Error occured when calling Arlo authentication url $hash->{url}: $err";
Log3 $name, 3, $jsonData;
$account->{STATE} = 'login failed';
if ($tryReconnect && defined($hash->{RETRY})) {
Log3 $name, 3, 'Retry Arlo Login in 60 seconds.s';
InternalTimer(gettimeofday() + 60, "Arlo_Login", $hash);
}
return undef;
} elsif ($jsonData) {
my $response;
eval {
$response = decode_json $jsonData;
if ($response->{meta}{code} == 200) {
Arlo_SetCookies($account, $hash->{httpheader});
Log3 $name, 5, "Response from Arlo authentication: $jsonData";
} else {
delete $hash->{RETRY};
Log3 $name, 2, "Arlo authentictaion call was not successful: $jsonData";
$response = undef;
}
};
if ($@) {
Log3 $name, 3, 'Invalid Arlo auth callback response: '.$jsonData;
}
return $response;
delete $hash->{helper}{token};
my $mailServer = AttrVal($name, 'mailServer', '');
if ($mailServer eq '') {
Log3 $name, 1, 'Bei 2-Faktor-Authentifizierung muss das Attribut mailServer gesetzt sein, damit die Mail mit dem Authentifizerungs-Code abgerufen werden kann.';
return;
}
my $tmpFile = '/tmp/arlo';
system "python3 contrib/49_Arlo.py $hash->{helper}{username} $hash->{helper}{password} $mailServer $hash->{helper}{mailUser} $hash->{helper}{mailPassword} > $tmpFile &";
open(my $fh, '<', $tmpFile);
$hash->{helper}{pythonFh} = $fh;
$hash->{helper}{pythonTimeout} = gettimeofday() + 120;
InternalTimer(gettimeofday() + 1, "Arlo_ReadPythonResult", $hash);
}
sub Arlo_LoginCallback($$$) {
my ($hash, $err, $jsonData) = @_;
my $response = Arlo_DefaultAuthCallback($hash, $err, $jsonData, 1);
if (defined($response)) {
my $account = $modules{$MODULE}{defptr}{"account"};
my $data = $response->{data};
if (!$data->{authCompleted}) {
Log3 $account->{NAME}, 3, 'Request second factor.';
$account->{helper}{token} = $data->{token};
$account->{STATE} = 'getFactors';
if (defined($account->{helper}{factorId})) {
Arlo_StartAuth($account);
} else {
my $validateData = $data->{authenticated};
Arlo_Request($account, "https://ocapi-app.arlo.com/api/getFactors?data=$validateData", 'GET', undef, undef, \&Arlo_ReadFactorsCallback);
}
} else {
Arlo_ValidateAccessToken($account, $data);
}
}
}
sub Arlo_ReadFactorsCallback($$$) {
my ($hash, $err, $jsonData) = @_;
my $response = Arlo_DefaultAuthCallback($hash, $err, $jsonData);
if (defined($response)) {
my $account = $modules{$MODULE}{defptr}{"account"};
my @items = @{$response->{data}{items}};
foreach my $item (@items) {
if ($item->{factorType} eq 'EMAIL') {
my $factorId = $item->{factorId};
$account->{helper}{factorId} = $factorId;
Arlo_StartAuth($account);
return;
}
}
}
}
sub Arlo_StartAuth($) {
sub Arlo_ReadPythonResult($) {
my ($hash) = @_;
my $postData = {factorId => $hash->{helper}{factorId}};
$hash->{STATE} = 'startAuth';
Arlo_Request($hash, 'https://ocapi-app.arlo.com/api/startAuth', 'POST', $postData, undef, \&Arlo_StartAuthCallback);
}
sub Arlo_StartAuthCallback($$$) {
my ($hash, $err, $jsonData) = @_;
my $response = Arlo_DefaultAuthCallback($hash, $err, $jsonData);
if (defined($response)) {
my $account = $modules{$MODULE}{defptr}{"account"};
$account->{STATE} = 'awaiting2FA';
$account->{helper}{factorAuthCode} = $response->{data}{factorAuthCode};
Log3 $account->{NAME}, 3, 'Arlo Login is waiting for second factor.';
InternalTimer(gettimeofday() + 5, "Arlo_Check2FAMail", $account);
if (gettimeofday() > $hash->{helper}{pythonTimeout}) {
$hash->{STATE} = 'login timeout';
Arlo_ClosePythonFile($hash);
return;
}
}
sub Arlo_LoginSecondFactor($$) {
my ($hash, $code) = @_;
my $factorAuthCode = $hash->{helper}{factorAuthCode};
if (defined($factorAuthCode)) {
my $postData = {factorAuthCode => $factorAuthCode, otp => $code};
Arlo_Request($hash, 'https://ocapi-app.arlo.com/api/finishAuth', 'POST', $postData, undef, \&Arlo_FinishAuthCallback);
} else {
Log3 $hash->{NAME}, 3, "FactorAuthCode is empty, can't login second factor.";
my $fh = $hash->{helper}{pythonFh};
my $line = <$fh>;
while (defined($line)) {
$line =~ s/\s+$//;
if ($line eq "end") {
Arlo_ClosePythonFile($hash);
Arlo_Request($hash, '/users/session/v2', 'GET', undef, undef, \&Arlo_FinishLogin);
return;
}
my $p = index($line, ': ');
my $key = substr($line, 0, $p);
my $value = substr($line, $p + 2);
if ($key eq 'error') {
$hash->{STATE} = 'login failed';
Log3 $hash->{NAME}, 2, "Arlo: $value";
Arlo_ClosePythonFile($hash);
return;
} elsif ($key eq 'log') {
Log3 $hash->{NAME}, 3, "Arlo: $value";
} elsif ($key eq 'status') {
$hash->{STATE} = $value;
} elsif ($key eq 'cookies') {
$hash->{helper}{cookies} = $value;
} elsif ($key eq 'token') {
$hash->{helper}{token} = $value;
} elsif ($key eq 'userId') {
$hash->{helper}{userId} = $value;
} else {
Log3 $hash->{NAME}, 2, "Arlo: unknown command $line";
}
$line = <$fh>;
}
InternalTimer(gettimeofday() + 1, "Arlo_ReadPythonResult", $hash);
}
sub Arlo_FinishAuthCallback($$$) {
my ($hash, $err, $jsonData) = @_;
my $response = Arlo_DefaultAuthCallback($hash, $err, $jsonData);
if (defined($response)) {
my $account = $modules{$MODULE}{defptr}{"account"};
my $data = $response->{data};
Arlo_ValidateAccessToken($account, $data);
delete $account->{hash}{factorAuthCode};
}
}
sub Arlo_ValidateAccessToken($$) {
my ($hash, $data) = @_;
$hash->{helper}{token} = $data->{token};
$hash->{helper}{userId} = $data->{userId};
my $validateData = $data->{authenticated};
Arlo_Request($hash, "https://ocapi-app.arlo.com/api/validateAccessToken?data=$validateData", 'GET', undef, undef, \&Arlo_ValidateAccessTokenCallback);
}
sub Arlo_ValidateAccessTokenCallback($$$) {
my ($hash, $err, $jsonData) = @_;
my $response = Arlo_DefaultAuthCallback($hash, $err, $jsonData);
if (defined($response)) {
my $account = $modules{$MODULE}{defptr}{"account"};
Arlo_Request($account, '/users/session/v2', 'GET', undef, undef, \&Arlo_FinishLogin);
}
sub Arlo_ClosePythonFile($) {
my ($hash) = @_;
my $fh = $hash->{helper}{pythonFh};
close($fh);
unlink('/tmp/arlo');
delete $hash->{helper}{pythonFh};
delete $hash->{helper}{pythonTimeout};
}
sub Arlo_FinishLogin($$$) {
@ -1275,7 +1194,6 @@ sub Arlo_FinishLogin($$$) {
if (defined($response)) {
my $account = $modules{$MODULE}{defptr}{"account"};
$account->{SSE_STATUS} = 200;
delete $account->{RETRY};
$account->{STATE} = 'active';
Arlo_Request($account, '/users/devices');
@ -1309,9 +1227,10 @@ sub Arlo_EventQueue($) {
my $token = $hash->{helper}{token};
delete $hash->{RESPONSE_TIMEOUT};
my $headers = "Authorization: ".$token."\r\nAccept: text/event-stream\r\nReferer: https://myapi.arlo.com\r\n".
"User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:69.0) Gecko/20100101 Firefox/69.0\r\nCookie: ".$cookies;
my $con = {url => 'https://myapi.arlo.com/hmsweb/client/subscribe', method => "GET", header => $headers, keepalive => 1, host => 'myapi.arlo.com', httpversion => '1.1'};
my $headers = {'Auth-Version' => 2, Authorization => $token, Accept => 'text/event-stream', 'Access-Control-Request-Headers' => 'auth-version,authorization',
'Access-Control-Request-Method' => 'GET', Cookie => $cookies, Origin => 'https://my.arlo.com', Referer => 'https://myapi.arlo.com',
'User-Agent' => 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36' };
my $con = {url => 'https://myapi.arlo.com/hmsweb/client/subscribe', method => "GET", header => $headers, keepalive => 1, host => 'myapi.arlo.com', httpversion => '1.1', loglevel => 4};
my $err = HttpUtils_Connect($con);
if ($err) {
Log3 $name, 2, "Error in Arlo event queue: $err";
@ -1377,16 +1296,19 @@ sub Arlo_EventPolling($) {
}
} else {
if ($sseStatus == 299) {
$hash->{RETRY} = 1;
InternalTimer(gettimeofday() + 60, "Arlo_Login", $hash);
return;
} elsif ($sseStatus == 298) {
$hash->{SSE_STATUS} = 0;
InternalTimer(gettimeofday() + 5, "Arlo_EventQueue", $hash);
return;
} else {
if (defined($timeout) && $timeout < gettimeofday()) {
$hash->{SSE_STATUS} = 0;
Log3 $name, 3, "Arlo connection timeout, try to restart event listener.";
HttpUtils_Close($con);
Arlo_EventQueue($hash);
return;
$hash->{SSE_STATUS} = 0;
Log3 $name, 3, "Arlo connection timeout, try to restart event listener.";
HttpUtils_Close($con);
Arlo_EventQueue($hash);
return;
}
}
}
@ -1435,7 +1357,10 @@ sub Arlo_ProcessResponse($$) {
Log3 $hash->{NAME}, 2, "Arlo event queue error: session lost.";
$hash->{SSE_STATUS} = 299;
}
} elsif ($check ne 'event' && $check ne 'Cache' && $check ne 'Conte' && $check ne 'Date:' && $check ne 'Pragm' && $check ne 'Server'
} elsif ($check eq 'Vary:') {
Log3 $hash->{NAME}, 3, "Arlo event queue error: subscription declined (Header $line). Retry event subscription.";
$hash->{SSE_STATUS} = 298;
} elsif ($check ne 'event' && $check ne 'Cache' && $check ne 'Conte' && $check ne 'Date:' && $check ne 'Pragm' && $check ne 'Server' && $check ne 'Acces'
&& substr($check, 0, 2) ne 'X-' && $check ne 'trans' && $check ne 'Serve' && $check ne 'Expir' && $check ne 'Stric' && $check ne 'Trans'
&& $check ne 'Expec' && $check ne 'CF-RA' && $check ne 'CF-Ca' && $check ne 'reque' && $check ne 'x-tra' && $check ne 'cf-re') {
Log3 $hash->{NAME}, 2, "Invalid Arlo event response: $line";
@ -1536,71 +1461,6 @@ sub Arlo_ProcessEvent($$) {
}
}
sub Arlo_Check2FAMail($) {
my ($hash) = @_;
my $name = $hash->{NAME};
if ($hash->{STATE} ne 'awaiting2FA') {
return;
}
my $mailServer = AttrVal($name, 'mailServer', '');
if ($mailServer eq '') {
Log3 $name, 1, 'Bei 2-Faktor-Authentifizierung muss das Attribut mailServer gesetzt sein, damit die Mail mit dem Authentifizerungs-Code abgerufen werden kann.';
return;
}
my $mail_user = $hash->{helper}{mailUser};
my $mail_password = $hash->{helper}{mailPassword};
my $socket = IO::Socket::SSL->new(PeerAddr => $mailServer, PeerPort => 993, Timeout => 10);
my $client = Mail::IMAPClient->new(Socket => $socket, User => $mail_user, Password => $mail_password, Timeout => 10);
if (!$client->IsAuthenticated()) {
Log3 $name, 2, "E-Mail authentication error.";
$client->done();
return;
}
if (!$client->select("INBOX")) {
Log3 $name, 2, "Could not select email inbox.";
$client->done();
return;
}
my $expunge = 0;
my $code = undef;
for ($client->search('FROM', 'do_not_reply@arlo.com')) {
my $date = $client->date($_);
my $subject = $client->subject($_);
my $body = $client->body_string($_);
$body =~ /\s(\d{6})\s/g;
my $parsedCode = $1;
if ($parsedCode) {
$client->delete_message($_);
if (defined($code)) {
Log3 $name, 3, "Discarding old 2FA code $parsedCode";
}
$code = $parsedCode;
$expunge = 1;
} else {
Log3 $name, 4, "Ignoring Arlo mail from $date, subject: $subject";
}
}
if ($expunge > 0) {
$client->expunge("INBOX");
}
$client->done();
if (defined($code)) {
Log3 $name, 3, "Trying to complete 2FA Login with code $code";
Arlo_LoginSecondFactor($hash, $code);
}
InternalTimer(gettimeofday() + 5, "Arlo_Check2FAMail", $hash);
}
#
# Helper
#
@ -1672,14 +1532,6 @@ sub Arlo_decrypt($) {
<li>autocreate (subtype ACCOUNT)<br>
Reads all devices which are assigned to the Arlo account and creates FHEM devices, if the devices don't exist in FHEM.
</li>
<li>checkMail (Subtype ACCOUNT)<br>
If the login process is in status "awaiting2FA" this method checks whether there is a 2FA mail from Arlo. If so the code of this mail will be used to login.
You don't have to call "checkMail" manually because this is done automatically during the login process.
</li>
<li>loginSecondFactor (Subtype ACCOUNT)<br>
If you use 2-factor-authorization you have to provide a code sent by Arlo after logging on with your username and password This code can be given to the
login process here. If you have set your email password an mail server in the cloud device, you don't have to call "loginSecondFactor" because it's then done automatically.
</li>
<li>reconnect (subtype ACCOUNT)<br>
Connect or reconnect to the Arlo server. First FHEM logs in, then a SSE connection is established. This method is only used if the connection
to the Arlo server was interrupted.
@ -1849,15 +1701,6 @@ sub Arlo_decrypt($) {
<li>autocreate (Subtype ACCOUNT)<br>
Liest alle dem Arlo-Account zugeordneten Geräte und legt dafür FHEM-Devices an, falls es diese nicht schon gibt.
</li>
<li>checkMail (Subtype ACCOUNT)<br>
Falls der Login-Vorgang im Status "awaiting2FA" ist, wird geprüft, ob es eine 2FA-Mail von Arlo gibt. Falls ja, wird versucht, sich mit dem Code aus der neuesten Mail
anzumelden. "checkMail" muss normal nicht manuell aufgerufen werden, der Aufruf erfolgt automatisch im Login-Prozess.
</li>
<li>loginSecondFactor (Subtype ACCOUNT)<br>
Bei der 2FA-Anmeldung muss nach der Anmeldung mit Passwort ein zweiter Login-Schritt mit einem Code erfolgen, der für den Login-Vorgang erzeugt wird. Dieser Code
kann hier übergeben werden. Falls man das E-Mail-Passwort und den Mailserver hinterlegt, muss "loginSecondFactor" nicht selbst aufgerufen werden, da dies
dann automatisch im Login-Vorgang erfolgt.
</li>
<li>reconnect (Subtype ACCOUNT)<br>
Neuaufbau der Verbindung zum Arlo-Server. Zunächst loggt sich FHEM neu bei Arlo ein, danach wird die SSE-Verbindung aufgebaut. Wird nur benötigt,
falls unerwartete Verbindungsabbrüche auftreten.

205
fhem/contrib/49_Arlo.py Normal file
View File

@ -0,0 +1,205 @@
import base64
import sys
import time
import cloudscraper
import email
import imaplib
import re
class Arlo:
def __init__(self, tfa_mail_check) -> None:
self._tfa_mail_check = tfa_mail_check
browser = {
'browser': 'chrome',
'platform': 'linux',
'mobile': False
}
self._session = cloudscraper.create_scraper(browser=browser)
self._headers = {
"Accept": "application/json, text/plain, */*",
"Referer": "https://my.arlo.com",
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36",
"Source": "arloCamWeb"
}
self._baseUrl = "https://ocapi-app.arlo.com/api/"
self._token = None
def login(self, username, password):
status("login")
json = {"email": username, "password": encode(password), "language": "en", "EnvSource": "prod"}
attempt = 0
while attempt < 3:
attempt += 1
try:
r = self._session.post(self._baseUrl + "auth", json=json, headers=self._headers)
if r.status_code == 200:
data = _get_data(r)
if data is not None:
self._request_factors(data)
return
if r.status_code == 400:
error("Bad auth request - probably the credentials are wrong.")
return
except Exception as e:
log(e)
time.sleep(3)
status("loginFailed")
def _request_factors(self, data):
status("getFactors")
self._token = data["token"]
self._headers["Authorization"] = encode(self._token)
r = self._session.get(self._baseUrl + "getFactors?data=" + str(data["authenticated"]), headers=self._headers)
data = _get_data(r)
if data is None:
error("getFactors not successful, response code " + str(r.status_code))
return
factor_id = None
for factor in data["items"]:
if factor["factorType"] == "EMAIL":
self._auth_tfa(factor["factorId"])
return
error("email factor not found.")
def _auth_tfa(self, factor_id):
status("startAuth")
self._tfa_mail_check.open()
json = {"factorId": factor_id}
r = self._session.post(self._baseUrl + "startAuth", json=json, headers=self._headers)
data = _get_data(r)
if data is None:
error("startAuth not successful, response code " + str(r.status_code))
return
factor_auth_code = data["factorAuthCode"]
status("waitFor2FA")
code = self._tfa_mail_check.get()
self._tfa_mail_check.close()
log("Try to login with code " + code)
status("finishAuth")
json = {"factorAuthCode": factor_auth_code, "otp": code}
r = self._session.post(self._baseUrl + "finishAuth", json=json, headers=self._headers)
data = _get_data(r)
if data is None:
error("finishAuth not successful, response code " + str(r.status_code))
return
self._token = data["token"]
self._headers["Authorization"] = encode(self._token)
r = self._session.get(self._baseUrl + "validateAccessToken?data=" + str(data["authenticated"]),
headers=self._headers)
if r.status_code != 200:
error("validateAccessToken not successful, response code " + str(r.status_code))
return
print("cookies:", self._get_cookie_header())
print("token:", self._token)
print("userId:", data["userId"])
print("end")
def _get_cookie_header(self):
cookie_header = ""
for cookie in self._session.cookies:
if cookie_header != "":
cookie_header += "; "
cookie_header += cookie.name + "=" + cookie.value
return cookie_header
def _get_data(r):
if r.status_code != 200:
return None
try:
body = r.json()
except Exception as e:
log(r.content)
error(e)
return None
if "meta" in body:
if body["meta"]["code"] == 200:
return body["data"]
elif "success" in body:
if body["success"]:
if "data" in body:
return body["data"]
log(r.json())
return None
class TfaMailCheck:
def __init__(self, mail_server, username, password) -> None:
self._imap = None
self._mail_server = mail_server
self._username = username
self._password = password
def open(self):
self._imap = imaplib.IMAP4_SSL(self._mail_server)
res, status = self._imap.login(self._username, self._password)
if res.lower() != "ok":
return False
res, status = self._imap.select()
if res.lower() != "ok":
return False
res, ids = self._imap.search(None, "FROM", "do_not_reply@arlo.com")
for msg_id in ids[0].split():
self._determine_code_and_delete_mail(msg_id)
if res.lower() != "ok":
return False
def get(self):
timeout = time.time() + 100
while True:
time.sleep(5)
if time.time() > timeout:
return None
try:
self._imap.check()
res, ids = self._imap.search(None, "FROM", "do_not_reply@arlo.com")
for msg_id in ids[0].split():
code = self._determine_code_and_delete_mail(msg_id)
if code is not None:
return code
except Exception as e:
return None
def _determine_code_and_delete_mail(self, msg_id):
res, msg = self._imap.fetch(msg_id, "(RFC822)")
for part in email.message_from_bytes(msg[0][1]).walk():
if part.get_content_type() == "text/html":
for line in part.get_payload().splitlines():
code = re.match(r"^\W*(\d{6})\W*$", line)
if code is not None:
self._imap.store(msg_id, "+FLAGS", "\\Deleted")
return code.group(1)
def close(self):
self._imap.close()
self._imap.logout()
def status(status):
print("status:", status, flush=True)
def log(msg):
print("log:", msg, flush=True)
def error(msg):
print("error:", msg, flush=True)
def encode(s):
return base64.b64encode(s.encode()).decode()
if __name__ == '__main__':
if len(sys.argv) < 6:
error("5 arguments expected: arlo user, arlo password, imap server, email user, email password")
tfa_mail_check = TfaMailCheck(sys.argv[3], sys.argv[4], sys.argv[5])
arlo = Arlo(tfa_mail_check)
arlo.login(sys.argv[1], sys.argv[2])