This commit is contained in:
2022-10-23 01:39:27 +02:00
parent 8c17aab483
commit 1929b84685
4130 changed files with 479334 additions and 0 deletions

170
code_depricated/instafeed.1.pl Executable file
View File

@@ -0,0 +1,170 @@
#!usr/bin/perl
use strict;
use warnings;
use Data::Dumper;
use JSON::XS qw(encode_json decode_json);
use File::Slurp qw(read_file write_file);
use Getopt::Long qw(GetOptions);
my %config = (
'uploadPHP' => {
'USERNAME' => 'dreamyourmansion',
'PASSWORD' => 'H5AZ#dQZ5Ycf',
'DEBUG' => 1,
'TRUNCATED_DEBUG' => 1,
'PROXY_USER' => 'zino%40onlinehome.de',
'PROXY_PASSWORD' => 'zinomedial33t',
'PROXY_IP' => 'de435.nordvpn.com',
'PROXY_PORT' => 80,
},
'profile' => undef,
'imageDir' => './src/images/',
'SRCRoot' => './src/',
'DBFilepath' => '/home/pi/instafeed/src/db/db.dat',
'uploadPHP_CMD' => '/usr/bin/php /home/pi/instafeed/vendor/mgp25/instagram-php/examples/uploadPhoto.php',
'uploadPHP_DESCRIPTION_ADD' => "The most beautiful real estates in the world!\n\nBenefit from the flourishing housing market in Germany. Contact us now by DM.\n\nVom Mieter zum Eigentümer! Exklusives Portfolio: Kontaktiere uns jetzt per DM.",
'uploadPHP_TAGS' => '#investment #immobilie #mansionhouse #dream #poolhouse #villa #realestate #loft #awesome #lifestyle #motivation #luxury',
);
my (%data, %db, %profiles);
my $dbKeysStart = 0;
GetOptions ('profile=s' => \$config{'profile'}) or die "Usage: $0 --profile *name*\n";
die "Usage: $0 --profile *name*\n" if !$config{'profile'} ;
# MAIN
&UndumpFromFile();
#print Dumper \%db;
&DirectoryListing();
# print Dumper \%data;
&FindNewDataset();
&Summary();
sub UndumpFromFile {
&Delimiter((caller(0))[3]);
if (-e $config{'DBFilepath'}) {
my $json = read_file($config{'DBFilepath'}, { binmode => ':raw' });
if (!$json) {
warn "DB file $config{'DBFilepath'} is empty.\n";
return;
}
%db = %{ decode_json $json };
$dbKeysStart = scalar(keys(%db));
print "INFO: $config{'DBFilepath'} has " . $dbKeysStart . " keys.\n";
}
elsif (!-e $config{'DBFilepath'}) {
warn "INFO: NO DB file found at $config{'DBFilepath'}\n";
exit;
}
}
sub DirectoryListing {
&Delimiter((caller(0))[3]);
opendir(DIR, $config{'imageDir'});
my @files = grep(/\.jpg$/,readdir(DIR));
closedir(DIR);
%data = map { $_ => { 'FILEPATH' => "$config{'imageDir'}$_" } } @files;
# @hash{@keys} = undef;
}
sub Summary {
&Delimiter((caller(0))[3]);
print "$config{'DBFilepath'} has " . scalar(keys(%db)) . " keys (before $dbKeysStart).\n";
}
sub FindNewDataset {
&Delimiter((caller(0))[3]);
my $i = 0;
for my $key (keys %data) {
if (exists $db{$key}) {
print "OLD: $key\n";
}
elsif (!exists $db{$key}) {
print "NEW: $key\n";
my $success = &uploadPHP($data{$key}{'FILEPATH'});
if ($success) {
print "success is $success\n";
&AddToDB($key);
&WipeData($key);
last;
}
}
$i++;
}
if ($i == scalar(keys(%data))) {
warn "\nNO NEW FILES AVAILABLE.\n";
}
}
sub uploadPHP {
&Delimiter((caller(0))[3]);
my $filepath = shift;
my $success = 1;
my $captionText = "$config{'uploadPHP_DESCRIPTION_ADD'}\n\n$config{'uploadPHP_TAGS'}";
open PHPOUT, "$config{'uploadPHP_CMD'} $filepath \'$captionText\' $config{'uploadPHP'}{'USERNAME'} $config{'uploadPHP'}{'PASSWORD'} $config{'uploadPHP'}{'DEBUG'} $config{'uploadPHP'}{'TRUNCATED_DEBUG'} $config{'uploadPHP'}{'PROXY_USER'} $config{'uploadPHP'}{'PROXY_PASSWORD'} $config{'uploadPHP'}{'PROXY_IP'} $config{'uploadPHP'}{'PROXY_PORT'}|";
while (<PHPOUT>) {
print $_; # PRINT CURRENT PHP OUPUT LINE
if ($_ =~ m/error/) {
$success = 0;
}
}
return $success;
}
sub WipeData {
&Delimiter((caller(0))[3]);
my $key = shift;
print "Deleting $data{$key}{'FILEPATH'}...";
unlink($data{$key}{'FILEPATH'}) or die "Could not delete $data{$key}{'FILEPATH'}!\n";
print " Done.\n";
}
sub AddToDB {
&Delimiter((caller(0))[3]);
my $key = shift;
$data{$key}{'TIMESTAMP_UPLOADED'} = &GetTimestamp('YMDHMS');
$db{$key} = $data{$key};
my $json = encode_json \%db;
write_file($config{'DBFilepath'}, { binmode => ':raw' }, $json);
}
sub Delimiter {
my $SubName = shift;
print "\n" . "-" x 80 . "\nSUB " . $SubName . "\n" . '-' x 80 . "\n";
}
sub GetTimestamp {
#&Delimiter((caller(0))[3]);
my $switch = shift;
my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst)=localtime(time);
my $nice_timestamp;
if ($switch eq 'YMDHMS') {
$nice_timestamp = sprintf ( "%04d%02d%02d_%02d%02d%02d", $year+1900,$mon+1,$mday,$hour,$min,$sec);
}
elsif ($switch eq 'YMD') {
$nice_timestamp = sprintf ( "%04d%02d%02d", $year+1900,$mon+1,$mday);
}
elsif ($switch eq 'year') {
$nice_timestamp = $year+1900;
}
elsif ($switch eq 'month') {
$nice_timestamp = $mon+10;
}
else {
print "Invalid/no switch detected. Use: 'YMDHMS' / 'YMD'\n";
}
return $nice_timestamp;
}

253
code_depricated/instafeed.pl Executable file
View File

@@ -0,0 +1,253 @@
#!usr/bin/perl
use strict;
use warnings;
use Data::Dumper;
use JSON::XS qw(encode_json decode_json);
use File::Slurp qw(read_file write_file);
use Getopt::Long qw(GetOptions);
use File::Basename;
my %config = (
'profile' => undef,
'SRCRoot' => './src/',
'uploadPHP_CMD' => '/usr/bin/php ./vendor/mgp25/instagram-php/examples/uploadPhoto.php',
'uploadPHP_debug' => 1,
'uploadPHP_truncated_debug' => 1,
);
my %profile = (
'dreamyourmansion' => {
'DBFilepath' => './src/db/db_dreamyourmansion.dat',
'imageDir' => './src/images/dreamyourmansion',
'filename_as_title' => 0,
'uploadPHP' => {
'username' => 'dreamyourmansion',
'password' => 'nBLT!4H3aI@c',
'truncated_debug' => 1,
'proxy_user' => 'zino%40onlinehome.de',
'proxy_password' => 'zinomedial33t',
'proxy_ip' => 'de435.nordvpn.com',
'proxy_port' => 80,
'tags' => '#investment #immobilie #mansionhouse #dream #poolhouse #villa #realestate #loft #awesome #lifestyle #motivation #luxury',
'description_add' => "The most beautiful real estates in the world!\n\nBenefit from the flourishing housing market in Germany. Contact us now by DM.\n\nVom Mieter zum Eigentümer! Exklusives Portfolio: Kontaktiere uns jetzt per DM.",
},
},
'vstbestprices' => {
'DBFilepath' => './src/db/db_vstbestprices.dat',
'imageDir' => './src/images/vstbestprices',
'filename_as_title' => 1,
'uploadPHP' => {
'username' => 'vstbestprices',
'password' => 'Vst#1337vst#1337',
'truncated_debug' => 1,
'proxy_user' => 'zino%40onlinehome.de',
'proxy_password' => 'zinomedial33t',
'proxy_ip' => 'de435.nordvpn.com',
'proxy_port' => 80,
'tags' => '#Beats #FLStudio20 #Producer #Ableton #Beatmaker #Studio #ProTools #Music #DAW #LogicPro #FruityLoops #VST #VSTplugins #NativeInstruments #MIDI #Drums #AutoTune #Spectrasonics #Omnisphere #AutoTune #Plugins #Keyscape #Trilian #Logic',
'description_add' => 'INSTALLATION SUPPORT is included in all prices so you can relax and focus on producing!',
},
},
'vstbestprices_testing' => {
'DBFilepath' => './src/db/db_vstbestprices.dat',
'imageDir' => './src/images/vstbestprices',
'filename_as_title' => 1,
'uploadPHP' => {
'username' => 'adobebestprices',
'password' => 'vst#1337',
'truncated_debug' => 1,
'proxy_user' => 'zino%40onlinehome.de',
'proxy_password' => 'zinomedial33t',
'proxy_ip' => 'de435.nordvpn.com',
'proxy_port' => 80,
'tags' => '#Beats #FLStudio20 #Producer #Ableton #Beatmaker #Studio #ProTools #Music #DAW #LogicPro #FruityLoops #VST #VSTplugins #NativeInstruments #MIDI #Drums #AutoTune #Spectrasonics #Omnisphere #AutoTune #Plugins #Keyscape #Trilian #Logic',
'description_add' => 'INSTALLATION SUPPORT is included in all prices so you can relax and focus on producing!',
},
},
'adobebestprices' => {
'DBFilepath' => './src/db/db_adobebestprices.dat',
'imageDir' => './src/images/adobebestprices/',
'filename_as_title' => 0,
'uploadPHP' => {
'username' => 'adobebestprices',
'password' => 'vst#1337',
'truncated_debug' => 1,
'proxy_user' => 'zino%40onlinehome.de',
'proxy_password' => 'zinomedial33t',
'proxy_ip' => 'de435.nordvpn.com',
'proxy_port' => 80,
'tags' => '#adobe #photoshop #adobeillustrator #vector #illustrator #adobephotoshop #vectorart #graphicdesign #aftereffects #logo #cs6 #lightroom #graphic',
'description_add' => 'Photoshop, Lightroom, Illustrator, Dreamviewer, Premiere for WIN & MAC | Installation support is included in all our prices!',
},
},
);
my (%data, %db);
my $dbKeysStart = 0;
my $profile;
# MAIN
&CheckParameter();
&UndumpFromFile();
#print Dumper \%db;
&DirectoryListing();
print Dumper \%data;
&FindNewDataset();
&Summary();
sub CheckParameter {
&Delimiter((caller(0))[3]);
GetOptions ('profile=s' => \$config{'profile'}) or die "Usage: $0 --profile *name*\n";
die "Usage: $0 --profile *name*\n" if !$config{'profile'} ;
$profile = $config{'profile'};
if (!exists $profile{$profile}) {
print "Template for profile '$profile' does not exist. Following templates are available:\n";
print "'$_' " for keys(%profile);
print "\n";
die;
}
}
sub UndumpFromFile {
&Delimiter((caller(0))[3]);
if (-e $profile{$profile}{'DBFilepath'}) {
my $json = read_file($profile{$profile}{'DBFilepath'}, { binmode => ':raw' });
if (!$json) {
warn "DB file $profile{$profile}{'DBFilepath'} is empty.\n";
return;
}
%db = %{ decode_json $json };
$dbKeysStart = scalar(keys(%db));
print "INFO: $profile{$profile}{'DBFilepath'} has " . $dbKeysStart . " keys.\n";
}
elsif (!-e $profile{$profile}{'DBFilepath'}) {
print "INFO: NO DB file found at $profile{$profile}{'DBFilepath'}. Creating now... ";
write_file($profile{$profile}{'DBFilepath'}, '');
print "done.\n";
die "Please restart.";
# &UndumpFromFile();
}
}
sub DirectoryListing {
&Delimiter((caller(0))[3]);
# opendir(DIR, $profile{$profile}{'imageDir'});
# my @files = grep(/\.jpg$|\.png$|\.jpeg$|/,readdir(DIR));
# closedir(DIR);
my @files = glob ( "$profile{$profile}{'imageDir'}/*" );
%data = map { $_ => { 'FILEPATH' => "$_" } } @files;
}
sub Summary {
&Delimiter((caller(0))[3]);
print "$profile{$profile}{'DBFilepath'} has " . scalar(keys(%db)) . " keys (before $dbKeysStart).\n";
}
sub FindNewDataset {
&Delimiter((caller(0))[3]);
my $i = 0;
for my $key (keys %data) {
if (exists $db{$key}) {
print "OLD: $key\n";
}
elsif (!exists $db{$key}) {
print "NEW: $key\n";
my $success = &uploadPHP($data{$key}{'FILEPATH'});
&AddToDB($key);
&WipeData($key);
last;
# if ($success) {
# print "success is $success\n";
# &AddToDB($key);
# &WipeData($key);
# last;
# }
}
$i++;
}
if ($i == scalar(keys(%data))) {
warn "\nNO NEW FILES AVAILABLE.\n";
}
}
sub uploadPHP {
&Delimiter((caller(0))[3]);
my $filepath = shift;
my $success = 1;
my $captionText = "$profile{$profile}{'uploadPHP'}{'description_add'}\n\n$profile{$profile}{'uploadPHP'}{'tags'}";
if ($profile{$profile}{'filename_as_title'}) {
my $filename = basename($filepath);
$filename =~ s/(NO INSTALL)|(SymLink Installer)//g;
$filename =~ s/( , )|(\.[^.]+$)//g;
$captionText = "$filename\n\n" . $captionText;
# print Dumper $captionText;
}
open PHPOUT, "$config{'uploadPHP_CMD'} \'$filepath\' \'$captionText\' $profile{$profile}{'uploadPHP'}{'username'} $profile{$profile}{'uploadPHP'}{'password'} $config{'uploadPHP_debug'} $config{'uploadPHP_truncated_debug'} $profile{$profile}{'uploadPHP'}{'proxy_user'} $profile{$profile}{'uploadPHP'}{'proxy_password'} $profile{$profile}{'uploadPHP'}{'proxy_ip'} $profile{$profile}{'uploadPHP'}{'proxy_port'}|";
while (<PHPOUT>) {
print $_; # PRINT CURRENT PHP OUPUT LINE
if ($_ =~ m/error/) {
$success = 0;
}
}
return $success;
}
sub WipeData {
&Delimiter((caller(0))[3]);
my $key = shift;
print "Deleting $data{$key}{'FILEPATH'}...";
unlink($data{$key}{'FILEPATH'}) or die "Could not delete $data{$key}{'FILEPATH'}!\n";
print " Done.\n";
}
sub AddToDB {
&Delimiter((caller(0))[3]);
my $key = shift;
$data{$key}{'TIMESTAMP_UPLOADED'} = &GetTimestamp('YMDHMS');
$db{$key} = $data{$key};
my $json = encode_json \%db;
write_file($profile{$profile}{'DBFilepath'}, { binmode => ':raw' }, $json);
}
sub Delimiter {
my $SubName = shift;
print "\n" . "-" x 80 . "\nSUB " . $SubName . "\n" . '-' x 80 . "\n";
}
sub GetTimestamp {
#&Delimiter((caller(0))[3]);
my $switch = shift;
my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst)=localtime(time);
my $nice_timestamp;
if ($switch eq 'YMDHMS') {
$nice_timestamp = sprintf ( "%04d%02d%02d_%02d%02d%02d", $year+1900,$mon+1,$mday,$hour,$min,$sec);
}
elsif ($switch eq 'YMD') {
$nice_timestamp = sprintf ( "%04d%02d%02d", $year+1900,$mon+1,$mday);
}
elsif ($switch eq 'year') {
$nice_timestamp = $year+1900;
}
elsif ($switch eq 'month') {
$nice_timestamp = $mon+10;
}
else {
print "Invalid/no switch detected. Use: 'YMDHMS' / 'YMD'\n";
}
return $nice_timestamp;
}

5
composer.json Executable file
View File

@@ -0,0 +1,5 @@
{
"require": {
"mgp25/instagram-php": "^7.0"
}
}

1294
composer.lock generated Executable file

File diff suppressed because it is too large Load Diff

0
foo Executable file
View File

261
instafeed.pl Executable file
View File

@@ -0,0 +1,261 @@
#!usr/bin/perl
use strict;
use warnings;
use Data::Dumper;
use JSON::XS qw(encode_json decode_json);
use File::Slurp qw(read_file write_file);
use Getopt::Long qw(GetOptions);
use File::Basename;
use HTTP::Cookies;
use Cwd qw(cwd);
my %config = (
'profile' => undef,
'SRCRoot' => './src/',
'uploadPHP_CMD' => '/usr/bin/php ./vendor/mgp25/instagram-php/examples/uploadPhoto.php',
'uploadPHP_debug' => 1,
'uploadPHP_truncated_debug' => 1,
'uploadPHP_autoload' => cwd . '/src/mpg25-instagram-api/vendor/autoload.php',
);
my %profile = (
'dreamyourmansion' => {
'DBFilepath' => './src/db/db_dreamyourmansion.dat',
'imageDir' => './src/images/dreamyourmansion',
'filename_as_title' => 0,
'uploadPHP' => {
'username' => 'dreamyourmansion',
'password' => 'nBLT!4H3aI@c',
'truncated_debug' => 1,
'proxy_user' => 'zino%40onlinehome.de',
'proxy_password' => 'zinomedial33t',
'proxy_ip' => 'de786.nordvpn.com',
'proxy_port' => 80,
'tags' => '#investment #immobilie #mansionhouse #dream #poolhouse #villa #realestate #loft #awesome #lifestyle #motivation #luxury',
'description_add' => "The most beautiful real estates in the world!",
},
},
'vstbestprices' => {
'DBFilepath' => './src/db/db_vstbestprices.dat',
'imageDir' => './src/images/vstbestprices',
'filename_as_title' => 1,
'uploadPHP' => {
'username' => 'vstbestprices',
'password' => 'Vst#1337vst#1337',
'truncated_debug' => 1,
'proxy_user' => 'zino%40onlinehome.de',
'proxy_password' => 'zinomedial33t',
'proxy_ip' => 'de435.nordvpn.com',
'proxy_port' => 80,
'tags' => '#Beats #FLStudio20 #Producer #Ableton #Beatmaker #Studio #ProTools #Music #DAW #LogicPro #FruityLoops #VST #VSTplugins #NativeInstruments #MIDI #Drums #AutoTune #Spectrasonics #Omnisphere #AutoTune #Plugins #Keyscape #Trilian #Logic',
'description_add' => 'INSTALLATION SUPPORT is included in all prices so you can relax and focus on producing!',
},
},
'vstbestprices_testing' => {
'DBFilepath' => './src/db/db_vstbestprices.dat',
'imageDir' => './src/images/vstbestprices',
'filename_as_title' => 1,
'uploadPHP' => {
'username' => 'adobebestprices',
'password' => 'vst#1337',
'truncated_debug' => 1,
'proxy_user' => 'zino%40onlinehome.de',
'proxy_password' => 'zinomedial33t',
'proxy_ip' => 'de435.nordvpn.com',
'proxy_port' => 80,
'tags' => '#Beats #FLStudio20 #Producer #Ableton #Beatmaker #Studio #ProTools #Music #DAW #LogicPro #FruityLoops #VST #VSTplugins #NativeInstruments #MIDI #Drums #AutoTune #Spectrasonics #Omnisphere #AutoTune #Plugins #Keyscape #Trilian #Logic',
'description_add' => 'INSTALLATION SUPPORT is included in all prices so you can relax and focus on producing!',
},
},
'adobebestprices' => {
'DBFilepath' => './src/db/db_adobebestprices.dat',
'imageDir' => './src/images/adobebestprices/',
'filename_as_title' => 0,
'uploadPHP' => {
'username' => 'adobebestprices',
'password' => 'vst#1337',
'truncated_debug' => 1,
'proxy_user' => 'zino%40onlinehome.de',
'proxy_password' => 'zinomedial33t',
'proxy_ip' => 'de435.nordvpn.com',
'proxy_port' => 80,
'tags' => '#adobe #photoshop #adobeillustrator #vector #illustrator #adobephotoshop #vectorart #graphicdesign #aftereffects #logo #cs6 #lightroom #graphic',
'description_add' => 'Photoshop, Lightroom, Illustrator, Dreamviewer, Premiere for WIN & MAC | Installation support is included in all our prices!',
},
},
);
my (%data, %db);
my $dbKeysStart = 0;
my $profile;
# MAIN
&CheckParameter();
&UndumpFromFile();
#print Dumper \%db;
&DirectoryListing();
print Dumper \%data;
&FindNewDataset();
&Summary();
sub CheckParameter {
&Delimiter((caller(0))[3]);
GetOptions ('profile=s' => \$config{'profile'}) or die &PrintUsage();
die &PrintUsage() if !$config{'profile'};
$profile = $config{'profile'};
if (!exists $profile{$profile}) {
print "Profile '$profile' does not exist.\n";
&PrintUsage();
die;
}
}
sub PrintUsage {
print "Usage: $0 --profile *name*\n";
print "Following profiles are available:\n";
print "* '$_'\n" for keys(%profile);
}
sub UndumpFromFile {
&Delimiter((caller(0))[3]);
if (-e $profile{$profile}{'DBFilepath'}) {
my $json = read_file($profile{$profile}{'DBFilepath'}, { binmode => ':raw' });
if (!$json) {
warn "DB file $profile{$profile}{'DBFilepath'} is empty.\n";
return;
}
%db = %{ decode_json $json };
$dbKeysStart = scalar(keys(%db));
print "INFO: $profile{$profile}{'DBFilepath'} has " . $dbKeysStart . " keys.\n";
}
elsif (!-e $profile{$profile}{'DBFilepath'}) {
print "INFO: NO DB file found at $profile{$profile}{'DBFilepath'}. Creating now... ";
write_file($profile{$profile}{'DBFilepath'}, '');
print "done.\n";
die "Please restart.";
# &UndumpFromFile();
}
}
sub DirectoryListing {
&Delimiter((caller(0))[3]);
# opendir(DIR, $profile{$profile}{'imageDir'});
# my @files = grep(/\.jpg$|\.png$|\.jpeg$|/,readdir(DIR));
# closedir(DIR);
my @files = glob ( "$profile{$profile}{'imageDir'}/*" );
%data = map { $_ => { 'FILEPATH' => "$_" } } @files;
}
sub Summary {
&Delimiter((caller(0))[3]);
print "$profile{$profile}{'DBFilepath'} has " . scalar(keys(%db)) . " keys (before $dbKeysStart).\n";
}
sub FindNewDataset {
&Delimiter((caller(0))[3]);
my $i = 0;
for my $key (keys %data) {
if (exists $db{$key}) {
print "OLD: $key\n";
}
elsif (!exists $db{$key}) {
print "NEW: $key\n";
my $success = &uploadPHP($data{$key}{'FILEPATH'});
&AddToDB($key);
&WipeData($key);
last;
# if ($success) {
# print "success is $success\n";
# &AddToDB($key);
# &WipeData($key);
# last;
# }
}
$i++;
}
if ($i == scalar(keys(%data))) {
warn "\nNO NEW FILES AVAILABLE.\n";
}
}
sub uploadPHP {
&Delimiter((caller(0))[3]);
my $filepath = shift;
my $success = 1;
my $captionText = "$profile{$profile}{'uploadPHP'}{'description_add'}\n\n$profile{$profile}{'uploadPHP'}{'tags'}";
if ($profile{$profile}{'filename_as_title'}) {
my $filename = basename($filepath);
$filename =~ s/(NO INSTALL)|(SymLink Installer)//g;
$filename =~ s/( , )|(\.[^.]+$)//g;
$captionText = "$filename\n\n" . $captionText;
# print Dumper $captionText;
}
open PHPOUT, "$config{'uploadPHP_CMD'} \'$filepath\' \'$captionText\' $profile{$profile}{'uploadPHP'}{'username'} $profile{$profile}{'uploadPHP'}{'password'} $config{'uploadPHP_debug'} $config{'uploadPHP_truncated_debug'} $profile{$profile}{'uploadPHP'}{'proxy_user'} $profile{$profile}{'uploadPHP'}{'proxy_password'} $profile{$profile}{'uploadPHP'}{'proxy_ip'} $profile{$profile}{'uploadPHP'}{'proxy_port'} \'$config{'uploadPHP_autoload'}\'|";
while (<PHPOUT>) {
print $_; # PRINT CURRENT PHP OUPUT LINE
if ($_ =~ m/error/) {
$success = 0;
}
}
return $success;
}
sub WipeData {
&Delimiter((caller(0))[3]);
my $key = shift;
print "Deleting $data{$key}{'FILEPATH'}...";
unlink($data{$key}{'FILEPATH'}) or die "Could not delete $data{$key}{'FILEPATH'}!\n";
print " Done.\n";
}
sub AddToDB {
&Delimiter((caller(0))[3]);
my $key = shift;
$data{$key}{'TIMESTAMP_UPLOADED'} = &GetTimestamp('YMDHMS');
$db{$key} = $data{$key};
my $json = encode_json \%db;
write_file($profile{$profile}{'DBFilepath'}, { binmode => ':raw' }, $json);
}
sub Delimiter {
my $SubName = shift;
print "\n" . "-" x 80 . "\nSUB " . $SubName . "\n" . '-' x 80 . "\n";
}
sub GetTimestamp {
#&Delimiter((caller(0))[3]);
my $switch = shift;
my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst)=localtime(time);
my $nice_timestamp;
if ($switch eq 'YMDHMS') {
$nice_timestamp = sprintf ( "%04d%02d%02d_%02d%02d%02d", $year+1900,$mon+1,$mday,$hour,$min,$sec);
}
elsif ($switch eq 'YMD') {
$nice_timestamp = sprintf ( "%04d%02d%02d", $year+1900,$mon+1,$mday);
}
elsif ($switch eq 'year') {
$nice_timestamp = $year+1900;
}
elsif ($switch eq 'month') {
$nice_timestamp = $mon+10;
}
else {
print "Invalid/no switch detected. Use: 'YMDHMS' / 'YMD'\n";
}
return $nice_timestamp;
}

View File

