From 01d6bb230c83196aaaa49d5bfd6ca9778a665ae2 Mon Sep 17 00:00:00 2001 From: maluk <> Date: Sun, 25 Oct 2020 10:12:17 +0000 Subject: [PATCH] 49_Arlo.pm: added 2-factor authentication git-svn-id: https://svn.fhem.de/fhem/trunk@23023 2b470e98-0d58-463d-a4d8-8e2adae1ed80 --- fhem/CHANGED | 1 + fhem/FHEM/49_Arlo.pm | 551 +++++++++++++++++++++++++++++++++---------- 2 files changed, 426 insertions(+), 126 deletions(-) diff --git a/fhem/CHANGED b/fhem/CHANGED index 1229b8116..5bb828999 100644 --- a/fhem/CHANGED +++ b/fhem/CHANGED @@ -1,5 +1,6 @@ # Add changes at the top of the list. Keep it in ASCII, and 80-char wide. # Do not insert empty lines here, update check depends on it. + - feature: 49_Arlo: Added 2-factor authentication - bugfix: 73_AutoShuttersControl: fix IsDay Fn for weekend condition - fix detected IsDay for Brightness - https://git.cooltux.net/FHEM/mod-AutoShuttersControl/issues/25 diff --git a/fhem/FHEM/49_Arlo.pm b/fhem/FHEM/49_Arlo.pm index 359a3a042..ad501662c 100644 --- a/fhem/FHEM/49_Arlo.pm +++ b/fhem/FHEM/49_Arlo.pm @@ -6,9 +6,11 @@ use strict; use warnings; use IO::Socket; use IO::Socket::INET; +use IO::Socket::SSL; use HTTP::Request; use HTTP::Cookies; use LWP::UserAgent; +use Mail::IMAPClient; use MIME::Base64; use HttpUtils; use JSON; @@ -21,7 +23,7 @@ sub Arlo_Initialize($$) { $hash->{UndefFn} = "Arlo_Undef"; $hash->{GetFn} = "Arlo_Get"; $hash->{SetFn} = "Arlo_Set"; - $hash->{AttrList} = "disable:1 expiryTime pingInterval updateInterval downloadDir downloadLink ssePollingInterval videoDownloadFix:0,1 ".$readingFnAttributes; + $hash->{AttrList} = "disable:1 expiryTime pingInterval updateInterval downloadDir downloadLink mailServer ssePollingInterval videoDownloadFix:0,1 ".$readingFnAttributes; $hash->{AttrFn} = "Arlo_Attr"; } @@ -31,16 +33,22 @@ sub Arlo_Define($$) { my @a = split("[ \t][ \t]*", $def); my $subtype = $a[2]; - if ($subtype eq 'ACCOUNT' && @a == 5) { + if ($subtype eq 'ACCOUNT' && @a >= 5) { my $user = Arlo_decrypt($a[3]); my $passwd = Arlo_decrypt($a[4]); + my $mailPasswd = ''; $hash->{helper}{username} = $user; $hash->{helper}{password} = $passwd; + if (@a > 5) { + $mailPasswd = Arlo_decrypt($a[5]); + $hash->{helper}{mailPassword} = $mailPasswd; + } $modules{$MODULE}{defptr}{"account"} = $hash; my $cryptUser = Arlo_encrypt($user); my $cryptPasswd = Arlo_encrypt($passwd); - $hash->{DEF} = "ACCOUNT $cryptUser $cryptPasswd"; + my $cryptMailPasswd = Arlo_encrypt($mailPasswd); + $hash->{DEF} = "ACCOUNT $cryptUser $cryptPasswd $cryptMailPasswd"; InternalTimer(gettimeofday() + 3, "Arlo_Login", $hash); } elsif (($subtype eq 'BASESTATION' || $subtype eq 'ROUTER') && @a == 5) { @@ -148,8 +156,7 @@ sub Arlo_Attr($$$) { if ($attrName eq 'disable') { RemoveInternalTimer($hash); if ($cmd eq 'del') { - Arlo_Login($hash); - $hash->{STATE} = 'active'; + InternalTimer(gettimeofday() + 1, "Arlo_Login", $hash); } else { Arlo_Logout($hash); $hash->{STATE} = 'disabled'; @@ -177,8 +184,12 @@ 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 readModes:noArg reconnect:noArg updateReadings:noArg "; + return "Unknown argument $opt, choose one of autocreate:noArg checkMail:noArg loginSecondFactor readModes:noArg reconnect:noArg updateReadings:noArg "; } } elsif ($subtype eq 'BASESTATION' || $subtype eq 'ROUTER') { if (!Arlo_SetBasestationCmd($hash, $opt, $value)) { @@ -394,31 +405,39 @@ sub Arlo_Event($$) { # sub Arlo_PrepareRequest($$;$$$$) { - my ($hash, $urlSuffix, $method, $body, $additionalHeader) = @_; + my ($hash, $url, $method, $body, $additionalHeader) = @_; $method = "GET" if (!defined($method)); my $account = $modules{$MODULE}{defptr}{"account"}; - if ($account->{STATE} eq 'inactive') { - Arlo_Login($account); - } - my $name = $account->{NAME}; - my $cookies = $account->{helper}{cookies}; my $token = $account->{helper}{token}; - my $headers = "Authorization: ".$token."\r\nReferer: https://my.arlo.com\r\nContent-Type: application/json; charset=utf-8\r\nCookie: ".$cookies. - "\r\nschemaVersion: 1\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:69.0) Gecko/20100101 Firefox/69.0"; - $headers = $headers."\r\n".$additionalHeader if (defined($additionalHeader)); - Log3 $name, 5, "Header: $headers"; + my $cookies = $account->{helper}{cookies}; + my $serviceHeaders; - my $url = 'https://my.arlo.com/hmsweb'.$urlSuffix; - Log3 $name, 5, "URL: $url"; + 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 $request = {url => $url, method => $method, header => $headers, host => 'my.arlo.com'}; + my $headers = ''; + $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."\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'}; if (defined($body)) { my $bodyJson = encode_json $body; - Log3 $name, 5, "Body: $bodyJson"; + Log3 $name, 5, "Arlo body: $bodyJson"; $request->{data} = $bodyJson; } @@ -430,16 +449,88 @@ sub Arlo_Request($$;$$$$$$) { my $request = Arlo_PrepareRequest($hash, $urlSuffix, $method, $body, $additionalHeader); if (defined($callback)) { - $request->{callback} = $callback; + $request->{callbackArlo} = $callback; } else { - $request->{callback} = \&Arlo_DefaultCallback; + $request->{callbackArlo} = \&Arlo_DefaultCallback; } - if (defined($origin)) { $request->{origin} = $origin; } + + # request für HttpUtils_NonblockingGet($request); + my $err = HttpUtils_Connect($request); + if ($err) { + $request->{callbackArlo}($request, $err, ''); + return; + } - HttpUtils_NonblockingGet($request); + $request->{buf} = ''; + $request->{readCounter} = 0; + delete($request->{httpdatalen}); + delete($request->{httpheader}); + InternalTimer(gettimeofday() + 0.5, 'Arlo_HttpRead', $request); +} + +sub Arlo_HttpRead($) { + my ($hash) = @_; + + if (!defined($hash->{conn})) { + Log3 $hash, 3, "Arlo HTTP connection not defined, stop reading."; + return; + } + + my ($rout, $rin) = ('', ''); + vec($rin, $hash->{conn}->fileno(), 1) = 1; + Log3 $hash, 4, "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; + } + $hash->{buf} = $hash->{buf} . $buf; + $nfound = select($rout=$rin, undef, undef, 0.1); + } + + if (HttpUtils_DataComplete($hash)) { + Log3 $hash, 4, "Arlo read http DataComplete"; + my ($err, $ret, $redirect) = HttpUtils_ParseAnswer($hash); + Log3 $hash, 5, "Arlo data: $ret"; + $hash->{callbackArlo}($hash, $err, $ret); + return; + } + + my $counter = $hash->{readCounter}; + if ($counter < 8) { + $hash->{readCounter} = $counter + 1; + InternalTimer(gettimeofday() + 0.5, 'Arlo_HttpRead', $hash); + } else { + HttpUtils_Close($hash); + $hash->{callbackArlo}($hash, "$hash->{addr} timed out", ''); + } +} + +sub Arlo_RequestWithLogin($$;$$$$$$) { + my ($hash, $urlSuffix, $method, $body, $additionalHeader, $callback, $origin) = @_; + + my $account = $modules{$MODULE}{defptr}{"account"}; + if ($account->{STATE} eq 'inactive') { + if (defined($account->{helper}{followUpRequest})) { + return 'Please wait until login is finished.'; + } + + Arlo_Login($account); + my $followUpRequest = {hash => $hash, urlSuffix => $urlSuffix, method => $method, body => $body, additionalHeader => $additionalHeader, callback => $callback, origin => $origin}; + $account->{helper}{followUpRequest} = $followUpRequest; + return 'Action will be executed after login.'; + } + + Arlo_Request($hash, $urlSuffix, $method, $body, $additionalHeader, $callback, $origin); } sub Arlo_BlockingRequest($$;$$$$) { @@ -460,39 +551,64 @@ sub Arlo_DefaultCallback($$$) { eval { $response = decode_json $jsonData; if ($response->{success}) { + Arlo_SetCookies($account, $hash->{httpheader}); Log3 $name, 5, "Response from Arlo: $jsonData"; } else { - my $logLevel = 2; + my $logLevel = 2; if ($response->{data}) { - my $data = $response->{data}; - my $origin = $hash->{origin}; - if ($origin && $data->{error} eq '2059' && $data->{reason} eq 'Device is offline.') { + my $data = $response->{data}; + my $origin = $hash->{origin}; + if ($origin && $data->{error} eq '2059' && $data->{reason} eq 'Device is offline.') { readingsSingleUpdate($origin, 'state', 'offline', 1) if (ReadingsVal($origin->{NAME}, 'state', '') ne 'offline'); - $logLevel = 5; - } 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; + $logLevel = 5; + } 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; - } - } + $logLevel = 5; + } + } Log3 $name, $logLevel, "Arlo call was not successful: $jsonData"; $response = undef; } }; if ($@) { - Log3 $hash->{NAME}, 3, 'Invalid Arlo callback response: '.$jsonData; + Log3 $name, 3, 'Invalid Arlo callback response: '.$jsonData; } return $response; } } +sub Arlo_SetCookies($$) { + my ($hash, $httpHeader) = @_; + + my %cookies; + if (defined($hash->{helper}{cookies})) { + foreach my $cookie (split("; ", $hash->{helper}{cookies})) { + my($key, $val) = split(/=/, $cookie, 2); + $cookies{$key} = $val; + } + } + + if (defined($httpHeader)) { + my @header = split("\n", $httpHeader); + foreach my $line (@header) { + if ($line =~ m/^Set-Cookie: ([^;]+)/g) { + my($key, $val) = split(/=/, $1, 2); + $cookies{$key} = $val; + } + } + } + + $hash->{helper}{cookies} = join('; ', map { "$_=$cookies{$_}" } keys %cookies); +} + sub Arlo_CreateDevices($) { my ($hash) = @_; - Arlo_Request($hash, '/users/devices', 'GET', undef, undef, \&Arlo_CreateDevicesCallback); + Arlo_RequestWithLogin($hash, '/users/devices', 'GET', undef, undef, \&Arlo_CreateDevicesCallback); } sub Arlo_CreateDevice($$$$$$$;$) { @@ -582,13 +698,17 @@ sub Arlo_Notify($$;$) { my ($hash, $body, $callback) = @_; my ($account, $deviceId, $xCloudId) = Arlo_PreparePostRequest($hash, $body); Log3 $account->{NAME}, 4, "Notify $deviceId, action: $body->{action} $body->{resource}"; - Arlo_Request($account, '/users/devices/notify/'.$deviceId, 'POST', $body, 'xcloudId: '.$xCloudId, $callback, $hash); + Arlo_RequestWithLogin($account, '/users/devices/notify/'.$deviceId, 'POST', $body, 'xcloudId: '.$xCloudId, $callback, $hash); } sub Arlo_Subscribe($) { my ($hash) = @_; my $account = $modules{$MODULE}{defptr}{'account'}; my $userId = $account->{helper}{userId}; + if (!defined($userId)) { + Log3 $account->{NAME}, 3, 'User id missing in subscribe request.'; + return; + } my $basestationId = $hash->{serialNumber}; my @devices = ($basestationId); my $props = {devices => \@devices}; @@ -620,12 +740,12 @@ sub Arlo_SubscribeCallback($$$) { sub Arlo_Unsubscribe($) { my ($hash) = @_; - Arlo_Request($hash, '/client/unsubscribe'); + Arlo_RequestWithLogin($hash, '/client/unsubscribe'); } sub Arlo_ReadModes($) { my ($hash) = @_; - Arlo_Request($hash, '/users/automation/definitions?uniqueIds=all', 'GET', undef, undef, \&Arlo_ReadModesCallback); + Arlo_RequestWithLogin($hash, '/users/automation/definitions?uniqueIds=all', 'GET', undef, undef, \&Arlo_ReadModesCallback); }; sub Arlo_ReadModesCallback($$$) { @@ -656,9 +776,9 @@ sub Arlo_ReadCamerasAndLights($) { if (defined($hash->{basestationSerialNumber}) && $hash->{basestationSerialNumber} eq $hash->{serialNumber}) { my $mode = {action => 'get', resource => 'modes', publishResponse => \0}; Arlo_PreparePostRequest($hash, $mode); - push @body, $mode; + push @body, $mode; } - Arlo_Request($account, '/users/devices/notify/'.$deviceId, 'POST', \@body, 'xcloudId: '.$xCloudId); + Arlo_RequestWithLogin($account, '/users/devices/notify/'.$deviceId, 'POST', \@body, 'xcloudId: '.$xCloudId); } sub Arlo_UpdateReadings($) { @@ -675,7 +795,7 @@ sub Arlo_UpdateReadings($) { sub Arlo_UpdateBasestationReadings($) { my ($hash) = @_; - Arlo_Request($hash, '/users/devices/automation/active', 'GET', undef, undef, \&Arlo_UpdateReadingsCallback); + Arlo_RequestWithLogin($hash, '/users/devices/automation/active', 'GET', undef, undef, \&Arlo_UpdateReadingsCallback); } sub Arlo_UpdateReadingsCallback($$$) { @@ -787,7 +907,7 @@ sub Arlo_DoSetBasestationMode($$) { my $automation = {deviceId => $hash->{serialNumber}, timestamp => $now, activeModes => \@modes, activeSchedules => \@schedules}; my @automations = ($automation); my $body = { activeAutomations => \@automations }; - Arlo_Request($hash, '/users/devices/automation/active', 'POST', $body); + Arlo_RequestWithLogin($hash, '/users/devices/automation/active', 'POST', $body); } } @@ -822,7 +942,7 @@ sub Arlo_Snapshot($) { my $body = {action => 'set', resource => "cameras/$cameraId", publishResponse => \1, properties => $props}; my ($account, $basestationId, $xCloudId) = Arlo_PreparePostRequest($basestation, $body); Log3 $account->{NAME}, 4, "Take snapshot for camera $cameraId."; - Arlo_Request($account, '/users/devices/fullFrameSnapshot', 'POST', $body, 'xcloudId: '.$xCloudId); + Arlo_RequestWithLogin($account, '/users/devices/fullFrameSnapshot', 'POST', $body, 'xcloudId: '.$xCloudId); } sub Arlo_StartRecording($) { @@ -839,7 +959,7 @@ sub Arlo_StartRecording($) { my ($account, $basestationId, $xCloudId) = Arlo_PreparePostRequest($basestation, $body); Log3 $account->{NAME}, 4, "Start streaming for camera $cameraId."; $hash->{FOLLOW_CALL} = 'startRecord'; - Arlo_Request($account, '/users/devices/startStream', 'POST', $body, 'xcloudId: '.$xCloudId); + Arlo_RequestWithLogin($account, '/users/devices/startStream', 'POST', $body, 'xcloudId: '.$xCloudId); InternalTimer(gettimeofday() + 10, "Arlo_CheckStreamStart", $hash); } return undef; @@ -869,7 +989,7 @@ sub Arlo_CameraAction($$) { my $basestationId = $hash->{basestationSerialNumber}; my $body = {xcloudId => $xCloudId, parentId => $basestationId, deviceId => $cameraId, olsonTimeZone => 'Europe/Berlin'}; Log3 $account->{NAME}, 4, "Action $action for camera $cameraId."; - Arlo_Request($account, '/users/devices/'.$action, 'POST', $body, 'xcloudId: '.$xCloudId); + Arlo_RequestWithLogin($account, '/users/devices/'.$action, 'POST', $body, 'xcloudId: '.$xCloudId); } sub Arlo_SetBrightness($$) { @@ -948,6 +1068,7 @@ sub Arlo_SetLightState($$) { sub Arlo_GetRecordings($$) { my ($hash, $date) = @_; my $body = {dateFrom => $date, dateTo => $date}; + # new https://myapi.arlo.com/hmsweb/users/library?eventId=FE!c985adde-bed3-4aa2-abe9-3baab559adae&time=1602097428821 my ($err, $jsonData) = Arlo_BlockingRequest($hash, '/users/library', 'POST', $body); my $response = Arlo_DefaultCallback($hash, $err, $jsonData); my @result = (); @@ -1002,79 +1123,170 @@ sub Arlo_Login($) { $hash->{STATE} = 'login'; delete $hash->{EXPIRY}; + delete $hash->{helper}{followUpRequest}; my $password = encode_base64($hash->{helper}{password}, ''); - my $input = {email => $hash->{helper}{username}, password => $password, EnvSource => 'prod', language => 'de'}; - my $postData = encode_json $input; - my $header = ['Content-Type' => 'application/json; charset=utf-8', 'Auth-Version' => 2, 'Referer' => 'https://my.arlo.com/']; - - my $cookie_jar = HTTP::Cookies->new; - my $ua = LWP::UserAgent->new(cookie_jar => $cookie_jar, agent => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:72.0) Gecko/20100101 Firefox/72.0'); - my $req = HTTP::Request->new('POST', 'https://ocapi-app.arlo.com/api/auth', $header, $postData); - my $resp = $ua->request($req); - - if ($resp->is_success) { - my $loginPhase = 'Authenticate'; + 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 { - my $respObj = decode_json $resp->decoded_content; - if ($respObj->{meta}{code} == 200) { - $loginPhase = 'ValidateAccessToken'; - my $data = $respObj->{data}; - $hash->{helper}{token} = $data->{token}; - $hash->{helper}{userId} = $data->{userId}; - my $validateData = $data->{authenticated}; - my $authorization = encode_base64($data->{token}, ''); - $header = ['Content-Type' => 'application/json; charset=utf-8', 'Auth-Version' => 2, 'Authorization' => $authorization, 'Referer' => 'https://my.arlo.com/']; - $req = HTTP::Request->new('GET', 'https://ocapi-app.arlo.com/api/validateAccessToken?data='.$validateData, $header); - $resp = $ua->request($req); - if ($resp->is_success) { - $respObj = decode_json $resp->decoded_content; - if ($respObj->{meta}{code} == 200) { - $loginPhase = 'GetSession'; - $header = ['Content-Type' => 'application/json; charset=utf-8', 'Auth-Version' => 2, 'Authorization' => $data->{token}, 'Referer' => 'https://my.arlo.com/']; - $req = HTTP::Request->new('GET', 'https://my.arlo.com/hmsweb/users/session/v2', $header); - $resp = $ua->request($req); - if ($resp->is_success) { - $respObj = decode_json $resp->decoded_content; - if ($respObj->{success}) { - $cookie_jar->extract_cookies($resp); - $hash->{helper}{cookies} = Arlo_GetCookies($cookie_jar); - Log3 $name, 5, $hash->{helper}{cookies}; - $hash->{SSE_STATUS} = 200; - delete $hash->{RETRY}; - $hash->{STATE} = 'active'; - Arlo_Request($hash, '/users/devices'); - Arlo_EventQueue($hash); - Arlo_Ping($hash); - if (!defined($hash->{MODES})) { - InternalTimer(gettimeofday() + 5, "Arlo_ReadModes", $hash); - } - InternalTimer(gettimeofday() + 30, "Arlo_Poll", $hash); - $loginPhase = ''; - } - } - } - } + $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 ($@ || $loginPhase ne '') { - Log3 $name, 2, 'Invalid Arlo response for login request during phase '.$loginPhase.': '.$resp->decoded_content; + if ($@) { + Log3 $name, 3, 'Invalid Arlo auth callback response: '.$jsonData; } - } else { - my $status_line = $resp->status_line; - if ($status_line =~ /401/) { - delete $hash->{RETRY}; - Log3 $name, 2, 'Arlo Login not successful, wrong username or password.'; + return $response; + } +} + +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 { - Log3 $name, 2, "Arlo Login not successful, status $status_line"; + Arlo_ValidateAccessToken($account, $data); } } - if (defined($hash->{RETRY})) { - Log3 $name, 3, 'Retry Arlo Login in 60 seconds.s'; - InternalTimer(gettimeofday() + 60, "Arlo_Login", $hash); +} + +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($) { + 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); } } +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."; + } +} + +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_FinishLogin($$$) { + my ($hash, $err, $jsonData) = @_; + my $response = Arlo_DefaultCallback($hash, $err, $jsonData); + if (defined($response)) { + my $account = $modules{$MODULE}{defptr}{"account"}; + $account->{SSE_STATUS} = 200; + delete $account->{RETRY}; + $account->{STATE} = 'active'; + Arlo_Request($account, '/users/devices'); + + my $req = $account->{helper}{followUpRequest}; + if (defined($req)) { + Arlo_Request($req->{hash}, $req->{urlSuffix}, $req->{method}, $req->{body}, $req->{additionalHeader}, $req->{callback}, $req->{origin}); + delete $account->{helper}{followUpRequest}; + } + + Arlo_EventQueue($account); + Arlo_Ping($account); + if (!defined($account->{MODES})) { + InternalTimer(gettimeofday() + 5, "Arlo_ReadModes", $account); + } + InternalTimer(gettimeofday() + 30, "Arlo_Poll", $account); + } +} + sub Arlo_Logout($) { my ($hash) = @_; RemoveInternalTimer($hash); @@ -1083,16 +1295,6 @@ sub Arlo_Logout($) { $hash->{STATE} = 'inactive'; } -sub Arlo_GetCookies($) { - my ($cookie_jar) = @_; - my $result = ''; - $cookie_jar->scan(sub { - $result .= '; ' if ($result ne ''); - $result .= $_[1]."=".$_[2]; - }); - return $result; -} - sub Arlo_EventQueue($) { my ($hash) = @_; my $name = $hash->{NAME}; @@ -1100,9 +1302,9 @@ sub Arlo_EventQueue($) { my $token = $hash->{helper}{token}; delete $hash->{RESPONSE_TIMEOUT}; - my $headers = "Authorization: ".$token."\r\nAccept: text/event-stream\r\nReferer: https://my.arlo.com\r\n". + 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://my.arlo.com/hmsweb/client/subscribe', method => "GET", header => $headers, keepalive => 1, host => 'my.arlo.com', httpversion => '1.1'}; + my $con = {url => 'https://myapi.arlo.com/hmsweb/client/subscribe', method => "GET", header => $headers, keepalive => 1, host => 'myapi.arlo.com', httpversion => '1.1'}; my $err = HttpUtils_Connect($con); if ($err) { Log3 $name, 2, "Error in Arlo event queue: $err"; @@ -1228,7 +1430,7 @@ sub Arlo_ProcessResponse($$) { } } elsif ($check ne 'event' && $check ne 'Cache' && $check ne 'Conte' && $check ne 'Date:' && $check ne 'Pragm' && $check ne 'Server' && 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 '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"; } } @@ -1327,6 +1529,71 @@ 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 Attribute mailServer gesetzt sein, damit die Mail mit dem Authentifizerungs-Code abgerufen werden kann.'; + return; + } + + my $username = $hash->{helper}{username}; + 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 => $username, 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 # @@ -1379,9 +1646,11 @@ sub Arlo_decrypt($) {

Define

+

mailServer

+ +

videoDownloadFix