Skip to content

Commit

Permalink
Add EXPERIMENTAL support for https://ircv3.net/specs/extensions/webirc
Browse files Browse the repository at this point in the history
…#346 #444 #546

  This implementation allow a Convos admin to set an environment
  variable to enable WEBIRC. Example:

      #!/bin/sh
      export CONVOS_WEBIRC_PASSWORD_LOCALHOST=some_super_secret_password
      export CONVOS_WEBIRC_PASSWORD_MY_SERVER=some_super_secret_password
      ./script/convos daemon

  The part after "CONVOS_WEBIRC_PASSWORD_" is the connection ID, in
  upper case, without the "irc-" prefix and special characters (such as
  "-") translated into "_".

  Setting the environment variable will cause the following IRC commmand
  to be sent to the server:

      WEBIRC some_super_secret_password convos <hostname> <ip>

  "hostname" will fallback to "ip" if the IP could not resolved.

  IMPORTANT! The "ip" will only update after a USER has gone to the
  connection settings and hit "Update". The default "ip" (until a user
  have updated the settings) will be 127.0.0.1. Also, the "ip" will not
  get updated when the user change IP. It will only get updated when the
  user actively goes to the connection settings and hit "Update".
  • Loading branch information
Jan Henning Thorsen committed Jan 12, 2021
1 parent 5dca909 commit 1f9c23b
Show file tree
Hide file tree
Showing 5 changed files with 80 additions and 24 deletions.
2 changes: 2 additions & 0 deletions lib/Convos/Controller/Connection.pm
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ sub create {

my $url = Mojo::URL->new($json->{url} || '');
$url->path("/$json->{conversation_id}") if $json->{conversation_id};
$url->query->param(remote_address => $self->tx->remote_address);

if ($self->settings('forced_connection')) {
my $default_connection = Mojo::URL->new($self->settings('default_connection'));
Expand Down Expand Up @@ -77,6 +78,7 @@ sub update {

my $url = Mojo::URL->new($json->{url} || '');
$url = $connection->url unless $url->host;
$url->query->param(remote_address => $self->tx->remote_address);

unless ($self->settings('forced_connection')) {
$url->scheme($json->{protocol} || $connection->url->scheme || '');
Expand Down
35 changes: 27 additions & 8 deletions lib/Convos/Core/Connection/Irc.pm
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use Mojo::JSON qw(false true);
use Mojo::Parameters;
use Mojo::Util qw(b64_decode b64_encode gzip gunzip term_escape trim);
use Parse::IRC ();
use Socket;
use Time::HiRes 'time';

use constant IS_TESTING => $ENV{HARNESS_ACTIVE} || 0;
Expand All @@ -18,6 +19,7 @@ use constant PERIDOC_INTERVAL => $ENV{CONVOS_IRC_PERIDOC_INTERVAL} || 60;
require Convos;
our $VERSION = Convos->VERSION;

our $CONVOS_URL = 'https://convos.chat';
our %CTCP_QUOTE = ("\012" => 'n', "\015" => 'r', "\0" => '0', "\cP" => "\cP");

my %CLASS_DATA;
Expand All @@ -31,7 +33,7 @@ sub disconnect_p {
$self->{myinfo}{authenticated} = false;
$self->{myinfo}{capabilities} = {};
$self->{disconnecting} = 1; # Prevent getting queued
$self->_write("QUIT :https://convos.chat", sub { $self->_stream_remove($p) });
$self->_write("QUIT :$CONVOS_URL", sub { $self->_stream_remove($p) });
return $p;
}

Expand Down Expand Up @@ -1033,19 +1035,27 @@ sub _stream {
$self->SUPER::_stream($loop, $err, $stream);
return if $err;

my $url = $self->url;
my $nick = $self->nick;
my $user = $url->username || $nick;
$user =~ s/^[^a-zA-Z0-9]/x/;
my $mode = $url->query->param('mode') || 0;
my $url = $self->url;
if (my $password = $self->_web_irc_password) {
my $remote_address = $url->query->param('remote_address') || '127.0.0.1';
my $remote_hostname = gethostbyaddr(inet_aton($remote_address), AF_INET) || $remote_address;
$self->_write(sprintf "WEBIRC %s %s %s %s\r\n",
$password, 'convos', $remote_hostname, $remote_address);
}

$self->_write("CAP LS\r\n");
$self->_write(sprintf "PASS %s\r\n", $url->password)
if length $url->password and !$self->_sasl_mechanism;

my $nick = $self->nick;
$self->_write("NICK $nick\r\n");

my $convos = "https://convos.chat";
my $mode = $url->query->param('mode') || 0;
my $user = $url->username || $nick;
my $realname = $url->query->param('realname');
$realname = $realname ? "$realname via $convos" : $convos;
$realname = $realname ? "$realname via $CONVOS_URL" : $CONVOS_URL;

$user =~ s/^[^a-zA-Z0-9]/x/;
$self->_write("USER $user $mode * :$realname\r\n");
}

Expand Down Expand Up @@ -1095,6 +1105,15 @@ CHUNK:
}
}

sub _web_irc_password {
my $name = shift->name;
state $pw = {};
return $pw->{$name} if $pw->{$name};
my $key = sprintf 'CONVOS_WEBIRC_PASSWORD_%s', uc $name;
$key =~ s!\W!_!g;
return $pw->{$key} = Mojo::URL->new($ENV{$key} || '');
}

# This method is used to write a message to the IRC server and wait for a
# response in the form of one or more events.
#
Expand Down
5 changes: 3 additions & 2 deletions t/Server/Irc.pm
Original file line number Diff line number Diff line change
Expand Up @@ -208,15 +208,16 @@ sub _patch_connection_class {
my $self = shift;

Mojo::Util::monkey_patch(
$self->connection_class => can => sub {
$self->connection_class,
can => sub {
my ($conn, $method) = @_;
return shift->SUPER::can(@_)
unless ref $conn and $conn->name eq 'server' && $method =~ m!^_\w+_event_!;
return sub { $self->emit($method => $conn, pop) };
}
);

Mojo::Util::monkey_patch($self->connection_class => write => sub { shift->_write(@_) })
Mojo::Util::monkey_patch($self->connection_class, write => sub { shift->_write(@_) })
unless $self->connection_class->can('write');
}

Expand Down
30 changes: 30 additions & 0 deletions t/irc-webirc.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!perl
BEGIN { $ENV{CONVOS_SKIP_CONNECT} = 1 }
use lib '.';
use t::Helper;
use t::Server::Irc;
use Convos::Core;
use Convos::Core::Backend::File;

$ENV{CONVOS_WEBIRC_PASSWORD_EXAMPLE} = 'secret_passphrase';

my $server = t::Server::Irc->new->start;
my $core = Convos::Core->new;
my $user = $core->user({email => '[email protected]'});
$user->save_p->$wait_success;

my $connection = $user->connection({name => 'example', protocol => 'irc'});
$connection->save_p->$wait_success;

$server->client($connection)->server_event_ok(
'_irc_event_webirc',
sub {
my ($connection, $msg) = @_;
is_deeply $msg->{params}, [qw(secret_passphrase convos localhost 127.0.0.1)], 'webirc message';
}
)->server_event_ok('_irc_event_cap')->server_event_ok('_irc_event_nick')
->server_write_ok(":example CAP * LS :\r\n")->client_event_ok('_irc_event_cap')
->server_write_ok(['welcome.irc'])->client_event_ok('_irc_event_rpl_welcome')
->process_ok('webirc');

done_testing;
32 changes: 18 additions & 14 deletions t/web-connections.t
Original file line number Diff line number Diff line change
Expand Up @@ -37,30 +37,31 @@ $t->get_ok('/api/connections')->status_is(200)->json_is(
protocol => 'irc',
service_accounts => [qw(chanserv nickserv)],
state => 'disconnected',
url => 'irc://irc.example.com:6667',
url => 'irc://irc.example.com:6667?remote_address=127.0.0.1',
wanted_state => 'disconnected',
}
)->json_is('/connections/1/connection_id', 'irc-localhost')
->json_is('/connections/1/name', 'localhost')
->json_is('/connections/1/wanted_state', 'disconnected')
->json_is('/connections/1/url', "irc://localhost:$port");
->json_is('/connections/1/url', "irc://localhost:$port?remote_address=127.0.0.1");

$t->post_ok('/api/connection/irc-doesnotexist', json => {url => 'foo://example.com:9999'})
->status_is(404);
$t->post_ok('/api/connection/irc-example', json => {})->status_is(200);

my $connection = $user->get_connection('irc-localhost');
$t->post_ok('/api/connection/irc-localhost', json => {url => "irc://localhost:$port"})
->status_is(200)->json_is('/name' => 'localhost')->json_is('/state' => 'disconnected');
$t->post_ok('/api/connection/irc-localhost', json => {url => 'irc://example.com:9999'})
->status_is(200)->json_is('/name' => 'localhost')
->json_like('/url' => qr{irc://example\.com:9999});
$t->post_ok('/api/connection/irc-localhost',
json => {url => "irc://localhost:$port?remote_address=127.0.0.1"})->status_is(200)
->json_is('/name' => 'localhost')->json_is('/state' => 'disconnected');
$t->post_ok('/api/connection/irc-localhost',
json => {url => 'irc://example.com:9999?remote_address=127.0.0.1'})->status_is(200)
->json_is('/name' => 'localhost')->json_like('/url' => qr{irc://example\.com:9999});

$connection->state(disconnected => '');
$t->post_ok('/api/connection/irc-localhost',
json => {url => 'irc://example.com:9999', wanted_state => 'connected'})->status_is(200)
->json_is('/name' => 'localhost')->json_is('/state' => 'queued')
->json_is('/url' => 'irc://example.com:9999?nick=superman&tls=1');
->json_is('/url' => 'irc://example.com:9999?remote_address=127.0.0.1&nick=superman&tls=1');

$connection->state(connected => '');
$t->post_ok(
Expand All @@ -71,26 +72,29 @@ $t->post_ok(
}
)->status_is(200)->json_is('/name' => 'localhost')->json_is('/state' => 'connected')
->json_is('/on_connect_commands', ['/msg NickServ identify s3cret', '/msg too_cool 123'])
->json_is('/url' => 'irc://example.com:9999?tls=1&nick=superman');
->json_is('/url' => 'irc://example.com:9999?tls=1&remote_address=127.0.0.1&nick=superman');

$t->post_ok('/api/connection/irc-localhost',
json => {url => 'irc://foo:[email protected]:9999?tls=0&nick=superman'})->status_is(200)
->json_is('/url' => 'irc://foo:[email protected]:9999?tls=0&nick=superman')
->json_is('/url' => 'irc://foo:[email protected]:9999?tls=0&nick=superman&remote_address=127.0.0.1')
->json_is('/state' => 'queued');

$connection->state(connected => '');
$t->post_ok('/api/connection/irc-localhost',
json =>
{url => 'irc://foo:[email protected]:9999?tls=0&nick=superman', wanted_state => 'connected'})
->status_is(200)->json_is('/url' => 'irc://foo:[email protected]:9999?tls=0&nick=superman')
->status_is(200)
->json_is(
'/url' => 'irc://foo:[email protected]:9999?tls=0&nick=superman&remote_address=127.0.0.1')
->json_is('/state' => 'queued');

is $connection->TO_JSON(1)->{url}, 'irc://foo:[email protected]:9999?tls=0&nick=superman',
'to json url';
is $connection->TO_JSON(1)->{url},
'irc://foo:[email protected]:9999?tls=0&nick=superman&remote_address=127.0.0.1', 'to json url';

$t->post_ok('/api/connection/irc-localhost',
json => {url => 'irc://foo:[email protected]:9999?tls=0&nick=superman'})->status_is(200);
is $connection->TO_JSON(1)->{url}, 'irc://foo:[email protected]:9999?tls=0&nick=superman',
is $connection->TO_JSON(1)->{url},
'irc://foo:[email protected]:9999?tls=0&remote_address=127.0.0.1&nick=superman',
'no change with same username';

$t->get_ok('/api/connections')->status_is(200)->json_is('/connections/1/on_connect_commands',
Expand Down

0 comments on commit 1f9c23b

Please sign in to comment.