@@ -0,0 +1,170 @@
#!usr/bin/perl
use strict;
use warnings;
use Data::Dumper;
use JSON::XS qw(encode_json decode_json);
use File::Slurp qw(read_file write_file);
use Getopt::Long qw(GetOptions);
my %config = (
'uploadPHP' => {
'USERNAME' => 'dreamyourmansion',
'PASSWORD' => 'H5AZ#dQZ5Ycf',
'DEBUG' => 1,
'TRUNCATED_DEBUG' => 1,
'PROXY_USER' => 'zino%40onlinehome.de',
'PROXY_PASSWORD' => 'zinomedial33t',
'PROXY_IP' => 'de435.nordvpn.com',
'PROXY_PORT' => 80,
},
'profile' => undef,
'imageDir' => './src/images/',
'SRCRoot' => './src/',
'DBFilepath' => '/home/pi/instafeed/src/db/db.dat',
'uploadPHP_CMD' => '/usr/bin/php /home/pi/instafeed/vendor/mgp25/instagram-php/examples/uploadPhoto.php',
'uploadPHP_DESCRIPTION_ADD' => "The most beautiful real estates in the world!\n\nBenefit from the flourishing housing market in Germany. Contact us now by DM.\n\nVom Mieter zum Eigentümer! Exklusives Portfolio: Kontaktiere uns jetzt per DM.",
'uploadPHP_TAGS' => '#investment #immobilie #mansionhouse #dream #poolhouse #villa #realestate #loft #awesome #lifestyle #motivation #luxury',
);
my (%data, %db, %profiles);
my $dbKeysStart = 0;
GetOptions ('profile=s' => \$config{'profile'}) or die "Usage: $0 --profile *name*\n";
die "Usage: $0 --profile *name*\n" if !$config{'profile'} ;
# MAIN
&UndumpFromFile();
#print Dumper \%db;
&DirectoryListing();
# print Dumper \%data;
&FindNewDataset();
&Summary();
sub UndumpFromFile {
&Delimiter((caller(0))[3]);
if (-e $config{'DBFilepath'}) {
my $json = read_file($config{'DBFilepath'}, { binmode => ':raw' });
if (!$json) {
warn "DB file $config{'DBFilepath'} is empty.\n";
return;
}
%db = %{ decode_json $json };
$dbKeysStart = scalar(keys(%db));
print "INFO: $config{'DBFilepath'} has " . $dbKeysStart . " keys.\n";
}
elsif (!-e $config{'DBFilepath'}) {
warn "INFO: NO DB file found at $config{'DBFilepath'}\n";
exit;
}
}
sub DirectoryListing {
&Delimiter((caller(0))[3]);
opendir(DIR, $config{'imageDir'});
my @files = grep(/\.jpg$/,readdir(DIR));
closedir(DIR);
%data = map { $_ => { 'FILEPATH' => "$config{'imageDir'}$_" } } @files;
# @hash{@keys} = undef;
}
sub Summary {
&Delimiter((caller(0))[3]);
print "$config{'DBFilepath'} has " . scalar(keys(%db)) . " keys (before $dbKeysStart).\n";
}
sub FindNewDataset {
&Delimiter((caller(0))[3]);
my $i = 0;
for my $key (keys %data) {
if (exists $db{$key}) {
print "OLD: $key\n";
}
elsif (!exists $db{$key}) {
print "NEW: $key\n";
my $success = &uploadPHP($data{$key}{'FILEPATH'});
if ($success) {
print "success is $success\n";
&AddToDB($key);
&WipeData($key);
last;
}
}
$i++;
}
if ($i == scalar(keys(%data))) {
warn "\nNO NEW FILES AVAILABLE.\n";
}
}
sub uploadPHP {
&Delimiter((caller(0))[3]);
my $filepath = shift;
my $success = 1;
my $captionText = "$config{'uploadPHP_DESCRIPTION_ADD'}\n\n$config{'uploadPHP_TAGS'}";
open PHPOUT, "$config{'uploadPHP_CMD'} $filepath \'$captionText\' $config{'uploadPHP'}{'USERNAME'} $config{'uploadPHP'}{'PASSWORD'} $config{'uploadPHP'}{'DEBUG'} $config{'uploadPHP'}{'TRUNCATED_DEBUG'} $config{'uploadPHP'}{'PROXY_USER'} $config{'uploadPHP'}{'PROXY_PASSWORD'} $config{'uploadPHP'}{'PROXY_IP'} $config{'uploadPHP'}{'PROXY_PORT'}|";
while (<PHPOUT>) {
print $_; # PRINT CURRENT PHP OUPUT LINE
if ($_ =~ m/error/) {
$success = 0;
}
}
return $success;
}
sub WipeData {
&Delimiter((caller(0))[3]);
my $key = shift;
print "Deleting $data{$key}{'FILEPATH'}...";
unlink($data{$key}{'FILEPATH'}) or die "Could not delete $data{$key}{'FILEPATH'}!\n";
print " Done.\n";
}
sub AddToDB {
&Delimiter((caller(0))[3]);
my $key = shift;
$data{$key}{'TIMESTAMP_UPLOADED'} = &GetTimestamp('YMDHMS');
$db{$key} = $data{$key};
my $json = encode_json \%db;
write_file($config{'DBFilepath'}, { binmode => ':raw' }, $json);
}
sub Delimiter {
my $SubName = shift;
print "\n" . "-" x 80 . "\nSUB " . $SubName . "\n" . '-' x 80 . "\n";
}
sub GetTimestamp {
#&Delimiter((caller(0))[3]);
my $switch = shift;
my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst)=localtime(time);
my $nice_timestamp;
if ($switch eq 'YMDHMS') {
$nice_timestamp = sprintf ( "%04d%02d%02d_%02d%02d%02d", $year+1900,$mon+1,$mday,$hour,$min,$sec);
}
elsif ($switch eq 'YMD') {
$nice_timestamp = sprintf ( "%04d%02d%02d", $year+1900,$mon+1,$mday);
}
elsif ($switch eq 'year') {
$nice_timestamp = $year+1900;
}
elsif ($switch eq 'month') {
$nice_timestamp = $mon+10;
}
else {
print "Invalid/no switch detected. Use: 'YMDHMS' / 'YMD'\n";
}
return $nice_timestamp;
}

View File

@@ -0,0 +1,253 @@
#!usr/bin/perl
use strict;
use warnings;
use Data::Dumper;
use JSON::XS qw(encode_json decode_json);
use File::Slurp qw(read_file write_file);
use Getopt::Long qw(GetOptions);
use File::Basename;
my %config = (
'profile' => undef,
'SRCRoot' => './src/',
'uploadPHP_CMD' => '/usr/bin/php ./vendor/mgp25/instagram-php/examples/uploadPhoto.php',
'uploadPHP_debug' => 1,
'uploadPHP_truncated_debug' => 1,
);
my %profile = (
'dreamyourmansion' => {
'DBFilepath' => './src/db/db_dreamyourmansion.dat',
'imageDir' => './src/images/dreamyourmansion',
'filename_as_title' => 0,
'uploadPHP' => {
'username' => 'dreamyourmansion',
'password' => 'nBLT!4H3aI@c',
'truncated_debug' => 1,
'proxy_user' => 'zino%40onlinehome.de',
'proxy_password' => 'zinomedial33t',
'proxy_ip' => 'de435.nordvpn.com',
'proxy_port' => 80,
'tags' => '#investment #immobilie #mansionhouse #dream #poolhouse #villa #realestate #loft #awesome #lifestyle #motivation #luxury',
'description_add' => "The most beautiful real estates in the world!\n\nBenefit from the flourishing housing market in Germany. Contact us now by DM.\n\nVom Mieter zum Eigentümer! Exklusives Portfolio: Kontaktiere uns jetzt per DM.",
},
},
'vstbestprices' => {
'DBFilepath' => './src/db/db_vstbestprices.dat',
'imageDir' => './src/images/vstbestprices',
'filename_as_title' => 1,
'uploadPHP' => {
'username' => 'vstbestprices',
'password' => 'Vst#1337vst#1337',
'truncated_debug' => 1,
'proxy_user' => 'zino%40onlinehome.de',
'proxy_password' => 'zinomedial33t',
'proxy_ip' => 'de435.nordvpn.com',
'proxy_port' => 80,
'tags' => '#Beats #FLStudio20 #Producer #Ableton #Beatmaker #Studio #ProTools #Music #DAW #LogicPro #FruityLoops #VST #VSTplugins #NativeInstruments #MIDI #Drums #AutoTune #Spectrasonics #Omnisphere #AutoTune #Plugins #Keyscape #Trilian #Logic',
'description_add' => 'INSTALLATION SUPPORT is included in all prices so you can relax and focus on producing!',
},
},
'vstbestprices_testing' => {
'DBFilepath' => './src/db/db_vstbestprices.dat',
'imageDir' => './src/images/vstbestprices',
'filename_as_title' => 1,
'uploadPHP' => {
'username' => 'adobebestprices',
'password' => 'vst#1337',
'truncated_debug' => 1,
'proxy_user' => 'zino%40onlinehome.de',
'proxy_password' => 'zinomedial33t',
'proxy_ip' => 'de435.nordvpn.com',
'proxy_port' => 80,
'tags' => '#Beats #FLStudio20 #Producer #Ableton #Beatmaker #Studio #ProTools #Music #DAW #LogicPro #FruityLoops #VST #VSTplugins #NativeInstruments #MIDI #Drums #AutoTune #Spectrasonics #Omnisphere #AutoTune #Plugins #Keyscape #Trilian #Logic',
'description_add' => 'INSTALLATION SUPPORT is included in all prices so you can relax and focus on producing!',
},
},
'adobebestprices' => {
'DBFilepath' => './src/db/db_adobebestprices.dat',
'imageDir' => './src/images/adobebestprices/',
'filename_as_title' => 0,
'uploadPHP' => {
'username' => 'adobebestprices',
'password' => 'vst#1337',
'truncated_debug' => 1,
'proxy_user' => 'zino%40onlinehome.de',
'proxy_password' => 'zinomedial33t',
'proxy_ip' => 'de435.nordvpn.com',
'proxy_port' => 80,
'tags' => '#adobe #photoshop #adobeillustrator #vector #illustrator #adobephotoshop #vectorart #graphicdesign #aftereffects #logo #cs6 #lightroom #graphic',
'description_add' => 'Photoshop, Lightroom, Illustrator, Dreamviewer, Premiere for WIN & MAC | Installation support is included in all our prices!',
},
},
);
my (%data, %db);
my $dbKeysStart = 0;
my $profile;
# MAIN
&CheckParameter();
&UndumpFromFile();
#print Dumper \%db;
&DirectoryListing();
print Dumper \%data;
&FindNewDataset();
&Summary();
sub CheckParameter {
&Delimiter((caller(0))[3]);
GetOptions ('profile=s' => \$config{'profile'}) or die "Usage: $0 --profile *name*\n";
die "Usage: $0 --profile *name*\n" if !$config{'profile'} ;
$profile = $config{'profile'};
if (!exists $profile{$profile}) {
print "Template for profile '$profile' does not exist. Following templates are available:\n";
print "'$_' " for keys(%profile);
print "\n";
die;
}
}
sub UndumpFromFile {
&Delimiter((caller(0))[3]);
if (-e $profile{$profile}{'DBFilepath'}) {
my $json = read_file($profile{$profile}{'DBFilepath'}, { binmode => ':raw' });
if (!$json) {
warn "DB file $profile{$profile}{'DBFilepath'} is empty.\n";
return;
}
%db = %{ decode_json $json };
$dbKeysStart = scalar(keys(%db));
print "INFO: $profile{$profile}{'DBFilepath'} has " . $dbKeysStart . " keys.\n";
}
elsif (!-e $profile{$profile}{'DBFilepath'}) {
print "INFO: NO DB file found at $profile{$profile}{'DBFilepath'}. Creating now... ";
write_file($profile{$profile}{'DBFilepath'}, '');
print "done.\n";
die "Please restart.";
# &UndumpFromFile();
}
}
sub DirectoryListing {
&Delimiter((caller(0))[3]);
# opendir(DIR, $profile{$profile}{'imageDir'});
# my @files = grep(/\.jpg$|\.png$|\.jpeg$|/,readdir(DIR));
# closedir(DIR);
my @files = glob ( "$profile{$profile}{'imageDir'}/*" );
%data = map { $_ => { 'FILEPATH' => "$_" } } @files;
}
sub Summary {
&Delimiter((caller(0))[3]);
print "$profile{$profile}{'DBFilepath'} has " . scalar(keys(%db)) . " keys (before $dbKeysStart).\n";
}
sub FindNewDataset {
&Delimiter((caller(0))[3]);
my $i = 0;
for my $key (keys %data) {
if (exists $db{$key}) {
print "OLD: $key\n";
}
elsif (!exists $db{$key}) {
print "NEW: $key\n";
my $success = &uploadPHP($data{$key}{'FILEPATH'});
&AddToDB($key);
&WipeData($key);
last;
# if ($success) {
# print "success is $success\n";
# &AddToDB($key);
# &WipeData($key);
# last;
# }
}
$i++;
}
if ($i == scalar(keys(%data))) {
warn "\nNO NEW FILES AVAILABLE.\n";
}
}
sub uploadPHP {
&Delimiter((caller(0))[3]);
my $filepath = shift;
my $success = 1;
my $captionText = "$profile{$profile}{'uploadPHP'}{'description_add'}\n\n$profile{$profile}{'uploadPHP'}{'tags'}";
if ($profile{$profile}{'filename_as_title'}) {
my $filename = basename($filepath);
$filename =~ s/(NO INSTALL)|(SymLink Installer)//g;
$filename =~ s/( , )|(\.[^.]+$)//g;
$captionText = "$filename\n\n" . $captionText;
# print Dumper $captionText;
}
open PHPOUT, "$config{'uploadPHP_CMD'} \'$filepath\' \'$captionText\' $profile{$profile}{'uploadPHP'}{'username'} $profile{$profile}{'uploadPHP'}{'password'} $config{'uploadPHP_debug'} $config{'uploadPHP_truncated_debug'} $profile{$profile}{'uploadPHP'}{'proxy_user'} $profile{$profile}{'uploadPHP'}{'proxy_password'} $profile{$profile}{'uploadPHP'}{'proxy_ip'} $profile{$profile}{'uploadPHP'}{'proxy_port'}|";
while (<PHPOUT>) {
print $_; # PRINT CURRENT PHP OUPUT LINE
if ($_ =~ m/error/) {
$success = 0;
}
}
return $success;
}
sub WipeData {
&Delimiter((caller(0))[3]);
my $key = shift;
print "Deleting $data{$key}{'FILEPATH'}...";
unlink($data{$key}{'FILEPATH'}) or die "Could not delete $data{$key}{'FILEPATH'}!\n";
print " Done.\n";
}
sub AddToDB {
&Delimiter((caller(0))[3]);
my $key = shift;
$data{$key}{'TIMESTAMP_UPLOADED'} = &GetTimestamp('YMDHMS');
$db{$key} = $data{$key};
my $json = encode_json \%db;
write_file($profile{$profile}{'DBFilepath'}, { binmode => ':raw' }, $json);
}
sub Delimiter {
my $SubName = shift;
print "\n" . "-" x 80 . "\nSUB " . $SubName . "\n" . '-' x 80 . "\n";
}
sub GetTimestamp {
#&Delimiter((caller(0))[3]);
my $switch = shift;
my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst)=localtime(time);
my $nice_timestamp;
if ($switch eq 'YMDHMS') {
$nice_timestamp = sprintf ( "%04d%02d%02d_%02d%02d%02d", $year+1900,$mon+1,$mday,$hour,$min,$sec);
}
elsif ($switch eq 'YMD') {
$nice_timestamp = sprintf ( "%04d%02d%02d", $year+1900,$mon+1,$mday);
}
elsif ($switch eq 'year') {
$nice_timestamp = $year+1900;
}
elsif ($switch eq 'month') {
$nice_timestamp = $mon+10;
}
else {
print "Invalid/no switch detected. Use: 'YMDHMS' / 'YMD'\n";
}
return $nice_timestamp;
}

5
instafeed/composer.json Executable file
View File

@@ -0,0 +1,5 @@
{
"require": {
"mgp25/instagram-php": "^7.0"
}
}

1294
instafeed/composer.lock generated Executable file

File diff suppressed because it is too large Load Diff

0
instafeed/foo Executable file
View File

261
instafeed/instafeed.pl Executable file
View File

@@ -0,0 +1,261 @@
#!usr/bin/perl
use strict;
use warnings;
use Data::Dumper;
use JSON::XS qw(encode_json decode_json);
use File::Slurp qw(read_file write_file);
use Getopt::Long qw(GetOptions);
use File::Basename;
use HTTP::Cookies;
use Cwd qw(cwd);
my %config = (
'profile' => undef,
'SRCRoot' => './src/',
'uploadPHP_CMD' => '/usr/bin/php ./vendor/mgp25/instagram-php/examples/uploadPhoto.php',
'uploadPHP_debug' => 1,
'uploadPHP_truncated_debug' => 1,
'uploadPHP_autoload' => cwd . '/src/mpg25-instagram-api/vendor/autoload.php',
);
my %profile = (
'dreamyourmansion' => {
'DBFilepath' => './src/db/db_dreamyourmansion.dat',
'imageDir' => './src/images/dreamyourmansion',
'filename_as_title' => 0,
'uploadPHP' => {
'username' => 'dreamyourmansion',
'password' => 'nBLT!4H3aI@c',
'truncated_debug' => 1,
'proxy_user' => 'zino%40onlinehome.de',
'proxy_password' => 'zinomedial33t',
'proxy_ip' => 'de786.nordvpn.com',
'proxy_port' => 80,
'tags' => '#investment #immobilie #mansionhouse #dream #poolhouse #villa #realestate #loft #awesome #lifestyle #motivation #luxury',
'description_add' => "The most beautiful real estates in the world!",
},
},
'vstbestprices' => {
'DBFilepath' => './src/db/db_vstbestprices.dat',
'imageDir' => './src/images/vstbestprices',
'filename_as_title' => 1,
'uploadPHP' => {
'username' => 'vstbestprices',
'password' => 'Vst#1337vst#1337',
'truncated_debug' => 1,
'proxy_user' => 'zino%40onlinehome.de',
'proxy_password' => 'zinomedial33t',
'proxy_ip' => 'de435.nordvpn.com',
'proxy_port' => 80,
'tags' => '#Beats #FLStudio20 #Producer #Ableton #Beatmaker #Studio #ProTools #Music #DAW #LogicPro #FruityLoops #VST #VSTplugins #NativeInstruments #MIDI #Drums #AutoTune #Spectrasonics #Omnisphere #AutoTune #Plugins #Keyscape #Trilian #Logic',
'description_add' => 'INSTALLATION SUPPORT is included in all prices so you can relax and focus on producing!',
},
},
'vstbestprices_testing' => {
'DBFilepath' => './src/db/db_vstbestprices.dat',
'imageDir' => './src/images/vstbestprices',
'filename_as_title' => 1,
'uploadPHP' => {
'username' => 'adobebestprices',
'password' => 'vst#1337',
'truncated_debug' => 1,
'proxy_user' => 'zino%40onlinehome.de',
'proxy_password' => 'zinomedial33t',
'proxy_ip' => 'de435.nordvpn.com',
'proxy_port' => 80,
'tags' => '#Beats #FLStudio20 #Producer #Ableton #Beatmaker #Studio #ProTools #Music #DAW #LogicPro #FruityLoops #VST #VSTplugins #NativeInstruments #MIDI #Drums #AutoTune #Spectrasonics #Omnisphere #AutoTune #Plugins #Keyscape #Trilian #Logic',
'description_add' => 'INSTALLATION SUPPORT is included in all prices so you can relax and focus on producing!',
},
},
'adobebestprices' => {
'DBFilepath' => './src/db/db_adobebestprices.dat',
'imageDir' => './src/images/adobebestprices/',
'filename_as_title' => 0,
'uploadPHP' => {
'username' => 'adobebestprices',
'password' => 'vst#1337',
'truncated_debug' => 1,
'proxy_user' => 'zino%40onlinehome.de',
'proxy_password' => 'zinomedial33t',
'proxy_ip' => 'de435.nordvpn.com',
'proxy_port' => 80,
'tags' => '#adobe #photoshop #adobeillustrator #vector #illustrator #adobephotoshop #vectorart #graphicdesign #aftereffects #logo #cs6 #lightroom #graphic',
'description_add' => 'Photoshop, Lightroom, Illustrator, Dreamviewer, Premiere for WIN & MAC | Installation support is included in all our prices!',
},
},
);
my (%data, %db);
my $dbKeysStart = 0;
my $profile;
# MAIN
&CheckParameter();
&UndumpFromFile();
#print Dumper \%db;
&DirectoryListing();
print Dumper \%data;
&FindNewDataset();
&Summary();
sub CheckParameter {
&Delimiter((caller(0))[3]);
GetOptions ('profile=s' => \$config{'profile'}) or die &PrintUsage();
die &PrintUsage() if !$config{'profile'};
$profile = $config{'profile'};
if (!exists $profile{$profile}) {
print "Profile '$profile' does not exist.\n";
&PrintUsage();
die;
}
}
sub PrintUsage {
print "Usage: $0 --profile *name*\n";
print "Following profiles are available:\n";
print "* '$_'\n" for keys(%profile);
}
sub UndumpFromFile {
&Delimiter((caller(0))[3]);
if (-e $profile{$profile}{'DBFilepath'}) {
my $json = read_file($profile{$profile}{'DBFilepath'}, { binmode => ':raw' });
if (!$json) {
warn "DB file $profile{$profile}{'DBFilepath'} is empty.\n";
return;
}
%db = %{ decode_json $json };
$dbKeysStart = scalar(keys(%db));
print "INFO: $profile{$profile}{'DBFilepath'} has " . $dbKeysStart . " keys.\n";
}
elsif (!-e $profile{$profile}{'DBFilepath'}) {
print "INFO: NO DB file found at $profile{$profile}{'DBFilepath'}. Creating now... ";
write_file($profile{$profile}{'DBFilepath'}, '');
print "done.\n";
die "Please restart.";
# &UndumpFromFile();
}
}
sub DirectoryListing {
&Delimiter((caller(0))[3]);
# opendir(DIR, $profile{$profile}{'imageDir'});
# my @files = grep(/\.jpg$|\.png$|\.jpeg$|/,readdir(DIR));
# closedir(DIR);
my @files = glob ( "$profile{$profile}{'imageDir'}/*" );
%data = map { $_ => { 'FILEPATH' => "$_" } } @files;
}
sub Summary {
&Delimiter((caller(0))[3]);
print "$profile{$profile}{'DBFilepath'} has " . scalar(keys(%db)) . " keys (before $dbKeysStart).\n";
}
sub FindNewDataset {
&Delimiter((caller(0))[3]);
my $i = 0;
for my $key (keys %data) {
if (exists $db{$key}) {
print "OLD: $key\n";
}
elsif (!exists $db{$key}) {
print "NEW: $key\n";
my $success = &uploadPHP($data{$key}{'FILEPATH'});
&AddToDB($key);
&WipeData($key);
last;
# if ($success) {
# print "success is $success\n";
# &AddToDB($key);
# &WipeData($key);
# last;
# }
}
$i++;
}
if ($i == scalar(keys(%data))) {
warn "\nNO NEW FILES AVAILABLE.\n";
}
}
sub uploadPHP {
&Delimiter((caller(0))[3]);
my $filepath = shift;
my $success = 1;
my $captionText = "$profile{$profile}{'uploadPHP'}{'description_add'}\n\n$profile{$profile}{'uploadPHP'}{'tags'}";
if ($profile{$profile}{'filename_as_title'}) {
my $filename = basename($filepath);
$filename =~ s/(NO INSTALL)|(SymLink Installer)//g;
$filename =~ s/( , )|(\.[^.]+$)//g;
$captionText = "$filename\n\n" . $captionText;
# print Dumper $captionText;
}
open PHPOUT, "$config{'uploadPHP_CMD'} \'$filepath\' \'$captionText\' $profile{$profile}{'uploadPHP'}{'username'} $profile{$profile}{'uploadPHP'}{'password'} $config{'uploadPHP_debug'} $config{'uploadPHP_truncated_debug'} $profile{$profile}{'uploadPHP'}{'proxy_user'} $profile{$profile}{'uploadPHP'}{'proxy_password'} $profile{$profile}{'uploadPHP'}{'proxy_ip'} $profile{$profile}{'uploadPHP'}{'proxy_port'} \'$config{'uploadPHP_autoload'}\'|";
while (<PHPOUT>) {
print $_; # PRINT CURRENT PHP OUPUT LINE
if ($_ =~ m/error/) {
$success = 0;
}
}
return $success;
}
sub WipeData {
&Delimiter((caller(0))[3]);
my $key = shift;
print "Deleting $data{$key}{'FILEPATH'}...";
unlink($data{$key}{'FILEPATH'}) or die "Could not delete $data{$key}{'FILEPATH'}!\n";
print " Done.\n";
}
sub AddToDB {
&Delimiter((caller(0))[3]);
my $key = shift;
$data{$key}{'TIMESTAMP_UPLOADED'} = &GetTimestamp('YMDHMS');
$db{$key} = $data{$key};
my $json = encode_json \%db;
write_file($profile{$profile}{'DBFilepath'}, { binmode => ':raw' }, $json);
}
sub Delimiter {
my $SubName = shift;
print "\n" . "-" x 80 . "\nSUB " . $SubName . "\n" . '-' x 80 . "\n";
}
sub GetTimestamp {
#&Delimiter((caller(0))[3]);
my $switch = shift;
my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst)=localtime(time);
my $nice_timestamp;
if ($switch eq 'YMDHMS') {
$nice_timestamp = sprintf ( "%04d%02d%02d_%02d%02d%02d", $year+1900,$mon+1,$mday,$hour,$min,$sec);
}
elsif ($switch eq 'YMD') {
$nice_timestamp = sprintf ( "%04d%02d%02d", $year+1900,$mon+1,$mday);
}
elsif ($switch eq 'year') {
$nice_timestamp = $year+1900;
}
elsif ($switch eq 'month') {
$nice_timestamp = $mon+10;
}
else {
print "Invalid/no switch detected. Use: 'YMDHMS' / 'YMD'\n";
}
return $nice_timestamp;
}

View File

@@ -0,0 +1,23 @@
--------------------------------------------------------------------------------
SUB main::CheckParameter
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
SUB main::UndumpFromFile
--------------------------------------------------------------------------------
INFO: ./src/db/db_dreamyourmansion.dat has 191 keys.
--------------------------------------------------------------------------------
SUB main::DirectoryListing
--------------------------------------------------------------------------------
$VAR1 = {};
--------------------------------------------------------------------------------
SUB main::FindNewDataset
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
SUB main::Summary
--------------------------------------------------------------------------------
./src/db/db_dreamyourmansion.dat has 191 keys (before 191).

23
instafeed/log/vstbestprices.log Executable file
View File

@@ -0,0 +1,23 @@
--------------------------------------------------------------------------------
SUB main::CheckParameter
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
SUB main::UndumpFromFile
--------------------------------------------------------------------------------
INFO: ./src/db/db_vstbestprices.dat has 115 keys.
--------------------------------------------------------------------------------
SUB main::DirectoryListing
--------------------------------------------------------------------------------
$VAR1 = {};
--------------------------------------------------------------------------------
SUB main::FindNewDataset
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
SUB main::Summary
--------------------------------------------------------------------------------
./src/db/db_vstbestprices.dat has 115 keys (before 115).

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 KiB

7
instafeed/vendor/autoload.php vendored Executable file
View File

@@ -0,0 +1,7 @@
<?php
// autoload.php @generated by Composer
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInit331ae81537359437b02a96bc74f11b80::getLoader();

738
instafeed/vendor/bin/lazydoctor vendored Executable file
View File

