#!/usr/bin/perl
# CGI:IRC - http://cgiirc.sourceforge.net/
# Copyright (C) 2000-2006 David Leadbeater
# vim:set ts=3 expandtab shiftwidth=3 cindent:
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
# Uncomment this if the server doesn't chdir (Boa).
# BEGIN { (my $dir = $0) =~ s|[^/]+$||; chdir($dir) }
require 5.004;
use strict;
use lib qw{./modules ./interfaces};
use vars qw(
$VERSION @handles %inbuffer $select_bits @output
$unixfh $ircfh $cookie $ctcptime $intime $pingtime
$timer $event $config $cgi $irc $format $formatname $interface $ioptions
$regexpicon %regexpicon
$config_path $help_path
);
($VERSION =
'$Name: rel_0_5_9 $ 0_5_CVS $Id: nph-irc.cgi,v 1.120 2006/06/06 18:50:12 dgl Exp $'
) =~ s/^.*?(\d\S+) .*?(\d{4}\/\S+) .*$/$1/;
$VERSION .= " ($2)";
$VERSION =~ s/_/./g;
use Socket;
use Symbol; # gensym
$|++;
# Check for IPV6. Bit yucky but avoids errors when module isn't present
BEGIN {
eval('use Socket6; $::IPV6++ if defined $Socket6::VERSION');
unless(defined $::IPV6) {
$::IPV6 = 0;
eval('sub AF_INET6 {0};sub NI_NUMERICHOST {0};sub NI_NUMERICSERV {}');
}
# then check for Encode
$::ENCODE = 0;
eval("use Encode;");
$::ENCODE = 1 unless $@;
}
# My own Modules
use Timer;
use Event;
use IRC;
use Command;
require 'parse.pl';
for('', '/etc/cgiirc/', '/etc/') {
last if -r ($config_path = $_) . 'cgiirc.config';
}
for('docs/', '/usr/share/doc/cgiirc/') {
last if -r ($help_path = $_) . 'help.html';
}
my $needtodie = 0;
$SIG{HUP} = $SIG{INT} = $SIG{TERM} = sub { $needtodie = 1 };
# Pipe isn't bad..
$SIG{PIPE} = 'IGNORE';
$SIG{__DIE__} = sub {
error("Program ending: @_");
};
# DEBUG
#use Carp;
#$SIG{__DIE__} = \&confess;
#### Network Functions
## Returns the address of a host (handles both IPv4 and IPv6)
## Return value: (ipv4,ipv6)
sub net_hostlookup {
my($host) = @_;
if($::IPV6) {
my($family,$socktype, $proto, $saddr, $canonname, @res) =
getaddrinfo($host, undef, AF_UNSPEC, SOCK_STREAM);
return undef unless $family;
my($addr, $port) = getnameinfo($saddr, NI_NUMERICHOST | NI_NUMERICSERV);
=pod
my $ip = config_set('prefer_v6')
? ($ipv6 ? $ipv6 : $ipv4)
: ($ipv4 ? $ipv4 : $ipv6);
=cut
return $addr;
}else{ # IPv4
my $ip = (gethostbyname($host))[4];
return $ip ? inet_ntoa($ip) : undef;
}
}
## Connects a tcp socket and returns the file handle
## inet_addr should be the output of net_gethostbyname
sub net_tcpconnect {
my($inet_addr, $port) = @_;
my $fh = Symbol::gensym;
my $family = ($inet_addr !~ /:/ ? AF_INET : AF_INET6);
socket($fh, $family, SOCK_STREAM,
getprotobyname('tcp')) or return(0, $!);
setsockopt($fh, SOL_SOCKET, SO_KEEPALIVE, pack("l", 1)) or return(0, $!);
my $saddr;
if($inet_addr !~ /:/) {
$saddr = sockaddr_in($port, inet_aton($inet_addr));
if(config_set('vhost')) {
(my $vhost) = $config->{vhost} =~ /([^ ]+)/;
bind($fh, pack_sockaddr_in(0, inet_aton($vhost)));
}else{
bind($fh, pack_sockaddr_in(0, inet_aton('0.0.0.0')));
}
}else{
$saddr = sockaddr_in6($port, inet_pton(AF_INET6, $inet_addr));
if(config_set('vhost6')) {
# this needs testing...
(my $vhost) = $config->{vhost6} =~ /([^ ]+)/;
bind($fh, pack_sockaddr_in6(0, inet_pton(AF_INET6, $vhost)));
}
}
if($family == AF_INET) {
my($localport,$localip) = sockaddr_in(getsockname $fh);
irc_write_server(inet_ntoa($localip), $localport, $inet_addr, $port);
}else{
my($localport,$localip) = sockaddr_in6(getsockname $fh);
irc_write_server(inet_pton(AF_INET6, $localip), $localport, $inet_addr, $port);
}
$SIG{ALRM} = sub { die "xtimeout" };
eval {
local $SIG{__DIE__} = undef;
alarm 60;
connect($fh, $saddr) or die "$!\n";
};
alarm 0;
if($@ =~ /xtimeout/) {
return(0, "Connection timed out (60 seconds)");
}elsif($@) {
chomp(my $error = $@);
return(0, "$error connecting to $inet_addr:$port");
}
net_autoflush($fh);
return($fh);
}
## Opens a UNIX Domain Listening socket
## Passed just the filename, returns 1 on success, 0 on failure
sub net_unixconnect {
my($local) = @_;
my $fh = Symbol::gensym;
if(-e $local) {
return 0 unless unlink $local;
}
socket($fh, PF_UNIX, SOCK_STREAM, 0) or return (0, $!);
bind($fh, sockaddr_un($local)) or return (0, $!);
listen($fh, SOMAXCONN) or return (0, $!);
net_autoflush($fh);
return $fh;
}
sub net_autoflush {
my $fh = shift;
select $fh;
$| = 1;
select STDOUT;
}
## Send data to specific filehandle (and deal with encodings for irc)..
sub net_send {
my($fh,$data) = @_;
if($::ENCODE && $fh == $ircfh) {
my $output = Encode::encode($config->{'irc charset'}, $data);
$output = $data unless defined $output;
syswrite($fh, $output, length $output);
}elsif($::ENCODE) {
my $output = Encode::encode('utf8', $data);
$output = $data unless defined $output;
syswrite($fh, $output, length $output);
}else{
syswrite($fh, $data, length $data);
}
}
#### Select Helper Functions
## Code adapted from IO::Select.pm by Graham Barr
## Adds file handle into @handles and fileno into the bit vector
sub select_add {
my($fh) = @_;
my $fileno = select_fileno($fh);
$handles[$fileno] = $fh;
select_makebits();
}
## Deletes the filehandle and fileno
sub select_del {
my($fh) = @_;
my $fileno = select_fileno($fh);
if(!$fileno) {
for(0 .. $#handles) {
$fileno = $_, last if $handles[$_] == $fh;
}
}
return unless defined $handles[$fileno];
$handles[$fileno] = undef;
select_makebits();
}
## Returns a fileno
sub select_fileno {
fileno(shift);
}
sub select_makebits {
$select_bits = '';
for(2 .. $#handles) {
next unless defined $handles[$_] && ref $handles[$_];
vec($select_bits, select_fileno($handles[$_]), 1) = 1;
}
}
## Returns list of handles with input waiting
sub select_canread {
my($timeout) = @_;
my $read = $select_bits;
if(select($read, undef, undef, $timeout) > 0) {
my @out;
for(0 .. $#handles) {
push(@out, $handles[$_]) if vec($read, $_, 1);
}
return @out;
}
return ();
}
## Closes and deletes a filehandle
sub select_close {
my($fh) = @_;
return irc_close() if $ircfh == $fh;
select_del($fh);
close($fh);
}
#### Format Functions
## Loads the format given to it, or the default
sub load_format {
$formatname = $config->{format};
if($cgi->{format} && $cgi->{format} !~ /[^A-Za-z0-9]/) {
$formatname = $cgi->{format};
}
return parse_config($config_path . 'formats/' . $formatname);
}
## Prints a nicely formatted line
## the format is the format name to use, taken from the %format hash
## the params are passed to the format
sub format_out {
my($formatname, $info, $params) = @_;
return unless exists $format->{$formatname};
return unless $format->{$formatname};
my $line = format_parse($format->{$formatname}, $info, $params);
$line = format_colourhtml($line);
interface_lineout($info, $line);
}
sub message {
my($formatname, @params) = @_;
my $info = { target => 'Status', activity => 1, type => $formatname };
format_out($formatname, $info, \@params);
}
## Formats IRC Colours and Styles into HTML and makes URLs clickable
sub format_colourhtml {
my($line) = @_;
# Used as a token for replaces
my $tok = "\004";
$line =~ s/$tok//g;
$line =~ s/\&/$tok\&$tok/g;
$line =~ s/$tok\<$tok/g;
$line =~ s/>/$tok\>$tok/g;
$line =~ s/"/$tok\"$tok/g;
$line =~ s{((https?|ftp):\/\/[^$ ]+)(?![^<]*>)}{$interface->link(format_remove($1), format_linkshorten($1))}gie;
$line =~ s{(^|\s)(www\..*?)([\.,]?($|\s)|\)|\002)(?![^<]*>)}{"$1" . $interface->link(format_remove("http://$2"), $2) . $3}gie;
if(exists $ioptions->{smilies} && $ioptions->{smilies}) {
$line =~ s{(?)}{
my($sm, $tmp) = ($1, $1);
for(keys %regexpicon) {
next unless $sm =~ /^$_$/;
$tmp = $interface->smilie("$config->{image_path}/$regexpicon{$_}.gif", $regexpicon{$_}, $sm);
last;
}
$tmp
}ge;
}
$line =~ s/$tok//g;
$line =~ s/( {2,})/' ' x (length $1)/eg;
return format_remove($line) if $config->{removecolour};
if($line =~ /[\002\003\017\022\037]/) {
$line=~ s/\003(\d{1,2})(\,(\d{1,2})|)([^\003\017]*|.*?$)/
my $me = "{$bg}."\" "
}
$me .= "color=\"$format->{$fg}\">$4<\/font>";
$me
/eg;
$line =~ s/\002(.*?)(\002|\017|$)/$1<\/b>/g;
$line =~ s/\022(.*?)(\022|\017|$)/$1<\/u>/g;
$line =~ s/\037(.*?)(\037|\017|$)/$1<\/u>/g;
}
return format_remove($line);
}
sub format_init_smilies {
%regexpicon = (
'\;-?\)' => 'wink',
'\;-?D' => 'grin',
':\'\(?' => 'cry',
':-?/(?!\S)' => 'notsure',
':-?[xX]' => 'confused',
':-?\]' => 'embarassed',
':-?\*' => 'love',
':-?[pP]' => 'tongue',
':-?\)' => 'happy',
'\:-?D' => 'cheesy',
':-?\(' => 'unhappy',
':-[oO]' => 'surprised',
'8-?\)' => 'cool',
':-?\|' => 'flat',
':\'\)' => 'happycry',
"\004\>\004:-?/" => 'hmmm',
"\004\>\004:-?\\(" => 'angry',
':-?\*\*' => 'kiss',
':-z' => 'sleep',
':-\.' => 'sorry',
'8-@' => 'what',
);
$regexpicon = '(' . join('|', sort { length $b <=> length $a } keys %regexpicon) . ')';
}
sub format_linkshorten {
my $link = shift;
if(config_set('linkshorten')) {
return substr($link, 0, $config->{linkshorten})
. (length $link > $config->{linkshorten} ? '...' : '');
}else{
return substr($link, 0, 120)
. (length $link > 120 ? '...' : '');
}
}
## Removes all IRC formating characters
sub format_remove {
my($line) = @_;
$line =~ s/\003(\d{1,2})(\,(\d{1,2})|)//g;
$line =~ s/[\x00-\x1f]//g;
return $line;
}
## Lowlevel code that deals with the format parsing
## No longer supports nested
sub format_parse {
my($line, $info, $params) = @_;
return unless defined $line;
my($match, $name, $param);
$line =~ s{
( # format
\{
([^\}\s]+)
\s?([^\}]+)?
\}
# variables
| (\$[A-Za-z0-9-]+)
| (\%(?:\d{1,2}|n|_|%))
)
}{
($match, $name, $param) = ($1, $2, $3);
if($match =~ /^[\$%]/) {
format_varexpand($match, $info, $params);
}elsif(!exists $format->{$name}) {
error("Invalid format ($name) called: $line");
}else{
format_parse($format->{$name}, $info,
[map {format_varexpand($_, $info, $params)} split / /,
defined $param ? $param : '']);
}
}egx;
return $line;
}
sub format_varexpand {
$_ = shift;
my($info, $params) = @_;
return '' unless defined;
if(s/^\$//) {
if(ref $params && /^(\d+)\-$/) {
return join(' ', @$params[$1 .. @$params - 1]);
}elsif(!/\D/) {
return $params->[$_] if ref $params && defined $params->[$_];
return '';
}elsif(/^VERSION$/) {
return $VERSION;
}elsif(/^T$/ && exists $info->{target}) {
return $info->{target};
}elsif(/^N$/) {
return $irc->{nick}
}elsif(/^S$/) {
return $irc->{server};
}
}elsif(s/^%//) {
if(/^_$/) {
return "\002";
}elsif(/^n$/) {
return "\003$format->{fg},$format->{bg}";
}elsif(/^%$/) {
return "%";
}elsif(/^\d+$/) {
return "\003$_";
}
return "\%$_";
}
return $_;
}
#### Interface Functions
## Loads the default interface.
sub load_interface {
my $name = defined $cgi->{interface} ? $cgi->{interface} : 'default';
($name) = $name =~ /([a-z0-9]+)/i;
require("./interfaces/$name.pm");
$ioptions = parse_interface_cookie();
for(keys %$config) {
next unless s/^interface //;
next if exists $ioptions->{$_};
$ioptions->{$_} = $config->{"interface $_"};
}
$interface = $name->new($event,$timer, $config, $ioptions);
my $bg = $format->{$format->{bg}};
my $fg = $format->{$format->{fg}};
$interface->header($config, $cgi, $bg, $fg);
return $interface;
}
sub interface_show {
my($show, $input) = @_;
return '' unless $interface->exists($show);
return $interface->$show($input, $irc, $config);
}
sub interface_keepalive {
$interface->keepalive($irc, $config);
}
sub interface_lineout {
my($type, $target, $html) = @_;
push(@output, $interface->makeline($type, $target, $html));
}
#### Unix Domain Socket Functions
## Opens the listening socket
sub load_socket {
error('Communication socket name is invalid')
if !$cgi->{R} or $cgi->{R} =~ /[^A-Za-z0-9]/;
($cgi->{R}) = $cgi->{R} =~ /([A-Za-z0-9]+)/;
error('Communication socket already exists')
if -e $config->{socket_prefix}.$cgi->{R};
mkdir($config->{socket_prefix}.$cgi->{R}, 0700) or error("Mkdir error: $!");
open(IP, ">$config->{socket_prefix}$cgi->{R}/ip") or error("Open error: $!");
print IP "$ENV{REMOTE_ADDR}\n";
my $client_ip = $ENV{HTTP_X_FORWARDED_FOR};
$client_ip = $ENV{HTTP_CLIENT_IP} unless defined $client_ip;
print IP "$client_ip\n" if defined $client_ip;
close(IP);
my($socket,$error) =
net_unixconnect($config->{socket_prefix}.$cgi->{R}.'/sock');
error("Error opening socket: $error") unless ref $socket;
select_add($socket);
return $socket;
}
sub unix_in {
my($fh, $line) = @_;
my $input = parse_query($line, ($line =~ /&xmlhttp/ ? 2 : 0));
if($cookie && (!defined $input->{COOKIE} || $input->{COOKIE} ne $cookie)) {
net_send($fh, "Content-type: text/html\r\n\r\nInvalid cookie\r\n");
select_close($fh);
return;
}
$pingtime = time;
$intime = $pingtime if $input->{cmd} eq 'say'
&& $input->{say} ne '/noop';
if($input->{cmd}) {
my $now = time;
utime($now, $now, "$config->{socket_prefix}$cgi->{R}/sock");
input_command($input->{cmd}, $input, $fh, $line);
}
net_send($fh, "Content-type: text/html\r\n\r\n");
if(defined $input->{item} && $input->{item} =~ /^\w+$/) {
net_send($fh, interface_show($input->{item}, $input));
}
select_close($fh);
}
sub input_command {
my($command, $params, $fh, $line) = @_;
if($command eq 'say') {
say_command($params->{say}, $params->{target});
}elsif($command eq 'paste') {
$params = parse_query($line, 1 + ($line =~ /&xmlhttp/ ? 2 : 0));
for(split /\n/, $params->{say}) {
s/\r$//;
next unless $_;
say_command($_, $params->{target});
}
}elsif($command eq 'quit') {
net_send($fh, "Content-type: text/html\r\n\r\nquit\r\n"); # avoid errors
irc_close("");
}elsif($command eq 'options' && length $params->{name} && length $params->{value}) {
$ioptions->{$params->{name}} = $params->{value};
$interface->setoption($params->{name}, $params->{value});
# write proper cookie code one day.
net_send($fh, "Set-Cookie: cgiirc$params->{name}=$params->{value}; path=/; expires=Sun, 01-Jan-2011 00:00:00 GMT\r\n");
}
}
sub say_command {
my($say, $target) = @_;
return unless length $say;
$say =~ s/(\n|\r|\0|\001)//sg;
$target =~ s/(\n|\r|\0|\001)//sg;
if(!config_set('disable_format_input')) {
$say =~ s/\%C/\003/g;
$say =~ s/\%B/\002/g;
$say =~ s/\%U/\037/g;
}
if($say =~ m!^/!) {
if($say =~ s!^/ /!/!) {
irc_send_message($target, $say);
}else{
(my $command, my $params) = $say =~ m|^/([^ ]+)(?: (.+))?$|;
unless(defined $command && length $command) {
return;
}
$command = Command->expand($command);
unless(access_command($command)) {
message('command denied', $command);
return;
}
my $error = Command->run($event, $irc, $command, $target, defined $params ? $params : '', $config, $interface);
return 1 if $error == 100;
if($error == 2) {
message('command notparams', $error);
}else{
message('command error', $error);
}
return 0;
}
}else{
irc_send_message($target, $say);
}
}
#### Access Checking Functions
sub config_set {
my($option) = @_;
return 1 if defined $config->{$option} && $config->{$option};
0;
}
sub access_ipcheck {
return unless config_set('ip_access_file') || config_set("max_users");
my($ip, $hostname) = @_;
my($ipn) = inet_aton($ip);
my($ipaccess_match) = 0;
my($limit) = undef;
my %ips = list_connected_ips();
my $total = 0;
$total += $ips{$_} for keys %ips;
if(config_set("max_users") && $total > $config->{max_users}) {
message('access denied', 'Too many connections (global)');
irc_close();
}
return unless config_set('ip_access_file');
for my $ipaccess_file (split(',', $config->{ip_access_file})) {
# If any of the files don't exist, we just skip them.
open(IP, "<" . ($ipaccess_file =~ m!^/! ? '' : $config_path)
. $ipaccess_file) or next;
while() {
chomp;
next if /^\s*(#|$)/;
s/\s+#.*$//g;
my($check);
($check, $limit) = split(' ', $_, 2);
if ($check =~ /\//) {
# IP address with subnet mask
my($addr,$mask) = split('/', $check, 2);
$mask = "1" x $mask . "0" x (32-$mask);
$mask = pack ("B32", $mask);
$mask = inet_ntoa($mask & $ipn);
if($addr eq $mask) {
$ipaccess_match = 1;
}
} else {
# IP or hostname (we check both)
# XXX: someone could make their hostname resolve to
# 127.0.0.1.foobar.com and it would match eg 127.*.*.*
# I don't think it's that serious and if it really is a
# problem, 127.0.0.0/8 wouldn't match.
$check =~ s/\./\\./g;
$check =~ s/\?/./g;
my $ipcheck = $check;
$ipcheck =~ s/\*/\\d+/g;
$check =~ s/\*/.*/g;
if($ip =~ /^$ipcheck$/) {
$ipaccess_match = 1;
} elsif($hostname =~ /^$check$/i) {
$ipaccess_match = 1;
}
}
# We stop parsing, if this line matched.
last if $ipaccess_match;
}
close(IP);
# We don't parse more files, if a line in the last file matched.
last if $ipaccess_match;
}
# If we got a matching line...
if($ipaccess_match) {
# We just accept the client, if there is no limit defined.
return unless defined $limit;
if($limit == 0) {
message('access denied', "No connections allowed from your hostname $hostname or your IP address $ip");
} elsif($ips{$ip} >= $limit) {
message('access denied', 'Too many connections');
} else {
return;
}
} else {
message('access denied', 'No connections allowed');
}
irc_close();
}
sub access_dnsbl {
my $ip = shift;
return unless config_set('dnsbl');
my $arpa = join '.', reverse split /\./, $ip;
for my $zone(split ' ', $config->{dnsbl}) {
my $res = net_hostlookup("$arpa.$zone.");
if(defined $res) {
message('access denied', "Found in DNS black list $zone (your IP is $ip, result: $res)");
irc_close();
}
}
}
sub list_connected_ips {
my %ips = ();
(my $dir, my $prefix) = $config->{socket_prefix} =~ /^(.*\/)([^\/]+)$/;
opendir(TMPDIR, "$dir") or return ();
for(readdir TMPDIR) {
next unless /^\Q$prefix\E/;
next unless -o $dir . $_ && -d $dir . $_;
next unless -f "$dir$_/server";
open(TMP, "<$dir$_/ip") or next;
chomp(my $tmp = );
$ips{$tmp}++;
close(TMP);
}
closedir(TMPDIR);
return %ips;
}
sub access_configcheck {
my($type, $check) = @_;
if(config_set("default_$type")) {
my %tmp;
@tmp{split /,\s*/, lc $config->{"default_$type"}} = 1;
return 1 if exists $tmp{lc $check};
}
return 0 unless config_set('allow_non_default') && config_set("access_$type");
return 1 if $check =~ /^$config->{"access_$type"}$/i;
0;
}
sub access_command {
my($command) = @_;
return 1 unless config_set('access_command');
for(split / /, $config->{access_command}) {
if(/^!(.*)/) {
return 0 if $command =~ /^$1/i;
}else{
return 1 if $command =~ /^$_/i;
}
}
return 1;
}
sub encode_ip {
return join('',map(sprintf("%0.2x", $_), split(/\./,shift)));
}
# Resolve host *and* do checks against hosts that are allowed to connect.
# Note: this follows proxies (via X-Forwarded-For header - but only if the
# proxy is listed in the trusted-proxy file).
sub access_check_host {
my $ip = defined $_[0] ? $_[0] : $ENV{REMOTE_ADDR};
my $ipn = inet_aton($ip);
access_dnsbl($ip);
my($hostname) = gethostbyaddr($ipn, AF_INET);
unless(defined $hostname && $hostname) {
access_ipcheck($ip, $ip);
return($ip, $ip);
}
# Check reverse == forward
my(undef,undef,undef,undef,@ips) = gethostbyname($hostname);
my $ok = 0;
for(@ips) {
$ok = 1 if $_ eq $ipn;
}
if(!$ok) {
access_ipcheck($ip, $ip);
return($ip, $ip);
}
access_ipcheck($ip, $hostname);
my $client_ip = $ENV{HTTP_X_FORWARDED_FOR};
$client_ip = $ENV{HTTP_CLIENT_IP} unless defined $client_ip;
if(defined $client_ip
&& $client_ip =~ /((\d{1,3}\.){3}\d{1,3})$/
&& !defined $_[1]) { # check proxy but only once
my $proxyip = $1;
return($hostname, $ip) if $proxyip =~ /^(192\.168|127|10|172\.(1[6789]|2\d|3[01]))\./;
open(TRUST, "<${config_path}trusted-proxy") or return($hostname, $ip);
while() {
chomp;
s/\*/.*/g;
s/\?/./g;
return access_check_host($proxyip, 1) if $hostname =~ /^$_$/i;
}
close TRUST;
}
return($hostname, $ip);
}
sub session_timeout {
return unless defined $intime;
if(config_set('session_timeout') &&
(time - $config->{session_timeout}) > $intime) {
message('session timeout');
irc_close('Session timeout');
}elsif($interface->ping && $pingtime < time - 300) {
irc_close('Ping timeout');
}elsif($interface->ping && $pingtime < time - 240) {
$interface->sendping;
}
}
#### IRC Functions
## Opens the connection to IRC
sub irc_connect {
my($server, $port) = @_;
error("No server specified") unless $server;
message('looking up', $server);
flushoutput(); # this stuff can block - keep the user informed
my $ip = net_hostlookup($server);
unless(defined $ip) {
error("Looking up address: $! ($?)");
}
message('connecting', $server, $ip, $port);
flushoutput();
my($fh, $error) = net_tcpconnect($ip, $port);
error("Connecting to IRC: $error") unless ref $fh;
select_add($fh);
return $fh;
}
sub irc_write_server {
my($lip, $lport, $rip, $rport) = @_;
open(S, ">$config->{socket_prefix}$cgi->{R}/server")
or error("Opening server file: $!");
print S "$rip:$rport\n$lip:$lport\n";
close(S);
}
## Sends data to the irc connection
sub irc_out {
my($event,$fh,$data) = @_;
$data = $fh, $fh = $event if !$data;
net_send($fh, $data . "\r\n");
}
sub irc_close {
my $message = shift;
$message = 'EOF' unless defined $message;
$message = (config_set('quit_prefix') ? $config->{quit_prefix} : "CGI:IRC") .
($message ? " ($message)" : '');
flushoutput();
exit unless ref $unixfh;
close($unixfh);
my $t = $config->{socket_prefix} . $cgi->{R};
unlink("$t/sock", "$t/ip", "$t/server", "$t/ident");
exit unless rmdir($t);
exit unless ref $ircfh;
net_send($ircfh, "QUIT :$message\r\n");
my $info = { target => '-all', activity => 1 };
my $close = format_colourhtml(format_parse($format->{'irc close'}, $info));
my $url = defined $config->{form_redirect} ? $config->{form_redirect} : $config->{script_login};
$close =~ s/\((.*?)\)/"(" . $interface->reconnect($url, $1) . ")"/e;
interface_lineout($info, $close);
flushoutput();
$interface->end if ref $interface;
sleep 1;
close($ircfh);
exit;
}
sub irc_connected {
my($event, $self, $server, $nick) = @_;
open(SERVER, ">>$config->{socket_prefix}$cgi->{R}/server")
or error("Writing to server file; $!");
print SERVER "$server\n$nick\n";
close(SERVER);
my $key;
$key = $1 if $cgi->{chan} =~ s/ (.+)$//;
unless(access_configcheck('channel', $cgi->{chan})) {
message('access channel denied', $cgi->{chan});
$cgi->{chan} = (split /,/, $config->{default_channel})[0];
}
$irc->join($cgi->{chan} . (defined $key ? ' ' . $key : ''));
say_command($_, 'Status') for split(/;/, $config->{perform});
}
sub irc_send_message {
my($target, $text) = @_;
$event->handle('message ' .
(
$irc->is_channel($target) ? 'public'
: 'private' .
($interface->query ? ' window' : '')
) . ' own',
{ target => $target, create => 1 },
$irc->{nick}, $irc->{myhost}, $text);
$irc->msg($target,$text);
}
sub irc_event {
my($event, $name, $info, @params) = @_;
return if $name =~ /^user /;
$info->{type} = $name;
if($name =~ /^raw/) {
#message('default', "Unhandled numeric: $name");
my $params = $params[0];
$info->{activity} = 1;
$info->{target} = defined $params->{params}->[2] ? $params->{params}->[2] : 'Status';
@params = (join(' ', defined $params->{params}->[2]
? @{$params->{params}}[2 .. @{$params->{params}} - 1]
: ''),
defined $params->{text}
? $params->{text}
: '');
}elsif($name =~ /^ctcp/) {
return irc_ctcp($name, $info, @params);
}elsif($name eq 'message public' && $params[2] =~ /^\Q$irc->{nick}\E\W/i) {
$info->{activity} = 3;
$name = 'message public hilight';
}elsif($name eq 'message private' && $interface->query) {
$name = 'message private window';
}
if(exists $format->{$name}) {
format_out($name, $info, \@params);
}else{
format_out('default', $info, \@params);
}
}
sub irc_ctcp {
my($name, $info, $to, $nick, $host, $command, $params) = @_;
if($name eq 'ctcp own msg') {
format_out('ctcp own msg', $info, [$nick, $host, $command, $params]);
}elsif($name =~ /^ctcp msg /) {
if(uc($command) eq 'KILL') {
return unless config_set('admin password');
my $crypt = $config->{'admin password'};
my($password, $reason) = split ' ', $params, 2;
return unless length $password and length $crypt;
if(crypt($password, substr($crypt, 0, 2)) eq $crypt) {
message('kill ok', $nick, $reason);
irc_out($ircfh, "QUIT :Killed ($nick ($reason))");
irc_close();
}else{
message('kill wrong', $nick, $reason);
}
}elsif(uc($command) eq 'ACTION' && $irc->is_channel($info->{target})) {
format_out('action public', $info, [$nick, $host, $params]);
return;
}elsif(uc($command) eq 'ACTION') {
format_out('action private', $info, [$nick, $host, $params]);
return;
}elsif(uc($command) eq 'DCC' && lc $to eq lc $irc->{nick}) {
format_out('not supported', $info, [$nick, $host, $params, "DCC"]);
}else{
format_out('ctcp msg', $info, [$to, $nick, $host, $command, $params]);
}
if(defined $ctcptime && $ctcptime > time-4) {
$ctcptime = time;
return;
}
$ctcptime = time;
if(uc($command) eq 'VERSION') {
$irc->ctcpreply($nick, $command,
"CGI:IRC $VERSION - http://cgiirc.sf.net/");
}elsif(uc($command) eq 'PING') {
return if $params =~ /[^0-9 ]/ || length $params > 50;
unless($interface->ctcpping($nick, $params)) {
$irc->ctcpreply($nick, $command, $params);
}
}elsif(uc($command) eq 'USERINFO') {
my $client_ip = $ENV{HTTP_X_FORWARDED_FOR};
$client_ip = $ENV{HTTP_CLIENT_IP} unless defined $client_ip;
$client_ip = 'none' unless defined $client_ip;
$irc->ctcpreply($nick, $command,
config_set('extra_userinfo') ?
"IP: $ENV{REMOTE_ADDR} - Proxy: $ENV{HTTP_VIA} - " .
"Forward IP: $client_ip - User Agent: " .
"$ENV{HTTP_USER_AGENT} - Host: $ENV{SERVER_NAME}"
: "$ENV{REMOTE_ADDR} - $ENV{HTTP_USER_AGENT}"
);
}elsif(uc($command) eq 'TIME') {
$irc->ctcpreply($nick, $command,
scalar localtime());
}elsif(uc($command) eq 'DCC' && lc $to eq lc $irc->{nick}) {
my($type, $subtype) = split ' ', $params;
$type .= " $subtype";
$type = substr($type, 0, 20);
$irc->ctcpreply($nick, $command, "REJECT $type Not Supported");
}
}else{
if(uc($command) eq 'PING') {
$params = time - $params . " seconds";
}
format_out('ctcp reply', $info, [$nick, $host, $command, $params]);
}
}
#### prints a very simple header
sub header {
print "HTTP/1.0 200 OK\r\n" if $0 =~ /nph-/;
print join("\r\n",
'Content-type: text/html; charset=utf-8',
'Pragma: no-cache',
'Cache-control: must-revalidate, no-cache, no-store',
'Expires: -1',
"\r\n");
}
sub flushoutput {
if(@output) {
$interface->lines(@output);
@output = ( );
}
}
#### Error Reporting
sub error {
my $message = "@_";
header() unless $config;
if(defined $interface && ref $interface) {
flushoutput();
if(ref $format) {
my $format = format_parse($format->{error}, {}, [$message]);
$format = format_colourhtml($format);
$interface->error($format);
}else{
$interface->error("Error: $message");
}
}else{
print "An error occured: $message\n";
}
print STDERR "[" . scalar localtime() . "] CGI:IRC Error: $message (" . join(' ',(caller(1))[3,2]) . ")";
irc_close("Error");
}
#### Init
sub init {
$timer = new Timer;
$event = new Event;
$timer->addforever(interval => 15, code => \&interface_keepalive);
$event->add('irc out', code => \&irc_out);
$event->add('unhandled', code => \&irc_event);
$event->add('server connected', code => \&irc_connected);
$config = parse_config($config_path . 'cgiirc.config');
$config->{socket_prefix} ||= '/tmp/cgiirc-';
($config->{socket_prefix}) = $config->{socket_prefix} =~ /(.*)/;
$config->{encoded_ip} = 2 unless exists $config->{encoded_ip};
$config->{access_command} = '!quote' unless exists $config->{access_command};
$config->{format} ||= 'default';
$timer->addforever(interval => 30, code => \&session_timeout);
$cgi = parse_query($ENV{QUERY_STRING});
format_init_smilies();
$format = load_format($cgi->{format});
$cookie = parse_cookie();
header();
error('No CGI Input') unless keys %$cgi;
$cgi->{serv} ||= (split /,/, $config->{default_server})[0];
$cgi->{chan} ||= (split /,/, $config->{default_channel})[0];
$cgi->{port} ||= $config->{default_port};
$cgi->{nick} ||= $config->{default_nick};
$cgi->{name} ||= $config->{default_name};
if($::ENCODE) {
eval {
local $SIG{__DIE__};
binmode STDOUT, ":utf8";
};
}
$cgi->{nick} =~ s/\?/int rand 10/eg;
$interface = load_interface();
if(config_set('login secret')) {
require Digest::MD5;
my $token = Digest::MD5::md5_hex($cgi->{time}
. $config->{'login secret'} . $cgi->{R});
if($token ne $cgi->{token}) {
error("Invalid login token!");
# 30 seconds should be enough (there's no user interaction)
} elsif((time - 30) > $cgi->{time}) {
error("Login token out of date, try logging in again!");
}
}
$cgi->{charset} ||= $config->{'irc charset'} || 'utf8';
if($cgi->{charset} && $::ENCODE && Encode::find_encoding($cgi->{charset})) {
$config->{'irc charset'} = $cgi->{charset};
} elsif($cgi->{charset} && $::ENCODE) {
if($cgi->{charset} =~ /\(([^ )]+)/ || $cgi->{charset} =~ /([^ ]+)/) {
my $charset = $1;
if(Encode::find_encoding($charset)) {
$config->{'irc charset'} = $charset;
} else {
message('default', "Unknown encoding: $charset");
$config->{'irc charset'} = 'utf8';
}
}
}
my($resolved, $resolvedip) = access_check_host($ENV{REMOTE_ADDR});
unless(access_configcheck('server', $cgi->{serv})) {
message('access server denied', $cgi->{serv});
$cgi->{serv} = (split /,/, $config->{default_server})[0];
}
($cgi->{serv}) = $cgi->{serv} =~ /([^ ]+)/; # untaint hack.
if($cgi->{serv} =~ s/:(\d+)$//) {
$cgi->{port} = $1;
}
unless(access_configcheck('port', $cgi->{port})) {
message('access port denied', $cgi->{port});
$cgi->{port} = (split /,/, $config->{default_port})[0];
}
($cgi->{port}) = $cgi->{port} =~ /(\d+)/;
if(config_set('encoded_ip')) {
$cgi->{name} = '[' .
($config->{encoded_ip} <= 2
? encode_ip($resolvedip)
# The resolved hostname in realname if set to 3.
: $resolved
)
. '] ' . $cgi->{name};
}
if(config_set('realhost_as_password')) {
$cgi->{pass} = "CGIIRC_${resolvedip}_${resolved}";
}
my $preconnect;
if(config_set('webirc_password')) {
$preconnect = "WEBIRC $config->{webirc_password} cgiirc $resolved $resolvedip";
}
$unixfh = load_socket();
if(exists $ENV{REMOTE_USER}) {
open(IDENT, ">$config->{socket_prefix}$cgi->{R}/ident")
or error("Ident file: $!");
print IDENT "$ENV{REMOTE_USER}\n";
close(IDENT);
}
message('cgiirc welcome') if exists $format->{'cgiirc welcome'};
$ircfh = irc_connect($cgi->{serv}, $cgi->{port});
$irc = IRC->new(
event => $event,
timer => $timer,
fh => $ircfh,
nick => $cgi->{nick},
# yet another form of host spoofing uses these..
server => $resolvedip,
host => $resolved,
password => defined $cgi->{pass}
? $cgi->{pass}
: (config_set('server_password')
? $config->{server_password}
: ''
),
realname => $cgi->{name},
user => config_set('encoded_ip') && $config->{encoded_ip} > 1
? encode_ip($resolvedip)
: (config_set('default_user')
? $config->{default_user}
: $cgi->{nick}
),
preconnect => $preconnect,
);
# It is usually better to use 'server connected' (this is for the JS
# interface so it knows the script has started ok).
$event->handle("user connected", $irc);
$interface->sendping if $interface->ping;
$intime = $pingtime = time;
}
#### Main loop
sub main_loop {
error("Required objects not loaded")
unless ref $timer
and ref $event
and ref $config;
while(1) {
my @ready = select_canread(2);
for my $fh(@ready) {
if($fh == $unixfh) {
my $newfh = Symbol::gensym;
if(accept($newfh, $fh)) {
net_autoflush($newfh);
select_add($newfh);
}
}else{
my($tmp,$char);
$tmp = sysread( $fh, $char, 4096 );
select_close($fh) unless defined $tmp && length $char;
$inbuffer{$fh} .= $char;
while (my($theline,$therest)=$inbuffer{$fh} =~ /([^\n]*)\n(.*)/s ) {
$inbuffer{$fh} = $therest;
$theline =~ s/\r$//;
if($fh == $ircfh) {
if($::ENCODE) {
my $input = Encode::decode($config->{'irc charset'}, $theline);
$theline = $input if defined $input;
}
$irc->in($theline);
}else{
unix_in($fh,$theline);
}
}
flushoutput();
}
}
irc_close() if $needtodie;
$timer->run;
}
}
init();
main_loop();