Using IO::Socket::IP with Mojo::IOLoop - perl

I am trying to achieve what these lines of code do synchronously, but asynchronously via Mojo::IOLoop and Mojo::Promises:
my $address = '192.168.1.240';
my $sock = IO::Socket::IP->new(PeerAddr => $address,
PeerPort => '9999',
Proto => 'tcp');
$sock->send($on);
my $data;
$sock->recv($data, 2048);
print $data;
This is used to communicate with a smartplug, and a promise-based interface would allow me to abstract out things, so that different plugs (some communicating via HTTP, some via TCP) could be handled in the same way.
Right now I've achieved the result by doing this
sub talk_to_plug_p {
my ($addr, $command) = #_;
my $promise = Mojo::Promise->new;
my $port = 9999;
my $sock = IO::Socket::IP->new(PeerAddr => $addr,
PeerPort => $port,
Proto => 'tcp');
return $promise->reject("Could not open socket on $addr at port $port") unless $sock;
my $id = Mojo::IOLoop->client({ handle => $sock } => sub {
my ($loop, $err, $stream) = #_;
$stream->on(read => sub {
my ($stream, $bytes) = #_;
# -------------------
# THIS LOOKS BAD
# -------------------
remove_id($loop);
$promise->resolve($bytes);
});
$stream->on(error => sub {
my $err = shift;
$promise->reject($err);
});
$stream->write($command);
});
sub remove_id { shift->remove($id) };
return $promise;
}
talk_to_plug_p('192.168.1.240', $on)->then(sub { print #_ })->catch(sub { print shift });
Mojo::IOLoop->start unless Mojo::IOLoop->is_running;
Now this works but it kind of looks wrong - especially the remove_id part, and I imagine there must eb a better way than this. I just could not find it.
What would be a cleaner way? i.e.: one that closes the client when bytes have been read, or where I can explicitly close the connection or something similar.

Related

How to write a client program for Net::WebSocket::Server program?

I have a server program that listens on 9000 port. But I can't find a way to write a client program for that server that connects server at 9000 port. Here is the main part of server program:
use strict;
use warnings;
use Net::WebSocket::Server;
my $port = "9000";
my $msg_count = 0;
print "starting server on $port \n\n";
my $count = 2400;
Net::WebSocket::Server->new(
listen => $port,
silence_max => 5,
tick_period => 300,
on_tick => sub {
my ($serv) = #_;
print "connections >> " . $serv->connections . "\n";
print $_->ip() for( $serv->connections() ); print "\n";
print $_->port() for( $serv->connections() ); print "\n\n";
$count++;
},
on_connect => sub {
my ($serv, $conn) = #_;
$conn->on(
handshake => sub {
my ($conn, $handshake) = #_;
my $tmp = $handshake->req->origin;
print "here ... $tmp \n\n";
},
utf8 => sub {
my ($conn, $msg) = #_;
my $IP = $conn->ip();
my $PORT = $conn->port();
my $SERVER = $conn->server();
my $SOCKET = $conn->socket();
my $str = Dumper $SOCKET;
I searched internet and what that sounds understandable to me is the following client program:
use strict;
use warnings;
use IO::Socket::SSL;
my $cl=IO::Socket::SSL->new("http://localhost:9000") or die "error=$!, ssl_error=$SSL_ERROR";
if($cl) {
$cl->connect_SSL or die $#;
# Something about certificates?
$cl->syswrite("Command");
close($cl);
}
But its not working. The error client program generates is as follows:
Expected 'PeerService' at client2.pl line 5.
I am newbie in Socket programming and currently understanding websockets programming in Perl.
Note: I am on windows platform.
I ran the example code suggested https://stackoverflow.com/questions/37318581/simple-perl-websocket-client. It gives error "Can't use an undefined value as a subroutine reference at C:/Strawberry/perl/site/lib/Protocol/WebSocket/Client.pm line 103.":
use strict;
use warnings;
use Protocol::WebSocket::Client;
my $client = Protocol::WebSocket::Client->new(url => 'ws://localhost:9000') or die "$!";
my $reply = "Free\n";
# Sends a correct handshake header
$client->connect or die "$!";
# Register on connect handler
$client->on(
connect => sub {
$client->write('hi there');
}
) or die "$!";
# Parses incoming data and on every frame calls on_read
$client->read($reply);
print "$reply\n";
# Sends correct close header
$client->disconnect;
Please investigate following demo code snippets for WebSocket Server and Client.
Note: please do not forget to alter code to match your server origin (ip address and port)
use strict;
use warnings;
use feature 'say';
use Net::WebSocket::Server;
my $origin = 'http://192.168.1.160:8080'; # server origin
my $port = 8080;
$| = 1;
say "Starting server on $port";
Net::WebSocket::Server->new(
listen => $port,
tick_period => 60,
on_tick => sub {
my ($serv) = #_;
my $stamp = 'Server time: ' . scalar localtime;
$_->send_utf8($stamp) for $serv->connections;
},
on_connect => sub {
my ($serv, $conn) = #_;
$conn->on(
handshake => sub {
my ($conn, $handshake) = #_;
$conn->disconnect() unless $handshake->req->origin eq $origin;
},
ready => sub {
my ($conn) = #_;
say "Client: connect IP $conn->{ip} PORT $conn->{port}";
my $msg = 'Connected server time is ' . scalar localtime . "\n";
$_->send_utf8($msg) for $conn->server->connections;
},
utf8 => sub {
my ($conn, $msg) = #_;
say "Client message: $conn->{ip} $msg";
$_->send_utf8('Server reply: ' . $msg)
for $conn->server->connections;
$conn->disconnect() if $msg eq 'exit';
},
binary => sub {
my ($conn, $msg) = #_;
$_->send_binary($msg) for $conn->server->connections;
},
pong => sub {
my ($conn, $msg) = #_;
$_->send_utf8($msg) for $conn->server->connections;
},
disconnect => sub {
my ($conn, $code, $reason) = #_;
say "Client: disconnect IP $conn->{ip} PORT $conn->{port}";
},
);
},
)->start;
Client
use strict;
use warnings;
use feature 'say';
use IO::Async::Loop;
use Net::Async::WebSocket::Client;
my $HOST = '192.168.1.160';
my $PORT = 8080;
my $loop = IO::Async::Loop->new;
my $client = Net::Async::WebSocket::Client->new(
on_text_frame => sub {
my ( $self, $frame ) = #_;
say $frame;
},
);
my $input = IO::Async::Stream->new_for_stdin(
on_read => sub {
my ( $self, $buffref, $eof ) = #_;
my $msg;
$msg = $1 while $$buffref =~ s/^(.*)\n//;
$client->send_text_frame( $msg );
$loop->loop_stop if $msg eq 'exit';
return 0;
},
);
$loop->add( $client );
$loop->add( $input );
$client->connect(
url => "ws://$HOST:$PORT/"
)->then( sub {
say 'Successfully connected to server';
$client->send_text_frame( scalar localtime );
})->get;
$loop->run;
say 'Bye, until next time';
exit 0;
References:
Net::WebSocket::Server
Net::Async::WebSocket::Client
IO::Async::Loop

Perl Issue with concurrent requests with IO::Async and Future::Utils

I'm trying to use an IO loop to send concurrent requests (5) to a pool of hosts (3), but the code stops after 3 requests. I've had help to kickstart this code, but I certainly understand most of it now. What I don't get is why the number of processed requests is linked to the number of hosts in my pool of hosts. The objective of the code is to determine routing information from a given IP.
use strict;
use warnings;
use Net::OpenSSH;
use IO::Async::Loop;
use Future::Utils 'fmap_concat';
my #hosts = qw(host1 host2 host3);
my #ssh;
my $user = 'myuser';
my $pass = 'mypassword';
foreach my $host (#hosts) {
my $ssh = Net::OpenSSH->new(host => $host, user => $user, password => $pass, master_opts => [-o => "StrictHostKeyChecking=no"]);
die "Failed to connect to $host: " . $ssh->error if $ssh->error;
push #ssh, $ssh;
}
my #ipv4 = (
'ip1','ip2','ip3','ip4','ip5'
);
my $loop = IO::Async::Loop->new;
my $future = fmap_concat {
my $ip = shift;
my $ssh = shift #ssh;
my $cmd = 'show ip route '.$ip.' | i \*';
my #remote_cmd = $ssh->make_remote_command($cmd);
return $loop->run_process(command => \#remote_cmd)
->transform(done => sub { [#_] })
->on_ready(sub { push #ssh, $ssh });
} generate => sub { return () unless #ssh and #ipv4; shift #ipv4 }, concurrent => scalar #ssh;
my #results = $future->get;
foreach my $result (#results) {
my ($exit, $stdout) = #$result;
print $stdout, "\n";
}
Here are the results
Connection to host1 closed by remote host.
Connection to host2 closed by remote host.
Connection to host3 closed by remote host.
* ip1, from host1, 3w0d ago, via GigabitEthernet0/0/0
* ip2, from host2, 7w0d ago, via GigabitEthernet0/0/0
* ip3, from host3, 3w0d ago, via GigabitEthernet0/0/1
After researching on the problem, I found out that network devices such as Cisco might have an issue handling several requests over the same connection. So the code changed a bit in a way where a new connection is opened everytime the future is called instead of using a pool of pre-opened connections.
use strict;
use warnings;
use Net::OpenSSH;
use IO::Async::Loop;
use Future::Utils 'fmap_concat';
my #hosts = qw(host1 host2 host3);
my #ssh;
my $user = 'myuser';
my $pass = 'mypassword';
my #ipv4 = (
'ip1','ip2','ip3','ip4','ip5'
);
my $loop = IO::Async::Loop->new;
my $future = fmap_concat {
my $ip = shift;
my $host = shift #hosts;
my $ssh = Net::OpenSSH->new(host => $host, user => $user, password => $pass, master_opts => [-o => "StrictHostKeyChecking=no"]);
die "Failed to connect to $host: " . $ssh->error if $ssh->error;
my $cmd = 'show ip route '.$ip.' | i \*|^Routing entry';
my #remote_cmd = $ssh->make_remote_command($cmd);
return $loop->run_process(command => \#remote_cmd)
->transform(done => sub { [#_] })
->on_ready(sub { push #hosts, $host ; });
} generate => sub { return () unless #hosts and #ipv4; shift #ipv4 }, concurrent => scalar #hosts;
my #results = $future->get;
foreach my $result (#results) {
my ($exit, $stdout) = #$result;
print $stdout, "\n";
}
But this has leaded to other problems with underlying openssh library.
It looks like there was a race condition with the ssh connection not being released properly when the future was being invoked on the $host again.
undef $ssh fixed it
->on_ready(sub { undef $ssh; push #hosts, $host ; });

WebSocket server from scratch showing opcode -1

I am trying to create a simple WebSocket server in perl from scratch, when I tried it in Google Chrome it gave me opcode -1, How can I fix it?
websocket.pl
#!/usr/bin/perl -w
use strict;
use IO::Socket::INET;
use Digest::SHA1 "sha1_base64";
$| = 1;
my $magic_string = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
# Create a server
my $socket = IO::Socket::INET->new( LocalHost => 'localhost',
LocalPort => 7777,
Proto => 'tcp',
Listen => 5,
Reuse => 1
) || die "$!";
print "Server is running on port 7777\n";
while (1) {
my $client = $socket->accept();
my $key = "";
# Get the Request
my $data = "";
while (my $line = <$client>) {
$data .= $line;
}
# Get the Sec-WebSocket-Key value
foreach my $line ( split /\n/ => $data ) {
if ( $line =~ /^Sec-WebSocket-Key: (\S+)/ ) {
$key = $1;
}
}
print "Sec-WebSocket-Key: $key\n";
# Create the Sec-WebSocket-Accept header value
my $accept = sha1_base64($key);
$accept .= "="x(4-(length($accept)%4));
print "Sec-WebSocket-Accept: $accept\n";
# Response
print $client "HTTP/1.1 101 Switching Protocols\r\n";
print $client "Upgrade: websocket\r\n";
print $client "Connection: Upgrade\r\n";
print $client "Sec-WebSocket-Accept: $accept\r\n\r\n";
shutdown($client, 1);
}
$socket->close();
I am pretty sure that the key returned to website is correct, so where is the problem? What went wrong?
ws.js
var ws = new WebSocket("ws://localhost:7777/");
ws.onopen = function() {
alert("connected!");
ws.send( 'Hello server' );
};
ws.onclose = function() {
alert( 'Connection is closed... ');
};
Web Browser network traffic
Edit
Stefan Becker: Yea, I know, but in this case I was sure that the request is under 1024 bytes, I've fixed it, thanks.
(Opcode -1) is a generic error. In your case it is a bad Sec-WebSocket-Accept header. You forgot to use $magic_string:
my $accept = sha1_base64($key.$magic_string);
Also while (my $line = <$client>) { will probably run forever. You need to check for an empty line.

Perl IO::Async parallel tcp connections

I've got a problem that I can't easily find the solution to for some reason.
I try to build multiple parallel TCP connections to a server via IO::Async.
My goal is to run TCP connections in parallel. The connections do not need to communicate between themselves but I need to catch and save output of them in a hash.
The following code is an example with a single connection.
#!/usr/bin/perl
use strict;
use warnings;
use IO::Async::Loop;
use IO::Async::Stream;
my $CRLF = "\x0d\x0a"; # because \r\n is not portable
my $HOST = shift #ARGV or die "Need HOST";
my $PORT = shift #ARGV or die "Need PORT";
my $loop = IO::Async::Loop->new;
my $socket;
$loop->connect(
host => $HOST,
service => $PORT,
socktype => 'stream',
on_connected => sub { $socket = shift },
on_resolve_error => sub { die "Cannot resolve - $_[0]\n" },
on_connect_error => sub { die "Cannot connect\n" },
);
$loop->loop_once until defined $socket;
# $socket is just an IO::Socket reference
my $peeraddr = $socket->peerhost . ":" . $socket->peerport;
print STDERR "Connected to $peeraddr\n";
# We need to create a cross-connected pair of Streams. Can't do that
# easily without a temporary variable
my ( $socketstream, $stdiostream );
$socketstream = IO::Async::Stream->new(
handle => $socket,
on_read => sub {
my ( undef, $buffref, $eof ) = #_;
while( $$buffref =~ s/^(.*)$CRLF// ) {
$stdiostream->write( $1 . "\n" );
}
return 0;
},
on_closed => sub {
print STDERR "Closed connection to $peeraddr\n";
$stdiostream->close_when_empty;
},
);
$loop->add( $socketstream );
$stdiostream = IO::Async::Stream->new_for_stdio(
on_read => sub {
my ( undef, $buffref, $eof ) = #_;
while( $$buffref =~ s/^(.*)\n// ) {
$socketstream->write( $1 . $CRLF );
}
return 0;
},
on_closed => sub {
$socketstream->close_when_empty;
},
);
$loop->add( $stdiostream );
$loop->await_all( $socketstream->new_close_future, $stdiostream->new_close_future );
How could I modify this code to handle an IP list as asynchronous connections and store output in a dedicated hash?
Finally maybe to limit max parallel connection to 100.
Any ideas?

Creating A Single Threaded Server with AnyEvent (Perl)

I'm working on creating a local service to listen on localhost and provide a basic call and response type interface. What I'd like to start with is a baby server that you can connect to over telnet and echoes what it receives.
I've heard AnyEvent is great for this, but the documentation for AnyEvent::Socket does not give a very good example how to do this. I'd like to build this with AnyEvent, AnyEvent::Socket and AnyEvent::Handle.
Right now the little server code looks like this:
#!/usr/bin/env perl
use AnyEvent;
use AnyEvent::Handle;
use AnyEvent::Socket;
my $cv = AnyEvent->condvar;
my $host = '127.0.0.1';
my $port = 44244;
tcp_server($host, $port, sub {
my($fh) = #_;
my $cv = AnyEvent->condvar;
my $handle;
$handle = AnyEvent::Handle->new(
fh => $fh,
poll => "r",
on_read => sub {
my($self) = #_;
print "Received: " . $self->rbuf . "\n";
$cv->send;
}
);
$cv->recv;
});
print "Listening on $host\n";
$cv->wait;
This doesn't work and also if I telnet to localhost:44244 I get this:
EV: error in callback (ignoring): AnyEvent::CondVar:
recursive blocking wait attempted at server.pl line 29.
I think if I understand how to make a small single threaded server that I can connect to over telnet and prints out whatever its given and then waits for more input, I could take it a lot further from there. Any ideas?
You're blocking inside a callback. That's not allowed. There are a few ways to handle this. My preference is to launch a Coro thread from within the tcp_server callback. But without Coro, something like this might be what you're looking for:
#!/usr/bin/env perl5.16.2
use AnyEvent;
use AnyEvent::Handle;
use AnyEvent::Socket;
my $cv = AE::cv;
my $host = '127.0.0.1';
my $port = 44244;
my %connections;
tcp_server(
$host, $port, sub {
my ($fh) = #_;
print "Connected...\n";
my $handle;
$handle = AnyEvent::Handle->new(
fh => $fh,
poll => 'r',
on_read => sub {
my ($self) = #_;
print "Received: " . $self->rbuf . "\n";
},
on_eof => sub {
my ($hdl) = #_;
$hdl->destroy();
},
);
$connections{$handle} = $handle; # keep it alive.
return;
});
print "Listening on $host\n";
$cv->recv;
Note that I'm only waiting on one condvar. And I'm storing the handles to keep the AnyEvent::Handle objects alive longer. Work to clean up the $self->rbuf is left as an excersise for the reader :-)
Question cross-posted, answer, too :-)
I have heard good things about AnyEvent as well, but have not used it. I wrote a small nonblocking server in the past using IO::Select. There is an example in the documentation for that module (I've added a few lines):
use IO::Select;
use IO::Socket;
$lsn = new IO::Socket::INET(Listen => 1, LocalPort => 8080);
$sel = new IO::Select( $lsn );
while(#ready = $sel->can_read) {
foreach $fh (#ready) {
if($fh == $lsn) {
# Create a new socket
$new = $lsn->accept;
$sel->add($new);
}
else {
# Process socket
my $input = <$fh>;
print $fh "Hello there. You said: $input\n";
# Maybe we have finished with the socket
$sel->remove($fh);
$fh->close;
}
}
}
I'm not sure what your condvar is trying to trigger there. Use it to send state, like:
#!/usr/bin/env perl
use AnyEvent;
use AnyEvent::Handle;
use AnyEvent::Socket;
my $host = '127.0.0.1';
my $port = 44244;
my $exit = AnyEvent->condvar;
tcp_server($host, $port, sub {
my($fh) = #_;
my $handle; $handle = AnyEvent::Handle->new(
fh => $fh,
poll => "r",
on_read => sub {
my($self) = #_;
print "Received: " . $self->rbuf . "\n";
if ($self->rbuf eq 'exit') {
$exit->send;
}
}
);
});
print "Listening on $host\n";
$exit->recv;