@@ -0,0 +1,738 @@
#!/usr/bin/env php
<?php
/*
* Copyright 2017 The LazyJsonMapper Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* The Lazy Doctor.
*
* This tool automatically builds up-to-date class documentation for all classes
* that derive from LazyJsonMapper, and documents their virtual properties and
* functions by adding "@property" and "@method" declarations to their class
* PHPdoc blocks. That documentation is necessary for things like code analysis
* tools and IDE autocomplete for all of your virtual properties/functions!
*
* LazyDoctor also performs class diagnostics: It compiles the class property
* maps of _all_ of your LazyJsonMapper-based classes, which means that you can
* be 100% sure that all of your maps are valid if this tool runs successfully.
*
* To see all available options, type "./vendor/bin/lazydoctor --help".
*
* To begin, simply point the "--composer" option at the "composer.json" file
* for your project. Just be aware that your project MUST be based on PSR-4
* autoloading, with ONE OR MORE defined autoload-namespaces in "composer.json".
*
* You can read more about PSR-4 autoloading at the official Composer site:
*
* https://getcomposer.org/doc/04-schema.md#autoload
*
* This tool will ONLY parse classes that properly follow PSR-4 autoloading!
*
* In addition to the composer-file, you'll also have to specify whether you
* want to document virtual "--properties", virtual "--functions", or BOTH.
* Note that we'll ONLY document them when the individual classes support that
* kind of access (when it hasn't disabled its class-option for that feature).
*
* If you don't provide any documentation flags, we'll instead REMOVE all of
* the current class "virtual @property/@method" documentation. That can be
* useful for restoring your files if you don't want to document them anymore.
*
* You should also be aware of "--document-overridden", which will document
* virtual properties or functions even if they're already manually defined
* (overridden) by the class or its parents. That can be useful if you want to
* ensure that you ALWAYS document the virtual properties/functions in all of
* your class files even when it has been overridden somewhere. In that case,
* you should ensure that your overridden properties/functions have the SAME or
* a COMPATIBLE signature & return value, so their auto-docs are still correct.
*
* Tip: You can use the "--validate-only" param to check docs without writing to
* disk. That can be useful when making a non-destructive Git "pre-commit hook"
* to validate your repo and ensure that all files have updated documentation!
*
* This script always uses exit codes to indicate success. A non-zero exit code
* indicates that some files needed new class docs (in "--validate-only" mode),
* or that there was a general problem while processing.
*
* Lastly, you may appreciate the ability to silence all unimportant status
* messages! To do so, simply use ">/dev/null" to send STDOUT to the void...
* That way you'll ONLY see critical status messages during the processing.
*/
set_time_limit(0);
date_default_timezone_set('UTC');
// Verify minimum PHP version.
if (!defined('PHP_VERSION_ID') || PHP_VERSION_ID < 50600) {
fwrite(STDERR, 'LazyDoctor requires PHP 5.6 or higher.'.PHP_EOL);
exit(1);
}
// Register a simple GetOptionKit autoloader. This is fine because
// GetOptionKit has no 3rd party library dependencies.
spl_autoload_register(function ($class) {
// Check if this is a "GetOptionKit" load-request.
static $prefix = 'GetOptionKit\\';
static $len = 13; // strlen($prefix)
if (strncmp($prefix, $class, $len) !== 0) {
return;
}
// Find the "GetOptionKit" source folder.
static $dirs = [
__DIR__.'/../../../corneltek/getoptionkit/src',
__DIR__.'/../vendor/corneltek/getoptionkit/src',
];
$baseDir = null;
foreach ($dirs as $dir) {
if (is_dir($dir) && ($dir = realpath($dir)) !== false) {
$baseDir = $dir;
break;
}
}
if ($baseDir === null) {
return;
}
// Get the relative class name.
$relativeClass = substr($class, $len);
// Generate PSR-4 file path to the class.
$file = sprintf('%s/%s.php', $baseDir, str_replace('\\', '/', $relativeClass));
if (is_file($file)) {
require $file;
}
});
// Parse command line options...
use GetOptionKit\OptionCollection;
use GetOptionKit\OptionParser;
use GetOptionKit\OptionPrinter\ConsoleOptionPrinter;
$specs = new OptionCollection();
$specs->add('c|composer:=file', 'Path to your project\'s composer.json file.');
$specs->add('p|properties?=boolean', 'Document virtual properties (if enabled for the classes).');
$specs->add('f|functions?=boolean', 'Document virtual functions (if enabled for the classes).');
$specs->add('o|document-overridden?=boolean', 'Always document virtual functions/properties even when they have been manually overridden by the class (or its parents).');
$specs->add('w|windows?=boolean', 'Generate Windows-style ("\r\n") documentation line endings instead of the default Unix-style ("\n").');
$specs->add('validate-only?=boolean', 'Validate current docs for all classes but don\'t write anything to disk.');
$specs->add('h|help?=boolean', 'Show all available options.');
try {
$parser = new OptionParser($specs);
$result = $parser->parse($argv);
$options = [
'composer' => isset($result->keys['composer']) ? $result->keys['composer']->value : null,
'properties' => isset($result->keys['properties']) && $result->keys['properties']->value !== false,
'functions' => isset($result->keys['functions']) && $result->keys['functions']->value !== false,
'document-overridden' => isset($result->keys['document-overridden']) && $result->keys['document-overridden']->value !== false,
'windows' => isset($result->keys['windows']) && $result->keys['windows']->value !== false,
'validate-only' => isset($result->keys['validate-only']) && $result->keys['validate-only']->value !== false,
'help' => isset($result->keys['help']) && $result->keys['help']->value !== false,
];
} catch (Exception $e) {
// Warns in case of invalid option values.
fwrite(STDERR, $e->getMessage().PHP_EOL);
exit(1);
}
// Verify options...
echo '[ LazyDoctor ]'.PHP_EOL.PHP_EOL;
if ($options['composer'] === null || $options['help']) {
if ($options['composer'] === null) {
fwrite(STDERR, 'You must provide the --composer option.'.PHP_EOL.PHP_EOL);
}
$printer = new ConsoleOptionPrinter();
echo 'Available options:'.PHP_EOL.PHP_EOL;
echo $printer->render($specs);
exit($options['composer'] === null && !$options['help'] ? 1 : 0);
}
if ($options['composer']->getBasename() !== 'composer.json') {
fwrite(STDERR, 'You must point to your project\'s composer.json file.'.PHP_EOL.'You used: "'.$options['composer']->getRealPath().'".'.PHP_EOL);
exit(1);
}
// Decode the composer.json file...
$json = @json_decode(file_get_contents($options['composer']->getRealPath()), true);
if ($json === null) {
fwrite(STDERR, 'Unable to decode composer.json.'.PHP_EOL);
exit(1);
}
// Determine the project folder's real root path...
$projectRoot = $options['composer']->getPathInfo()->getRealPath();
// Determine their namespace PSR-4 paths via their project's composer.json...
$namespaces = [];
foreach (['autoload', 'autoload-dev'] as $type) {
if (!isset($json[$type]['psr-4']) || !is_array($json[$type]['psr-4'])) {
continue;
}
foreach ($json[$type]['psr-4'] as $namespace => $dir) {
// We don't support composer's empty "fallback" namespaces.
if ($namespace === '') {
fwrite(STDERR, 'Encountered illegal unnamed PSR-4 autoload namespace in composer.json.'.PHP_EOL);
exit(1);
}
// Ensure that the namespace ends in backslash.
if (substr_compare($namespace, '\\', strlen($namespace) - 1, 1) !== 0) {
fwrite(STDERR, 'Encountered illegal namespace "'.$namespace.'" (does not end in backslash) in composer.json.'.PHP_EOL);
exit(1);
}
// Ensure that the value is a string.
// NOTE: We allow empty strings, which corresponds to root folder.
if (!is_string($dir)) {
fwrite(STDERR, 'Encountered illegal non-string value for namespace "'.$namespace.'".'.PHP_EOL);
exit(1);
}
// Now resolve the path name...
$path = sprintf('%s/%s', $projectRoot, $dir);
$realpath = realpath($path);
if ($realpath === false) {
fwrite(STDERR, 'Unable to resolve real path for "'.$path.'".'.PHP_EOL);
exit(1);
}
// We don't allow the same directory to be defined multiple times.
if (isset($namespaces[$realpath])) {
fwrite(STDERR, 'Encountered duplicate namespace directory "'.$realpath.'" in composer.json.'.PHP_EOL);
exit(1);
}
// And we're done! The namespace and its path have been resolved.
$namespaces[$realpath] = $namespace;
}
}
// Verify that we found some namespaces...
if (empty($namespaces)) {
fwrite(STDERR, 'There are no PSR-4 autoload namespaces in your composer.json.'.PHP_EOL);
exit(1);
}
// Now load the project's autoload.php file.
// NOTE: This is necessary so that we can autoload their classes...
$autoload = sprintf('%s/vendor/autoload.php', $projectRoot);
$realautoload = realpath($autoload);
if ($realautoload === false) {
fwrite(STDERR, 'Unable to find the project\'s Composer autoloader ("'.$autoload.'").'.PHP_EOL);
exit(1);
}
require $realautoload;
// Verify that their project's autoloader contains LazyJsonMapper...
if (!class_exists('\LazyJsonMapper\LazyJsonMapper', true)) { // TRUE = Autoload.
fwrite(STDERR, 'Target project doesn\'t contain the LazyJsonMapper library.'.PHP_EOL);
exit(1);
}
// Alright, display the current options...
echo 'Project: "'.$projectRoot.'".'.PHP_EOL
.'- Documentation Line Endings: '.($options['windows'] ? 'Windows ("\r\n")' : 'Unix ("\n")').'.'.PHP_EOL
.'- ['.($options['properties'] ? 'X' : ' ').'] Document Virtual Properties ("@property").'.PHP_EOL
.'- ['.($options['functions'] ? 'X' : ' ').'] Document Virtual Functions ("@method").'.PHP_EOL
.'- ['.($options['document-overridden'] ? 'X' : ' ').'] Document Overridden Properties/Functions.'.PHP_EOL;
if ($options['validate-only']) {
echo '- This is a validation run. Nothing will be written to disk.'.PHP_EOL;
}
// We can now use our custom classes, since the autoloader has been imported...
use LazyJsonMapper\Exception\LazyJsonMapperException;
use LazyJsonMapper\Export\PropertyDescription;
use LazyJsonMapper\Property\PropertyMapCache;
use LazyJsonMapper\Property\PropertyMapCompiler;
use LazyJsonMapper\Utilities;
/**
* Automatic LazyJsonMapper-class documentation generator.
*
* @copyright 2017 The LazyJsonMapper Project
* @license http://www.apache.org/licenses/LICENSE-2.0
* @author SteveJobzniak (https://github.com/SteveJobzniak)
*/
class LazyClassDocumentor
{
/** @var PropertyMapCache */
private static $_propertyMapCache;
/** @var array */
private $_compiledPropertyMapLink;
/** @var ReflectionClass */
private $_reflector;
/** @var array */
private $_options;
/** @var string Newline sequence. */
private $_nl;
/**
* Constructor.
*
* @param string $class
* @param array $options
*
* @throws ReflectionException
*/
public function __construct(
$class,
array $options)
{
if (self::$_propertyMapCache === null) {
self::$_propertyMapCache = new PropertyMapCache();
}
$this->_reflector = new ReflectionClass($class);
$this->_options = $options;
$this->_nl = $options['windows'] ? "\r\n" : "\n";
}
/**
* Process the current class.
*
* @throws ReflectionException
* @throws LazyJsonMapperException
*
* @return bool `TRUE` if on-disk file has correct docs, otherwise `FALSE`.
*/
public function process()
{
// Only process user-defined classes (never any built-in PHP classes).
if (!$this->_reflector->isUserDefined()) {
return true;
}
// There's nothing to do if this isn't a LazyJsonMapper subclass.
// NOTE: This properly skips "\LazyJsonMapper\LazyJsonMapper" itself.
if (!$this->_reflector->isSubclassOf('\LazyJsonMapper\LazyJsonMapper')) {
return true;
}
// Compile this class property map if not yet built and cached.
$thisClassName = $this->_reflector->getName();
if (!isset(self::$_propertyMapCache->classMaps[$thisClassName])) {
try {
PropertyMapCompiler::compileClassPropertyMap( // Throws.
self::$_propertyMapCache,
$thisClassName
);
} catch (Exception $e) {
fwrite(STDERR, '> Unable to compile the class property map for "'.$thisClassName.'". Reason: '.$e->getMessage().PHP_EOL);
return false;
}
}
// Now link to the property map cache for our current class.
$this->_compiledPropertyMapLink = &self::$_propertyMapCache->classMaps[$thisClassName];
// Get the current class comment (string if ok, FALSE if none exists).
$currentDocComment = $this->_reflector->getDocComment();
if (is_string($currentDocComment)) {
$currentDocComment = trim($currentDocComment);
}
// Extract all relevant lines from the current comment.
$finalDocLines = $this->_extractRelevantLines($currentDocComment);
// Generate the automatic summary line (classname followed by period).
$autoSummaryLine = $this->_reflector->getShortName().'.';
// If the 1st line is a classname followed by a period, update the name.
// NOTE: This ensures that we update all outdated auto-added classnames,
// and the risk of false positives is very low since we only document
// `LazyJsonMapper`-based classes with a `OneWord.`-style summary line.
// NOTE: Regex is from http://php.net/manual/en/language.oop5.basic.php,
// and yes we must run it in NON-UNICODE MODE, so that it parses on a
// byte by byte basis exactly like the real PHP classname interpreter.
if (
isset($finalDocLines[0]) // The 1st line MUST exist to proceed.
&& preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*\.$/', $finalDocLines[0])
) {
$finalDocLines[0] = $autoSummaryLine;
}
// Generate the magic documentation lines for the current class.
$magicDocLines = $this->_generateMagicDocs();
if (!empty($magicDocLines)) {
// If there are no lines already... add the automatic summary line.
if (empty($finalDocLines)) {
$finalDocLines[] = $autoSummaryLine;
}
// Check the 1st char of the 1st line. If it's an @tag of any kind,
// insert automatic summary line at top and empty line after that.
elseif ($finalDocLines[0][0] === '@') {
array_unshift(
$finalDocLines,
$autoSummaryLine,
''
);
}
$finalDocLines[] = ''; // Add empty line before our magic docs.
$finalDocLines = array_merge($finalDocLines, array_values($magicDocLines));
}
unset($magicDocLines);
// Generate the final doc-comment that this class is supposed to have.
if (!empty($finalDocLines)) {
// This will generate even if the class only contained an existing
// summary/tags and nothing was added by our magic handler.
foreach ($finalDocLines as &$line) {
$line = ($line === '' ? ' *' : " * {$line}");
}
unset($line);
$finalDocComment = sprintf(
'/**%s%s%s */',
$this->_nl,
implode($this->_nl, $finalDocLines),
$this->_nl
);
} else {
// The FALSE signifies that we want no class doc-block at all...
$finalDocComment = false;
}
unset($finalDocLines);
// There's nothing to do if the doc-comment is already correct.
// NOTE: Both values are FALSE if no doc-comment exists and none wanted.
if ($currentDocComment === $finalDocComment) {
return true;
}
// The docs mismatch. If this is a validate-run, just return false now.
if ($this->_options['validate-only']) {
fwrite(STDERR, '> Outdated class docs encountered in "'.$thisClassName.'". Aborting scan...'.PHP_EOL);
return false;
}
// Load the contents of the file...
$classFileName = $this->_reflector->getFileName();
$fileLines = @file($classFileName);
if ($fileLines === false) {
fwrite(STDERR, '> Unable to read class file from disk: "'.$classFileName.'".'.PHP_EOL);
return false;
}
// Split the file into lines BEFORE the class and lines AFTER the class.
$classLine = $this->_reflector->getStartLine();
$startLines = array_slice($fileLines, 0, $classLine - 1);
$endLines = array_slice($fileLines, $classLine - 1);
unset($fileLines);
// Insert the new class documentation using a very careful algorithm.
if ($currentDocComment !== false) {
// Since the class already had PHPdoc, remove it and insert new doc.
// NOTE: A valid PHPdoc (getDocComment()) always starts with
// "/**[whitespace]". If it's just a "/*" or something like
// "/**Foo", then it's not detected by getDocComment(). However, the
// comment may be several lines above the class. So we'll have to do
// an intelligent search to find the old class-comment. As for the
// ending tag "*/", PHP doesn't care about whitespace around that.
// And it also doesn't let the user escape the "*/", which means
// that if we see that sequence we KNOW it's the end of a comment!
// NOTE: We'll search for the latest "/**[whitespace]" block and
// remove all lines from that until its closest "*/".
$deleteFrom = null;
$deleteTo = null;
for ($i = count($startLines) - 1; $i >= 0; --$i) {
if (strpos($startLines[$i], '*/') !== false) {
$deleteTo = $i;
}
if (preg_match('/^\s*\/\*\*\s/u', $startLines[$i])) {
$deleteFrom = $i;
break;
}
}
// Ensure that we have found valid comment-offsets.
if ($deleteFrom === null || $deleteTo === null || $deleteTo < $deleteFrom) {
fwrite(STDERR, '> Unable to parse current class comment on disk: "'.$classFileName.'".'.PHP_EOL);
return false;
}
// Now update the startLines array to replace the doc-comment...
foreach ($startLines as $k => $v) {
if ($k === $deleteFrom && $finalDocComment !== false) {
// We've found the first line of the old comment, and we
// have a new comment. So replace that array entry.
$startLines[$k] = $finalDocComment.$this->_nl;
} elseif ($k >= $deleteFrom && $k <= $deleteTo) {
// Delete all other comment lines, including the first line
// if we had no new doc-comment.
unset($startLines[$k]);
}
// Break if we've reached the final line to delete.
if ($k >= $deleteTo) {
break;
}
}
} elseif ($finalDocComment !== false) {
// There's no existing doc-comment. Just add ours above the class.
// NOTE: This only does something if we had a new comment to insert,
// which we SHOULD have since we came this far in this scenario...
$startLines[] = $finalDocComment.$this->_nl;
}
// Generate the new file contents.
$newFileContent = implode($startLines).implode($endLines);
unset($startLines);
unset($endLines);
// Perform an atomic file-write to disk, which ensures that we will
// never be able to corrupt the class-files on disk via partial writes.
$written = Utilities::atomicWrite($classFileName, $newFileContent);
if ($written !== false) {
echo '> Wrote updated class documentation to disk: "'.$classFileName.'".'.PHP_EOL;
return true;
} else {
fwrite(STDERR, '> Unable to write new class documentation to disk: "'.$classFileName.'".'.PHP_EOL);
return false;
}
}
/**
* Extracts all relevant lines from a doc-comment.
*
* @param string $currentDocComment
*
* @return array
*/
private function _extractRelevantLines(
$currentDocComment)
{
if (!is_string($currentDocComment)) {
return [];
}
// Remove the leading and trailing doc-comment tags (/** and */).
$currentDocComment = preg_replace('/(^\s*\/\*\*\s*|\s*\*\/$)/u', '', $currentDocComment);
// Process all lines. Skip all @method and @property lines.
$relevantLines = [];
$lines = preg_split('/\r?\n|\r/u', $currentDocComment);
foreach ($lines as $line) {
// Remove leading and trailing whitespace, and leading asterisks.
$line = trim(preg_replace('/^\s*\*+/u', '', $line));
// Skip this line if it's a @method or @property line.
// NOTE: Removing them is totally safe, because the LazyJsonMapper
// class has marked all of its magic property/function handlers as
// final, which means that people's subclasses CANNOT override them
// to add their own magic methods/properties. So therefore we KNOW
// that ALL existing @method/@property class doc lines belong to us!
if (preg_match('/^@(?:method|property)/u', $line)) {
continue;
}
$relevantLines[] = $line;
}
// Remove trailing empty lines from the relevant lines.
for ($i = count($relevantLines) - 1; $i >= 0; --$i) {
if ($relevantLines[$i] === '') {
unset($relevantLines[$i]);
} else {
break;
}
}
// Remove leading empty lines from the relevant lines.
foreach ($relevantLines as $k => $v) {
if ($v !== '') {
break;
}
unset($relevantLines[$k]);
}
// Return a re-indexed (properly 0-indexed) array.
return array_values($relevantLines);
}
/**
* Generate PHPdoc lines for all magic properties and functions.
*
* @throws ReflectionException
* @throws LazyJsonMapperException
*
* @return array
*/
private function _generateMagicDocs()
{
// Check whether we should (and can) document properties and functions.
$documentProperties = $this->_options['properties'] && $this->_reflector->getConstant('ALLOW_VIRTUAL_PROPERTIES');
$documentFunctions = $this->_options['functions'] && $this->_reflector->getConstant('ALLOW_VIRTUAL_FUNCTIONS');
if (!$documentProperties && !$documentFunctions) {
return [];
}
// Export all JSON properties, with RELATIVE class-paths when possible.
// NOTE: We will document ALL properties. Even ones inherited from
// parents/imported maps. This ensures that users who are manually
// reading the source code can see EVERYTHING without needing an IDE.
$properties = [];
$ownerClassName = $this->_reflector->getName();
foreach ($this->_compiledPropertyMapLink as $propName => $propDef) {
$properties[$propName] = new PropertyDescription( // Throws.
$ownerClassName,
$propName,
$propDef,
true // Use relative class-paths when possible.
);
}
// Build the magic documentation...
$magicDocLines = [];
foreach (['functions', 'properties'] as $docType) {
if (($docType === 'functions' && !$documentFunctions)
|| ($docType === 'properties' && !$documentProperties)) {
continue;
}
// Generate all lines for the current magic tag type...
$lineStorage = [];
foreach ($properties as $property) {
if ($docType === 'functions') {
// We will only document useful functions (not the "has",
// since those are useless for properties that are fully
// defined in the class map).
foreach (['get', 'set', 'is', 'unset'] as $funcType) {
// Generate the function name, ie "getSomething", and
// skip this function if it's already defined as a REAL
// (overridden) function in this class or its parents.
$functionName = $funcType.$property->func_case;
if (!$this->_options['document-overridden'] && $this->_reflector->hasMethod($functionName)) {
continue;
}
// Alright, the function doesn't exist as a real class
// function, or the user wants to document it anyway...
// Document it via its calculated signature.
// NOTE: Classtypes use paths relative to current class!
$functionSignature = $property->{'function_'.$funcType};
$lineStorage[$functionName] = sprintf('@method %s', $functionSignature);
}
} elseif ($docType === 'properties') {
// Skip this property if it's already defined as a REAL
// (overridden) property in this class or its parents.
if (!$this->_options['document-overridden'] && $this->_reflector->hasProperty($property->name)) {
continue;
}
// Alright, the property doesn't exist as a real class
// property, or the user wants to document it anyway...
// Document it via its calculated signature.
// NOTE: Classtypes use paths relative to current class!
$lineStorage[$property->name] = sprintf(
'@property %s $%s',
$property->type,
$property->name
);
}
}
// Skip this tag type if there was nothing to document...
if (empty($lineStorage)) {
continue;
}
// Insert empty line separators between different magic tag types.
if (!empty($magicDocLines)) {
$magicDocLines[] = '';
}
// Reorder lines by name and add them to the magic doc lines.
// NOTE: We use case sensitivity so that "getComments" and
// "getCommentThreads" etc aren't placed next to each other.
ksort($lineStorage, SORT_NATURAL); // Case-sensitive natural order.
$magicDocLines = array_merge($magicDocLines, array_values($lineStorage));
}
return $magicDocLines;
}
}
// Now process all PHP files under all of the project's namespace folders.
foreach ($namespaces as $realpath => $namespace) {
echo PHP_EOL.'Processing namespace "'.$namespace.'".'.PHP_EOL.'- Path: "'.$realpath.'".'.PHP_EOL;
$realpathlen = strlen($realpath);
$iterator = new RegexIterator(
new RecursiveIteratorIterator(new RecursiveDirectoryIterator($realpath)),
'/\.php$/i', RecursiveRegexIterator::GET_MATCH
);
foreach ($iterator as $file => $ext) {
// Determine the real path to the file (compatible with $realpath).
$realfile = realpath($file);
if ($realfile === false) {
fwrite(STDERR, 'Unable to determine real path to file "'.$file.'".'.PHP_EOL);
exit(1);
}
// Now ensure that the file starts with the expected path...
if (strncmp($realpath, $realfile, $realpathlen) !== 0) {
fwrite(STDERR, 'Unexpected path to file "'.$realfile.'". Does not match project path.'.PHP_EOL);
exit(1);
}
$class = substr($realfile, $realpathlen);
// Remove the leading slash for the folder...
if ($class[0] !== '/' && $class[0] !== '\\') {
fwrite(STDERR, 'Unexpected path to file "'.$realfile.'". Does not match project path.'.PHP_EOL);
exit(1);
}
$class = substr($class, 1);
// And now just generate the final class name...
$class = sprintf(
'%s%s',
$namespace,
str_replace('/', '\\', preg_replace('/\.php$/ui', '', $class))
);
// Some files may not contain classes. For example, some people have
// functions.php files with functions, etc. So before we proceed, just
// ensure that the generated class name actually exists.
// NOTE: class_exists() ignores interfaces. Only finds classes. Good.
if (!class_exists($class, true)) { // TRUE = Autoload.
continue;
}
// Now process the current class.
$documentor = new LazyClassDocumentor($class, $options);
$result = $documentor->process();
if (!$result) {
if ($options['validate-only']) {
fwrite(STDERR, '> One or more files need updated class documentation or contain other errors.'.PHP_EOL);
} else {
fwrite(STDERR, '> Error while processing class "'.$class.'". Aborting...'.PHP_EOL);
}
exit(1);
}
}
}

View File

@@ -0,0 +1,21 @@
# The MIT License (MIT)
Copyright (c) 2015 Sebastian Mößler <code@binsoul.de>
> Permission is hereby granted, free of charge, to any person obtaining a copy
> of this software and associated documentation files (the "Software"), to deal
> in the Software without restriction, including without limitation the rights
> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
> copies of the Software, and to permit persons to whom the Software is
> furnished to do so, subject to the following conditions:
>
> The above copyright notice and this permission notice shall be included in
> all copies or substantial portions of the Software.
>
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
> THE SOFTWARE.

View File

