The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.
# Jabber::Component::Proxy
# (c) DJ Adams 2001

# $Id: Proxy.pm,v 1.3 2002/04/01 18:02:32 dj Exp $

=head1 NAME

Jabber::Component::Proxy - A simple proxy for Jabber Components

=head1 SYNOPSIS

  use Jabber::Component::Proxy

  # Create proxy
  my $proxy = new Jabber::Component::Proxy(
    server    => 'localhost:6789',
    identauth => 'conference.qmacro.dyndns.org',
    realcomp  => 'conference.internal',
    rulefile  => './access.xml',
  );  

  $proxy->start;

=head1 DESCRIPTION

Jabber::Component::Proxy is a simple proxy mechanism that you can
use to control access to your Jabber services. If you attach a
component to your Jabber server, and give that component a 'real'
resolvable FQDN, people on other Jabber servers can access that 
component. 

This might be what you want. But what if you want to allow access
to some people but not others? How can you control access on a
domain name basis, for example? Currently component access is 
all or nothing:

- give a component a resolvable name and the world can use it
- give a component an internal name and no one but those connected
  the same Jabber server as the component can use it

(This is assuming of course you're running an s2s service).

You can effect a sort of access control, for non-local users, using
this module. 

=head1 VERSION

0.01 (early)

=head1 AUTHOR

DJ Adams

=head1 SEE ALSO

Jabber::Connection

=cut

package Jabber::Component::Proxy;

use vars qw($VERSION);
$VERSION = '0.02';

use Jabber::Connection;
use Jabber::NodeFactory;
use XML::XPath;
use XML::XPath::XMLParser;

use warnings;
use strict;


sub new {

  my $class = shift; my %args = @_;
  my $self = {};

  # My (the client's) host/port and Identity
  $self->{server} = $args{server};
  $self->{realcomp} = $args{realcomp};
  $self->{rulefile} = $args{rulefile};
  ($self->{id}, $self->{pass}) = split(':', $args{identauth});

  die "Bad rulefile $args{rulefile}"
    unless -f $args{rulefile} and -r $args{rulefile};

  # Connect to Jabber
  $self->{connection} = new Jabber::Connection(
    server    => $self->{server},
    localname => $self->{id},
    ns        => 'jabber:component:accept',
#   log       => 1,
#   debug     => 1,
  );

  $self->{connection}->connect
      or  die "oops: ".$self->{connection}->lastError;
  _debug("Connected");

  $self->{connection}->auth($self->{pass});
  _debug("Authenticated");

  # Node factory
  $self->{nf} = new Jabber::NodeFactory;

  # Set up handlers
  $self->{connection}->register_handler('iq', sub { $self->_proxy(@_) } );
  $self->{connection}->register_handler('message', sub { $self->_proxy(@_) } );
  $self->{connection}->register_handler('presence', sub { $self->_proxy(@_) } );

  # Set up HUP handler
  $SIG{HUP} = sub { $self->_readrules };

  # Set up end handler
  $SIG{KILL} = $SIG{TERM} = $SIG{INT} = sub { $self->_cleanup };

  _debug("Handlers set up");

  bless $self, $class;

  # Read in rules
  $self->_readrules;

  return $self;
 
}


# Start the proxy
sub start {

  my $self = shift;

  _debug("Starting proxy");

  # Go!
  $self->{connection}->start;

}


sub _debug {

  print STDERR "DEBUG: @_\n";

}


sub _proxy {

  my $self = shift;
  my $node = shift;

  my $from = _breakJID($node->attr('from'));
  my $to = _breakJID($node->attr('to'));

  # Going back OUT
  if ($from->{host} eq $self->{realcomp}) {
    $node->attr('from', _makeJID($from->{user}, $self->{id}, $from->{resource}));
    $node->attr('to',pack("H*",$to->{user}));
  }

  # Coming IN
  else {

    # Only proceed if allowed
    my $userhost = _makeJID($from->{user}, $from->{host}, undef);
    if ($self->_access($userhost)) {
      $node->attr('to', _makeJID($to->{user}, $self->{realcomp}, $to->{resource}));
      $node->attr('from', _makeJID(unpack("H*",$node->attr('from')), $self->{id}, $from->{resource}));
    }

    # Deny if not allowed
    else {
      _debug("denying $userhost");
      $node->attr('type', 'error');
      my $error = $node->insertTag('error');
      $error->attr('code', 403);
      $error->data('Forbidden');
      $node = _toFrom($node);
    }

  }

  $self->{connection}->send($node);


}


sub _access {

  my $self = shift;
  my $from = shift;

  my $allowed = 0;

  if (exists $self->{rules}->{allow}) {
    foreach my $rule (@{$self->{rules}->{allow}}) {
      $allowed = 1, last if $from =~ /$rule$/;
    }
  }
  else { $allowed = 1 }

  if ($allowed and exists $self->{rules}->{deny}) {
    foreach my $rule (@{$self->{rules}->{deny}}) {
      $allowed = 0, last if $from =~ /$rule$/;
    }
  }

  return $allowed;

}


sub _toFrom {
  my $node = shift;
  my $to = $node->attr('to');
  $node->attr('to', $node->attr('from'));
  $node->attr('from', $to);
  return $node; 
}


sub _breakJID {

  my $jid = shift;
  my ($u, $h, $r) = $jid =~ m/^([^@]+(?=\@))?\@?([\w+\.\-]+)\/?([\w \-]+)?$/;
  return {
    user => $u,
    host => $h,
    resource => $r,
  };

}


sub _makeJID {

  my ($u, $h, $r) = @_;
  my $jid;
  $jid = $u.'@' if defined($u);
  $jid.=$h;
  $jid.= '/'.$r if defined($r);
  return $jid;

}


sub _readrules {

  my $self = shift;

  _debug("Reading access rules");
  delete $self->{rules};
  my $xp = XML::XPath->new(filename => $self->{rulefile});
  foreach (qw/allow deny/) {
    foreach my $node ($xp->find("/access/$_/address")->get_nodelist) {
      _debug("Adding $_ rule for ".$node->string_value);
      push(@{$self->{rules}->{$_}}, $node->string_value);
    }
  }

}


sub _cleanup {

  my $self = shift;

  _debug("Shutting down");
  $self->{connection}->disconnect;

  exit;

}


1;