mirror of
https://github.com/fhem/fhem-mirror.git
synced 2025-04-21 07:56: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:
parent
926b14a9c8
commit
07aabba9d1
@ -1,281 +1,331 @@
|
||||
package main;
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
use HTTP::Request;
|
||||
use LWP::UserAgent;
|
||||
use IO::Socket::SSL;
|
||||
use utf8;
|
||||
use Crypt::CBC;
|
||||
use Crypt::Cipher::AES;
|
||||
|
||||
my @gets = ('dummy');
|
||||
|
||||
sub
|
||||
gcmsend_Initialize($)
|
||||
sub gcmsend_Initialize($)
|
||||
{
|
||||
my ($hash) = @_;
|
||||
$hash->{DefFn} = "gcmsend_Define";
|
||||
$hash->{NotifyFn} = "gcmsend_notify";
|
||||
$hash->{SetFn} = "gcmsend_set";
|
||||
$hash->{AttrList} = "loglevel:0,1,2,3,4,5 regIds apiKey stateFilter vibrate deviceFilter playSound";
|
||||
my ($hash) = @_;
|
||||
$hash->{DefFn} = "gcmsend_Define";
|
||||
$hash->{NotifyFn} = "gcmsend_notify";
|
||||
$hash->{AttrFn} = "gcmsend_attr";
|
||||
$hash->{SetFn} = "gcmsend_set";
|
||||
$hash->{AttrList} = "loglevel:0,1,2,3,4,5 regIds apiKey stateFilter vibrate deviceFilter cryptKey";
|
||||
}
|
||||
|
||||
sub
|
||||
gcmsend_set {
|
||||
my ($hash, @a) = @_;
|
||||
my $v = @a[1];
|
||||
if ($v eq "delete_saved_states") {
|
||||
$hash->{STATES} = {};
|
||||
return "deleted";
|
||||
} elsif($v eq "send") {
|
||||
my $msg = "";
|
||||
for (my $i = 2; $i < int(@a); $i++) {
|
||||
if (! ($msg eq "")) {
|
||||
$msg .= " ";
|
||||
}
|
||||
$msg .= @a[$i];
|
||||
sub gcmsend_attr {
|
||||
my ($cmd, $name, $aName, $aVal) = @_;
|
||||
if (not $aName eq "cryptKey") {
|
||||
return undef;
|
||||
}
|
||||
return gcmsend_sendMessage($hash, $msg);
|
||||
} else {
|
||||
return "unknown set value, choose one of delete_saved_states send";
|
||||
}
|
||||
$aVal = sprintf("%016s", $aVal);
|
||||
$aVal = substr $aVal, length($aVal) - 16, 16;
|
||||
$_[3] = $aVal;
|
||||
return undef;
|
||||
}
|
||||
|
||||
sub
|
||||
gcmsend_Define($$)
|
||||
sub gcmsend_set {
|
||||
my ($hash, @a) = @_;
|
||||
my $v = @a[1];
|
||||
if ($v eq "delete_saved_states") {
|
||||
$hash->{STATES} = { };
|
||||
return "deleted";
|
||||
} elsif ($v eq "send") {
|
||||
my $msg = "";
|
||||
for (my $i = 2; $i < int(@a); $i++) {
|
||||
if (!($msg eq "")) {
|
||||
$msg .= " ";
|
||||
}
|
||||
$msg .= @a[$i];
|
||||
}
|
||||
return gcmsend_sendMessage($hash, $msg);
|
||||
} else {
|
||||
return "unknown set value, choose one of delete_saved_states send";
|
||||
}
|
||||
}
|
||||
|
||||
sub gcmsend_Define($$)
|
||||
{
|
||||
my ($hash, $def) = @_;
|
||||
my ($hash, $def) = @_;
|
||||
|
||||
my @args = split("[ \t]+", $def);
|
||||
my @args = split("[ \t]+", $def);
|
||||
|
||||
if (int(@args) < 1)
|
||||
{
|
||||
return "gcmsend_Define: too many arguments. Usage:\n" .
|
||||
"define <name> gcmsend";
|
||||
}
|
||||
return "Invalid arguments. Usage: \n define <name> gcmsend" if(int(@a) != 0);
|
||||
if (int(@args) < 1)
|
||||
{
|
||||
return "gcmsend_Define: too many arguments. Usage:\n".
|
||||
"define <name> gcmsend";
|
||||
}
|
||||
return "Invalid arguments. Usage: \n define <name> gcmsend" if (int(@args) != 2);
|
||||
|
||||
$hash->{STATE} = 'Initialized';
|
||||
$hash->{STATE} = 'Initialized';
|
||||
|
||||
return undef;
|
||||
return undef;
|
||||
}
|
||||
|
||||
sub gcmsend_array_to_json(@) {
|
||||
my (@array) = @_;
|
||||
my $ret = "";
|
||||
my (@array) = @_;
|
||||
my $ret = "";
|
||||
|
||||
for (my $i = 0; $i < int(@array); $i++) {
|
||||
if ($i != 0) {
|
||||
$ret .= ",";
|
||||
for (my $i = 0; $i < int(@array); $i++) {
|
||||
if ($i != 0) {
|
||||
$ret .= ",";
|
||||
}
|
||||
my $value = @array[$i];
|
||||
$ret .= ("\"".$value."\"");
|
||||
}
|
||||
my $value = @array[$i];
|
||||
$ret .= ("\"" . $value . "\"");
|
||||
}
|
||||
|
||||
return "[" . $ret . "]";
|
||||
return "[".$ret."]";
|
||||
}
|
||||
|
||||
sub gcmsend_sendPayload($$) {
|
||||
my ($hash, $payload) = @_;
|
||||
sub gcmsend_sendPayload($%) {
|
||||
my ($hash, %payload) = @_;
|
||||
my %generalPayload = gcmsend_getGeneralPayload($hash);
|
||||
my %toSendPayload = (%generalPayload, %payload);
|
||||
my %encryptedPayload = gcmsend_encrypt($hash, %toSendPayload);
|
||||
|
||||
my $name = $hash->{NAME};
|
||||
my $jsonPayload = gcmsend_toJson(%encryptedPayload);
|
||||
|
||||
my $logLevel = GetLogLevel($name,5);
|
||||
my $name = $hash->{NAME};
|
||||
|
||||
my $client = LWP::UserAgent->new();
|
||||
my $regIdsText = AttrVal($name, "regIds", "");
|
||||
my $logLevel = GetLogLevel($name, 5);
|
||||
|
||||
my $apikey = AttrVal($name, "apiKey", "");
|
||||
my @registrationIds = split(/\|/, $regIdsText);
|
||||
my $client = LWP::UserAgent->new();
|
||||
my $regIdsText = AttrVal($name, "regIds", "");
|
||||
|
||||
if (int(@registrationIds) == 0) {
|
||||
Log $logLevel, "$name no registrationIds set.";
|
||||
my $apikey = AttrVal($name, "apiKey", "");
|
||||
my @registrationIds = split(/\|/, $regIdsText);
|
||||
|
||||
if (int(@registrationIds) == 0) {
|
||||
Log $logLevel, "$name no registrationIds set.";
|
||||
return undef;
|
||||
}
|
||||
return undef if (int(@registrationIds) == 0);
|
||||
|
||||
my $data =
|
||||
"{".
|
||||
"\"registration_ids\":".gcmsend_array_to_json(@registrationIds).",".
|
||||
"\"data\": $jsonPayload".
|
||||
"}";
|
||||
|
||||
Log $logLevel, "data is $jsonPayload";
|
||||
|
||||
my $req = HTTP::Request->new( POST => "https://android.googleapis.com/gcm/send" );
|
||||
$req->header( Authorization => 'key='.$apikey );
|
||||
$req->header( 'Content-Type' => 'application/json; charset=UTF-8' );
|
||||
$req->content( $data );
|
||||
|
||||
my $response = $client->request( $req );
|
||||
if (!$response->is_success) {
|
||||
Log $logLevel, "error during request: ".$response->status_line;
|
||||
$hash->{STATE} = $response->status_line;
|
||||
}
|
||||
$hash->{STATE} = "OK";
|
||||
return undef;
|
||||
}
|
||||
return undef if (int(@registrationIds) == 0);
|
||||
|
||||
my $unixTtimestamp = time*1000;
|
||||
|
||||
my $data =
|
||||
"{" .
|
||||
"\"registration_ids\":" . gcmsend_array_to_json(@registrationIds) . "," .
|
||||
"\"data\": $payload".
|
||||
"}";
|
||||
|
||||
Log $logLevel, "data is $payload";
|
||||
|
||||
my $req = HTTP::Request->new(POST => "https://android.googleapis.com/gcm/send");
|
||||
$req->header(Authorization => 'key='.$apikey);
|
||||
$req->header('Content-Type' => 'application/json; charset=UTF-8');
|
||||
$req->content($data);
|
||||
|
||||
my $response = $client->request($req);
|
||||
if (! $response->is_success) {
|
||||
Log $logLevel, "error during request: " . $response->status_line;
|
||||
$hash->{STATE} = $response->status_line;
|
||||
}
|
||||
$hash->{STATE} = "OK";
|
||||
return undef;
|
||||
}
|
||||
|
||||
sub gcmsend_fillGeneralPayload($$) {
|
||||
my ($hash, $payloadString) = @_;
|
||||
sub gcmsend_getGeneralPayload($) {
|
||||
my ($hash) = @_;
|
||||
|
||||
my $name = $hash->{NAME};
|
||||
my $name = $hash->{NAME};
|
||||
|
||||
my $vibrate = "false";
|
||||
if (AttrVal($name, "vibrate", "false") eq "true") {
|
||||
$vibrate = "true";
|
||||
}
|
||||
my $playSound = "false";
|
||||
if (AttrVal($name, "playSound", "false") eq "true") {
|
||||
$playSound = "true";
|
||||
}
|
||||
my $vibrate = "false";
|
||||
if (AttrVal($name, "vibrate", "false") eq "true") {
|
||||
$vibrate = "true";
|
||||
}
|
||||
|
||||
return $payloadString . "," .
|
||||
"\"source\":\"gcmsend_fhem\"," .
|
||||
"\"vibrate\":\"$vibrate\"," .
|
||||
"\"playSound\":\"$playSound\"";
|
||||
my $gcmName = $hash->{NAME};
|
||||
|
||||
my %generalPayload = (
|
||||
"source" => "gcmsend_fhem",
|
||||
"gcmDeviceName" => $gcmName,
|
||||
"vibrate" => "$vibrate"
|
||||
);
|
||||
return %generalPayload;
|
||||
}
|
||||
|
||||
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 =
|
||||
"\"deviceName\": \"$deviceName\"," .
|
||||
"\"changes\":\"$changes\"," .
|
||||
"\"type\":\"notify\"";
|
||||
sub gcmsend_toJson(%) {
|
||||
my (%hash) = @_;
|
||||
my @entries = ();
|
||||
|
||||
$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($$) {
|
||||
my ($hash, $message) = @_;
|
||||
my ($hash, $message) = @_;
|
||||
|
||||
my @parts = split(/\|/, $message);
|
||||
my @parts = split(/\|/, $message);
|
||||
|
||||
my $tickerText;
|
||||
my $contentTitle;
|
||||
my $contentText;
|
||||
my $notifyId = 1;
|
||||
my $tickerText;
|
||||
my $contentTitle;
|
||||
my $contentText;
|
||||
my $notifyId = 1;
|
||||
|
||||
my $length = int(@parts);
|
||||
my $length = int(@parts);
|
||||
|
||||
if ($length == 3 || $length == 4) {
|
||||
$tickerText = @parts[0];
|
||||
$contentTitle = @parts[1];
|
||||
$contentText = @parts[2];
|
||||
if ($length == 3 || $length == 4) {
|
||||
$tickerText = @parts[0];
|
||||
$contentTitle = @parts[1];
|
||||
$contentText = @parts[2];
|
||||
|
||||
if ($length == 4) {
|
||||
my $notifyIdText = @parts[3];
|
||||
if (!(@parts[3] =~ m/[1-9][0-9]*/)) {
|
||||
return "notifyId must be numeric and positive";
|
||||
}
|
||||
$notifyId = @parts[3];
|
||||
if ($length == 4) {
|
||||
my $notifyIdText = @parts[3];
|
||||
if (!(@parts[3] =~ m/[1-9][0-9]*/)) {
|
||||
return "notifyId must be numeric and positive";
|
||||
}
|
||||
$notifyId = @parts[3];
|
||||
}
|
||||
} else {
|
||||
return "Illegal message format. Required format is \r\n ".
|
||||
"tickerText|contentTitle|contentText[|NotifyID]";
|
||||
}
|
||||
} else {
|
||||
return "Illegal message format. Required format is \r\n " .
|
||||
"tickerText|contentTitle|contentText[|NotifyID]";
|
||||
}
|
||||
my %payload = (
|
||||
"tickerText" => $tickerText,
|
||||
"contentTitle" => $contentTitle,
|
||||
"contentText" => $contentText,
|
||||
"notifyId" => $notifyId,
|
||||
"type" => "message"
|
||||
);
|
||||
gcmsend_sendPayload($hash, %payload);
|
||||
|
||||
my $payload =
|
||||
"\"tickerText\":\"$tickerText\"," .
|
||||
"\"contentTitle\":\"$contentTitle\"," .
|
||||
"\"contentText\":\"$contentText\"," .
|
||||
"\"notifyId\":\"$notifyId\"," .
|
||||
"\"source\":\"gcmsend_fhem\"," .
|
||||
"\"type\":\"message\""
|
||||
;
|
||||
|
||||
$payload = "{" . gcmsend_fillGeneralPayload($hash, $payload) . "}";
|
||||
|
||||
gcmsend_sendPayload($hash, $payload);
|
||||
|
||||
return undef;
|
||||
return undef;
|
||||
}
|
||||
|
||||
|
||||
sub gcmsend_getLastDeviceStatesFor($$)
|
||||
{
|
||||
my ($gcm, $deviceName) = @_;
|
||||
my ($gcm, $deviceName) = @_;
|
||||
|
||||
if (! $gcm->{STATES}) {
|
||||
$gcm->{STATES} = {};
|
||||
}
|
||||
if (!$gcm->{STATES}) {
|
||||
$gcm->{STATES} = { };
|
||||
}
|
||||
|
||||
my $states = $gcm->{STATES};
|
||||
if (!$states->{$deviceName}) {
|
||||
$states->{$deviceName} = {};
|
||||
}
|
||||
my $states = $gcm->{STATES};
|
||||
if (!$states->{$deviceName}) {
|
||||
$states->{$deviceName} = { };
|
||||
}
|
||||
|
||||
return $states->{$deviceName};
|
||||
return $states->{$deviceName};
|
||||
}
|
||||
|
||||
sub gcmsend_notify($$)
|
||||
{
|
||||
my ($gcm, $dev) = @_;
|
||||
my ($gcm, $dev) = @_;
|
||||
|
||||
my $logLevel = GetLogLevel($gcm,5);
|
||||
my $logLevel = GetLogLevel($gcm, 5);
|
||||
|
||||
my $name = $dev->{NAME};
|
||||
my $gcmName = $gcm->{NAME};
|
||||
my $name = $dev->{NAME};
|
||||
my $gcmName = $gcm->{NAME};
|
||||
|
||||
my $deviceFilter = AttrVal($gcm->{NAME}, "deviceFilter", "");
|
||||
my $deviceFilter = AttrVal($gcm->{NAME}, "deviceFilter", "");
|
||||
|
||||
return if $name eq $gcmName;
|
||||
return if(!$dev->{CHANGED}); # Some previous notify deleted the array.
|
||||
return if (! ($deviceFilter eq "") && !($name =~ m/$deviceFilter/));
|
||||
return if $name eq $gcmName;
|
||||
return if (!$dev->{CHANGED}); # Some previous notify deleted the array.
|
||||
return if (!($deviceFilter eq "") && !($name =~ m/$deviceFilter/));
|
||||
|
||||
my $stateFilter = AttrVal($gcm->{NAME}, "stateFilter", "");
|
||||
my $stateFilter = AttrVal($gcm->{NAME}, "stateFilter", "");
|
||||
|
||||
my $lastDeviceStates = gcmsend_getLastDeviceStatesFor($gcm, $name);
|
||||
my $lastDeviceStates = gcmsend_getLastDeviceStatesFor($gcm, $name);
|
||||
|
||||
my $val = "";
|
||||
my $nrOfFieldChanges = int(@{$dev->{CHANGED}});
|
||||
my $sendFieldCount = 0;
|
||||
my $val = "";
|
||||
my $nrOfFieldChanges = int(@{$dev->{CHANGED}});
|
||||
my $sendFieldCount = 0;
|
||||
|
||||
for (my $i = 0; $i < $nrOfFieldChanges; $i++) {
|
||||
my @keyValue = split(":", $dev->{CHANGED}[$i]);
|
||||
my $length = int($keyValue);
|
||||
|
||||
my $change = $dev->{CHANGED}[$i];
|
||||
for (my $i = 0; $i < $nrOfFieldChanges; $i++) {
|
||||
my @keyValue = split(":", $dev->{CHANGED}[$i]);
|
||||
my $change = $dev->{CHANGED}[$i];
|
||||
|
||||
|
||||
# We need to find out a key and a value for each field update.
|
||||
# For state updates, we have not field, which is why we simply
|
||||
# put it to "state".
|
||||
# For all other updates the notify value is delimited by ":",
|
||||
# which we use to find out the value and the key.
|
||||
my $key;
|
||||
my $value;
|
||||
my $position = index($change, ':');
|
||||
if ($position == -1) {
|
||||
$key = "state";
|
||||
$value = $keyValue[0];
|
||||
} else {
|
||||
$key = substr($change, 0, $position);
|
||||
$value = substr($change, $position + 2, length($change));
|
||||
# We need to find out a key and a value for each field update.
|
||||
# For state updates, we have not field, which is why we simply
|
||||
# put it to "state".
|
||||
# For all other updates the notify value is delimited by ":",
|
||||
# which we use to find out the value and the key.
|
||||
my $key;
|
||||
my $value;
|
||||
my $position = index($change, ':');
|
||||
if ($position == -1) {
|
||||
$key = "state";
|
||||
$value = $keyValue[0];
|
||||
} else {
|
||||
$key = substr($change, 0, $position);
|
||||
$value = substr($change, $position + 2, length($change));
|
||||
}
|
||||
|
||||
if (!($stateFilter eq "") && !($value =~ m/$stateFilter/)) {
|
||||
Log $logLevel,
|
||||
"$gcmName $name: ignoring $key, as value $value is blocked by stateFilter regexp.";
|
||||
} elsif ($value eq "") {
|
||||
Log $logLevel, "$gcmName $name: ignoring $key, as value is empty.";
|
||||
} elsif ($lastDeviceStates->{$key} && $lastDeviceStates->{$key} eq $value) {
|
||||
my $savedValue = $lastDeviceStates->{$key};
|
||||
Log $logLevel,
|
||||
"$gcmName $name: ignoring $key, save value is $savedValue, value is $value";
|
||||
} else {
|
||||
$lastDeviceStates->{$key} = $value;
|
||||
# Multiple field updates are separated by <|>.
|
||||
if ($sendFieldCount != 0) {
|
||||
$val .= "<|>";
|
||||
}
|
||||
$sendFieldCount += 1;
|
||||
$val .= "$key:$value";
|
||||
}
|
||||
}
|
||||
|
||||
if (! ($stateFilter eq "") && ! ($value =~ m/$stateFilter/)) {
|
||||
Log $logLevel, "$gcmName $name: ignoring $key, as value $value is blocked by stateFilter regexp.";
|
||||
} elsif ($value eq "") {
|
||||
Log $logLevel, "$gcmName $name: ignoring $key, as value is empty.";
|
||||
} elsif ($lastDeviceStates->{$key} && $lastDeviceStates->{$key} eq $value) {
|
||||
my $savedValue = $lastDeviceStates->{$key};
|
||||
Log $logLevel, "$gcmName $name: ignoring $key, save value is $savedValue, value is $value";
|
||||
} else {
|
||||
$lastDeviceStates->{$key} = $value;
|
||||
# Multiple field updates are separated by <|>.
|
||||
if ($sendFieldCount != 0) {
|
||||
$val .= "<|>";
|
||||
}
|
||||
$sendFieldCount += 1;
|
||||
$val .= "$key:$value";
|
||||
if ($sendFieldCount > 0) {
|
||||
gcmsend_sendNotify($gcm, $name, $val);
|
||||
}
|
||||
}
|
||||
if ($sendFieldCount > 0) {
|
||||
gcmsend_sendNotify($gcm, $name, $val);
|
||||
}
|
||||
}
|
||||
|
||||
1;
|
||||
@ -314,7 +364,8 @@ sub gcmsend_notify($$)
|
||||
Notes:
|
||||
<ul>
|
||||
<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>
|
||||
|
||||
@ -349,8 +400,7 @@ sub gcmsend_notify($$)
|
||||
<br />Make the receiving device vibrate upon receiving the message. Must be true or false.</li>
|
||||
<li><a name="gcmsend_deviceFilter"><code>attr <name> deviceFilter <regexp></a>
|
||||
<br />Send a GCM notify only is the device name matches the given filter regexp.</li>
|
||||
<li><a name="gcmsend_playSound"><code attr <name> playSound <true|false></a>
|
||||
<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>
|
||||
<li><a name="gcmsend_cryptKey"><code>attr <name> cryptKey <key></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>
|
||||
</ul>
|
||||
</ul>
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user