@@ -0,0 +1,145 @@
# net-mqtt-client-react
[![Latest Version on Packagist][ico-version]][link-packagist]
[![Software License][ico-license]](LICENSE.md)
[![Total Downloads][ico-downloads]][link-downloads]
This package provides an asynchronous MQTT client built on the [React socket](https://github.com/reactphp/socket) library. All client methods return a promise which is fulfilled if the operation succeeded or rejected if the operation failed. Incoming messages of subscribed topics are delivered via the "message" event.
## Install
Via composer:
``` bash
$ composer require binsoul/net-mqtt-client-react
```
## Example
Connect to a public broker and run forever.
``` php
<?php
use BinSoul\Net\Mqtt\Client\React\ReactMqttClient;
use BinSoul\Net\Mqtt\Connection;
use BinSoul\Net\Mqtt\DefaultMessage;
use BinSoul\Net\Mqtt\DefaultSubscription;
use BinSoul\Net\Mqtt\Message;
use BinSoul\Net\Mqtt\Subscription;
use React\Socket\DnsConnector;
use React\Socket\TcpConnector;
include 'vendor/autoload.php';
// Setup client
$loop = \React\EventLoop\Factory::create();
$dnsResolverFactory = new \React\Dns\Resolver\Factory();
$connector = new DnsConnector(new TcpConnector($loop), $dnsResolverFactory->createCached('8.8.8.8', $loop));
$client = new ReactMqttClient($connector, $loop);
// Bind to events
$client->on('open', function () use ($client) {
// Network connection established
echo sprintf("Open: %s:%s\n", $client->getHost(), $client->getPort());
});
$client->on('close', function () use ($client, $loop) {
// Network connection closed
echo sprintf("Close: %s:%s\n", $client->getHost(), $client->getPort());
$loop->stop();
});
$client->on('connect', function (Connection $connection) {
// Broker connected
echo sprintf("Connect: client=%s\n", $connection->getClientID());
});
$client->on('disconnect', function (Connection $connection) {
// Broker disconnected
echo sprintf("Disconnect: client=%s\n", $connection->getClientID());
});
$client->on('message', function (Message $message) {
// Incoming message
echo 'Message';
if ($message->isDuplicate()) {
echo ' (duplicate)';
}
if ($message->isRetained()) {
echo ' (retained)';
}
echo ': '.$message->getTopic().' => '.mb_strimwidth($message->getPayload(), 0, 50, '...');
echo "\n";
});
$client->on('warning', function (\Exception $e) {
echo sprintf("Warning: %s\n", $e->getMessage());
});
$client->on('error', function (\Exception $e) use ($loop) {
echo sprintf("Error: %s\n", $e->getMessage());
$loop->stop();
});
// Connect to broker
$client->connect('test.mosquitto.org')->then(
function () use ($client) {
// Subscribe to all topics
$client->subscribe(new DefaultSubscription('#'))
->then(function (Subscription $subscription) {
echo sprintf("Subscribe: %s\n", $subscription->getFilter());
})
->otherwise(function (\Exception $e) {
echo sprintf("Error: %s\n", $e->getMessage());
});
// Publish humidity once
$client->publish(new DefaultMessage('sensors/humidity', '55%'))
->then(function (Message $message) {
echo sprintf("Publish: %s => %s\n", $message->getTopic(), $message->getPayload());
})
->otherwise(function (\Exception $e) {
echo sprintf("Error: %s\n", $e->getMessage());
});
// Publish a random temperature every 10 seconds
$generator = function () {
return mt_rand(-20, 30);
};
$client->publishPeriodically(10, new DefaultMessage('sensors/temperature'), $generator)
->progress(function (Message $message) {
echo sprintf("Publish: %s => %s\n", $message->getTopic(), $message->getPayload());
})
->otherwise(function (\Exception $e) {
echo sprintf("Error: %s\n", $e->getMessage());
});
}
);
$loop->run();
```
## Testing
``` bash
$ composer test
```
## License
The MIT License (MIT). Please see [License File](LICENSE.md) for more information.
[ico-version]: https://img.shields.io/packagist/v/binsoul/net-mqtt-client-react.svg?style=flat-square
[ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square
[ico-downloads]: https://img.shields.io/packagist/dt/binsoul/net-mqtt-client-react.svg?style=flat-square
[link-packagist]: https://packagist.org/packages/binsoul/net-mqtt-client-react
[link-downloads]: https://packagist.org/packages/binsoul/net-mqtt-client-react
[link-author]: https://github.com/binsoul

View File

@@ -0,0 +1,51 @@
{
"name": "binsoul/net-mqtt-client-react",
"description": "Asynchronous MQTT client built on React",
"keywords": [
"net",
"mqtt",
"client"
],
"homepage": "https://github.com/binsoul/net-mqtt-client-react",
"license": "MIT",
"authors": [
{
"name": "Sebastian Mößler",
"email": "code@binsoul.de",
"homepage": "https://github.com/binsoul",
"role": "Developer"
}
],
"require": {
"php": "~5.6|~7.0",
"binsoul/net-mqtt": "~0.2",
"react/promise": "~2.0",
"react/socket": "~0.8"
},
"require-dev": {
"phpunit/phpunit": "~4.0||~5.0",
"friendsofphp/php-cs-fixer": "~1.0"
},
"autoload": {
"psr-4": {
"BinSoul\\Net\\Mqtt\\Client\\React\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"BinSoul\\Test\\Net\\Mqtt\\Client\\React\\": "tests"
}
},
"scripts": {
"test": "phpunit",
"fix-style": [
"php-cs-fixer fix src",
"php-cs-fixer fix tests"
]
},
"extra": {
"branch-alias": {
"dev-master": "1.0-dev"
}
}
}

View File

@@ -0,0 +1,112 @@
<?php
namespace BinSoul\Net\Mqtt\Client\React;
use BinSoul\Net\Mqtt\Flow;
use BinSoul\Net\Mqtt\Packet;
use React\Promise\Deferred;
/**
* Decorates flows with data required for the {@see ReactMqttClient} class.
*/
class ReactFlow implements Flow
{
/** @var Flow */
private $decorated;
/** @var Deferred */
private $deferred;
/** @var Packet */
private $packet;
/** @var bool */
private $isSilent;
/**
* Constructs an instance of this class.
*
* @param Flow $decorated
* @param Deferred $deferred
* @param Packet $packet
* @param bool $isSilent
*/
public function __construct(Flow $decorated, Deferred $deferred, Packet $packet = null, $isSilent = false)
{
$this->decorated = $decorated;
$this->deferred = $deferred;
$this->packet = $packet;
$this->isSilent = $isSilent;
}
public function getCode()
{
return $this->decorated->getCode();
}
public function start()
{
$this->packet = $this->decorated->start();
return $this->packet;
}
public function accept(Packet $packet)
{
return $this->decorated->accept($packet);
}
public function next(Packet $packet)
{
$this->packet = $this->decorated->next($packet);
return $this->packet;
}
public function isFinished()
{
return $this->decorated->isFinished();
}
public function isSuccess()
{
return $this->decorated->isSuccess();
}
public function getResult()
{
return $this->decorated->getResult();
}
public function getErrorMessage()
{
return $this->decorated->getErrorMessage();
}
/**
* Returns the associated deferred.
*
* @return Deferred
*/
public function getDeferred()
{
return $this->deferred;
}
/**
* Returns the current packet.
*
* @return Packet
*/
public function getPacket()
{
return $this->packet;
}
/**
* Indicates if the flow should emit events.
*
* @return bool
*/
public function isSilent()
{
return $this->isSilent;
}
}

View File

@@ -0,0 +1,701 @@
<?php
namespace BinSoul\Net\Mqtt\Client\React;
use BinSoul\Net\Mqtt\Connection;
use BinSoul\Net\Mqtt\DefaultConnection;
use BinSoul\Net\Mqtt\DefaultIdentifierGenerator;
use BinSoul\Net\Mqtt\Flow;
use BinSoul\Net\Mqtt\Flow\IncomingPublishFlow;
use BinSoul\Net\Mqtt\Flow\OutgoingConnectFlow;
use BinSoul\Net\Mqtt\Flow\OutgoingDisconnectFlow;
use BinSoul\Net\Mqtt\Flow\OutgoingPingFlow;
use BinSoul\Net\Mqtt\Flow\OutgoingPublishFlow;
use BinSoul\Net\Mqtt\Flow\OutgoingSubscribeFlow;
use BinSoul\Net\Mqtt\Flow\OutgoingUnsubscribeFlow;
use BinSoul\Net\Mqtt\DefaultMessage;
use BinSoul\Net\Mqtt\IdentifierGenerator;
use BinSoul\Net\Mqtt\Message;
use BinSoul\Net\Mqtt\Packet;
use BinSoul\Net\Mqtt\Packet\PublishRequestPacket;
use BinSoul\Net\Mqtt\StreamParser;
use BinSoul\Net\Mqtt\Subscription;
use Evenement\EventEmitter;
use React\EventLoop\Timer\TimerInterface;
use React\Promise\Deferred;
use React\EventLoop\LoopInterface;
use React\Promise\ExtendedPromiseInterface;
use React\Promise\RejectedPromise;
use React\Socket\ConnectorInterface;
use React\Stream\DuplexStreamInterface;
/**
* Connects to a MQTT broker and subscribes to topics or publishes messages.
*
* The following events are emitted:
* - open - The network connection to the server is established.
* - close - The network connection to the server is closed.
* - warning - An event of severity "warning" occurred.
* - error - An event of severity "error" occurred.
*
* If a flow finishes it's result is also emitted, e.g.:
* - connect - The client connected to the broker.
* - disconnect - The client disconnected from the broker.
* - subscribe - The client subscribed to a topic filter.
* - unsubscribe - The client unsubscribed from topic filter.
* - publish - A message was published.
* - message - A message was received.
*/
class ReactMqttClient extends EventEmitter
{
/** @var ConnectorInterface */
private $connector;
/** @var LoopInterface */
private $loop;
/** @var DuplexStreamInterface */
private $stream;
/** @var StreamParser */
private $parser;
/** @var IdentifierGenerator */
private $identifierGenerator;
/** @var string */
private $host;
/** @var int */
private $port;
/** @var Connection */
private $connection;
/** @var bool */
private $isConnected = false;
/** @var bool */
private $isConnecting = false;
/** @var bool */
private $isDisconnecting = false;
/** @var TimerInterface[] */
private $timer = [];
/** @var ReactFlow[] */
private $receivingFlows = [];
/** @var ReactFlow[] */
private $sendingFlows = [];
/** @var ReactFlow */
private $writtenFlow;
/**
* Constructs an instance of this class.
*
* @param ConnectorInterface $connector
* @param LoopInterface $loop
* @param IdentifierGenerator $identifierGenerator
* @param StreamParser $parser
*/
public function __construct(
ConnectorInterface $connector,
LoopInterface $loop,
IdentifierGenerator $identifierGenerator = null,
StreamParser $parser = null
) {
$this->connector = $connector;
$this->loop = $loop;
$this->parser = $parser;
if ($this->parser === null) {
$this->parser = new StreamParser();
}
$this->parser->onError(function (\Exception $e) {
$this->emitWarning($e);
});
$this->identifierGenerator = $identifierGenerator;
if ($this->identifierGenerator === null) {
$this->identifierGenerator = new DefaultIdentifierGenerator();
}
}
/**
* Return the host.
*
* @return string
*/
public function getHost()
{
return $this->host;
}
/**
* Return the port.
*
* @return string
*/
public function getPort()
{
return $this->port;
}
/**
* Indicates if the client is connected.
*
* @return bool
*/
public function isConnected()
{
return $this->isConnected;
}
/**
* Returns the underlying stream or null if the client is not connected.
*
* @return DuplexStreamInterface|null
*/
public function getStream()
{
return $this->stream;
}
/**
* Connects to a broker.
*
* @param string $host
* @param int $port
* @param Connection $connection
* @param int $timeout
*
* @return ExtendedPromiseInterface
*/
public function connect($host, $port = 1883, Connection $connection = null, $timeout = 5)
{
if ($this->isConnected || $this->isConnecting) {
return new RejectedPromise(new \LogicException('The client is already connected.'));
}
$this->isConnecting = true;
$this->isConnected = false;
$this->host = $host;
$this->port = $port;
if ($connection === null) {
$connection = new DefaultConnection();
}
if ($connection->isCleanSession()) {
$this->cleanPreviousSession();
}
if ($connection->getClientID() === '') {
$connection = $connection->withClientID($this->identifierGenerator->generateClientID());
}
$deferred = new Deferred();
$this->establishConnection($this->host, $this->port, $timeout)
->then(function (DuplexStreamInterface $stream) use ($connection, $deferred, $timeout) {
$this->stream = $stream;
$this->emit('open', [$connection, $this]);
$this->registerClient($connection, $timeout)
->then(function (Connection $connection) use ($deferred) {
$this->isConnecting = false;
$this->isConnected = true;
$this->connection = $connection;
$this->emit('connect', [$connection, $this]);
$deferred->resolve($this->connection);
})
->otherwise(function (\Exception $e) use ($deferred, $connection) {
$this->isConnecting = false;
$this->emitError($e);
$deferred->reject($e);
if ($this->stream !== null) {
$this->stream->close();
}
$this->emit('close', [$connection, $this]);
});
})
->otherwise(function (\Exception $e) use ($deferred) {
$this->isConnecting = false;
$this->emitError($e);
$deferred->reject($e);
});
return $deferred->promise();
}
/**
* Disconnects from a broker.
*
* @return ExtendedPromiseInterface
*/
public function disconnect()
{
if (!$this->isConnected || $this->isDisconnecting) {
return new RejectedPromise(new \LogicException('The client is not connected.'));
}
$this->isDisconnecting = true;
$deferred = new Deferred();
$this->startFlow(new OutgoingDisconnectFlow($this->connection), true)
->then(function (Connection $connection) use ($deferred) {
$this->isDisconnecting = false;
$this->isConnected = false;
$this->emit('disconnect', [$connection, $this]);
$deferred->resolve($connection);
if ($this->stream !== null) {
$this->stream->close();
}
})
->otherwise(function () use ($deferred) {
$this->isDisconnecting = false;
$deferred->reject($this->connection);
});
return $deferred->promise();
}
/**
* Subscribes to a topic filter.
*
* @param Subscription $subscription
*
* @return ExtendedPromiseInterface
*/
public function subscribe(Subscription $subscription)
{
if (!$this->isConnected) {
return new RejectedPromise(new \LogicException('The client is not connected.'));
}
return $this->startFlow(new OutgoingSubscribeFlow([$subscription], $this->identifierGenerator));
}
/**
* Unsubscribes from a topic filter.
*
* @param Subscription $subscription
*
* @return ExtendedPromiseInterface
*/
public function unsubscribe(Subscription $subscription)
{
if (!$this->isConnected) {
return new RejectedPromise(new \LogicException('The client is not connected.'));
}
return $this->startFlow(new OutgoingUnsubscribeFlow([$subscription], $this->identifierGenerator));
}
/**
* Publishes a message.
*
* @param Message $message
*
* @return ExtendedPromiseInterface
*/
public function publish(Message $message)
{
if (!$this->isConnected) {
return new RejectedPromise(new \LogicException('The client is not connected.'));
}
return $this->startFlow(new OutgoingPublishFlow($message, $this->identifierGenerator));
}
/**
* Calls the given generator periodically and publishes the return value.
*
* @param int $interval
* @param Message $message
* @param callable $generator
*
* @return ExtendedPromiseInterface
*/
public function publishPeriodically($interval, Message $message, callable $generator)
{
if (!$this->isConnected) {
return new RejectedPromise(new \LogicException('The client is not connected.'));
}
$deferred = new Deferred();
$this->timer[] = $this->loop->addPeriodicTimer(
$interval,
function () use ($message, $generator, $deferred) {
$this->publish($message->withPayload($generator($message->getTopic())))->then(
function ($value) use ($deferred) {
$deferred->notify($value);
},
function (\Exception $e) use ($deferred) {
$deferred->reject($e);
}
);
}
);
return $deferred->promise();
}
/**
* Emits warnings.
*
* @param \Exception $e
*/
private function emitWarning(\Exception $e)
{
$this->emit('warning', [$e, $this]);
}
/**
* Emits errors.
*
* @param \Exception $e
*/
private function emitError(\Exception $e)
{
$this->emit('error', [$e, $this]);
}
/**
* Establishes a network connection to a server.
*
* @param string $host
* @param int $port
* @param int $timeout
*
* @return ExtendedPromiseInterface
*/
private function establishConnection($host, $port, $timeout)
{
$deferred = new Deferred();
$timer = $this->loop->addTimer(
$timeout,
function () use ($deferred, $timeout) {
$exception = new \RuntimeException(sprintf('Connection timed out after %d seconds.', $timeout));
$deferred->reject($exception);
}
);
$this->connector->connect($host.':'.$port)
->always(function () use ($timer) {
$this->loop->cancelTimer($timer);
})
->then(function (DuplexStreamInterface $stream) use ($deferred) {
$stream->on('data', function ($data) {
$this->handleReceive($data);
});
$stream->on('close', function () {
$this->handleClose();
});
$stream->on('error', function (\Exception $e) {
$this->handleError($e);
});
$deferred->resolve($stream);
})
->otherwise(function (\Exception $e) use ($deferred) {
$deferred->reject($e);
});
return $deferred->promise();
}
/**
* Registers a new client with the broker.
*
* @param Connection $connection
* @param int $timeout
*
* @return ExtendedPromiseInterface
*/
private function registerClient(Connection $connection, $timeout)
{
$deferred = new Deferred();
$responseTimer = $this->loop->addTimer(
$timeout,
function () use ($deferred, $timeout) {
$exception = new \RuntimeException(sprintf('No response after %d seconds.', $timeout));
$deferred->reject($exception);
}
);
$this->startFlow(new OutgoingConnectFlow($connection, $this->identifierGenerator), true)
->always(function () use ($responseTimer) {
$this->loop->cancelTimer($responseTimer);
})->then(function (Connection $connection) use ($deferred) {
$this->timer[] = $this->loop->addPeriodicTimer(
floor($connection->getKeepAlive() * 0.75),
function () {
$this->startFlow(new OutgoingPingFlow());
}
);
$deferred->resolve($connection);
})->otherwise(function (\Exception $e) use ($deferred) {
$deferred->reject($e);
});
return $deferred->promise();
}
/**
* Handles incoming data.
*
* @param string $data
*/
private function handleReceive($data)
{
if (!$this->isConnected && !$this->isConnecting) {
return;
}
$flowCount = count($this->receivingFlows);
$packets = $this->parser->push($data);
foreach ($packets as $packet) {
$this->handlePacket($packet);
}
if ($flowCount > count($this->receivingFlows)) {
$this->receivingFlows = array_values($this->receivingFlows);
}
$this->handleSend();
}
/**
* Handles an incoming packet.
*
* @param Packet $packet
*/
private function handlePacket(Packet $packet)
{
switch ($packet->getPacketType()) {
case Packet::TYPE_PUBLISH:
/* @var PublishRequestPacket $packet */
$message = new DefaultMessage(
$packet->getTopic(),
$packet->getPayload(),
$packet->getQosLevel(),
$packet->isRetained(),
$packet->isDuplicate()
);
$this->startFlow(new IncomingPublishFlow($message, $packet->getIdentifier()));
break;
case Packet::TYPE_CONNACK:
case Packet::TYPE_PINGRESP:
case Packet::TYPE_SUBACK:
case Packet::TYPE_UNSUBACK:
case Packet::TYPE_PUBREL:
case Packet::TYPE_PUBACK:
case Packet::TYPE_PUBREC:
case Packet::TYPE_PUBCOMP:
$flowFound = false;
foreach ($this->receivingFlows as $index => $flow) {
if ($flow->accept($packet)) {
$flowFound = true;
unset($this->receivingFlows[$index]);
$this->continueFlow($flow, $packet);
break;
}
}
if (!$flowFound) {
$this->emitWarning(
new \LogicException(sprintf('Received unexpected packet of type %d.', $packet->getPacketType()))
);
}
break;
default:
$this->emitWarning(
new \LogicException(sprintf('Cannot handle packet of type %d.', $packet->getPacketType()))
);
}
}
/**
* Handles outgoing packets.
*/
private function handleSend()
{
$flow = null;
if ($this->writtenFlow !== null) {
$flow = $this->writtenFlow;
$this->writtenFlow = null;
}
if (count($this->sendingFlows) > 0) {
$this->writtenFlow = array_shift($this->sendingFlows);
$this->stream->write($this->writtenFlow->getPacket());
}
if ($flow !== null) {
if ($flow->isFinished()) {
$this->loop->nextTick(function () use ($flow) {
$this->finishFlow($flow);
});
} else {
$this->receivingFlows[] = $flow;
}
}
}
/**
* Handles closing of the stream.
*/
private function handleClose()
{
foreach ($this->timer as $timer) {
$this->loop->cancelTimer($timer);
}
$this->timer = [];
$connection = $this->connection;
$this->isConnecting = false;
$this->isDisconnecting = false;
$this->isConnected = false;
$this->connection = null;
$this->stream = null;
if ($connection !== null) {
$this->emit('close', [$connection, $this]);
}
}
/**
* Handles errors of the stream.
*
* @param \Exception $e
*/
private function handleError(\Exception $e)
{
$this->emitError($e);
}
/**
* Starts the given flow.
*
* @param Flow $flow
* @param bool $isSilent
*
* @return ExtendedPromiseInterface
*/
private function startFlow(Flow $flow, $isSilent = false)
{
try {
$packet = $flow->start();
} catch (\Exception $e) {
$this->emitError($e);
return new RejectedPromise($e);
}
$deferred = new Deferred();
$internalFlow = new ReactFlow($flow, $deferred, $packet, $isSilent);
if ($packet !== null) {
if ($this->writtenFlow !== null) {
$this->sendingFlows[] = $internalFlow;
} else {
$this->stream->write($packet);
$this->writtenFlow = $internalFlow;
$this->handleSend();
}
} else {
$this->loop->nextTick(function () use ($internalFlow) {
$this->finishFlow($internalFlow);
});
}
return $deferred->promise();
}
/**
* Continues the given flow.
*
* @param ReactFlow $flow
* @param Packet $packet
*/
private function continueFlow(ReactFlow $flow, Packet $packet)
{
try {
$response = $flow->next($packet);
} catch (\Exception $e) {
$this->emitError($e);
return;
}
if ($response !== null) {
if ($this->writtenFlow !== null) {
$this->sendingFlows[] = $flow;
} else {
$this->stream->write($response);
$this->writtenFlow = $flow;
$this->handleSend();
}
} elseif ($flow->isFinished()) {
$this->loop->nextTick(function () use ($flow) {
$this->finishFlow($flow);
});
}
}
/**
* Finishes the given flow.
*
* @param ReactFlow $flow
*/
private function finishFlow(ReactFlow $flow)
{
if ($flow->isSuccess()) {
if (!$flow->isSilent()) {
$this->emit($flow->getCode(), [$flow->getResult(), $this]);
}
$flow->getDeferred()->resolve($flow->getResult());
} else {
$result = new \RuntimeException($flow->getErrorMessage());
$this->emitWarning($result);
$flow->getDeferred()->reject($result);
}
}
/**
* Cleans previous session by rejecting all pending flows.
*/
private function cleanPreviousSession()
{
$error = new \RuntimeException('Connection has been closed.');
foreach ($this->receivingFlows as $receivingFlow) {
$receivingFlow->getDeferred()->reject($error);
}
foreach ($this->sendingFlows as $sendingFlow) {
$sendingFlow->getDeferred()->reject($error);
}
$this->receivingFlows = [];
$this->sendingFlows = [];
}
}

21
instafeed/vendor/binsoul/net-mqtt/LICENSE.md vendored Executable file
View File

@@ -0,0 +1,21 @@
# The MIT License (MIT)
Copyright (c) 2015 Sebastian Mößler <code@binsoul.de>
> Permission is hereby granted, free of charge, to any person obtaining a copy
> of this software and associated documentation files (the "Software"), to deal
> in the Software without restriction, including without limitation the rights
> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
> copies of the Software, and to permit persons to whom the Software is
> furnished to do so, subject to the following conditions:
>
> The above copyright notice and this permission notice shall be included in
> all copies or substantial portions of the Software.
>
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
> THE SOFTWARE.

36
instafeed/vendor/binsoul/net-mqtt/README.md vendored Executable file
View File

@@ -0,0 +1,36 @@
# net-mqtt
[![Latest Version on Packagist][ico-version]][link-packagist]
[![Software License][ico-license]](LICENSE.md)
[![Total Downloads][ico-downloads]][link-downloads]
MQTT is a machine-to-machine (M2M) / Internet of Things (IoT) connectivity protocol. It provides a lightweight method of carrying out messaging using a publish/subscribe model.
This package implements the MQTT protocol versions 3.1 and 3.1.1.
## Install
Via composer:
``` bash
$ composer require binsoul/net-mqtt
```
## Testing
``` bash
$ composer test
```
## License
The MIT License (MIT). Please see [License File](LICENSE.md) for more information.
[ico-version]: https://img.shields.io/packagist/v/binsoul/net-mqtt.svg?style=flat-square
[ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square
[ico-downloads]: https://img.shields.io/packagist/dt/binsoul/net-mqtt.svg?style=flat-square
[link-packagist]: https://packagist.org/packages/binsoul/net-mqtt
[link-downloads]: https://packagist.org/packages/binsoul/net-mqtt
[link-author]: https://github.com/binsoul

View File

@@ -0,0 +1,47 @@
{
"name": "binsoul/net-mqtt",
"description": "MQTT protocol implementation",
"keywords": [
"net",
"mqtt"
],
"homepage": "https://github.com/binsoul/net-mqtt",
"license": "MIT",
"authors": [
{
"name": "Sebastian Mößler",
"email": "code@binsoul.de",
"homepage": "https://github.com/binsoul",
"role": "Developer"
}
],
"require": {
"php": "~5.6|~7.0"
},
"require-dev": {
"phpunit/phpunit": "~4.0||~5.0",
"friendsofphp/php-cs-fixer": "~1.0"
},
"autoload": {
"psr-4": {
"BinSoul\\Net\\Mqtt\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"BinSoul\\Test\\Net\\Mqtt\\": "tests"
}
},
"scripts": {
"test": "phpunit",
"fix-style": [
"php-cs-fixer fix src",
"php-cs-fixer fix tests"
]
},
"extra": {
"branch-alias": {
"dev-master": "1.0-dev"
}
}
}

View File

@@ -0,0 +1,90 @@
<?php
namespace BinSoul\Net\Mqtt;
/**
* Represents the connection of a MQTT client.
*/
interface Connection
{
/**
* @return int
*/
public function getProtocol();
/**
* @return string
*/
public function getClientID();
/**
* @return bool
*/
public function isCleanSession();
/**
* @return string
*/
public function getUsername();
/**
* @return string
*/
public function getPassword();
/**
* @return Message|null
*/
public function getWill();
/**
* @return int
*/
public function getKeepAlive();
/**
* Returns a new connection with the given protocol.
*
* @param int $protocol
*
* @return self
*/
public function withProtocol($protocol);
/**
* Returns a new connection with the given client id.
*
* @param string $clientID
*
* @return self
*/
public function withClientID($clientID);
/**
* Returns a new connection with the given credentials.
*
* @param string $username
* @param string $password
*
* @return self
*/
public function withCredentials($username, $password);
/**
* Returns a new connection with the given will.
*
* @param Message $will
*
* @return self
*/
public function withWill(Message $will);
/**
* Returns a new connection with the given keep alive timeout.
*
* @param int $timeout
*
* @return self
*/
public function withKeepAlive($timeout);
}

View File

@@ -0,0 +1,129 @@
<?php
namespace BinSoul\Net\Mqtt;
/**
* Provides a default implementation of the {@see Connection} interface.
*/
class DefaultConnection implements Connection
{
/** @var string */
private $username;
/** @var string */
private $password;
/** @var Message|null */
private $will;
/** @var string */
private $clientID;
/** @var int */
private $keepAlive;
/** @var int */
private $protocol;
/** @var bool */
private $clean;
/**
* Constructs an instance of this class.
*
* @param string $username
* @param string $password
* @param Message|null $will
* @param string $clientID
* @param int $keepAlive
* @param int $protocol
* @param bool $clean
*/
public function __construct(
$username = '',
$password = '',
Message $will = null,
$clientID = '',
$keepAlive = 60,
$protocol = 4,
$clean = true
) {
$this->username = $username;
$this->password = $password;
$this->will = $will;
$this->clientID = $clientID;
$this->keepAlive = $keepAlive;
$this->protocol = $protocol;
$this->clean = $clean;
}
public function getProtocol()
{
return $this->protocol;
}
public function getClientID()
{
return $this->clientID;
}
public function isCleanSession()
{
return $this->clean;
}
public function getUsername()
{
return $this->username;
}
public function getPassword()
{
return $this->password;
}
public function getWill()
{
return $this->will;
}
public function getKeepAlive()
{
return $this->keepAlive;
}
public function withProtocol($protocol)
{
$result = clone $this;
$result->protocol = $protocol;
return $result;
}
public function withClientID($clientID)
{
$result = clone $this;
$result->clientID = $clientID;
return $result;
}
public function withCredentials($username, $password)
{
$result = clone $this;
$result->username = $username;
$result->password = $password;
return $result;
}
public function withWill(Message $will = null)
{
$result = clone $this;
$result->will = $will;
return $result;
}
public function withKeepAlive($timeout)
{
$result = clone $this;
$result->keepAlive = $timeout;
return $result;
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace BinSoul\Net\Mqtt;
/**
* Provides a default implementation of the {@see IdentifierGenerator} interface.
*/
class DefaultIdentifierGenerator implements IdentifierGenerator
{
/** @var int */
private $currentIdentifier = 0;
public function generatePacketID()
{
++$this->currentIdentifier;
if ($this->currentIdentifier > 0xFFFF) {
$this->currentIdentifier = 1;
}
return $this->currentIdentifier;
}
public function generateClientID()
{
if (function_exists('random_bytes')) {
$data = random_bytes(9);
} elseif (function_exists('openssl_random_pseudo_bytes')) {
$data = openssl_random_pseudo_bytes(9);
} else {
$data = '';
for ($i = 1; $i <= 8; ++$i) {
$data = chr(mt_rand(0, 255)).$data;
}
}
return 'BNMCR'.bin2hex($data);
}
}

View File

@@ -0,0 +1,119 @@
<?php
namespace BinSoul\Net\Mqtt;
/**
* Provides a default implementation of the {@see Message} interface.
*/
class DefaultMessage implements Message
{
/** @var string */
private $topic;
/** @var string */
private $payload;
/** @var bool */
private $isRetained;
/** @var bool */
private $isDuplicate = false;
/** @var int */
private $qosLevel;
/**
* Constructs an instance of this class.
*
* @param string $topic
* @param string $payload
* @param int $qosLevel
* @param bool $retain
* @param bool $isDuplicate
*/
public function __construct($topic, $payload = '', $qosLevel = 0, $retain = false, $isDuplicate = false)
{
$this->topic = $topic;
$this->payload = $payload;
$this->isRetained = $retain;
$this->qosLevel = $qosLevel;
$this->isDuplicate = $isDuplicate;
}
public function getTopic()
{
return $this->topic;
}
public function getPayload()
{
return $this->payload;
}
public function getQosLevel()
{
return $this->qosLevel;
}
public function isDuplicate()
{
return $this->isDuplicate;
}
public function isRetained()
{
return $this->isRetained;
}
public function withTopic($topic)
{
$result = clone $this;
$result->topic = $topic;
return $result;
}
public function withPayload($payload)
{
$result = clone $this;
$result->payload = $payload;
return $result;
}
public function withQosLevel($level)
{
$result = clone $this;
$result->qosLevel = $level;
return $result;
}
public function retain()
{
$result = clone $this;
$result->isRetained = true;
return $result;
}
public function release()
{
$result = clone $this;
$result->isRetained = false;
return $result;
}
public function duplicate()
{
$result = clone $this;
$result->isDuplicate = true;
return $result;
}
public function original()
{
$result = clone $this;
$result->isDuplicate = false;
return $result;
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace BinSoul\Net\Mqtt;
/**
* Provides a default implementation of the {@see Subscription} interface.
*/
class DefaultSubscription implements Subscription
{
/** @var string */
private $filter;
/** @var int */
private $qosLevel;
/**
* Constructs an instance of this class.
*
* @param string $filter
* @param int $qosLevel
*/
public function __construct($filter, $qosLevel = 0)
{
$this->filter = $filter;
$this->qosLevel = $qosLevel;
}
public function getFilter()
{
return $this->filter;
}
public function getQosLevel()
{
return $this->qosLevel;
}
public function withFilter($filter)
{
$result = clone $this;
$result->filter = $filter;
return $result;
}
public function withQosLevel($level)
{
$result = clone $this;
$result->qosLevel = $level;
return $result;
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace BinSoul\Net\Mqtt\Exception;
/**
* Will be thrown if the end of a stream is reached but more bytes were requested.
*/
class EndOfStreamException extends \Exception
{
}

View File

@@ -0,0 +1,10 @@
<?php
namespace BinSoul\Net\Mqtt\Exception;
/**
* Will be thrown if a packet is malformed.
*/
class MalformedPacketException extends \Exception
{
}

View File

@@ -0,0 +1,10 @@
<?php
namespace BinSoul\Net\Mqtt\Exception;
/**
* Will be thrown if a packet type is unknown.
*/
class UnknownPacketTypeException extends \Exception
{
}

View File

@@ -0,0 +1,71 @@
<?php
namespace BinSoul\Net\Mqtt;
/**
* Represents a sequence of packages exchanged between clients and brokers.
*/
interface Flow
{
/**
* Returns the unique code.
*
* @return string
*/
public function getCode();
/**
* Starts the flow.
*
* @return Packet|null First packet of the flow
*
* @throws \Exception
*/
public function start();
/**
* Indicates if the flow can handle the given packet.
*
* @param Packet $packet Packet to accept
*
* @return bool
*/
public function accept(Packet $packet);
/**
* Continues the flow.
*
* @param Packet $packet Packet to respond
*
* @return Packet|null Next packet of the flow
*/
public function next(Packet $packet);
/**
* Indicates if the flow is finished.
*
* @return bool
*/
public function isFinished();
/**
* Indicates if the flow finished successfully.
*
* @return bool
*/
public function isSuccess();
/**
* Returns the result of the flow if it finished successfully.
*
* @return mixed
*/
public function getResult();
/**
* Returns an error message if the flow didn't finish successfully.
*
* @return string
*/
public function getErrorMessage();
}

View File

@@ -0,0 +1,74 @@
<?php
namespace BinSoul\Net\Mqtt\Flow;
use BinSoul\Net\Mqtt\Flow;
use BinSoul\Net\Mqtt\Packet;
/**
* Provides an abstract implementation of the {@see Flow} interface.
*/
abstract class AbstractFlow implements Flow
{
/** @var bool */
private $isFinished = false;
/** @var bool */
private $isSuccess = false;
/** @var mixed */
private $result;
/** @var string */
private $error = '';
public function accept(Packet $packet)
{
return false;
}
public function next(Packet $packet)
{
}
public function isFinished()
{
return $this->isFinished;
}
public function isSuccess()
{
return $this->isFinished && $this->isSuccess;
}
public function getResult()
{
return $this->result;
}
public function getErrorMessage()
{
return $this->error;
}
/**
* Marks the flow as successful and sets the result.
*
* @param mixed|null $result
*/
protected function succeed($result = null)
{
$this->isFinished = true;
$this->isSuccess = true;
$this->result = $result;
}
/**
* Marks the flow as failed and sets the error message.
*
* @param string $error
*/
protected function fail($error = '')
{
$this->isFinished = true;
$this->isSuccess = false;
$this->error = $error;
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace BinSoul\Net\Mqtt\Flow;
use BinSoul\Net\Mqtt\Packet\PingResponsePacket;
/**
* Represents a flow starting with an incoming PING packet.
*/
class IncomingPingFlow extends AbstractFlow
{
public function getCode()
{
return 'pong';
}
public function start()
{
$this->succeed();
return new PingResponsePacket();
}
}

View File

@@ -0,0 +1,80 @@
<?php
namespace BinSoul\Net\Mqtt\Flow;
use BinSoul\Net\Mqtt\Message;
use BinSoul\Net\Mqtt\Packet;
use BinSoul\Net\Mqtt\Packet\PublishAckPacket;
use BinSoul\Net\Mqtt\Packet\PublishCompletePacket;
use BinSoul\Net\Mqtt\Packet\PublishReceivedPacket;
use BinSoul\Net\Mqtt\Packet\PublishReleasePacket;
/**
* Represents a flow starting with an incoming PUBLISH packet.
*/
class IncomingPublishFlow extends AbstractFlow
{
/** @var int|null */
private $identifier;
/** @var Message */
private $message;
/**
* Constructs an instance of this class.
*
* @param Message $message
* @param int|null $identifier
*/
public function __construct(Message $message, $identifier = null)
{
$this->message = $message;
$this->identifier = $identifier;
}
public function getCode()
{
return 'message';
}
public function start()
{
$packet = null;
$emit = true;
if ($this->message->getQosLevel() === 1) {
$packet = new PublishAckPacket();
} elseif ($this->message->getQosLevel() === 2) {
$packet = new PublishReceivedPacket();
$emit = false;
}
if ($packet !== null) {
$packet->setIdentifier($this->identifier);
}
if ($emit) {
$this->succeed($this->message);
}
return $packet;
}
public function accept(Packet $packet)
{
if ($this->message->getQosLevel() !== 2 || $packet->getPacketType() !== Packet::TYPE_PUBREL) {
return false;
}
/* @var PublishReleasePacket $packet */
return $packet->getIdentifier() === $this->identifier;
}
public function next(Packet $packet)
{
$this->succeed($this->message);
$response = new PublishCompletePacket();
$response->setIdentifier($this->identifier);
return $response;
}
}

View File

@@ -0,0 +1,70 @@
<?php
namespace BinSoul\Net\Mqtt\Flow;
use BinSoul\Net\Mqtt\Connection;
use BinSoul\Net\Mqtt\IdentifierGenerator;
use BinSoul\Net\Mqtt\Packet;
use BinSoul\Net\Mqtt\Packet\ConnectRequestPacket;
use BinSoul\Net\Mqtt\Packet\ConnectResponsePacket;
/**
* Represents a flow starting with an outgoing CONNECT packet.
*/
class OutgoingConnectFlow extends AbstractFlow
{
/** @var Connection */
private $connection;
/**
* Constructs an instance of this class.
*
* @param Connection $connection
* @param IdentifierGenerator $generator
*/
public function __construct(Connection $connection, IdentifierGenerator $generator)
{
$this->connection = $connection;
if ($this->connection->getClientID() === '') {
$this->connection = $this->connection->withClientID($generator->generateClientID());
}
}
public function getCode()
{
return 'connect';
}
public function start()
{
$packet = new ConnectRequestPacket();
$packet->setProtocolLevel($this->connection->getProtocol());
$packet->setKeepAlive($this->connection->getKeepAlive());
$packet->setClientID($this->connection->getClientID());
$packet->setCleanSession($this->connection->isCleanSession());
$packet->setUsername($this->connection->getUsername());
$packet->setPassword($this->connection->getPassword());
$will = $this->connection->getWill();
if ($will !== null && $will->getTopic() !== '' && $will->getPayload() !== '') {
$packet->setWill($will->getTopic(), $will->getPayload(), $will->getQosLevel(), $will->isRetained());
}
return $packet;
}
public function accept(Packet $packet)
{
return $packet->getPacketType() === Packet::TYPE_CONNACK;
}
public function next(Packet $packet)
{
/** @var ConnectResponsePacket $packet */
if ($packet->isSuccess()) {
$this->succeed($this->connection);
} else {
$this->fail($packet->getErrorName());
}
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace BinSoul\Net\Mqtt\Flow;
use BinSoul\Net\Mqtt\Connection;
use BinSoul\Net\Mqtt\Packet\DisconnectRequestPacket;
/**
* Represents a flow starting with an outgoing DISCONNECT packet.
*/
class OutgoingDisconnectFlow extends AbstractFlow
{
/** @var Connection */
private $connection;
/**
* Constructs an instance of this class.
*
* @param Connection $connection
*/
public function __construct(Connection $connection)
{
$this->connection = $connection;
}
public function getCode()
{
return 'disconnect';
}
public function start()
{
$this->succeed($this->connection);
return new DisconnectRequestPacket();
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace BinSoul\Net\Mqtt\Flow;
use BinSoul\Net\Mqtt\Packet;
use BinSoul\Net\Mqtt\Packet\PingRequestPacket;
/**
* Represents a flow starting with an outgoing PING packet.
*/
class OutgoingPingFlow extends AbstractFlow
{
public function getCode()
{
return 'ping';
}
public function start()
{
return new PingRequestPacket();
}
public function accept(Packet $packet)
{
return $packet->getPacketType() === Packet::TYPE_PINGRESP;
}
public function next(Packet $packet)
{
$this->succeed();
}
}

View File

@@ -0,0 +1,102 @@
<?php
namespace BinSoul\Net\Mqtt\Flow;
use BinSoul\Net\Mqtt\IdentifierGenerator;
use BinSoul\Net\Mqtt\Message;
use BinSoul\Net\Mqtt\Packet;
use BinSoul\Net\Mqtt\Packet\PublishAckPacket;
use BinSoul\Net\Mqtt\Packet\PublishCompletePacket;
use BinSoul\Net\Mqtt\Packet\PublishReceivedPacket;
use BinSoul\Net\Mqtt\Packet\PublishReleasePacket;
use BinSoul\Net\Mqtt\Packet\PublishRequestPacket;
/**
* Represents a flow starting with an outgoing PUBLISH packet.
*/
class OutgoingPublishFlow extends AbstractFlow
{
/** @var int|null */
private $identifier;
/** @var Message */
private $message;
/** @var bool */
private $receivedPubRec = false;
/**
* Constructs an instance of this class.
*
* @param Message $message
* @param IdentifierGenerator $generator
*/
public function __construct(Message $message, IdentifierGenerator $generator)
{
$this->message = $message;
if ($this->message->getQosLevel() > 0) {
$this->identifier = $generator->generatePacketID();
}
}
public function getCode()
{
return 'publish';
}
public function start()
{
$packet = new PublishRequestPacket();
$packet->setTopic($this->message->getTopic());
$packet->setPayload($this->message->getPayload());
$packet->setRetained($this->message->isRetained());
$packet->setDuplicate($this->message->isDuplicate());
$packet->setQosLevel($this->message->getQosLevel());
if ($this->message->getQosLevel() === 0) {
$this->succeed($this->message);
} else {
$packet->setIdentifier($this->identifier);
}
return $packet;
}
public function accept(Packet $packet)
{
if ($this->message->getQosLevel() === 0) {
return false;
}
$packetType = $packet->getPacketType();
if ($packetType === Packet::TYPE_PUBACK && $this->message->getQosLevel() === 1) {
/* @var PublishAckPacket $packet */
return $packet->getIdentifier() === $this->identifier;
} elseif ($this->message->getQosLevel() === 2) {
if ($packetType === Packet::TYPE_PUBREC) {
/* @var PublishReceivedPacket $packet */
return $packet->getIdentifier() === $this->identifier;
} elseif ($this->receivedPubRec && $packetType === Packet::TYPE_PUBCOMP) {
/* @var PublishCompletePacket $packet */
return $packet->getIdentifier() === $this->identifier;
}
}
return false;
}
public function next(Packet $packet)
{
$packetType = $packet->getPacketType();
if ($packetType === Packet::TYPE_PUBACK || $packetType === Packet::TYPE_PUBCOMP) {
$this->succeed($this->message);
} elseif ($packetType === Packet::TYPE_PUBREC) {
$this->receivedPubRec = true;
$response = new PublishReleasePacket();
$response->setIdentifier($this->identifier);
return $response;
}
}
}

View File

@@ -0,0 +1,82 @@
<?php
namespace BinSoul\Net\Mqtt\Flow;
use BinSoul\Net\Mqtt\IdentifierGenerator;
use BinSoul\Net\Mqtt\Packet;
use BinSoul\Net\Mqtt\Packet\SubscribeRequestPacket;
use BinSoul\Net\Mqtt\Packet\SubscribeResponsePacket;
use BinSoul\Net\Mqtt\Subscription;
/**
* Represents a flow starting with an outgoing SUBSCRIBE packet.
*/
class OutgoingSubscribeFlow extends AbstractFlow
{
/** @var int */
private $identifier;
/** @var Subscription[] */
private $subscriptions;
/**
* Constructs an instance of this class.
*
* @param Subscription[] $subscriptions
* @param IdentifierGenerator $generator
*/
public function __construct(array $subscriptions, IdentifierGenerator $generator)
{
$this->subscriptions = array_values($subscriptions);
$this->identifier = $generator->generatePacketID();
}
public function getCode()
{
return 'subscribe';
}
public function start()
{
$packet = new SubscribeRequestPacket();
$packet->setTopic($this->subscriptions[0]->getFilter());
$packet->setQosLevel($this->subscriptions[0]->getQosLevel());
$packet->setIdentifier($this->identifier);
return $packet;
}
public function accept(Packet $packet)
{
if ($packet->getPacketType() !== Packet::TYPE_SUBACK) {
return false;
}
/* @var SubscribeResponsePacket $packet */
return $packet->getIdentifier() === $this->identifier;
}
public function next(Packet $packet)
{
/* @var SubscribeResponsePacket $packet */
$returnCodes = $packet->getReturnCodes();
if (count($returnCodes) !== count($this->subscriptions)) {
throw new \LogicException(
sprintf(
'SUBACK: Expected %d return codes but got %d.',
count($this->subscriptions),
count($returnCodes)
)
);
}
foreach ($returnCodes as $index => $code) {
if ($packet->isError($code)) {
$this->fail(sprintf('Failed to subscribe to "%s".', $this->subscriptions[$index]->getFilter()));
return;
}
}
$this->succeed($this->subscriptions[0]);
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace BinSoul\Net\Mqtt\Flow;
use BinSoul\Net\Mqtt\IdentifierGenerator;
use BinSoul\Net\Mqtt\Packet;
use BinSoul\Net\Mqtt\Packet\UnsubscribeRequestPacket;
use BinSoul\Net\Mqtt\Packet\UnsubscribeResponsePacket;
use BinSoul\Net\Mqtt\Subscription;
/**
* Represents a flow starting with an outgoing UNSUBSCRIBE packet.
*/
class OutgoingUnsubscribeFlow extends AbstractFlow
{
/** @var int */
private $identifier;
/** @var Subscription[] */
private $subscriptions;
/**
* Constructs an instance of this class.
*
* @param Subscription[] $subscriptions
* @param IdentifierGenerator $generator
*/
public function __construct(array $subscriptions, IdentifierGenerator $generator)
{
$this->subscriptions = array_values($subscriptions);
$this->identifier = $generator->generatePacketID();
}
public function getCode()
{
return 'unsubscribe';
}
public function start()
{
$packet = new UnsubscribeRequestPacket();
$packet->setTopic($this->subscriptions[0]->getFilter());
$packet->setIdentifier($this->identifier);
return $packet;
}
public function accept(Packet $packet)
{
if ($packet->getPacketType() !== Packet::TYPE_UNSUBACK) {
return false;
}
/* @var UnsubscribeResponsePacket $packet */
return $packet->getIdentifier() === $this->identifier;
}
public function next(Packet $packet)
{
$this->succeed($this->subscriptions[0]);
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace BinSoul\Net\Mqtt;
/**
* Generates identifiers.
*/
interface IdentifierGenerator
{
/**
* Generates a packet identifier between 1 and 0xFFFF.
*
* @return int
*/
public function generatePacketID();
/**
* Generates a client identifier of up to 23 bytes.
*
* @return string
*/
public function generateClientID();
}

View File

@@ -0,0 +1,99 @@
<?php
namespace BinSoul\Net\Mqtt;
/**
* Represents a message.
*/
interface Message
{
/**
* Returns the topic.
*
* @return string
*/
public function getTopic();
/**
* Returns the payload.
*
* @return string
*/
public function getPayload();
/**
* Returns the quality of service level.
*
* @return int
*/
public function getQosLevel();
/**
* Indicates if the message is a duplicate.
*
* @return bool
*/
public function isDuplicate();
/**
* Indicates if the message is retained.
*
* @return bool
*/
public function isRetained();
/**
* Returns a new message with the given topic.
*
* @param string $topic
*
* @return self
*/
public function withTopic($topic);
/**
* Returns a new message with the given payload.
*
* @param string $payload
*
* @return self
*/
public function withPayload($payload);
/**
* Returns a new message with the given quality of service level.
*
* @param int $level
*
* @return self
*/
public function withQosLevel($level);
/**
* Returns a new message flagged as retained.
*
* @return self
*/
public function retain();
/**
* Returns a new message flagged as not retained.
*
* @return self
*/
public function release();
/**
* Returns a new message flagged as duplicate.
*
* @return self
*/
public function duplicate();
/**
* Returns a new message flagged as original.
*
* @return self
*/
public function original();
}

View File

@@ -0,0 +1,52 @@
<?php
namespace BinSoul\Net\Mqtt;
/**
* Represent a packet of the MQTT protocol.
*/
interface Packet
{
const TYPE_CONNECT = 1;
const TYPE_CONNACK = 2;
const TYPE_PUBLISH = 3;
const TYPE_PUBACK = 4;
const TYPE_PUBREC = 5;
const TYPE_PUBREL = 6;
const TYPE_PUBCOMP = 7;
const TYPE_SUBSCRIBE = 8;
const TYPE_SUBACK = 9;
const TYPE_UNSUBSCRIBE = 10;
const TYPE_UNSUBACK = 11;
const TYPE_PINGREQ = 12;
const TYPE_PINGRESP = 13;
const TYPE_DISCONNECT = 14;
/**
* Returns the type of the packet.
*
* @return int
*/
public function getPacketType();
/**
* Reads the packet from the given stream.
*
* @param PacketStream $stream
*/
public function read(PacketStream $stream);
/**
* Writes the packet to the given stream.
*
* @param PacketStream $stream
*/
public function write(PacketStream $stream);
/**
* Returns the serialized form of the packet.
*
* @return string
*/
public function __toString();
}

View File

@@ -0,0 +1,279 @@
<?php
namespace BinSoul\Net\Mqtt\Packet;
use BinSoul\Net\Mqtt\Exception\MalformedPacketException;
use BinSoul\Net\Mqtt\PacketStream;
use BinSoul\Net\Mqtt\Packet;
/**
* Represents the base class for all packets.
*/
abstract class BasePacket implements Packet
{
/**
* Type of the packet. See {@see Packet}.
*
* @var int
*/
protected static $packetType = 0;
/**
* Flags of the packet.
*
* @var int
*/
protected $packetFlags = 0;
/**
* Number of bytes of a variable length packet.
*
* @var int
*/
protected $remainingPacketLength = 0;
public function __toString()
{
$output = new PacketStream();
$this->write($output);
return $output->getData();
}
public function read(PacketStream $stream)
{
$byte = $stream->readByte();
if ($byte >> 4 !== static::$packetType) {
throw new MalformedPacketException(
sprintf(
'Expected packet type %02x but got %02x.',
$byte >> 4,
static::$packetType
)
);
}
$this->packetFlags = $byte & 0x0F;
$this->readRemainingLength($stream);
}
public function write(PacketStream $stream)
{
$stream->writeByte(((static::$packetType & 0x0F) << 4) + ($this->packetFlags & 0x0F));
$this->writeRemainingLength($stream);
}
/**
* Reads the remaining length from the given stream.
*
* @param PacketStream $stream
*
* @throws MalformedPacketException
*/
private function readRemainingLength(PacketStream $stream)
{
$this->remainingPacketLength = 0;
$multiplier = 1;
do {
$encodedByte = $stream->readByte();
$this->remainingPacketLength += ($encodedByte & 127) * $multiplier;
$multiplier *= 128;
if ($multiplier > 128 * 128 * 128 * 128) {
throw new MalformedPacketException('Malformed remaining length.');
}
} while (($encodedByte & 128) !== 0);
}
/**
* Writes the remaining length to the given stream.
*
* @param PacketStream $stream
*/
private function writeRemainingLength(PacketStream $stream)
{
$x = $this->remainingPacketLength;
do {
$encodedByte = $x % 128;
$x = (int) ($x / 128);
if ($x > 0) {
$encodedByte |= 128;
}
$stream->writeByte($encodedByte);
} while ($x > 0);
}
public function getPacketType()
{
return static::$packetType;
}
/**
* Returns the packet flags.
*
* @return int
*/
public function getPacketFlags()
{
return $this->packetFlags;
}
/**
* Returns the remaining length.
*
* @return int
*/
public function getRemainingPacketLength()
{
return $this->remainingPacketLength;
}
/**
* Asserts that the packet flags have a specific value.
*
* @param int $value
* @param bool $fromPacket
*
* @throws MalformedPacketException
* @throws \InvalidArgumentException
*/
protected function assertPacketFlags($value, $fromPacket = true)
{
if ($this->packetFlags !== $value) {
$this->throwException(
sprintf(
'Expected flags %02x but got %02x.',
$value,
$this->packetFlags
),
$fromPacket
);
}
}
/**
* Asserts that the remaining length is greater than zero and has a specific value.
*
* @param int|null $value value to test or null if any value greater than zero is valid
* @param bool $fromPacket
*
* @throws MalformedPacketException
* @throws \InvalidArgumentException
*/
protected function assertRemainingPacketLength($value = null, $fromPacket = true)
{
if ($value === null && $this->remainingPacketLength === 0) {
$this->throwException('Expected payload but remaining packet length is zero.', $fromPacket);
}
if ($value !== null && $this->remainingPacketLength !== $value) {
$this->throwException(
sprintf(
'Expected remaining packet length of %d bytes but got %d.',
$value,
$this->remainingPacketLength
),
$fromPacket
);
}
}
/**
* Asserts that the given string is a well-formed MQTT string.
*
* @param string $value
* @param bool $fromPacket
*
* @throws MalformedPacketException
* @throws \InvalidArgumentException
*/
protected function assertValidStringLength($value, $fromPacket = true)
{
if (strlen($value) > 0xFFFF) {
$this->throwException(
sprintf(
'The string "%s" is longer than 65535 byte.',
substr($value, 0, 50)
),
$fromPacket
);
}
}
/**
* Asserts that the given string is a well-formed MQTT string.
*
* @param string $value
* @param bool $fromPacket
*
* @throws MalformedPacketException
* @throws \InvalidArgumentException
*/
protected function assertValidString($value, $fromPacket = true)
{
$this->assertValidStringLength($value, $fromPacket);
if (!mb_check_encoding($value, 'UTF-8')) {
$this->throwException(
sprintf(
'The string "%s" is not well-formed UTF-8.',
substr($value, 0, 50)
),
$fromPacket
);
}
if (preg_match('/[\xD8-\xDF][\x00-\xFF]|\x00\x00/x', $value)) {
$this->throwException(
sprintf(
'The string "%s" contains invalid characters.',
substr($value, 0, 50)
),
$fromPacket
);
}
}
/**
* Asserts that the given quality of service level is valid.
*
* @param int $level
* @param bool $fromPacket
*
* @throws MalformedPacketException
* @throws \InvalidArgumentException
*/
protected function assertValidQosLevel($level, $fromPacket = true)
{
if ($level < 0 || $level > 2) {
$this->throwException(
sprintf(
'Expected a quality of service level between 0 and 2 but got %d.',
$level
),
$fromPacket
);
}
}
/**
* Throws a MalformedPacketException for packet validation and an InvalidArgumentException otherwise.
*
* @param string $message
* @param bool $fromPacket
*
* @throws MalformedPacketException
* @throws \InvalidArgumentException
*/
protected function throwException($message, $fromPacket)
{
if ($fromPacket) {
throw new MalformedPacketException($message);
}
throw new \InvalidArgumentException($message);
}
}

View File

@@ -0,0 +1,405 @@
<?php
namespace BinSoul\Net\Mqtt\Packet;
use BinSoul\Net\Mqtt\Exception\MalformedPacketException;
use BinSoul\Net\Mqtt\PacketStream;
use BinSoul\Net\Mqtt\Packet;
/**
* Represents the CONNECT packet.
*/
class ConnectRequestPacket extends BasePacket
{
/** @var int */
private $protocolLevel = 4;
/** @var string */
private $protocolName = 'MQTT';
/** @var int */
private $flags = 2;
/** @var string */
protected $clientID = '';
/** @var int */
private $keepAlive = 60;
/** @var string */
private $willTopic = '';
/** @var string */
private $willMessage = '';
/** @var string */
private $username = '';
/** @var string */
private $password = '';
protected static $packetType = Packet::TYPE_CONNECT;
public function read(PacketStream $stream)
{
parent::read($stream);
$this->assertPacketFlags(0);
$this->assertRemainingPacketLength();
$this->protocolName = $stream->readString();
$this->protocolLevel = $stream->readByte();
$this->flags = $stream->readByte();
$this->keepAlive = $stream->readWord();
$this->clientID = $stream->readString();
if ($this->hasWill()) {
$this->willTopic = $stream->readString();
$this->willMessage = $stream->readString();
}
if ($this->hasUsername()) {
$this->username = $stream->readString();
}
if ($this->hasPassword()) {
$this->password = $stream->readString();
}
$this->assertValidWill();
$this->assertValidString($this->clientID);
$this->assertValidString($this->willTopic);
$this->assertValidString($this->username);
}
public function write(PacketStream $stream)
{
if ($this->clientID === '') {
$this->clientID = 'BinSoul'.mt_rand(100000, 999999);
}
$data = new PacketStream();
$data->writeString($this->protocolName);
$data->writeByte($this->protocolLevel);
$data->writeByte($this->flags);
$data->writeWord($this->keepAlive);
$data->writeString($this->clientID);
if ($this->hasWill()) {
$data->writeString($this->willTopic);
$data->writeString($this->willMessage);
}
if ($this->hasUsername()) {
$data->writeString($this->username);
}
if ($this->hasPassword()) {
$data->writeString($this->password);
}
$this->remainingPacketLength = $data->length();
parent::write($stream);
$stream->write($data->getData());
}
/**
* Returns the protocol level.
*
* @return int
*/
public function getProtocolLevel()
{
return $this->protocolLevel;
}
/**
* Sets the protocol level.
*
* @param int $value
*
* @throws \InvalidArgumentException
*/
public function setProtocolLevel($value)
{
if ($value < 3 || $value > 4) {
throw new \InvalidArgumentException(sprintf('Unknown protocol level %d.', $value));
}
$this->protocolLevel = $value;
if ($this->protocolLevel === 3) {
$this->protocolName = 'MQIsdp';
} elseif ($this->protocolLevel === 4) {
$this->protocolName = 'MQTT';
}
}
/**
* Returns the client id.
*
* @return string
*/
public function getClientID()
{
return $this->clientID;
}
/**
* Sets the client id.
*
* @param string $value
*/
public function setClientID($value)
{
$this->clientID = $value;
}
/**
* Returns the keep alive time in seconds.
*
* @return int
*/
public function getKeepAlive()
{
return $this->keepAlive;
}
/**
* Sets the keep alive time in seconds.
*
* @param int $value
*
* @throws \InvalidArgumentException
*/
public function setKeepAlive($value)
{
if ($value > 65535) {
throw new \InvalidArgumentException(
sprintf(
'Expected a keep alive time lower than 65535 but got %d.',
$value
)
);
}
$this->keepAlive = $value;
}
/**
* Indicates if the clean session flag is set.
*
* @return bool
*/
public function isCleanSession()
{
return ($this->flags & 2) === 2;
}
/**
* Changes the clean session flag.
*
* @param bool $value
*/
public function setCleanSession($value)
{
if ($value) {
$this->flags |= 2;
} else {
$this->flags &= ~2;
}
}
/**
* Indicates if a will is set.
*
* @return bool
*/
public function hasWill()
{
return ($this->flags & 4) === 4;
}
/**
* Returns the desired quality of service level of the will.
*
* @return int
*/
public function getWillQosLevel()
{
return ($this->flags & 24) >> 3;
}
/**
* Indicates if the will should be retained.
*
* @return bool
*/
public function isWillRetained()
{
return ($this->flags & 32) === 32;
}
/**
* Returns the will topic.
*
* @return string
*/
public function getWillTopic()
{
return $this->willTopic;
}
/**
* Returns the will message.
*
* @return string
*/
public function getWillMessage()
{
return $this->willMessage;
}
/**
* Sets the will.
*
* @param string $topic
* @param string $message
* @param int $qosLevel
* @param bool $isRetained
*
* @throws \InvalidArgumentException
*/
public function setWill($topic, $message, $qosLevel = 0, $isRetained = false)
{
$this->assertValidString($topic, false);
if ($topic === '') {
throw new \InvalidArgumentException('The topic must not be empty.');
}
$this->assertValidStringLength($message, false);
if ($message === '') {
throw new \InvalidArgumentException('The message must not be empty.');
}
$this->assertValidQosLevel($qosLevel, false);
$this->willTopic = $topic;
$this->willMessage = $message;
$this->flags |= 4;
$this->flags |= ($qosLevel << 3);
if ($isRetained) {
$this->flags |= 32;
} else {
$this->flags &= ~32;
}
}
/**
* Removes the will.
*/
public function removeWill()
{
$this->flags &= ~60;
$this->willTopic = '';
$this->willMessage = '';
}
/**
* Indicates if a username is set.
*
* @return bool
*/
public function hasUsername()
{
return $this->flags & 64;
}
/**
* Returns the username.
*
* @return string
*/
public function getUsername()
{
return $this->username;
}
/**
* Sets the username.
*
* @param string $value
*
* @throws \InvalidArgumentException
*/
public function setUsername($value)
{
$this->assertValidString($value, false);
$this->username = $value;
if ($this->username !== '') {
$this->flags |= 64;
} else {
$this->flags &= ~64;
}
}
/**
* Indicates if a password is set.
*
* @return bool
*/
public function hasPassword()
{
return $this->flags & 128;
}
/**
* Returns the password.
*
* @return string
*/
public function getPassword()
{
return $this->password;
}
/**
* Sets the password.
*
* @param string $value
*
* @throws \InvalidArgumentException
*/
public function setPassword($value)
{
$this->assertValidStringLength($value, false);
$this->password = $value;
if ($this->password !== '') {
$this->flags |= 128;
} else {
$this->flags &= ~128;
}
}
/**
* Asserts that all will flags and quality of service are correct.
*
* @throws MalformedPacketException
*/
private function assertValidWill()
{
if ($this->hasWill()) {
$this->assertValidQosLevel($this->getWillQosLevel(), true);
} else {
if ($this->getWillQosLevel() > 0) {
$this->throwException(
sprintf(
'Expected a will quality of service level of zero but got %d.',
$this->getWillQosLevel()
),
true
);
}
if ($this->isWillRetained()) {
$this->throwException('There is not will but the will retain flag is set.', true);
}
}
}
}

View File

@@ -0,0 +1,111 @@
<?php
namespace BinSoul\Net\Mqtt\Packet;
use BinSoul\Net\Mqtt\PacketStream;
use BinSoul\Net\Mqtt\Packet;
/**
* Represents the CONNACK packet.
*/
class ConnectResponsePacket extends BasePacket
{
/** @var string[][] */
private static $returnCodes = [
0 => [
'Connection accepted',
'',
],
1 => [
'Unacceptable protocol version',
'The Server does not support the level of the MQTT protocol requested by the client.',
],
2 => [
'Identifier rejected',
'The client identifier is correct UTF-8 but not allowed by the server.',
],
3 => [
'Server unavailable',
'The network connection has been made but the MQTT service is unavailable',
],
4 => [
'Bad user name or password',
'The data in the user name or password is malformed.',
],
5 => [
'Not authorized',
'The client is not authorized to connect.',
],
];
/** @var int */
private $flags = 0;
/** @var int */
private $returnCode;
protected static $packetType = Packet::TYPE_CONNACK;
protected $remainingPacketLength = 2;
public function read(PacketStream $stream)
{
parent::read($stream);
$this->assertPacketFlags(0);
$this->assertRemainingPacketLength(2);
$this->flags = $stream->readByte();
$this->returnCode = $stream->readByte();
}
public function write(PacketStream $stream)
{
$this->remainingPacketLength = 2;
parent::write($stream);
$stream->writeByte($this->flags);
$stream->writeByte($this->returnCode);
}
/**
* Returns the return code.
*
* @return int
*/
public function getReturnCode()
{
return $this->returnCode;
}
/**
* Indicates if the connection was successful.
*
* @return bool
*/
public function isSuccess()
{
return $this->returnCode === 0;
}
/**
* Indicates if the connection failed.
*
* @return bool
*/
public function isError()
{
return $this->returnCode > 0;
}
/**
* Returns a string representation of the returned error code.
*
* @return int
*/
public function getErrorName()
{
if (isset(self::$returnCodes[$this->returnCode])) {
return self::$returnCodes[$this->returnCode][0];
}
return 'Error '.$this->returnCode;
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace BinSoul\Net\Mqtt\Packet;
use BinSoul\Net\Mqtt\PacketStream;
use BinSoul\Net\Mqtt\Packet;
/**
* Represents the DISCONNECT packet.
*/
class DisconnectRequestPacket extends BasePacket
{
protected static $packetType = Packet::TYPE_DISCONNECT;
public function read(PacketStream $stream)
{
parent::read($stream);
$this->assertPacketFlags(0);
$this->assertRemainingPacketLength(0);
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace BinSoul\Net\Mqtt\Packet;
/**
* Provides methods for packets with an identifier.
*/
trait IdentifiablePacket
{
/** @var int */
private static $nextIdentifier = 0;
/** @var int|null */
protected $identifier;
/**
* Returns the identifier or generates a new one.
*
* @return int
*/
protected function generateIdentifier()
{
if ($this->identifier === null) {
++self::$nextIdentifier;
self::$nextIdentifier &= 0xFFFF;
$this->identifier = self::$nextIdentifier;
}
return $this->identifier;
}
/**
* Returns the identifier.
*
* @return int|null
*/
public function getIdentifier()
{
return $this->identifier;
}
/**
* Sets the identifier.
*
* @param int|null $value
*
* @throws \InvalidArgumentException
*/
public function setIdentifier($value)
{
if ($value !== null && ($value < 0 || $value > 0xFFFF)) {
throw new \InvalidArgumentException(
sprintf(
'Expected an identifier between 0x0000 and 0xFFFF but got %x',
$value
)
);
}
$this->identifier = $value;
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace BinSoul\Net\Mqtt\Packet;
use BinSoul\Net\Mqtt\PacketStream;
/**
* Provides a base class for PUB* packets.
*/
abstract class IdentifierOnlyPacket extends BasePacket
{
use IdentifiablePacket;
protected $remainingPacketLength = 2;
public function read(PacketStream $stream)
{
parent::read($stream);
$this->assertPacketFlags($this->getExpectedPacketFlags());
$this->assertRemainingPacketLength(2);
$this->identifier = $stream->readWord();
}
public function write(PacketStream $stream)
{
$this->remainingPacketLength = 2;
parent::write($stream);
$stream->writeWord($this->generateIdentifier());
}
/**
* Returns the expected packet flags.
*
* @return int
*/
protected function getExpectedPacketFlags()
{
return 0;
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace BinSoul\Net\Mqtt\Packet;
use BinSoul\Net\Mqtt\PacketStream;
use BinSoul\Net\Mqtt\Packet;
/**
* Represents the PINGREQ packet.
*/
class PingRequestPacket extends BasePacket
{
protected static $packetType = Packet::TYPE_PINGREQ;
public function read(PacketStream $stream)
{
parent::read($stream);
$this->assertPacketFlags(0);
$this->assertRemainingPacketLength(0);
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace BinSoul\Net\Mqtt\Packet;
use BinSoul\Net\Mqtt\PacketStream;
use BinSoul\Net\Mqtt\Packet;
/**
* Represents the PINGRESP packet.
*/
class PingResponsePacket extends BasePacket
{
protected static $packetType = Packet::TYPE_PINGRESP;
public function read(PacketStream $stream)
{
parent::read($stream);
$this->assertPacketFlags(0);
$this->assertRemainingPacketLength(0);
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace BinSoul\Net\Mqtt\Packet;
use BinSoul\Net\Mqtt\Packet;
/**
* Represents the PUBACK packet.
*/
class PublishAckPacket extends IdentifierOnlyPacket
{
protected static $packetType = Packet::TYPE_PUBACK;
}

View File

@@ -0,0 +1,13 @@
<?php
namespace BinSoul\Net\Mqtt\Packet;
use BinSoul\Net\Mqtt\Packet;
/**
* Represents the PUBCOMP packet.
*/
class PublishCompletePacket extends IdentifierOnlyPacket
{
protected static $packetType = Packet::TYPE_PUBCOMP;
}

View File

@@ -0,0 +1,13 @@
<?php
namespace BinSoul\Net\Mqtt\Packet;
use BinSoul\Net\Mqtt\Packet;
/**
* Represents the PUBREC packet.
*/
class PublishReceivedPacket extends IdentifierOnlyPacket
{
protected static $packetType = Packet::TYPE_PUBREC;
}

View File

@@ -0,0 +1,19 @@
<?php
namespace BinSoul\Net\Mqtt\Packet;
use BinSoul\Net\Mqtt\Packet;
/**
* Represents the PUBREL packet.
*/
class PublishReleasePacket extends IdentifierOnlyPacket
{
protected static $packetType = Packet::TYPE_PUBREL;
protected $packetFlags = 2;
protected function getExpectedPacketFlags()
{
return 2;
}
}

View File

@@ -0,0 +1,176 @@
<?php
namespace BinSoul\Net\Mqtt\Packet;
use BinSoul\Net\Mqtt\PacketStream;
use BinSoul\Net\Mqtt\Packet;
/**
* Represents the PUBLISH packet.
*/
class PublishRequestPacket extends BasePacket
{
use IdentifiablePacket;
/** @var string */
private $topic;
/** @var string */
private $payload;
protected static $packetType = Packet::TYPE_PUBLISH;
public function read(PacketStream $stream)
{
parent::read($stream);
$this->assertRemainingPacketLength();
$originalPosition = $stream->getPosition();
$this->topic = $stream->readString();
$this->identifier = null;
if ($this->getQosLevel() > 0) {
$this->identifier = $stream->readWord();
}
$payloadLength = $this->remainingPacketLength - ($stream->getPosition() - $originalPosition);
$this->payload = $stream->read($payloadLength);
$this->assertValidQosLevel($this->getQosLevel());
$this->assertValidString($this->topic);
}
public function write(PacketStream $stream)
{
$data = new PacketStream();
$data->writeString($this->topic);
if ($this->getQosLevel() > 0) {
$data->writeWord($this->generateIdentifier());
}
$data->write($this->payload);
$this->remainingPacketLength = $data->length();
parent::write($stream);
$stream->write($data->getData());
}
/**
* Returns the topic.
*
* @return string
*/
public function getTopic()
{
return $this->topic;
}
/**
* Sets the topic.
*
* @param string $value
*
* @throws \InvalidArgumentException
*/
public function setTopic($value)
{
$this->assertValidString($value, false);
if ($value === '') {
throw new \InvalidArgumentException('The topic must not be empty.');
}
$this->topic = $value;
}
/**
* Returns the payload.
*
* @return string
*/
public function getPayload()
{
return $this->payload;
}
/**
* Sets the payload.
*
* @param string $value
*/
public function setPayload($value)
{
$this->payload = $value;
}
/**
* Indicates if the packet is a duplicate.
*
* @return bool
*/
public function isDuplicate()
{
return ($this->packetFlags & 8) === 8;
}
/**
* Marks the packet as duplicate.
*
* @param bool $value
*/
public function setDuplicate($value)
{
if ($value) {
$this->packetFlags |= 8;
} else {
$this->packetFlags &= ~8;
}
}
/**
* Indicates if the packet is retained.
*
* @return bool
*/
public function isRetained()
{
return ($this->packetFlags & 1) === 1;
}
/**
* Marks the packet as retained.
*
* @param bool $value
*/
public function setRetained($value)
{
if ($value) {
$this->packetFlags |= 1;
} else {
$this->packetFlags &= ~1;
}
}
/**
* Returns the quality of service level.
*
* @return int
*/
public function getQosLevel()
{
return ($this->packetFlags & 6) >> 1;
}
/**
* Sets the quality of service level.
*
* @param int $value
*
* @throws \InvalidArgumentException
*/
public function setQosLevel($value)
{
$this->assertValidQosLevel($value, false);
$this->packetFlags |= ($value & 3) << 1;
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace BinSoul\Net\Mqtt\Packet;
use BinSoul\Net\Mqtt\Exception\MalformedPacketException;
use BinSoul\Net\Mqtt\PacketStream;
/**
* Represents the CONNECT packet with strict rules for client ids.
*/
class StrictConnectRequestPacket extends ConnectRequestPacket
{
public function read(PacketStream $stream)
{
parent::read($stream);
$this->assertValidClientID($this->clientID, true);
}
/**
* Sets the client id.
*
* @param string $value
*
* @throws \InvalidArgumentException
*/
public function setClientID($value)
{
$this->assertValidClientID($value, false);
$this->clientID = $value;
}
/**
* Asserts that a client id is shorter than 24 bytes and only contains characters 0-9, a-z or A-Z.
*
* @param string $value
* @param bool $fromPacket
*
* @throws MalformedPacketException
* @throws \InvalidArgumentException
*/
private function assertValidClientID($value, $fromPacket)
{
if (strlen($value) > 23) {
$this->throwException(
sprintf(
'Expected client id shorter than 24 bytes but got "%s".',
$value
),
$fromPacket
);
}
if ($value !== '' && !ctype_alnum($value)) {
$this->throwException(
sprintf(
'Expected a client id containing characters 0-9, a-z or A-Z but got "%s".',
$value
),
$fromPacket
);
}
}
}

View File

@@ -0,0 +1,101 @@
<?php
namespace BinSoul\Net\Mqtt\Packet;
use BinSoul\Net\Mqtt\PacketStream;
use BinSoul\Net\Mqtt\Packet;
/**
* Represents the SUBSCRIBE packet.
*/
class SubscribeRequestPacket extends BasePacket
{
use IdentifiablePacket;
/** @var string */
private $topic;
/** @var int */
private $qosLevel;
protected static $packetType = Packet::TYPE_SUBSCRIBE;
protected $packetFlags = 2;
public function read(PacketStream $stream)
{
parent::read($stream);
$this->assertPacketFlags(2);
$this->assertRemainingPacketLength();
$this->identifier = $stream->readWord();
$this->topic = $stream->readString();
$this->qosLevel = $stream->readByte();
$this->assertValidQosLevel($this->qosLevel);
$this->assertValidString($this->topic);
}
public function write(PacketStream $stream)
{
$data = new PacketStream();
$data->writeWord($this->generateIdentifier());
$data->writeString($this->topic);
$data->writeByte($this->qosLevel);
$this->remainingPacketLength = $data->length();
parent::write($stream);
$stream->write($data->getData());
}
/**
* Returns the topic.
*
* @return string
*/
public function getTopic()
{
return $this->topic;
}
/**
* Sets the topic.
*
* @param string $value
*
* @throws \InvalidArgumentException
*/
public function setTopic($value)
{
$this->assertValidString($value, false);
if ($value === '') {
throw new \InvalidArgumentException('The topic must not be empty.');
}
$this->topic = $value;
}
/**
* Returns the quality of service level.
*
* @return int
*/
public function getQosLevel()
{
return $this->qosLevel;
}
/**
* Sets the quality of service level.
*
* @param int $value
*
* @throws \InvalidArgumentException
*/
public function setQosLevel($value)
{
$this->assertValidQosLevel($value, false);
$this->qosLevel = $value;
}
}

View File

@@ -0,0 +1,132 @@
<?php
namespace BinSoul\Net\Mqtt\Packet;
use BinSoul\Net\Mqtt\Exception\MalformedPacketException;
use BinSoul\Net\Mqtt\PacketStream;
use BinSoul\Net\Mqtt\Packet;
/**
* Represents the SUBACK packet.
*/
class SubscribeResponsePacket extends BasePacket
{
use IdentifiablePacket;
private static $qosLevels = [
0 => ['Maximum QoS 0'],
1 => ['Maximum QoS 1'],
2 => ['Maximum QoS 2'],
128 => ['Failure'],
];
/** @var int[] */
private $returnCodes;
protected static $packetType = Packet::TYPE_SUBACK;
public function read(PacketStream $stream)
{
parent::read($stream);
$this->assertPacketFlags(0);
$this->assertRemainingPacketLength();
$this->identifier = $stream->readWord();
$returnCodeLength = $this->remainingPacketLength - 2;
for ($n = 0; $n < $returnCodeLength; ++$n) {
$returnCode = $stream->readByte();
$this->assertValidReturnCode($returnCode);
$this->returnCodes[] = $returnCode;
}
}
public function write(PacketStream $stream)
{
$data = new PacketStream();
$data->writeWord($this->generateIdentifier());
foreach ($this->returnCodes as $returnCode) {
$data->writeByte($returnCode);
}
$this->remainingPacketLength = $data->length();
parent::write($stream);
$stream->write($data->getData());
}
/**
* Indicates if the given return code is an error.
*
* @param int $returnCode
*
* @return bool
*/
public function isError($returnCode)
{
return $returnCode === 128;
}
/**
* Indicates if the given return code is an error.
*
* @param int $returnCode
*
* @return bool
*/
public function getReturnCodeName($returnCode)
{
if (isset(self::$qosLevels[$returnCode])) {
return self::$qosLevels[$returnCode][0];
}
return 'Unknown '.$returnCode;
}
/**
* Returns the return codes.
*
* @return int[]
*/
public function getReturnCodes()
{
return $this->returnCodes;
}
/**
* Sets the return codes.
*
* @param int[] $value
*
* @throws \InvalidArgumentException
*/
public function setReturnCodes(array $value)
{
foreach ($value as $returnCode) {
$this->assertValidReturnCode($returnCode, false);
}
$this->returnCodes = $value;
}
/**
* Asserts that a return code is valid.
*
* @param int $returnCode
* @param bool $fromPacket
*
* @throws MalformedPacketException
* @throws \InvalidArgumentException
*/
private function assertValidReturnCode($returnCode, $fromPacket = true)
{
if (!in_array($returnCode, [0, 1, 2, 128])) {
$this->throwException(
sprintf('Malformed return code %02x.', $returnCode),
$fromPacket
);
}
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace BinSoul\Net\Mqtt\Packet;
use BinSoul\Net\Mqtt\PacketStream;
use BinSoul\Net\Mqtt\Packet;
/**
* Represents the UNSUBSCRIBE packet.
*/
class UnsubscribeRequestPacket extends BasePacket
{
use IdentifiablePacket;
/** @var string */
private $topic;
protected static $packetType = Packet::TYPE_UNSUBSCRIBE;
protected $packetFlags = 2;
public function read(PacketStream $stream)
{
parent::read($stream);
$this->assertPacketFlags(2);
$this->assertRemainingPacketLength();
$originalPosition = $stream->getPosition();
do {
$this->identifier = $stream->readWord();
$this->topic = $stream->readString();
} while (($stream->getPosition() - $originalPosition) <= $this->remainingPacketLength);
}
public function write(PacketStream $stream)
{
$data = new PacketStream();
$data->writeWord($this->generateIdentifier());
$data->writeString($this->topic);
$this->remainingPacketLength = $data->length();
parent::write($stream);
$stream->write($data->getData());
}
/**
* Returns the topic.
*
* @return string
*/
public function getTopic()
{
return $this->topic;
}
/**
* Sets the topic.
*
* @param string $value
*/
public function setTopic($value)
{
$this->topic = $value;
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace BinSoul\Net\Mqtt\Packet;
use BinSoul\Net\Mqtt\Packet;
/**
* Represents the UNSUBACK packet.
*/
class UnsubscribeResponsePacket extends IdentifierOnlyPacket
{
protected static $packetType = Packet::TYPE_UNSUBACK;
}

View File

@@ -0,0 +1,67 @@
<?php
namespace BinSoul\Net\Mqtt;
use BinSoul\Net\Mqtt\Exception\UnknownPacketTypeException;
use BinSoul\Net\Mqtt\Packet\ConnectRequestPacket;
use BinSoul\Net\Mqtt\Packet\ConnectResponsePacket;
use BinSoul\Net\Mqtt\Packet\DisconnectRequestPacket;
use BinSoul\Net\Mqtt\Packet\PingRequestPacket;
use BinSoul\Net\Mqtt\Packet\PingResponsePacket;
use BinSoul\Net\Mqtt\Packet\PublishRequestPacket;
use BinSoul\Net\Mqtt\Packet\PublishAckPacket;
use BinSoul\Net\Mqtt\Packet\PublishCompletePacket;
use BinSoul\Net\Mqtt\Packet\PublishReceivedPacket;
use BinSoul\Net\Mqtt\Packet\PublishReleasePacket;
use BinSoul\Net\Mqtt\Packet\SubscribeRequestPacket;
use BinSoul\Net\Mqtt\Packet\SubscribeResponsePacket;
use BinSoul\Net\Mqtt\Packet\UnsubscribeRequestPacket;
use BinSoul\Net\Mqtt\Packet\UnsubscribeResponsePacket;
/**
* Builds instances of the {@see Packet} interface.
*/
class PacketFactory
{
/**
* Map of packet types to packet classes.
*
* @var string[]
*/
private static $mapping = [
Packet::TYPE_CONNECT => ConnectRequestPacket::class,
Packet::TYPE_CONNACK => ConnectResponsePacket::class,
Packet::TYPE_PUBLISH => PublishRequestPacket::class,
Packet::TYPE_PUBACK => PublishAckPacket::class,
Packet::TYPE_PUBREC => PublishReceivedPacket::class,
Packet::TYPE_PUBREL => PublishReleasePacket::class,
Packet::TYPE_PUBCOMP => PublishCompletePacket::class,
Packet::TYPE_SUBSCRIBE => SubscribeRequestPacket::class,
Packet::TYPE_SUBACK => SubscribeResponsePacket::class,
Packet::TYPE_UNSUBSCRIBE => UnsubscribeRequestPacket::class,
Packet::TYPE_UNSUBACK => UnsubscribeResponsePacket::class,
Packet::TYPE_PINGREQ => PingRequestPacket::class,
Packet::TYPE_PINGRESP => PingResponsePacket::class,
Packet::TYPE_DISCONNECT => DisconnectRequestPacket::class,
];
/**
* Builds a packet object for the given type.
*
* @param int $type
*
* @throws UnknownPacketTypeException
*
* @return Packet
*/
public function build($type)
{
if (!isset(self::$mapping[$type])) {
throw new UnknownPacketTypeException(sprintf('Unknown packet type %d.', $type));
}
$class = self::$mapping[$type];
return new $class();
}
}

View File

@@ -0,0 +1,216 @@
<?php
namespace BinSoul\Net\Mqtt;
use BinSoul\Net\Mqtt\Exception\EndOfStreamException;
/**
* Provides methods to operate on a stream of bytes.
*/
class PacketStream
{
/** @var string */
private $data;
/** @var int */
private $position;
/**
* Constructs an instance of this class.
*
* @param string $data initial data of the stream
*/
public function __construct($data = '')
{
$this->data = $data;
$this->position = 0;
}
/**
* @return string
*/
public function __toString()
{
return $this->data;
}
/**
* Returns the desired number of bytes.
*
* @param int $count
*
* @throws EndOfStreamException
*
* @return string
*/
public function read($count)
{
$contentLength = strlen($this->data);
if ($this->position > $contentLength || $count > $contentLength - $this->position) {
throw new EndOfStreamException(
sprintf(
'End of stream reached when trying to read %d bytes. content length=%d, position=%d',
$count,
$contentLength,
$this->position
)
);
}
$chunk = substr($this->data, $this->position, $count);
if ($chunk === false) {
$chunk = '';
}
$readBytes = strlen($chunk);
$this->position += $readBytes;
return $chunk;
}
/**
* Returns a single byte.
*
* @return int
*/
public function readByte()
{
return ord($this->read(1));
}
/**
* Returns a single word.
*
* @return int
*/
public function readWord()
{
return ($this->readByte() << 8) + $this->readByte();
}
/**
* Returns a length prefixed string.
*
* @return string
*/
public function readString()
{
$length = $this->readWord();
return $this->read($length);
}
/**
* Appends the given value.
*
* @param string $value
*/
public function write($value)
{
$this->data .= $value;
}
/**
* Appends a single byte.
*
* @param int $value
*/
public function writeByte($value)
{
$this->write(chr($value));
}
/**
* Appends a single word.
*
* @param int $value
*/
public function writeWord($value)
{
$this->write(chr(($value & 0xFFFF) >> 8));
$this->write(chr($value & 0xFF));
}
/**
* Appends a length prefixed string.
*
* @param string $string
*/
public function writeString($string)
{
$this->writeWord(strlen($string));
$this->write($string);
}
/**
* Returns the length of the stream.
*
* @return int
*/
public function length()
{
return strlen($this->data);
}
/**
* Returns the number of bytes until the end of the stream.
*
* @return int
*/
public function getRemainingBytes()
{
return $this->length() - $this->position;
}
/**
* Returns the whole content of the stream.
*
* @return string
*/
public function getData()
{
return $this->data;
}
/**
* Changes the internal position of the stream relative to the current position.
*
* @param int $offset
*/
public function seek($offset)
{
$this->position += $offset;
}
/**
* Returns the internal position of the stream.
*
* @return int
*/
public function getPosition()
{
return $this->position;
}
/**
* Sets the internal position of the stream.
*
* @param int $value
*/
public function setPosition($value)
{
$this->position = $value;
}
/**
* Removes all bytes from the beginning to the current position.
*/
public function cut()
{
$this->data = substr($this->data, $this->position);
if ($this->data === false) {
$this->data = '';
}
$this->position = 0;
}
}

View File

@@ -0,0 +1,90 @@
<?php
namespace BinSoul\Net\Mqtt;
use BinSoul\Net\Mqtt\Exception\EndOfStreamException;
use BinSoul\Net\Mqtt\Exception\MalformedPacketException;
use BinSoul\Net\Mqtt\Exception\UnknownPacketTypeException;
/**
* Provides methods to parse a stream of bytes into packets.
*/
class StreamParser
{
/** @var PacketStream */
private $buffer;
/** @var PacketFactory */
private $factory;
/** @var callable */
private $errorCallback;
/**
* Constructs an instance of this class.
*/
public function __construct()
{
$this->buffer = new PacketStream();
$this->factory = new PacketFactory();
}
/**
* Registers an error callback.
*
* @param callable $callback
*/
public function onError($callback)
{
$this->errorCallback = $callback;
}
/**
* Appends the given data to the internal buffer and parses it.
*
* @param string $data
*
* @return Packet[]
*/
public function push($data)
{
$this->buffer->write($data);
$result = [];
while ($this->buffer->getRemainingBytes() > 0) {
$type = $this->buffer->readByte() >> 4;
try {
$packet = $this->factory->build($type);
} catch (UnknownPacketTypeException $e) {
$this->handleError($e);
continue;
}
$this->buffer->seek(-1);
$position = $this->buffer->getPosition();
try {
$packet->read($this->buffer);
$result[] = $packet;
$this->buffer->cut();
} catch (EndOfStreamException $e) {
$this->buffer->setPosition($position);
break;
} catch (MalformedPacketException $e) {
$this->handleError($e);
}
}
return $result;
}
/**
* Executes the registered error callback.
*
* @param \Throwable $exception
*/
private function handleError($exception)
{
if ($this->errorCallback !== null) {
$callback = $this->errorCallback;
$callback($exception);
}
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace BinSoul\Net\Mqtt;
/**
* Represents a subscription.
*/
interface Subscription
{
/**
* Returns the topic filter.
*
* @return string
*/
public function getFilter();
/**
* Returns the quality of service level.
*
* @return int
*/
public function getQosLevel();
/**
* Returns a new subscription with the given topic filter.
*
* @param string $filter
*
* @return self
*/
public function withFilter($filter);
/**
* Returns a new subscription with the given quality of service level.
*
* @param int $level
*
* @return self
*/
public function withQosLevel($level);
}

View File

@@ -0,0 +1,53 @@
<?php
namespace BinSoul\Net\Mqtt;
/**
* Matches a topic filter with an actual topic.
*
* @author Alin Eugen Deac <ade@vestergaardcompany.com>
*/
class TopicMatcher
{
/**
* Check if the given topic matches the filter.
*
* @param string $filter e.g. A/B/+, A/B/#
* @param string $topic e.g. A/B/C, A/B/foo/bar/baz
*
* @return bool true if topic matches the pattern
*/
public function matches($filter, $topic)
{
// Created by Steffen (https://github.com/kernelguy)
$tokens = explode('/', $filter);
$parts = [];
for ($i = 0, $count = count($tokens); $i < $count; ++$i) {
$token = $tokens[$i];
switch ($token) {
case '+':
$parts[] = '[^/#\+]*';
break;
case '#':
if ($i === 0) {
$parts[] = '[^\+\$]*';
} else {
$parts[] = '[^\+]*';
}
break;
default:
$parts[] = str_replace('+', '\+', $token);
break;
}
}
$regex = implode('/', $parts);
$regex = str_replace('$', '\$', $regex);
$regex = ';^'.$regex.'$;';
return preg_match($regex, $topic) === 1;
}
}

View File

@@ -0,0 +1,2 @@
/vendor/
/composer.lock

View File

@@ -0,0 +1,27 @@
language: php
php:
# - 5.3 # requires old distro, see below
- 5.4
- 5.5
- 5.6
- 7
- hhvm # ignore errors, see below
# lock distro so new future defaults will not break the build
dist: trusty
matrix:
include:
- php: 5.3
dist: precise
allow_failures:
- php: hhvm
sudo: false
install:
- composer install --no-interaction
script:
- vendor/bin/phpunit --coverage-text

View File

@@ -0,0 +1,103 @@
# Changelog
## 1.3.0 (2018-02-13)
* Feature: Support communication over Unix domain sockets (UDS)
(#20 by @clue)
```php
// new: now supports communication over Unix domain sockets (UDS)
$proxy = new ProxyConnector('http+unix:///tmp/proxy.sock', $connector);
```
* Reduce memory consumption by avoiding circular reference from stream reader
(#18 by @valga)
* Improve documentation
(#19 by @clue)
## 1.2.0 (2017-08-30)
* Feature: Use socket error codes for connection rejections
(#17 by @clue)
```php
$promise = $proxy->connect('imap.example.com:143');
$promise->then(null, function (Exeption $e) {
if ($e->getCode() === SOCKET_EACCES) {
echo 'Failed to authenticate with proxy!';
}
throw $e;
});
```
* Improve test suite by locking Travis distro so new defaults will not break the build and
optionally exclude tests that rely on working internet connection
(#15 and #16 by @clue)
## 1.1.0 (2017-06-11)
* Feature: Support proxy authentication if proxy URL contains username/password
(#14 by @clue)
```php
// new: username/password will now be passed to HTTP proxy server
$proxy = new ProxyConnector('user:pass@127.0.0.1:8080', $connector);
```
## 1.0.0 (2017-06-10)
* First stable release, now following SemVer
> Contains no other changes, so it's actually fully compatible with the v0.3.2 release.
## 0.3.2 (2017-06-10)
* Fix: Fix rejecting invalid URIs and unexpected URI schemes
(#13 by @clue)
* Fix HHVM build for now again and ignore future HHVM build errors
(#12 by @clue)
* Documentation for Connector concepts (TCP/TLS, timeouts, DNS resolution)
(#11 by @clue)
## 0.3.1 (2017-05-10)
* Feature: Forward compatibility with upcoming Socket v1.0 and v0.8
(#10 by @clue)
## 0.3.0 (2017-04-10)
* Feature / BC break: Replace deprecated SocketClient with new Socket component
(#9 by @clue)
This implies that the `ProxyConnector` from this package now implements the
`React\Socket\ConnectorInterface` instead of the legacy
`React\SocketClient\ConnectorInterface`.
## 0.2.0 (2017-04-10)
* Feature / BC break: Update SocketClient to v0.7 or v0.6 and
use `connect($uri)` instead of `create($host, $port)`
(#8 by @clue)
```php
// old
$connector->create($host, $port)->then(function (Stream $conn) {
$conn->write("…");
});
// new
$connector->connect($uri)->then(function (ConnectionInterface $conn) {
$conn->write("…");
});
```
* Improve test suite by adding PHPUnit to require-dev
(#7 by @clue)
## 0.1.0 (2016-11-01)
* First tagged release

View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2016 Christian Lück
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,422 @@
# clue/http-proxy-react [![Build Status](https://travis-ci.org/clue/php-http-proxy-react.svg?branch=master)](https://travis-ci.org/clue/php-http-proxy-react)
Async HTTP proxy connector, use any TCP/IP-based protocol through an HTTP
CONNECT proxy server, built on top of [ReactPHP](https://reactphp.org).
HTTP CONNECT proxy servers (also commonly known as "HTTPS proxy" or "SSL proxy")
are commonly used to tunnel HTTPS traffic through an intermediary ("proxy"), to
conceal the origin address (anonymity) or to circumvent address blocking
(geoblocking). While many (public) HTTP CONNECT proxy servers often limit this
to HTTPS port `443` only, this can technically be used to tunnel any
TCP/IP-based protocol (HTTP, SMTP, IMAP etc.).
This library provides a simple API to create these tunneled connection for you.
Because it implements ReactPHP's standard
[`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface),
it can simply be used in place of a normal connector.
This makes it fairly simple to add HTTP CONNECT proxy support to pretty much any
existing higher-level protocol implementation.
* **Async execution of connections** -
Send any number of HTTP CONNECT requests in parallel and process their
responses as soon as results come in.
The Promise-based design provides a *sane* interface to working with out of
bound responses and possible connection errors.
* **Standard interfaces** -
Allows easy integration with existing higher-level components by implementing
ReactPHP's standard
[`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface).
* **Lightweight, SOLID design** -
Provides a thin abstraction that is [*just good enough*](http://en.wikipedia.org/wiki/Principle_of_good_enough)
and does not get in your way.
Builds on top of well-tested components and well-established concepts instead of reinventing the wheel.
* **Good test coverage** -
Comes with an automated tests suite and is regularly tested against actual proxy servers in the wild
**Table of contents**
* [Quickstart example](#quickstart-example)
* [Usage](#usage)
* [ProxyConnector](#proxyconnector)
* [Plain TCP connections](#plain-tcp-connections)
* [Secure TLS connections](#secure-tls-connections)
* [Connection timeout](#connection-timeout)
* [DNS resolution](#dns-resolution)
* [Authentication](#authentication)
* [Advanced secure proxy connections](#advanced-secure-proxy-connections)
* [Advanced Unix domain sockets](#advanced-unix-domain-sockets)
* [Install](#install)
* [Tests](#tests)
* [License](#license)
* [More](#more)
### Quickstart example
The following example code demonstrates how this library can be used to send a
secure HTTPS request to google.com through a local HTTP proxy server:
```php
$loop = React\EventLoop\Factory::create();
$proxy = new ProxyConnector('127.0.0.1:8080', new Connector($loop));
$connector = new Connector($loop, array(
'tcp' => $proxy,
'timeout' => 3.0,
'dns' => false
));
$connector->connect('tls://google.com:443')->then(function (ConnectionInterface $stream) {
$stream->write("GET / HTTP/1.1\r\nHost: google.com\r\nConnection: close\r\n\r\n");
$stream->on('data', function ($chunk) {
echo $chunk;
});
}, 'printf');
$loop->run();
```
See also the [examples](examples).
## Usage
### ProxyConnector
The `ProxyConnector` is responsible for creating plain TCP/IP connections to
any destination by using an intermediary HTTP CONNECT proxy.
```
[you] -> [proxy] -> [destination]
```
Its constructor simply accepts an HTTP proxy URL and a connector used to connect
to the proxy server address:
```php
$connector = new Connector($loop);
$proxy = new ProxyConnector('http://127.0.0.1:8080', $connector);
```
The proxy URL may or may not contain a scheme and port definition. The default
port will be `80` for HTTP (or `443` for HTTPS), but many common HTTP proxy
servers use custom ports (often the alternative HTTP port `8080`).
In its most simple form, the given connector will be a
[`\React\Socket\Connector`](https://github.com/reactphp/socket#connector) if you
want to connect to a given IP address as above.
This is the main class in this package.
Because it implements ReactPHP's standard
[`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface),
it can simply be used in place of a normal connector.
Accordingly, it provides only a single public method, the
[`connect()`](https://github.com/reactphp/socket#connect) method.
The `connect(string $uri): PromiseInterface<ConnectionInterface, Exception>`
method can be used to establish a streaming connection.
It returns a [Promise](https://github.com/reactphp/promise) which either
fulfills with a [ConnectionInterface](https://github.com/reactphp/socket#connectioninterface)
on success or rejects with an `Exception` on error.
This makes it fairly simple to add HTTP CONNECT proxy support to pretty much any
higher-level component:
```diff
- $client = new SomeClient($connector);
+ $proxy = new ProxyConnector('http://127.0.0.1:8080', $connector);
+ $client = new SomeClient($proxy);
```
#### Plain TCP connections
HTTP CONNECT proxies are most frequently used to issue HTTPS requests to your destination.
However, this is actually performed on a higher protocol layer and this
connector is actually inherently a general-purpose plain TCP/IP connector.
As documented above, you can simply invoke its `connect()` method to establish
a streaming plain TCP/IP connection and use any higher level protocol like so:
```php
$proxy = new ProxyConnector('http://127.0.0.1:8080', $connector);
$proxy->connect('tcp://smtp.googlemail.com:587')->then(function (ConnectionInterface $stream) {
$stream->write("EHLO local\r\n");
$stream->on('data', function ($chunk) use ($stream) {
echo $chunk;
});
});
```
You can either use the `ProxyConnector` directly or you may want to wrap this connector
in ReactPHP's [`Connector`](https://github.com/reactphp/socket#connector):
```php
$connector = new Connector($loop, array(
'tcp' => $proxy,
'dns' => false
));
$connector->connect('tcp://smtp.googlemail.com:587')->then(function (ConnectionInterface $stream) {
$stream->write("EHLO local\r\n");
$stream->on('data', function ($chunk) use ($stream) {
echo $chunk;
});
});
```
Note that HTTP CONNECT proxies often restrict which ports one may connect to.
Many (public) proxy servers do in fact limit this to HTTPS (443) only.
#### Secure TLS connections
This class can also be used if you want to establish a secure TLS connection
(formerly known as SSL) between you and your destination, such as when using
secure HTTPS to your destination site. You can simply wrap this connector in
ReactPHP's [`Connector`](https://github.com/reactphp/socket#connector) or the
low-level [`SecureConnector`](https://github.com/reactphp/socket#secureconnector):
```php
$proxy = new ProxyConnector('http://127.0.0.1:8080', $connector);
$connector = new Connector($loop, array(
'tcp' => $proxy,
'dns' => false
));
$connector->connect('tls://smtp.googlemail.com:465')->then(function (ConnectionInterface $stream) {
$stream->write("EHLO local\r\n");
$stream->on('data', function ($chunk) use ($stream) {
echo $chunk;
});
});
```
> Note how secure TLS connections are in fact entirely handled outside of
this HTTP CONNECT client implementation.
#### Connection timeout
By default, the `ProxyConnector` does not implement any timeouts for establishing remote
connections.
Your underlying operating system may impose limits on pending and/or idle TCP/IP
connections, anywhere in a range of a few minutes to several hours.
Many use cases require more control over the timeout and likely values much
smaller, usually in the range of a few seconds only.
You can use ReactPHP's [`Connector`](https://github.com/reactphp/socket#connector)
or the low-level
[`TimeoutConnector`](https://github.com/reactphp/socket#timeoutconnector)
to decorate any given `ConnectorInterface` instance.
It provides the same `connect()` method, but will automatically reject the
underlying connection attempt if it takes too long:
```php
$connector = new Connector($loop, array(
'tcp' => $proxy,
'dns' => false,
'timeout' => 3.0
));
$connector->connect('tcp://google.com:80')->then(function ($stream) {
// connection succeeded within 3.0 seconds
});
```
See also any of the [examples](examples).
> Note how connection timeout is in fact entirely handled outside of this
HTTP CONNECT client implementation.
#### DNS resolution
By default, the `ProxyConnector` does not perform any DNS resolution at all and simply
forwards any hostname you're trying to connect to the remote proxy server.
The remote proxy server is thus responsible for looking up any hostnames via DNS
(this default mode is thus called *remote DNS resolution*).
As an alternative, you can also send the destination IP to the remote proxy
server.
In this mode you either have to stick to using IPs only (which is ofen unfeasable)
or perform any DNS lookups locally and only transmit the resolved destination IPs
(this mode is thus called *local DNS resolution*).
The default *remote DNS resolution* is useful if your local `ProxyConnector` either can
not resolve target hostnames because it has no direct access to the internet or
if it should not resolve target hostnames because its outgoing DNS traffic might
be intercepted.
As noted above, the `ProxyConnector` defaults to using remote DNS resolution.
However, wrapping the `ProxyConnector` in ReactPHP's
[`Connector`](https://github.com/reactphp/socket#connector) actually
performs local DNS resolution unless explicitly defined otherwise.
Given that remote DNS resolution is assumed to be the preferred mode, all
other examples explicitly disable DNS resoltion like this:
```php
$connector = new Connector($loop, array(
'tcp' => $proxy,
'dns' => false
));
```
If you want to explicitly use *local DNS resolution*, you can use the following code:
```php
// set up Connector which uses Google's public DNS (8.8.8.8)
$connector = Connector($loop, array(
'tcp' => $proxy,
'dns' => '8.8.8.8'
));
```
> Note how local DNS resolution is in fact entirely handled outside of this
HTTP CONNECT client implementation.
#### Authentication
If your HTTP proxy server requires authentication, you may pass the username and
password as part of the HTTP proxy URL like this:
```php
$proxy = new ProxyConnector('http://user:pass@127.0.0.1:8080', $connector);
```
Note that both the username and password must be percent-encoded if they contain
special characters:
```php
$user = 'he:llo';
$pass = 'p@ss';
$proxy = new ProxyConnector(
rawurlencode($user) . ':' . rawurlencode($pass) . '@127.0.0.1:8080',
$connector
);
```
> The authentication details will be used for basic authentication and will be
transferred in the `Proxy-Authorization` HTTP request header for each
connection attempt.
If the authentication details are missing or not accepted by the remote HTTP
proxy server, it is expected to reject each connection attempt with a
`407` (Proxy Authentication Required) response status code and an exception
error code of `SOCKET_EACCES` (13).
#### Advanced secure proxy connections
Note that communication between the client and the proxy is usually via an
unencrypted, plain TCP/IP HTTP connection. Note that this is the most common
setup, because you can still establish a TLS connection between you and the
destination host as above.
If you want to connect to a (rather rare) HTTPS proxy, you may want use the
`https://` scheme (HTTPS default port 443) and use ReactPHP's
[`Connector`](https://github.com/reactphp/socket#connector) or the low-level
[`SecureConnector`](https://github.com/reactphp/socket#secureconnector)
instance to create a secure connection to the proxy:
```php
$connector = new Connector($loop);
$proxy = new ProxyConnector('https://127.0.0.1:443', $connector);
$proxy->connect('tcp://smtp.googlemail.com:587');
```
#### Advanced Unix domain sockets
HTTP CONNECT proxy servers support forwarding TCP/IP based connections and
higher level protocols.
In some advanced cases, it may be useful to let your HTTP CONNECT proxy server
listen on a Unix domain socket (UDS) path instead of a IP:port combination.
For example, this allows you to rely on file system permissions instead of
having to rely on explicit [authentication](#authentication).
You can simply use the `http+unix://` URI scheme like this:
```php
$proxy = new ProxyConnector('http+unix:///tmp/proxy.sock', $connector);
$proxy->connect('tcp://google.com:80')->then(function (ConnectionInterface $stream) {
// connected…
});
```
Similarly, you can also combine this with [authentication](#authentication)
like this:
```php
$proxy = new ProxyConnector('http+unix://user:pass@/tmp/proxy.sock', $connector);
```
> Note that Unix domain sockets (UDS) are considered advanced usage and PHP only
has limited support for this.
In particular, enabling [secure TLS](#secure-tls-connections) may not be
supported.
> Note that the HTTP CONNECT protocol does not support the notion of UDS paths.
The above works reasonably well because UDS is only used for the connection between
client and proxy server and the path will not actually passed over the protocol.
This implies that this does not support connecting to UDS destination paths.
## Install
The recommended way to install this library is [through Composer](https://getcomposer.org).
[New to Composer?](https://getcomposer.org/doc/00-intro.md)
This project follows [SemVer](http://semver.org/).
This will install the latest supported version:
```bash
$ composer require clue/http-proxy-react:^1.3
```
See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades.
This project aims to run on any platform and thus does not require any PHP
extensions and supports running on legacy PHP 5.3 through current PHP 7+ and
HHVM.
It's *highly recommended to use PHP 7+* for this project.
## Tests
To run the test suite, you first need to clone this repo and then install all
dependencies [through Composer](https://getcomposer.org):
```bash
$ composer install
```
To run the test suite, go to the project root and run:
```bash
$ php vendor/bin/phpunit
```
The test suite contains tests that rely on a working internet connection,
alternatively you can also run it like this:
```bash
$ php vendor/bin/phpunit --exclude-group internet
```
## License
MIT
## More
* If you want to learn more about how the
[`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface)
and its usual implementations look like, refer to the documentation of the underlying
[react/socket](https://github.com/reactphp/socket) component.
* If you want to learn more about processing streams of data, refer to the
documentation of the underlying
[react/stream](https://github.com/reactphp/stream) component.
* As an alternative to an HTTP CONNECT proxy, you may also want to look into
using a SOCKS (SOCKS4/SOCKS5) proxy instead.
You may want to use [clue/socks-react](https://github.com/clue/php-socks-react)
which also provides an implementation of the same
[`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface)
so that supporting either proxy protocol should be fairly trivial.
* If you're dealing with public proxies, you'll likely have to work with mixed
quality and unreliable proxies. You may want to look into using
[clue/connection-manager-extra](https://github.com/clue/php-connection-manager-extra)
which allows retrying unreliable ones, implying connection timeouts,
concurrently working with multiple connectors and more.
* If you're looking for an end-user HTTP CONNECT proxy server daemon, you may
want to use [LeProxy](https://leproxy.org/).

View File

@@ -0,0 +1,30 @@
{
"name": "clue/http-proxy-react",
"description": "Async HTTP proxy connector, use any TCP/IP-based protocol through an HTTP CONNECT proxy server, built on top of ReactPHP",
"keywords": ["HTTP", "CONNECT", "proxy", "ReactPHP", "async"],
"homepage": "https://github.com/clue/php-http-proxy-react",
"license": "MIT",
"authors": [
{
"name": "Christian Lück",
"email": "christian@lueck.tv"
}
],
"autoload": {
"psr-4": { "Clue\\React\\HttpProxy\\": "src/" }
},
"autoload-dev": {
"psr-4": { "Tests\\Clue\\React\\HttpProxy\\": "tests/" }
},
"require": {
"php": ">=5.3",
"react/promise": " ^2.1 || ^1.2.1",
"react/socket": "^1.0 || ^0.8.4",
"ringcentral/psr7": "^1.2"
},
"require-dev": {
"phpunit/phpunit": "^5.0 || ^4.8",
"react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3",
"clue/block-react": "^1.1"
}
}

View File

@@ -0,0 +1,30 @@
<?php
// A simple example which requests https://google.com/ through an HTTP CONNECT proxy.
// The proxy can be given as first argument and defaults to localhost:8080 otherwise.
use Clue\React\HttpProxy\ProxyConnector;
use React\Socket\Connector;
use React\Socket\ConnectionInterface;
require __DIR__ . '/../vendor/autoload.php';
$url = isset($argv[1]) ? $argv[1] : '127.0.0.1:8080';
$loop = React\EventLoop\Factory::create();
$proxy = new ProxyConnector($url, new Connector($loop));
$connector = new Connector($loop, array(
'tcp' => $proxy,
'timeout' => 3.0,
'dns' => false
));
$connector->connect('tls://google.com:443')->then(function (ConnectionInterface $stream) {
$stream->write("GET / HTTP/1.1\r\nHost: google.com\r\nConnection: close\r\n\r\n");
$stream->on('data', function ($chunk) {
echo $chunk;
});
}, 'printf');
$loop->run();

View File

@@ -0,0 +1,37 @@
<?php
// A simple example which requests https://google.com/ either directly or through
// an HTTP CONNECT proxy.
// The Proxy can be given as first argument or does not use a proxy otherwise.
// This example highlights how changing from direct connection to using a proxy
// actually adds very little complexity and does not mess with your actual
// network protocol otherwise.
use Clue\React\HttpProxy\ProxyConnector;
use React\Socket\Connector;
use React\Socket\ConnectionInterface;
require __DIR__ . '/../vendor/autoload.php';
$loop = React\EventLoop\Factory::create();
$connector = new Connector($loop);
// first argument given? use this as the proxy URL
if (isset($argv[1])) {
$proxy = new ProxyConnector($argv[1], $connector);
$connector = new Connector($loop, array(
'tcp' => $proxy,
'timeout' => 3.0,
'dns' => false
));
}
$connector->connect('tls://google.com:443')->then(function (ConnectionInterface $stream) {
$stream->write("GET / HTTP/1.1\r\nHost: google.com\r\nConnection: close\r\n\r\n");
$stream->on('data', function ($chunk) {
echo $chunk;
});
}, 'printf');
$loop->run();

View File

@@ -0,0 +1,32 @@
<?php
// A simple example which uses a plain SMTP connection to Googlemail through a HTTP CONNECT proxy.
// Proxy can be given as first argument and defaults to localhost:8080 otherwise.
// Please note that MANY public proxies do not allow SMTP connections, YMMV.
use Clue\React\HttpProxy\ProxyConnector;
use React\Socket\Connector;
use React\Socket\ConnectionInterface;
require __DIR__ . '/../vendor/autoload.php';
$url = isset($argv[1]) ? $argv[1] : '127.0.0.1:8080';
$loop = React\EventLoop\Factory::create();
$proxy = new ProxyConnector($url, new Connector($loop));
$connector = new Connector($loop, array(
'tcp' => $proxy,
'timeout' => 3.0,
'dns' => false
));
$connector->connect('tcp://smtp.googlemail.com:587')->then(function (ConnectionInterface $stream) {
$stream->write("EHLO local\r\n");
$stream->on('data', function ($chunk) use ($stream) {
echo $chunk;
$stream->write("QUIT\r\n");
});
}, 'printf');
$loop->run();

View File

@@ -0,0 +1,35 @@
<?php
// A simple example which uses a secure SMTP connection to Googlemail through a HTTP CONNECT proxy.
// Proxy can be given as first argument and defaults to localhost:8080 otherwise.
// This example highlights how changing from plain connections (see previous
// example) to using a secure connection actually adds very little complexity
// and does not mess with your actual network protocol otherwise.
// Please note that MANY public proxies do not allow SMTP connections, YMMV.
use Clue\React\HttpProxy\ProxyConnector;
use React\Socket\Connector;
use React\Socket\ConnectionInterface;
require __DIR__ . '/../vendor/autoload.php';
$url = isset($argv[1]) ? $argv[1] : '127.0.0.1:8080';
$loop = React\EventLoop\Factory::create();
$proxy = new ProxyConnector($url, new Connector($loop));
$connector = new Connector($loop, array(
'tcp' => $proxy,
'timeout' => 3.0,
'dns' => false
));
$connector->connect('tls://smtp.googlemail.com:465')->then(function (ConnectionInterface $stream) {
$stream->write("EHLO local\r\n");
$stream->on('data', function ($chunk) use ($stream) {
echo $chunk;
$stream->write("QUIT\r\n");
});
}, 'printf');
$loop->run();

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php" colors="true">
<testsuites>
<testsuite>
<directory>./tests/</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory>./src/</directory>
</whitelist>
</filter>
</phpunit>

View File

@@ -0,0 +1,213 @@
<?php
namespace Clue\React\HttpProxy;
use Exception;
use InvalidArgumentException;
use RuntimeException;
use RingCentral\Psr7;
use React\Promise;
use React\Promise\Deferred;
use React\Socket\ConnectionInterface;
use React\Socket\ConnectorInterface;
use React\Socket\FixedUriConnector;
/**
* A simple Connector that uses an HTTP CONNECT proxy to create plain TCP/IP connections to any destination
*
* [you] -> [proxy] -> [destination]
*
* This is most frequently used to issue HTTPS requests to your destination.
* However, this is actually performed on a higher protocol layer and this
* connector is actually inherently a general-purpose plain TCP/IP connector.
*
* Note that HTTP CONNECT proxies often restrict which ports one may connect to.
* Many (public) proxy servers do in fact limit this to HTTPS (443) only.
*
* If you want to establish a TLS connection (such as HTTPS) between you and
* your destination, you may want to wrap this connector in a SecureConnector
* instance.
*
* Note that communication between the client and the proxy is usually via an
* unencrypted, plain TCP/IP HTTP connection. Note that this is the most common
* setup, because you can still establish a TLS connection between you and the
* destination host as above.
*
* If you want to connect to a (rather rare) HTTPS proxy, you may want use its
* HTTPS port (443) and use a SecureConnector instance to create a secure
* connection to the proxy.
*
* @link https://tools.ietf.org/html/rfc7231#section-4.3.6
*/
class ProxyConnector implements ConnectorInterface
{
private $connector;
private $proxyUri;
private $proxyAuth = '';
/**
* Instantiate a new ProxyConnector which uses the given $proxyUrl
*
* @param string $proxyUrl The proxy URL may or may not contain a scheme and
* port definition. The default port will be `80` for HTTP (or `443` for
* HTTPS), but many common HTTP proxy servers use custom ports.
* @param ConnectorInterface $connector In its most simple form, the given
* connector will be a \React\Socket\Connector if you want to connect to
* a given IP address.
* @throws InvalidArgumentException if the proxy URL is invalid
*/
public function __construct($proxyUrl, ConnectorInterface $connector)
{
// support `http+unix://` scheme for Unix domain socket (UDS) paths
if (preg_match('/^http\+unix:\/\/(.*?@)?(.+?)$/', $proxyUrl, $match)) {
// rewrite URI to parse authentication from dummy host
$proxyUrl = 'http://' . $match[1] . 'localhost';
// connector uses Unix transport scheme and explicit path given
$connector = new FixedUriConnector(
'unix://' . $match[2],
$connector
);
}
if (strpos($proxyUrl, '://') === false) {
$proxyUrl = 'http://' . $proxyUrl;
}
$parts = parse_url($proxyUrl);
if (!$parts || !isset($parts['scheme'], $parts['host']) || ($parts['scheme'] !== 'http' && $parts['scheme'] !== 'https')) {
throw new InvalidArgumentException('Invalid proxy URL "' . $proxyUrl . '"');
}
// apply default port and TCP/TLS transport for given scheme
if (!isset($parts['port'])) {
$parts['port'] = $parts['scheme'] === 'https' ? 443 : 80;
}
$parts['scheme'] = $parts['scheme'] === 'https' ? 'tls' : 'tcp';
$this->connector = $connector;
$this->proxyUri = $parts['scheme'] . '://' . $parts['host'] . ':' . $parts['port'];
// prepare Proxy-Authorization header if URI contains username/password
if (isset($parts['user']) || isset($parts['pass'])) {
$this->proxyAuth = 'Proxy-Authorization: Basic ' . base64_encode(
rawurldecode($parts['user'] . ':' . (isset($parts['pass']) ? $parts['pass'] : ''))
) . "\r\n";
}
}
public function connect($uri)
{
if (strpos($uri, '://') === false) {
$uri = 'tcp://' . $uri;
}
$parts = parse_url($uri);
if (!$parts || !isset($parts['scheme'], $parts['host'], $parts['port']) || $parts['scheme'] !== 'tcp') {
return Promise\reject(new InvalidArgumentException('Invalid target URI specified'));
}
$host = trim($parts['host'], '[]');
$port = $parts['port'];
// construct URI to HTTP CONNECT proxy server to connect to
$proxyUri = $this->proxyUri;
// append path from URI if given
if (isset($parts['path'])) {
$proxyUri .= $parts['path'];
}
// parse query args
$args = array();
if (isset($parts['query'])) {
parse_str($parts['query'], $args);
}
// append hostname from URI to query string unless explicitly given
if (!isset($args['hostname'])) {
$args['hostname'] = $parts['host'];
}
// append query string
$proxyUri .= '?' . http_build_query($args, '', '&');;
// append fragment from URI if given
if (isset($parts['fragment'])) {
$proxyUri .= '#' . $parts['fragment'];
}
$auth = $this->proxyAuth;
return $this->connector->connect($proxyUri)->then(function (ConnectionInterface $stream) use ($host, $port, $auth) {
$deferred = new Deferred(function ($_, $reject) use ($stream) {
$reject(new RuntimeException('Connection canceled while waiting for response from proxy (ECONNABORTED)', defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103));
$stream->close();
});
// keep buffering data until headers are complete
$buffer = '';
$fn = function ($chunk) use (&$buffer, $deferred, $stream) {
$buffer .= $chunk;
$pos = strpos($buffer, "\r\n\r\n");
if ($pos !== false) {
// try to parse headers as response message
try {
$response = Psr7\parse_response(substr($buffer, 0, $pos));
} catch (Exception $e) {
$deferred->reject(new RuntimeException('Invalid response received from proxy (EBADMSG)', defined('SOCKET_EBADMSG') ? SOCKET_EBADMSG: 71, $e));
$stream->close();
return;
}
if ($response->getStatusCode() === 407) {
// map status code 407 (Proxy Authentication Required) to EACCES
$deferred->reject(new RuntimeException('Proxy denied connection due to invalid authentication ' . $response->getStatusCode() . ' (' . $response->getReasonPhrase() . ') (EACCES)', defined('SOCKET_EACCES') ? SOCKET_EACCES : 13));
return $stream->close();
} elseif ($response->getStatusCode() < 200 || $response->getStatusCode() >= 300) {
// map non-2xx status code to ECONNREFUSED
$deferred->reject(new RuntimeException('Proxy refused connection with HTTP error code ' . $response->getStatusCode() . ' (' . $response->getReasonPhrase() . ') (ECONNREFUSED)', defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111));
return $stream->close();
}
// all okay, resolve with stream instance
$deferred->resolve($stream);
// emit remaining incoming as data event
$buffer = (string)substr($buffer, $pos + 4);
if ($buffer !== '') {
$stream->emit('data', array($buffer));
$buffer = '';
}
return;
}
// stop buffering when 8 KiB have been read
if (isset($buffer[8192])) {
$deferred->reject(new RuntimeException('Proxy must not send more than 8 KiB of headers (EMSGSIZE)', defined('SOCKET_EMSGSIZE') ? SOCKET_EMSGSIZE : 90));
$stream->close();
}
};
$stream->on('data', $fn);
$stream->on('error', function (Exception $e) use ($deferred) {
$deferred->reject(new RuntimeException('Stream error while waiting for response from proxy (EIO)', defined('SOCKET_EIO') ? SOCKET_EIO : 5, $e));
});
$stream->on('close', function () use ($deferred) {
$deferred->reject(new RuntimeException('Connection to proxy lost while waiting for response (ECONNRESET)', defined('SOCKET_ECONNRESET') ? SOCKET_ECONNRESET : 104));
});
$stream->write("CONNECT " . $host . ":" . $port . " HTTP/1.1\r\nHost: " . $host . ":" . $port . "\r\n" . $auth . "\r\n");
return $deferred->promise()->then(function (ConnectionInterface $stream) use ($fn) {
// Stop buffering when connection has been established.
$stream->removeListener('data', $fn);
return new Promise\FulfilledPromise($stream);
});
}, function (Exception $e) use ($proxyUri) {
throw new RuntimeException('Unable to connect to proxy (ECONNREFUSED)', defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111, $e);
});
}
}

View File

@@ -0,0 +1,80 @@
<?php
namespace Tests\Clue\React\HttpProxy;
use PHPUnit_Framework_TestCase;
abstract class AbstractTestCase extends PHPUnit_Framework_TestCase
{
protected function expectCallableNever()
{
$mock = $this->createCallableMock();
$mock
->expects($this->never())
->method('__invoke');
return $mock;
}
protected function expectCallableOnce()
{
$mock = $this->createCallableMock();
$mock
->expects($this->once())
->method('__invoke');
return $mock;
}
protected function expectCallableOnceWith($value)
{
$mock = $this->createCallableMock();
$mock
->expects($this->once())
->method('__invoke')
->with($this->equalTo($value));
return $mock;
}
protected function expectCallableOnceWithExceptionCode($code)
{
$mock = $this->createCallableMock();
$mock
->expects($this->once())
->method('__invoke')
->with($this->callback(function ($e) use ($code) {
return $e->getCode() === $code;
}));
return $mock;
}
protected function expectCallableOnceParameter($type)
{
$mock = $this->createCallableMock();
$mock
->expects($this->once())
->method('__invoke')
->with($this->isInstanceOf($type));
return $mock;
}
/**
* @link https://github.com/reactphp/react/blob/master/tests/React/Tests/Socket/TestCase.php (taken from reactphp/react)
*/
protected function createCallableMock()
{
return $this->getMockBuilder('Tests\\Clue\\React\\HttpProxy\\CallableStub')->getMock();
}
}
class CallableStub
{
public function __invoke()
{
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace Tests\Clue\React\HttpProxy;
use React\EventLoop\Factory;
use Clue\React\HttpProxy\ProxyConnector;
use React\Socket\TcpConnector;
use React\Socket\DnsConnector;
use Clue\React\Block;
use React\Socket\SecureConnector;
/** @group internet */
class FunctionalTest extends AbstractTestCase
{
private $loop;
private $tcpConnector;
private $dnsConnector;
public function setUp()
{
$this->loop = Factory::create();
$this->tcpConnector = new TcpConnector($this->loop);
$f = new \React\Dns\Resolver\Factory();
$resolver = $f->create('8.8.8.8', $this->loop);
$this->dnsConnector = new DnsConnector($this->tcpConnector, $resolver);
}
public function testNonListeningSocketRejectsConnection()
{
$proxy = new ProxyConnector('127.0.0.1:9999', $this->dnsConnector);
$promise = $proxy->connect('google.com:80');
$this->setExpectedException('RuntimeException', 'Unable to connect to proxy', SOCKET_ECONNREFUSED);
Block\await($promise, $this->loop, 3.0);
}
public function testPlainGoogleDoesNotAcceptConnectMethod()
{
$proxy = new ProxyConnector('google.com', $this->dnsConnector);
$promise = $proxy->connect('google.com:80');
$this->setExpectedException('RuntimeException', '405 (Method Not Allowed)', SOCKET_ECONNREFUSED);
Block\await($promise, $this->loop, 3.0);
}
public function testSecureGoogleDoesNotAcceptConnectMethod()
{
if (!function_exists('stream_socket_enable_crypto')) {
$this->markTestSkipped('TLS not supported on really old platforms (HHVM < 3.8)');
}
$secure = new SecureConnector($this->dnsConnector, $this->loop);
$proxy = new ProxyConnector('https://google.com:443', $secure);
$promise = $proxy->connect('google.com:80');
$this->setExpectedException('RuntimeException', '405 (Method Not Allowed)', SOCKET_ECONNREFUSED);
Block\await($promise, $this->loop, 3.0);
}
public function testSecureGoogleDoesNotAcceptPlainStream()
{
$proxy = new ProxyConnector('google.com:443', $this->dnsConnector);
$promise = $proxy->connect('google.com:80');
$this->setExpectedException('RuntimeException', 'Connection to proxy lost', SOCKET_ECONNRESET);
Block\await($promise, $this->loop, 3.0);
}
}

View File

@@ -0,0 +1,333 @@
<?php
namespace Tests\Clue\React\HttpProxy;
use Clue\React\HttpProxy\ProxyConnector;
use React\Promise\Promise;
use React\Socket\ConnectionInterface;
class ProxyConnectorTest extends AbstractTestCase
{
private $connector;
public function setUp()
{
$this->connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock();
}
/**
* @expectedException InvalidArgumentException
*/
public function testInvalidProxy()
{
new ProxyConnector('///', $this->connector);
}
/**
* @expectedException InvalidArgumentException
*/
public function testInvalidProxyScheme()
{
new ProxyConnector('ftp://example.com', $this->connector);
}
/**
* @expectedException InvalidArgumentException
*/
public function testInvalidHttpsUnixScheme()
{
new ProxyConnector('https+unix:///tmp/proxy.sock', $this->connector);
}
public function testCreatesConnectionToHttpPort()
{
$promise = new Promise(function () { });
$this->connector->expects($this->once())->method('connect')->with('tcp://proxy.example.com:80?hostname=google.com')->willReturn($promise);
$proxy = new ProxyConnector('proxy.example.com', $this->connector);
$proxy->connect('google.com:80');
}
public function testCreatesConnectionToHttpPortAndPassesThroughUriComponents()
{
$promise = new Promise(function () { });
$this->connector->expects($this->once())->method('connect')->with('tcp://proxy.example.com:80/path?foo=bar&hostname=google.com#segment')->willReturn($promise);
$proxy = new ProxyConnector('proxy.example.com', $this->connector);
$proxy->connect('google.com:80/path?foo=bar#segment');
}
public function testCreatesConnectionToHttpPortAndObeysExplicitHostname()
{
$promise = new Promise(function () { });
$this->connector->expects($this->once())->method('connect')->with('tcp://proxy.example.com:80?hostname=www.google.com')->willReturn($promise);
$proxy = new ProxyConnector('proxy.example.com', $this->connector);
$proxy->connect('google.com:80?hostname=www.google.com');
}
public function testCreatesConnectionToHttpsPort()
{
$promise = new Promise(function () { });
$this->connector->expects($this->once())->method('connect')->with('tls://proxy.example.com:443?hostname=google.com')->willReturn($promise);
$proxy = new ProxyConnector('https://proxy.example.com', $this->connector);
$proxy->connect('google.com:80');
}
public function testCreatesConnectionToUnixPath()
{
$promise = new Promise(function () { });
$this->connector->expects($this->once())->method('connect')->with('unix:///tmp/proxy.sock')->willReturn($promise);
$proxy = new ProxyConnector('http+unix:///tmp/proxy.sock', $this->connector);
$proxy->connect('google.com:80');
}
public function testCancelPromiseWillCancelPendingConnection()
{
$promise = new Promise(function () { }, $this->expectCallableOnce());
$this->connector->expects($this->once())->method('connect')->willReturn($promise);
$proxy = new ProxyConnector('proxy.example.com', $this->connector);
$promise = $proxy->connect('google.com:80');
$this->assertInstanceOf('React\Promise\CancellablePromiseInterface', $promise);
$promise->cancel();
}
public function testWillWriteToOpenConnection()
{
$stream = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('close', 'write'))->getMock();
$stream->expects($this->once())->method('write')->with("CONNECT google.com:80 HTTP/1.1\r\nHost: google.com:80\r\n\r\n");
$promise = \React\Promise\resolve($stream);
$this->connector->expects($this->once())->method('connect')->willReturn($promise);
$proxy = new ProxyConnector('proxy.example.com', $this->connector);
$proxy->connect('google.com:80');
}
public function testWillProxyAuthorizationHeaderIfProxyUriContainsAuthentication()
{
$stream = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('close', 'write'))->getMock();
$stream->expects($this->once())->method('write')->with("CONNECT google.com:80 HTTP/1.1\r\nHost: google.com:80\r\nProxy-Authorization: Basic dXNlcjpwYXNz\r\n\r\n");
$promise = \React\Promise\resolve($stream);
$this->connector->expects($this->once())->method('connect')->willReturn($promise);
$proxy = new ProxyConnector('user:pass@proxy.example.com', $this->connector);
$proxy->connect('google.com:80');
}
public function testWillProxyAuthorizationHeaderIfProxyUriContainsOnlyUsernameWithoutPassword()
{
$stream = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('close', 'write'))->getMock();
$stream->expects($this->once())->method('write')->with("CONNECT google.com:80 HTTP/1.1\r\nHost: google.com:80\r\nProxy-Authorization: Basic dXNlcjo=\r\n\r\n");
$promise = \React\Promise\resolve($stream);
$this->connector->expects($this->once())->method('connect')->willReturn($promise);
$proxy = new ProxyConnector('user@proxy.example.com', $this->connector);
$proxy->connect('google.com:80');
}
public function testWillProxyAuthorizationHeaderIfProxyUriContainsAuthenticationWithPercentEncoding()
{
$user = 'h@llÖ';
$pass = '%secret?';
$stream = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('close', 'write'))->getMock();
$stream->expects($this->once())->method('write')->with("CONNECT google.com:80 HTTP/1.1\r\nHost: google.com:80\r\nProxy-Authorization: Basic " . base64_encode($user . ':' . $pass) . "\r\n\r\n");
$promise = \React\Promise\resolve($stream);
$this->connector->expects($this->once())->method('connect')->willReturn($promise);
$proxy = new ProxyConnector(rawurlencode($user) . ':' . rawurlencode($pass) . '@proxy.example.com', $this->connector);
$proxy->connect('google.com:80');
}
public function testWillProxyAuthorizationHeaderIfUnixProxyUriContainsAuthentication()
{
$stream = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('close', 'write'))->getMock();
$stream->expects($this->once())->method('write')->with("CONNECT google.com:80 HTTP/1.1\r\nHost: google.com:80\r\nProxy-Authorization: Basic dXNlcjpwYXNz\r\n\r\n");
$promise = \React\Promise\resolve($stream);
$this->connector->expects($this->once())->method('connect')->with('unix:///tmp/proxy.sock')->willReturn($promise);
$proxy = new ProxyConnector('http+unix://user:pass@/tmp/proxy.sock', $this->connector);
$proxy->connect('google.com:80');
}
public function testRejectsInvalidUri()
{
$this->connector->expects($this->never())->method('connect');
$proxy = new ProxyConnector('proxy.example.com', $this->connector);
$promise = $proxy->connect('///');
$promise->then(null, $this->expectCallableOnce());
}
public function testRejectsUriWithNonTcpScheme()
{
$this->connector->expects($this->never())->method('connect');
$proxy = new ProxyConnector('proxy.example.com', $this->connector);
$promise = $proxy->connect('tls://google.com:80');
$promise->then(null, $this->expectCallableOnce());
}
public function testRejectsIfConnectorRejects()
{
$promise = \React\Promise\reject(new \RuntimeException());
$this->connector->expects($this->once())->method('connect')->willReturn($promise);
$proxy = new ProxyConnector('proxy.example.com', $this->connector);
$promise = $proxy->connect('google.com:80');
$promise->then(null, $this->expectCallableOnce());
}
public function testRejectsAndClosesIfStreamWritesNonHttp()
{
$stream = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('close', 'write'))->getMock();
$promise = \React\Promise\resolve($stream);
$this->connector->expects($this->once())->method('connect')->willReturn($promise);
$proxy = new ProxyConnector('proxy.example.com', $this->connector);
$promise = $proxy->connect('google.com:80');
$stream->expects($this->once())->method('close');
$stream->emit('data', array("invalid\r\n\r\n"));
$promise->then(null, $this->expectCallableOnceWithExceptionCode(SOCKET_EBADMSG));
}
public function testRejectsAndClosesIfStreamWritesTooMuchData()
{
$stream = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('close', 'write'))->getMock();
$promise = \React\Promise\resolve($stream);
$this->connector->expects($this->once())->method('connect')->willReturn($promise);
$proxy = new ProxyConnector('proxy.example.com', $this->connector);
$promise = $proxy->connect('google.com:80');
$stream->expects($this->once())->method('close');
$stream->emit('data', array(str_repeat('*', 100000)));
$promise->then(null, $this->expectCallableOnceWithExceptionCode(SOCKET_EMSGSIZE));
}
public function testRejectsAndClosesIfStreamReturnsProyAuthenticationRequired()
{
$stream = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('close', 'write'))->getMock();
$promise = \React\Promise\resolve($stream);
$this->connector->expects($this->once())->method('connect')->willReturn($promise);
$proxy = new ProxyConnector('proxy.example.com', $this->connector);
$promise = $proxy->connect('google.com:80');
$stream->expects($this->once())->method('close');
$stream->emit('data', array("HTTP/1.1 407 Proxy Authentication Required\r\n\r\n"));
$promise->then(null, $this->expectCallableOnceWithExceptionCode(SOCKET_EACCES));
}
public function testRejectsAndClosesIfStreamReturnsNonSuccess()
{
$stream = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('close', 'write'))->getMock();
$promise = \React\Promise\resolve($stream);
$this->connector->expects($this->once())->method('connect')->willReturn($promise);
$proxy = new ProxyConnector('proxy.example.com', $this->connector);
$promise = $proxy->connect('google.com:80');
$stream->expects($this->once())->method('close');
$stream->emit('data', array("HTTP/1.1 403 Not allowed\r\n\r\n"));
$promise->then(null, $this->expectCallableOnceWithExceptionCode(SOCKET_ECONNREFUSED));
}
public function testResolvesIfStreamReturnsSuccess()
{
$stream = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('close', 'write'))->getMock();
$promise = \React\Promise\resolve($stream);
$this->connector->expects($this->once())->method('connect')->willReturn($promise);
$proxy = new ProxyConnector('proxy.example.com', $this->connector);
$promise = $proxy->connect('google.com:80');
$promise->then($this->expectCallableOnce('React\Stream\Stream'));
$never = $this->expectCallableNever();
$promise->then(function (ConnectionInterface $stream) use ($never) {
$stream->on('data', $never);
});
$stream->emit('data', array("HTTP/1.1 200 OK\r\n\r\n"));
}
public function testResolvesIfStreamReturnsSuccessAndEmitsExcessiveData()
{
$stream = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('close', 'write'))->getMock();
$promise = \React\Promise\resolve($stream);
$this->connector->expects($this->once())->method('connect')->willReturn($promise);
$proxy = new ProxyConnector('proxy.example.com', $this->connector);
$promise = $proxy->connect('google.com:80');
$once = $this->expectCallableOnceWith('hello!');
$promise->then(function (ConnectionInterface $stream) use ($once) {
$stream->on('data', $once);
});
$stream->emit('data', array("HTTP/1.1 200 OK\r\n\r\nhello!"));
}
public function testCancelPromiseWillCloseOpenConnectionAndReject()
{
$stream = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('close', 'write'))->getMock();
$stream->expects($this->once())->method('close');
$promise = \React\Promise\resolve($stream);
$this->connector->expects($this->once())->method('connect')->willReturn($promise);
$proxy = new ProxyConnector('proxy.example.com', $this->connector);
$promise = $proxy->connect('google.com:80');
$this->assertInstanceOf('React\Promise\CancellablePromiseInterface', $promise);
$promise->cancel();
$promise->then(null, $this->expectCallableOnceWithExceptionCode(SOCKET_ECONNABORTED));
}
}

View File

@@ -0,0 +1,2 @@
/vendor
/composer.lock

27
instafeed/vendor/clue/socks-react/.travis.yml vendored Executable file
View File

@@ -0,0 +1,27 @@
language: php
php:
# - 5.3 # requires old distro, see below
- 5.4
- 5.5
- 5.6
- 7
- hhvm # ignore errors, see below
# lock distro so new future defaults will not break the build
dist: trusty
matrix:
include:
- php: 5.3
dist: precise
allow_failures:
- php: hhvm
sudo: false
install:
- composer install --no-interaction
script:
- vendor/bin/phpunit --coverage-text

338
instafeed/vendor/clue/socks-react/CHANGELOG.md vendored Executable file
View File

@@ -0,0 +1,338 @@
# Changelog
## 0.8.7 (2017-12-17)
* Feature: Support SOCKS over TLS (`sockss://` URI scheme)
(#70 and #71 by @clue)
```php
// new: now supports SOCKS over TLS
$client = new Client('socks5s://localhost', $connector);
```
* Feature: Support communication over Unix domain sockets (UDS)
(#69 by @clue)
```php
// new: now supports SOCKS over Unix domain sockets (UDS)
$client = new Client('socks5+unix:///tmp/proxy.sock', $connector);
```
* Improve test suite by adding forward compatibility with PHPUnit 6
(#68 by @clue)
## 0.8.6 (2017-09-17)
* Feature: Forward compatibility with Evenement v3.0
(#67 by @WyriHaximus)
## 0.8.5 (2017-09-01)
* Feature: Use socket error codes for connection rejections
(#63 by @clue)
```php
$promise = $proxy->connect('imap.example.com:143');
$promise->then(null, function (Exeption $e) {
if ($e->getCode() === SOCKET_EACCES) {
echo 'Failed to authenticate with proxy!';
}
throw $e;
});
```
* Feature: Report matching SOCKS5 error codes for server side connection errors
(#62 by @clue)
* Fix: Fix SOCKS5 client receiving destination hostnames and
fix IPv6 addresses as hostnames for TLS certificates
(#64 and #65 by @clue)
* Improve test suite by locking Travis distro so new defaults will not break the build and
optionally exclude tests that rely on working internet connection
(#61 and #66 by @clue)
## 0.8.4 (2017-07-27)
* Feature: Server now passes client source address to Connector
(#60 by @clue)
## 0.8.3 (2017-07-18)
* Feature: Pass full remote URI as parameter to authentication callback
(#58 by @clue)
```php
// new third parameter passed to authentication callback
$server->setAuth(function ($user, $pass, $remote) {
$ip = parse_url($remote, PHP_URL_HOST);
return ($ip === '127.0.0.1');
});
```
* Fix: Fix connecting to IPv6 address via SOCKS5 server and validate target
URI so hostname can not contain excessive URI components
(#59 by @clue)
* Improve test suite by fixing HHVM build for now again and ignore future HHVM build errors
(#57 by @clue)
## 0.8.2 (2017-05-09)
* Feature: Forward compatibility with upcoming Socket v1.0 and v0.8
(#56 by @clue)
## 0.8.1 (2017-04-21)
* Update examples to use URIs with default port 1080 and accept proxy URI arguments
(#54 by @clue)
* Remove now unneeded dependency on `react/stream`
(#55 by @clue)
## 0.8.0 (2017-04-18)
* Feature: Merge `Server` class from clue/socks-server
(#52 by @clue)
```php
$socket = new React\Socket\Server(1080, $loop);
$server = new Clue\React\Socks\Server($loop, $socket);
```
> Upgrading from [clue/socks-server](https://github.com/clue/php-socks-server)?
The classes have been moved as-is, so you can simply start using the new
class name `Clue\React\Socks\Server` with no other changes required.
## 0.7.0 (2017-04-14)
* Feature / BC break: Replace depreacted SocketClient with Socket v0.7 and
use `connect($uri)` instead of `create($host, $port)`
(#51 by @clue)
```php
// old
$connector = new React\SocketClient\TcpConnector($loop);
$client = new Client(1080, $connector);
$client->create('google.com', 80)->then(function (Stream $conn) {
$conn->write("…");
});
// new
$connector = new React\Socket\TcpConnector($loop);
$client = new Client(1080, $connector);
$client->connect('google.com:80')->then(function (ConnectionInterface $conn) {
$conn->write("…");
});
```
* Improve test suite by adding PHPUnit to require-dev
(#50 by @clue)
## 0.6.0 (2016-11-29)
* Feature / BC break: Pass connector into `Client` instead of loop, remove unneeded deps
(#49 by @clue)
```php
// old (connector is create implicitly)
$client = new Client('127.0.0.1', $loop);
// old (connector can optionally be passed)
$client = new Client('127.0.0.1', $loop, $connector);
// new (connector is now mandatory)
$connector = new React\SocketClient\TcpConnector($loop);
$client = new Client('127.0.0.1', $connector);
```
* Feature / BC break: `Client` now implements `ConnectorInterface`, remove `Connector` adapter
(#47 by @clue)
```php
// old (explicit connector functions as an adapter)
$connector = $client->createConnector();
$promise = $connector->create('google.com', 80);
// new (client can be used as connector right away)
$promise = $client->create('google.com', 80);
```
* Feature / BC break: Remove `createSecureConnector()`, use `SecureConnector` instead
(#47 by @clue)
```php
// old (tight coupling and hidden dependency)
$tls = $client->createSecureConnector();
$promise = $tls->create('google.com', 443);
// new (more explicit, loose coupling)
$tls = new React\SocketClient\SecureConnector($client, $loop);
$promise = $tls->create('google.com', 443);
```
* Feature / BC break: Remove `setResolveLocal()` and local DNS resolution and default to remote DNS resolution, use `DnsConnector` instead
(#44 by @clue)
```php
// old (implicitly defaults to true, can be disabled)
$client->setResolveLocal(false);
$tcp = $client->createConnector();
$promise = $tcp->create('google.com', 80);
// new (always disabled, can be re-enabled like this)
$factory = new React\Dns\Resolver\Factory();
$resolver = $factory->createCached('8.8.8.8', $loop);
$tcp = new React\SocketClient\DnsConnector($client, $resolver);
$promise = $tcp->create('google.com', 80);
```
* Feature / BC break: Remove `setTimeout()`, use `TimeoutConnector` instead
(#45 by @clue)
```php
// old (timeout only applies to TCP/IP connection)
$client = new Client('127.0.0.1', …);
$client->setTimeout(3.0);
$tcp = $client->createConnector();
$promise = $tcp->create('google.com', 80);
// new (timeout can be added to any layer)
$client = new Client('127.0.0.1', …);
$tcp = new React\SocketClient\TimeoutConnector($client, 3.0, $loop);
$promise = $tcp->create('google.com', 80);
```
* Feature / BC break: Remove `setProtocolVersion()` and `setAuth()` mutators, only support SOCKS URI for protocol version and authentication (immutable API)
(#46 by @clue)
```php
// old (state can be mutated after instantiation)
$client = new Client('127.0.0.1', …);
$client->setProtocolVersion('5');
$client->setAuth('user', 'pass');
// new (immutable after construction, already supported as of v0.5.2 - now mandatory)
$client = new Client('socks5://user:pass@127.0.0.1', …);
```
## 0.5.2 (2016-11-25)
* Feature: Apply protocol version and username/password auth from SOCKS URI
(#43 by @clue)
```php
// explicitly use SOCKS5
$client = new Client('socks5://127.0.0.1', $loop);
// use authentication (automatically SOCKS5)
$client = new Client('user:pass@127.0.0.1', $loop);
```
* More explicit client examples, including proxy chaining
(#42 by @clue)
## 0.5.1 (2016-11-21)
* Feature: Support Promise cancellation
(#39 by @clue)
```php
$promise = $connector->create($host, $port);
$promise->cancel();
```
* Feature: Timeout now cancels pending connection attempt
(#39, #22 by @clue)
## 0.5.0 (2016-11-07)
* Remove / BC break: Split off Server to clue/socks-server
(#35 by @clue)
> Upgrading? Check [clue/socks-server](https://github.com/clue/php-socks-server) for details.
* Improve documentation and project structure
## 0.4.0 (2016-03-19)
* Feature: Support proper SSL/TLS connections with additional SSL context options
(#31, #33 by @clue)
* Documentation for advanced Connector setups (bindto, multihop)
(#32 by @clue)
## 0.3.0 (2015-06-20)
* BC break / Feature: Client ctor now accepts a SOCKS server URI
([#24](https://github.com/clue/php-socks-react/pull/24))
```php
// old
$client = new Client($loop, 'localhost', 9050);
// new
$client = new Client('localhost:9050', $loop);
```
* Feature: Automatically assume default SOCKS port (1080) if not given explicitly
([#26](https://github.com/clue/php-socks-react/pull/26))
* Improve documentation and test suite
## 0.2.1 (2014-11-13)
* Support React PHP v0.4 (while preserving BC with React PHP v0.3)
([#16](https://github.com/clue/php-socks-react/pull/16))
* Improve examples and add first class support for HHVM
([#15](https://github.com/clue/php-socks-react/pull/15) and [#17](https://github.com/clue/php-socks-react/pull/17))
## 0.2.0 (2014-09-27)
* BC break / Feature: Simplify constructors by making parameters optional.
([#10](https://github.com/clue/php-socks-react/pull/10))
The `Factory` has been removed, you can now create instances of the `Client`
and `Server` yourself:
```php
// old
$factory = new Factory($loop, $dns);
$client = $factory->createClient('localhost', 9050);
$server = $factory->createSever($socket);
// new
$client = new Client($loop, 'localhost', 9050);
$server = new Server($loop, $socket);
```
* BC break: Remove HTTP support and link to [clue/buzz-react](https://github.com/clue/php-buzz-react) instead.
([#9](https://github.com/clue/php-socks-react/pull/9))
HTTP operates on a different layer than this low-level SOCKS library.
Removing this reduces the footprint of this library.
> Upgrading? Check the [README](https://github.com/clue/php-socks-react#http-requests) for details.
* Fix: Refactored to support other, faster loops (libev/libevent)
([#12](https://github.com/clue/php-socks-react/pull/12))
* Explicitly list dependencies, clean up examples and extend test suite significantly
## 0.1.0 (2014-05-19)
* First stable release
* Async SOCKS `Client` and `Server` implementation
* Project was originally part of [clue/socks](https://github.com/clue/php-socks)
and was split off from its latest releave v0.4.0
([#1](https://github.com/clue/reactphp-socks/issues/1))
> Upgrading from clue/socks v0.4.0? Use namespace `Clue\React\Socks` instead of `Socks` and you're ready to go!
## 0.0.0 (2011-04-26)
* Initial concept, originally tracked as part of
[clue/socks](https://github.com/clue/php-socks)

21
instafeed/vendor/clue/socks-react/LICENSE vendored Executable file
View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2011 Christian Lück
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

1017
instafeed/vendor/clue/socks-react/README.md vendored Executable file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,28 @@
{
"name": "clue/socks-react",
"description": "Async SOCKS4, SOCKS4a and SOCKS5 proxy client and server implementation, built on top of ReactPHP",
"keywords": ["socks client", "socks server", "proxy", "tcp tunnel", "socks protocol", "async", "ReactPHP"],
"homepage": "https://github.com/clue/php-socks-react",
"license": "MIT",
"authors": [
{
"name": "Christian Lück",
"email": "christian@lueck.tv"
}
],
"autoload": {
"psr-4": {"Clue\\React\\Socks\\": "src/"}
},
"require": {
"php": ">=5.3",
"react/socket": "^1.0 || ^0.8.6",
"react/promise": "^2.1 || ^1.2",
"evenement/evenement": "~3.0|~1.0|~2.0"
},
"require-dev": {
"phpunit/phpunit": "^6.0 || ^5.7 || ^4.8.35",
"react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3",
"clue/connection-manager-extra": "^1.0 || ^0.7",
"clue/block-react": "^1.1"
}
}

View File

@@ -0,0 +1,30 @@
<?php
use Clue\React\Socks\Client;
use React\Socket\Connector;
use React\Socket\ConnectionInterface;
require __DIR__ . '/../vendor/autoload.php';
$proxy = isset($argv[1]) ? $argv[1] : '127.0.0.1:1080';
$loop = React\EventLoop\Factory::create();
$client = new Client($proxy, new Connector($loop));
$connector = new Connector($loop, array(
'tcp' => $client,
'timeout' => 3.0,
'dns' => false
));
echo 'Demo SOCKS client connecting to SOCKS server ' . $proxy . PHP_EOL;
$connector->connect('tcp://www.google.com:80')->then(function (ConnectionInterface $stream) {
echo 'connected' . PHP_EOL;
$stream->write("GET / HTTP/1.0\r\n\r\n");
$stream->on('data', function ($data) {
echo $data;
});
}, 'printf');
$loop->run();

View File

@@ -0,0 +1,30 @@
<?php
use Clue\React\Socks\Client;
use React\Socket\Connector;
use React\Socket\ConnectionInterface;
require __DIR__ . '/../vendor/autoload.php';
$proxy = isset($argv[1]) ? $argv[1] : '127.0.0.1:1080';
$loop = React\EventLoop\Factory::create();
$client = new Client($proxy, new Connector($loop));
$connector = new Connector($loop, array(
'tcp' => $client,
'timeout' => 3.0,
'dns' => false
));
echo 'Demo SOCKS client connecting to SOCKS server ' . $proxy . PHP_EOL;
$connector->connect('tls://www.google.com:443')->then(function (ConnectionInterface $stream) {
echo 'connected' . PHP_EOL;
$stream->write("GET / HTTP/1.0\r\n\r\n");
$stream->on('data', function ($data) {
echo $data;
});
}, 'printf');
$loop->run();

View File

@@ -0,0 +1,46 @@
<?php
use Clue\React\Socks\Client;
use React\Socket\Connector;
use React\Socket\ConnectionInterface;
require __DIR__ . '/../vendor/autoload.php';
if (!isset($argv[1])) {
echo 'No arguments given! Run with <proxy1> [<proxyN>...]' . PHP_EOL;
echo 'You can add 1..n proxies in the path' . PHP_EOL;
exit(1);
}
$path = array_slice($argv, 1);
// Alternatively, you can also hard-code this value like this:
//$path = array('127.0.0.1:9051', '127.0.0.1:9052', '127.0.0.1:9053');
$loop = React\EventLoop\Factory::create();
// set next SOCKS server chain via p1 -> p2 -> p3 -> destination
$connector = new Connector($loop);
foreach ($path as $proxy) {
$connector = new Client($proxy, $connector);
}
// please note how the client uses p3 (not p1!), which in turn then uses the complete chain
// this creates a TCP/IP connection to p1, which then connects to p2, then to p3, which then connects to the target
$connector = new Connector($loop, array(
'tcp' => $connector,
'timeout' => 3.0,
'dns' => false
));
echo 'Demo SOCKS client connecting to SOCKS proxy server chain ' . implode(' -> ', $path) . PHP_EOL;
$connector->connect('tls://www.google.com:443')->then(function (ConnectionInterface $stream) {
echo 'connected' . PHP_EOL;
$stream->write("GET / HTTP/1.0\r\n\r\n");
$stream->on('data', function ($data) {
echo $data;
});
}, 'printf');
$loop->run();

View File

@@ -0,0 +1,31 @@
<?php
use Clue\React\Socks\Client;
use React\Socket\Connector;
use React\Socket\ConnectionInterface;
require __DIR__ . '/../vendor/autoload.php';
$proxy = isset($argv[1]) ? $argv[1] : '127.0.0.1:1080';
$loop = React\EventLoop\Factory::create();
// set up DNS server to use (Google's public DNS)
$client = new Client($proxy, new Connector($loop));
$connector = new Connector($loop, array(
'tcp' => $client,
'timeout' => 3.0,
'dns' => '8.8.8.8'
));
echo 'Demo SOCKS client connecting to SOCKS server ' . $proxy . PHP_EOL;
$connector->connect('tls://www.google.com:443')->then(function (ConnectionInterface $stream) {
echo 'connected' . PHP_EOL;
$stream->write("GET / HTTP/1.0\r\n\r\n");
$stream->on('data', function ($data) {
echo $data;
});
}, 'printf');
$loop->run();

View File

@@ -0,0 +1,19 @@
<?php
use Clue\React\Socks\Server;
use React\Socket\Server as Socket;
require __DIR__ . '/../vendor/autoload.php';
$loop = React\EventLoop\Factory::create();
// listen on 127.0.0.1:1080 or first argument
$listen = isset($argv[1]) ? $argv[1] : '127.0.0.1:1080';
$socket = new Socket($listen, $loop);
// start a new server listening for incoming connection on the given socket
$server = new Server($loop, $socket);
echo 'SOCKS server listening on ' . $socket->getAddress() . PHP_EOL;
$loop->run();

View File

@@ -0,0 +1,24 @@
<?php
use Clue\React\Socks\Server;
use React\Socket\Server as Socket;
require __DIR__ . '/../vendor/autoload.php';
$loop = React\EventLoop\Factory::create();
// listen on 127.0.0.1:1080 or first argument
$listen = isset($argv[1]) ? $argv[1] : '127.0.0.1:1080';
$socket = new Socket($listen, $loop);
// start a new server listening for incoming connection on the given socket
// require authentication and hence make this a SOCKS5-only server
$server = new Server($loop, $socket);
$server->setAuthArray(array(
'tom' => 'god',
'user' => 'p@ssw0rd'
));
echo 'SOCKS5 server requiring authentication listening on ' . $socket->getAddress() . PHP_EOL;
$loop->run();

Some files were not shown because too many files have changed in this diff Show More