2
0
mirror of https://github.com/fhem/fhem-mirror.git synced 2025-04-20 07:16:03 +00:00

98_gcmsend: Add AES encryption

git-svn-id: https://svn.fhem.de/fhem/trunk@12117 2b470e98-0d58-463d-a4d8-8e2adae1ed80
This commit is contained in:
klassm 2016-09-04 15:24:20 +00:00
parent 926b14a9c8
commit 07aabba9d1

View File

@ -1,33 +1,45 @@
package main; package main;
use strict;
use warnings;
use HTTP::Request; use HTTP::Request;
use LWP::UserAgent; use LWP::UserAgent;
use IO::Socket::SSL; use IO::Socket::SSL;
use utf8; use utf8;
use Crypt::CBC;
use Crypt::Cipher::AES;
my @gets = ('dummy'); sub gcmsend_Initialize($)
sub
gcmsend_Initialize($)
{ {
my ($hash) = @_; my ($hash) = @_;
$hash->{DefFn} = "gcmsend_Define"; $hash->{DefFn} = "gcmsend_Define";
$hash->{NotifyFn} = "gcmsend_notify"; $hash->{NotifyFn} = "gcmsend_notify";
$hash->{AttrFn} = "gcmsend_attr";
$hash->{SetFn} = "gcmsend_set"; $hash->{SetFn} = "gcmsend_set";
$hash->{AttrList} = "loglevel:0,1,2,3,4,5 regIds apiKey stateFilter vibrate deviceFilter playSound"; $hash->{AttrList} = "loglevel:0,1,2,3,4,5 regIds apiKey stateFilter vibrate deviceFilter cryptKey";
} }
sub sub gcmsend_attr {
gcmsend_set { my ($cmd, $name, $aName, $aVal) = @_;
if (not $aName eq "cryptKey") {
return undef;
}
$aVal = sprintf("%016s", $aVal);
$aVal = substr $aVal, length($aVal) - 16, 16;
$_[3] = $aVal;
return undef;
}
sub gcmsend_set {
my ($hash, @a) = @_; my ($hash, @a) = @_;
my $v = @a[1]; my $v = @a[1];
if ($v eq "delete_saved_states") { if ($v eq "delete_saved_states") {
$hash->{STATES} = {}; $hash->{STATES} = { };
return "deleted"; return "deleted";
} elsif($v eq "send") { } elsif ($v eq "send") {
my $msg = ""; my $msg = "";
for (my $i = 2; $i < int(@a); $i++) { for (my $i = 2; $i < int(@a); $i++) {
if (! ($msg eq "")) { if (!($msg eq "")) {
$msg .= " "; $msg .= " ";
} }
$msg .= @a[$i]; $msg .= @a[$i];
@ -38,8 +50,7 @@ gcmsend_set {
} }
} }
sub sub gcmsend_Define($$)
gcmsend_Define($$)
{ {
my ($hash, $def) = @_; my ($hash, $def) = @_;
@ -47,10 +58,10 @@ gcmsend_Define($$)
if (int(@args) < 1) if (int(@args) < 1)
{ {
return "gcmsend_Define: too many arguments. Usage:\n" . return "gcmsend_Define: too many arguments. Usage:\n".
"define <name> gcmsend"; "define <name> gcmsend";
} }
return "Invalid arguments. Usage: \n define <name> gcmsend" if(int(@a) != 0); return "Invalid arguments. Usage: \n define <name> gcmsend" if (int(@args) != 2);
$hash->{STATE} = 'Initialized'; $hash->{STATE} = 'Initialized';
@ -66,18 +77,23 @@ sub gcmsend_array_to_json(@) {
$ret .= ","; $ret .= ",";
} }
my $value = @array[$i]; my $value = @array[$i];
$ret .= ("\"" . $value . "\""); $ret .= ("\"".$value."\"");
} }
return "[" . $ret . "]"; return "[".$ret."]";
} }
sub gcmsend_sendPayload($$) { sub gcmsend_sendPayload($%) {
my ($hash, $payload) = @_; my ($hash, %payload) = @_;
my %generalPayload = gcmsend_getGeneralPayload($hash);
my %toSendPayload = (%generalPayload, %payload);
my %encryptedPayload = gcmsend_encrypt($hash, %toSendPayload);
my $jsonPayload = gcmsend_toJson(%encryptedPayload);
my $name = $hash->{NAME}; my $name = $hash->{NAME};
my $logLevel = GetLogLevel($name,5); my $logLevel = GetLogLevel($name, 5);
my $client = LWP::UserAgent->new(); my $client = LWP::UserAgent->new();
my $regIdsText = AttrVal($name, "regIds", ""); my $regIdsText = AttrVal($name, "regIds", "");
@ -91,32 +107,30 @@ sub gcmsend_sendPayload($$) {
} }
return undef if (int(@registrationIds) == 0); return undef if (int(@registrationIds) == 0);
my $unixTtimestamp = time*1000;
my $data = my $data =
"{" . "{".
"\"registration_ids\":" . gcmsend_array_to_json(@registrationIds) . "," . "\"registration_ids\":".gcmsend_array_to_json(@registrationIds).",".
"\"data\": $payload". "\"data\": $jsonPayload".
"}"; "}";
Log $logLevel, "data is $payload"; Log $logLevel, "data is $jsonPayload";
my $req = HTTP::Request->new(POST => "https://android.googleapis.com/gcm/send"); my $req = HTTP::Request->new( POST => "https://android.googleapis.com/gcm/send" );
$req->header(Authorization => 'key='.$apikey); $req->header( Authorization => 'key='.$apikey );
$req->header('Content-Type' => 'application/json; charset=UTF-8'); $req->header( 'Content-Type' => 'application/json; charset=UTF-8' );
$req->content($data); $req->content( $data );
my $response = $client->request($req); my $response = $client->request( $req );
if (! $response->is_success) { if (!$response->is_success) {
Log $logLevel, "error during request: " . $response->status_line; Log $logLevel, "error during request: ".$response->status_line;
$hash->{STATE} = $response->status_line; $hash->{STATE} = $response->status_line;
} }
$hash->{STATE} = "OK"; $hash->{STATE} = "OK";
return undef; return undef;
} }
sub gcmsend_fillGeneralPayload($$) { sub gcmsend_getGeneralPayload($) {
my ($hash, $payloadString) = @_; my ($hash) = @_;
my $name = $hash->{NAME}; my $name = $hash->{NAME};
@ -124,28 +138,69 @@ sub gcmsend_fillGeneralPayload($$) {
if (AttrVal($name, "vibrate", "false") eq "true") { if (AttrVal($name, "vibrate", "false") eq "true") {
$vibrate = "true"; $vibrate = "true";
} }
my $playSound = "false";
if (AttrVal($name, "playSound", "false") eq "true") {
$playSound = "true";
}
return $payloadString . "," . my $gcmName = $hash->{NAME};
"\"source\":\"gcmsend_fhem\"," .
"\"vibrate\":\"$vibrate\"," . my %generalPayload = (
"\"playSound\":\"$playSound\""; "source" => "gcmsend_fhem",
"gcmDeviceName" => $gcmName,
"vibrate" => "$vibrate"
);
return %generalPayload;
} }
sub gcmsend_sendNotify($$$) { sub gcmsend_sendNotify($$$) {
my ($hash, $deviceName, $changes) = @_; my ($hash, $deviceName, $changes) = @_;
my %payload = (
"deviceName" => $deviceName,
"changes" => $changes,
"type" => "notify"
);
gcmsend_sendPayload($hash, %payload);
}
my $payload = sub gcmsend_toJson(%) {
"\"deviceName\": \"$deviceName\"," . my (%hash) = @_;
"\"changes\":\"$changes\"," . my @entries = ();
"\"type\":\"notify\"";
$payload = "{" . gcmsend_fillGeneralPayload($hash, $payload) . "}"; while (my ($key, $value) = each %hash) {
my $entry = "\"$key\":\"$value\"";
push @entries, $entry;
}
return "{".join(", ", @entries)."}";
}
gcmsend_sendPayload($hash, $payload); my %gcmsend_encrypt_keys = ("type" => "", "notifyId" => "", "changes" => "", "deviceName" => "",
"tickerText" => "", "contentText" => "", "contentTitle" => "");
sub gcmsend_encrypt($%) {
my ($hash, %payload) = @_;
my $key = AttrVal($hash->{NAME}, "cryptKey", "");
if ($key eq "") {
return %payload;
}
my $cipher = Crypt::CBC->new(
-cipher => 'Crypt::Cipher::AES',
-key => $key,
-iv => $key,
-padding => 'standard',
-header => 'none',
-blocksize => '16',
-literal_key => 1,
-keysize => 16
);
my %newPayload = ();
while (my ($key, $value) = each %payload) {
if (exists(%gcmsend_encrypt_keys->{$key})) {
my $padded = sprintf '%16s', $value;
my $length = length($padded);
%newPayload->{$key} = $cipher->encrypt_hex( $value );
} else {
%newPayload->{$key} = $value;
}
}
return %newPayload;
} }
sub gcmsend_sendMessage($$) { sub gcmsend_sendMessage($$) {
@ -173,22 +228,17 @@ sub gcmsend_sendMessage($$) {
$notifyId = @parts[3]; $notifyId = @parts[3];
} }
} else { } else {
return "Illegal message format. Required format is \r\n " . return "Illegal message format. Required format is \r\n ".
"tickerText|contentTitle|contentText[|NotifyID]"; "tickerText|contentTitle|contentText[|NotifyID]";
} }
my %payload = (
my $payload = "tickerText" => $tickerText,
"\"tickerText\":\"$tickerText\"," . "contentTitle" => $contentTitle,
"\"contentTitle\":\"$contentTitle\"," . "contentText" => $contentText,
"\"contentText\":\"$contentText\"," . "notifyId" => $notifyId,
"\"notifyId\":\"$notifyId\"," . "type" => "message"
"\"source\":\"gcmsend_fhem\"," . );
"\"type\":\"message\"" gcmsend_sendPayload($hash, %payload);
;
$payload = "{" . gcmsend_fillGeneralPayload($hash, $payload) . "}";
gcmsend_sendPayload($hash, $payload);
return undef; return undef;
} }
@ -198,13 +248,13 @@ sub gcmsend_getLastDeviceStatesFor($$)
{ {
my ($gcm, $deviceName) = @_; my ($gcm, $deviceName) = @_;
if (! $gcm->{STATES}) { if (!$gcm->{STATES}) {
$gcm->{STATES} = {}; $gcm->{STATES} = { };
} }
my $states = $gcm->{STATES}; my $states = $gcm->{STATES};
if (!$states->{$deviceName}) { if (!$states->{$deviceName}) {
$states->{$deviceName} = {}; $states->{$deviceName} = { };
} }
return $states->{$deviceName}; return $states->{$deviceName};
@ -214,7 +264,7 @@ sub gcmsend_notify($$)
{ {
my ($gcm, $dev) = @_; my ($gcm, $dev) = @_;
my $logLevel = GetLogLevel($gcm,5); my $logLevel = GetLogLevel($gcm, 5);
my $name = $dev->{NAME}; my $name = $dev->{NAME};
my $gcmName = $gcm->{NAME}; my $gcmName = $gcm->{NAME};
@ -222,8 +272,8 @@ sub gcmsend_notify($$)
my $deviceFilter = AttrVal($gcm->{NAME}, "deviceFilter", ""); my $deviceFilter = AttrVal($gcm->{NAME}, "deviceFilter", "");
return if $name eq $gcmName; return if $name eq $gcmName;
return if(!$dev->{CHANGED}); # Some previous notify deleted the array. return if (!$dev->{CHANGED}); # Some previous notify deleted the array.
return if (! ($deviceFilter eq "") && !($name =~ m/$deviceFilter/)); return if (!($deviceFilter eq "") && !($name =~ m/$deviceFilter/));
my $stateFilter = AttrVal($gcm->{NAME}, "stateFilter", ""); my $stateFilter = AttrVal($gcm->{NAME}, "stateFilter", "");
@ -235,8 +285,6 @@ sub gcmsend_notify($$)
for (my $i = 0; $i < $nrOfFieldChanges; $i++) { for (my $i = 0; $i < $nrOfFieldChanges; $i++) {
my @keyValue = split(":", $dev->{CHANGED}[$i]); my @keyValue = split(":", $dev->{CHANGED}[$i]);
my $length = int($keyValue);
my $change = $dev->{CHANGED}[$i]; my $change = $dev->{CHANGED}[$i];
@ -256,13 +304,15 @@ sub gcmsend_notify($$)
$value = substr($change, $position + 2, length($change)); $value = substr($change, $position + 2, length($change));
} }
if (! ($stateFilter eq "") && ! ($value =~ m/$stateFilter/)) { if (!($stateFilter eq "") && !($value =~ m/$stateFilter/)) {
Log $logLevel, "$gcmName $name: ignoring $key, as value $value is blocked by stateFilter regexp."; Log $logLevel,
"$gcmName $name: ignoring $key, as value $value is blocked by stateFilter regexp.";
} elsif ($value eq "") { } elsif ($value eq "") {
Log $logLevel, "$gcmName $name: ignoring $key, as value is empty."; Log $logLevel, "$gcmName $name: ignoring $key, as value is empty.";
} elsif ($lastDeviceStates->{$key} && $lastDeviceStates->{$key} eq $value) { } elsif ($lastDeviceStates->{$key} && $lastDeviceStates->{$key} eq $value) {
my $savedValue = $lastDeviceStates->{$key}; my $savedValue = $lastDeviceStates->{$key};
Log $logLevel, "$gcmName $name: ignoring $key, save value is $savedValue, value is $value"; Log $logLevel,
"$gcmName $name: ignoring $key, save value is $savedValue, value is $value";
} else { } else {
$lastDeviceStates->{$key} = $value; $lastDeviceStates->{$key} = $value;
# Multiple field updates are separated by <|>. # Multiple field updates are separated by <|>.
@ -314,7 +364,8 @@ sub gcmsend_notify($$)
Notes: Notes:
<ul> <ul>
<li>Module to send messages to GCM (Google Cloud Messaging).</li> <li>Module to send messages to GCM (Google Cloud Messaging).</li>
<li>Prerequisite is a GCM AcsendFieldCount with Google (see <a href="https://code.google.com/apis/console/">Google API Console</a></li> <li>Prerequisite is a GCM Account (see <a href="https://code.google.com/apis/console/">Google API Console</a></li>
<li>Futhermore <code>Crypt::CBC</code> and <code>Crypt::Cipher::AES</code> Perl modules have to be installed
</ul> </ul>
</ul> </ul>
@ -349,8 +400,7 @@ sub gcmsend_notify($$)
<br />Make the receiving device vibrate upon receiving the message. Must be true or false.</li> <br />Make the receiving device vibrate upon receiving the message. Must be true or false.</li>
<li><a name="gcmsend_deviceFilter"><code>attr &lt;name&gt; deviceFilter &lt;regexp&gt;</a> <li><a name="gcmsend_deviceFilter"><code>attr &lt;name&gt; deviceFilter &lt;regexp&gt;</a>
<br />Send a GCM notify only is the device name matches the given filter regexp.</li> <br />Send a GCM notify only is the device name matches the given filter regexp.</li>
<li><a name="gcmsend_playSound"><code attr &lt;name&gt; playSound &lt;true|false&gt;</a> <li><a name="gcmsend_cryptKey"><code>attr &lt;name&gt; cryptKey &lt;key&gt;</a> <br/>Some key to encrypt message content. The key must have a size of 16 bytes. If the key length does not match it will be either cut or padded to the required length. As encryption algorithm AES is used.</li>
<br />Specifies that the implementation of GCM should play a sound when an event is received. Note that andFHEM does not implement this attribute yet.</li>
</ul> </ul>
</ul> </ul>