1# -----------------------------------------------------------------------------
2# $Id: Client.pm 28573 2009-01-17 18:01:46Z topia $
3# -----------------------------------------------------------------------------
4# IrcIO::Clientはクライアントからの接続を受け、
5# IRCメッセージをやり取りするクラスです。
6# -----------------------------------------------------------------------------
7package IrcIO::Client;
8use strict;
9use warnings;
10use Carp;
11use base qw(IrcIO);
12use Crypt;
13use Multicast;
14use Mask;
15use LocalChannelManager;
16use NumericReply;
17use Tiarra::Resolver;
18use Tiarra::Socket;
19use Tiarra::Utils;
20utils->define_attr_getter(0, qw(logging_in username),
21			  qw(client_host client_addr client_host_repr));
22
23
24# 複数のパッケージを混在させてるとSelfLoaderが使えない…?
25#use SelfLoader;
26#SelfLoader->load_stubs; # このクラスには親クラスがあるから。(SelfLoaderのpodを参照)
27#1;
28#__DATA__
29
30sub new {
31    my ($class,$runloop,$sock) = @_;
32    my $this = $class->SUPER::new($runloop);
33    $this->attach($sock);
34    $this->{pass_received} = ''; # クライアントから受け取ったパスワード
35    $this->{nick} = ''; # ログイン時にクライアントから受け取ったnick。変更されない。
36    $this->{username} = ''; # 同username
37    $this->{logging_in} = 1; # ログイン中なら1
38    $this->{options} = {}; # クライアントが接続時に$key=value$で指定したオプション。
39    my $addr = $sock->peerhost;
40    $this->{client_host} = $this->{client_addr} = $addr;
41    ::printmsg("One client at ".$this->{client_addr}." connected to me. " .
42		   "Please wait to get hostname of this address.");
43    Tiarra::Resolver->paranoid_check($addr, sub {
44					 $this->accept(@_);
45				     });
46    $this;
47}
48
49sub accept {
50    my ($this, $paranoid_ok, $host, $entry) = @_;
51
52    $this->{client_host} = $paranoid_ok ? $host : $this->{client_addr};
53    $this->{client_host_repr} = Tiarra::Socket->repr_destination(
54	host => $this->{client_host},
55	addr => $this->{client_addr});
56
57    # このホストからの接続は許可されているか?
58    my $allowed_host = $this->_conf_general->client_allowed;
59    if (defined $allowed_host) {
60	unless (Mask::match($allowed_host,$this->{client_host}) ||
61		Mask::match($allowed_host,$this->{client_addr})) {
62	    # マッチしないのでdie。
63	    die "Disconnect the client at ".$this->{client_host_repr}.". The host is not allowed.\n";
64	}
65    }
66    ::printmsg("Accepted connection of the client at ".$this->{client_host_repr}.".");
67    $this->install;
68    $this;
69}
70
71sub disconnect {
72    my ($this, $genre, $errno, @params) = @_;
73
74    $this->SUPER::disconnect($genre, $errno, @params);
75    if (defined $errno) {
76	::printmsg($this->sock_errno_to_msg(
77	    $errno,
78	    "Disconnected Client from ".$this->{client_host_repr}.": $genre error"));
79    } else {
80	::printmsg("Disconnected Client from ".$this->{client_host_repr}.".");
81    }
82}
83
84sub fullname {
85    # このクライアントをtiarraから見たnick!username@userhostの形式で表現する。
86    my ($this,$type) = @_;
87    if (defined $type && $type eq 'error') {
88	$this->_runloop->current_nick.'['.$this->{username}.'@'.$this->{client_host}.']';
89    }
90    else {
91	$this->_runloop->current_nick.'!'.$this->{username}.'@'.$this->{client_host};
92    }
93}
94
95sub fullname_from_client {
96    # このクライアントをクライアントから見たnick!username@userhostの形式で表現する。
97    # この関数が返すnickは初めに受け取ったものである点に注意。
98    my $this = shift;
99    $this->{nick}.'!'.$this->{username}.'@'.$this->{client_host};
100}
101
102sub parse_realname {
103    my ($this,$realname) = @_;
104    return if !defined $realname;
105    # $key=value;key=value;...$
106    #
107    # 以下は全て有効で、同じ意味である。
108    # $ foo = bar; key=  value$
109    # $ foo=bar;key=value $
110    # $foo    =bar;key=  value    $
111
112    my $key = qr{[^=:]+?}; # キーとして許されるパターン
113    my $value = qr{[^;]*?}; # 値として許されるパターン
114    my $sep = qr{[:=]};
115    my $lastpair = qr{$key\s*$sep\s*$value};
116    my $pair = qr{$lastpair\s*;};
117
118    my $line = qr{^\$(?:\s*($pair)\s*)*\s*($lastpair)\s*\$$};
119    if (my @pairs = ($realname =~ m/$line/g)) {
120	%{$this->{options}} = map {
121	    m/^\s*($key)\s*$sep\s*($value)\s*;?$/;
122	} grep {
123	    defined;
124	} @pairs;
125    }
126}
127
128sub option {
129    # ログイン時に$key=value$で指定されたオプションを取得する。
130    # 指定されたキーに対する値が存在しなかった場合はundefを返す。
131    my ($this,$key) = @_;
132    if (defined $key) {
133	$this->{options}->{$key};
134    }
135    else {
136	croak "IrcIO::Client->option, Arg[1] was undef.";
137    }
138}
139
140sub option_or_default {
141    my ($this, $base, $config_prefix, $option_prefix, $default) = @_;
142    my $value;
143
144    utils->get_first_defined(
145	$this->option(utils->to_str($option_prefix).$base),
146	$this->_conf_general->get(utils->to_str($option_prefix).$base),
147	$default);
148}
149
150sub option_or_default_multiple {
151    my ($this, $base, $types, $config_prefix) = @_;
152
153    return utils->get_first_defined(
154	(map {
155	    $this->option(join('',utils->to_str($_, $base)));
156	} @$types),
157	(map {
158	    $this->_conf_general->get(
159		join('',utils->to_str($config_prefix, $_, $base)));
160	} @$types));
161}
162
163sub send_message {
164    my ($this,$msg) = @_;
165
166    # 各モジュールに通知
167    #$this->_runloop->notify_modules('notification_of_message_io',$msg,$this,'out');
168
169    $this->SUPER::send_message(
170	$msg,
171	$this->option_or_default_multiple('encoding', ['out-', ''], 'client-'));
172}
173
174sub read {
175    my ($this) = shift;
176    $this->SUPER::read(
177	$this->option_or_default_multiple('encoding', ['in-', ''], 'client-'));
178
179    # 接続が切れたら、各モジュールへ通知
180    if (!$this->connected) {
181	$this->_runloop->notify_modules('client_detached',$this);
182    }
183}
184
185sub pop_queue {
186    my $this = shift;
187    my $msg = $this->SUPER::pop_queue;
188
189    # クライアントがログイン中なら、ログインを受け付ける。
190    if (defined $msg) {
191	# 各モジュールに通知
192	#$this->_runloop->notify_modules('notification_of_message_io',$msg,$this,'in');
193
194	# ログイン作業中か?
195	if ($this->{logging_in}) {
196	    return $this->_receive_while_logging_in($msg);
197	}
198	else {
199	    return $this->_receive_after_logged_in($msg);
200	}
201    }
202    return $msg;
203}
204
205sub _receive_while_logging_in {
206    my ($this,$msg) = @_;
207
208    # NICK及びUSERを受け取った時点でそのログインの正当性を確認し、作業を終了する。
209    my $command = $msg->command;
210    if ($command eq 'PASS') {
211	$this->{pass_received} = $msg->params->[0];
212    }
213    elsif ($command eq 'NICK') {
214	$this->{nick} = $msg->params->[0];
215    }
216    elsif ($command eq 'USER') {
217	$this->{username} = $msg->param(0);
218	$this->parse_realname($msg->param(3));
219    }
220    elsif ($command eq 'PING') {
221	$this->send_message(
222	    $this->construct_irc_message(
223		Command => 'PONG',
224		Param => $msg->param(0)));
225    }
226    elsif ($command eq 'QUIT') {
227	$this->send_message(
228	    $this->construct_irc_message(
229		Command => 'ERROR',
230		Param => 'Closing Link: ['.$this->fullname_from_client.'] ()'));
231	$this->disconnect_after_writing;
232    }
233
234    if ($this->{nick} ne '' && $this->{username} ne '') {
235	# general/tiarra-passwordを取得
236	my $valid_password = $this->_conf_general->tiarra_password;
237	my $prefix = $this->_runloop->sysmsg_prefix('system');
238	if (defined $valid_password && $valid_password ne '' &&
239	    ! Crypt::check($this->{pass_received},$valid_password)) {
240	    # パスワードが正しくない。
241	    ::printmsg("Refused login of ".$this->fullname_from_client." because of bad password.");
242
243	    $this->send_message(
244		$this->construct_irc_message(Prefix => $prefix,
245			       Command => ERR_PASSWDMISMATCH,
246			       Params => [$this->{nick},'Password incorrect']));
247	    $this->send_message(
248		$this->construct_irc_message(Command => 'ERROR',
249			       Param => 'Closing Link: ['.$this->fullname_from_client.'] (Bad Password)'));
250		$this->disconnect_after_writing;
251	}
252	else {
253	    # パスワードが正しいか、指定されていない。
254	    ::printmsg('Accepted login of '.$this->fullname_from_client.
255			   ', from '.$this->{client_host_repr}.'.');
256	    if ((my $n_options = keys %{$this->{options}}) > 0) {
257		# オプションが指定されていたら表示する。
258		my $options = join ' ; ',map {
259		    "$_ = $this->{options}->{$_}";
260		} keys %{$this->{options}};
261		::printmsg('Given option'.($n_options == 1 ? '' : 's').': '.$options);
262	    }
263	    $this->{logging_in} = 0;
264
265	    # 実際にはループではない。
266	    while (1) {
267		$this->send_message(
268		    $this->construct_irc_message(Prefix => $prefix,
269						 Command => RPL_WELCOME,
270						 Params => [$this->{nick},'Welcome to the Internet Relay Network '.$this->fullname_from_client]));
271
272		my $current_nick = $this->_runloop->current_nick;
273		if ($this->{nick} ne $current_nick) {
274		    # クライアントが送ってきたnickとローカルのnickが食い違っているので正しいnickを教える。
275		    $this->send_message(
276			$this->construct_irc_message(Prefix => $this->fullname_from_client,
277						     Command => 'NICK',
278						     Param => $current_nick));
279		}
280
281		my $send_message = sub {
282		    my ($command, @params) = @_;
283		    $this->send_message(
284			$this->construct_irc_message(
285			    Prefix => $prefix,
286			    Command => $command,
287			    Params => [$current_nick,
288				       @params],
289			   ));
290		};
291
292		map {
293		    # ローカルnickとグローバルnickが食い違っていたらその旨を伝える。
294		    my $network_name = $_->network_name;
295		    my $global_nick = $_->current_nick;
296		    if ($global_nick ne $current_nick) {
297			$this->send_message(
298			    $this->construct_irc_message(
299				Prefix => $this->_runloop->sysmsg_prefix(qw(priv system)),
300				Command => 'NOTICE',
301				Params => [$current_nick,
302					   "*** Your global nick in $network_name is currently '$global_nick'."]));
303		    }
304		} values %{$this->_runloop->networks};
305
306		$send_message->(RPL_YOURHOST, "Your host is $prefix, running version ".::version());
307		if (!$this->_runloop->multi_server_mode_p) {
308		    # single server mode
309		    my $network = ($this->_runloop->networks_list)[0];
310
311		    if (defined $network) {
312			# send isupport
313			my $msg_tmpl = $this->construct_irc_message(
314			    Prefix => $prefix,
315			    Command => RPL_ISUPPORT,
316			    Params => [$current_nick],
317			   );
318			# last param is reserved for 'are supported...'
319			# and first param for nick
320			my $max_params = $this->irc_message_class->MAX_PARAMS - 2;
321			my @params = ();
322			my $length = 0;
323			my $flush_msg = sub {
324			    if (@params) {
325				my $msg = $msg_tmpl->clone;
326				$msg->push(@params);
327				$msg->push('are supported by this server');
328				$this->send_message($msg);
329			    }
330			    @params = ();
331			    $length = 0;
332			};
333			foreach my $key (keys %{$network->isupport}) {
334			    my $value = $network->isupport->{$key};
335			    my $str = length($value) ? ($key.'='.$value) : $key;
336			    $length += length($str) + 1; # $str and space
337			    # 余裕を見て400バイトを越えたら行を分ける。
338			    if ($length >= 400 || scalar(@params) >= $max_params) {
339				$flush_msg->();
340				$length = length($str);
341			    }
342			    push(@params, $str);
343			}
344			$flush_msg->();
345		    }
346		}
347		$send_message->(RPL_MOTDSTART, "- $prefix Message of the Day -");
348		foreach my $line (main::get_credit()) {
349		    $send_message->(RPL_MOTD, "- ".$line);
350		}
351		$send_message->(RPL_ENDOFMOTD, "End of MOTD command.");
352
353		# クライアントに出力。
354		# その結果切断されたらループを抜ける。
355		$this->flush;
356		last unless $this->connected;
357
358		# joinしている全てのチャンネルの情報をクライアント送る。
359		$this->inform_joinning_channels;
360
361		# 切断されていたらループを抜ける。
362		last unless $this->connected;
363
364		# 各モジュールにクライアント追加の通知を出す。
365		$this->_runloop->notify_modules('client_attached',$this);
366
367		# 必ずループを抜ける。
368		last;
369	    }
370	}
371    }
372    # ログイン作業中にクライアントから受け取ったいかなるメッセージもサーバーには送らない。
373    return undef;
374}
375
376sub _receive_after_logged_in {
377    my ($this,$msg) = @_;
378
379    # ログイン中でない。
380    my $command = $msg->command;
381
382    if ($command eq 'NICK') {
383	if (defined $msg->params) {
384	    # 形式が正しい限りNICKには常に成功して、RunLoopのカレントnickが変更になる。
385	    # ただしネットワーク名が明示されていた場合はカレントを変更しない。
386	    my $rawnick = $msg->params->[0];
387	    my ($nick,undef,$specified) = Multicast::detach($rawnick);
388	    if (Multicast::nick_p($nick)) {
389		unless ($specified) {
390		    if ($this->_runloop->multi_server_mode_p &&
391			    $this->_runloop->current_nick ne $rawnick) {
392			$this->_runloop->broadcast_to_clients(
393			    $this->construct_irc_message(
394				Command => 'NICK',
395				Param => $rawnick,
396				Remarks => {'fill-prefix-when-sending-to-client' => 1}));
397
398			$this->_runloop->set_current_nick($rawnick);
399		    }
400		}
401	    } else {
402		$this->send_message(
403		    $this->construct_irc_message(
404			Prefix => $this->_runloop->sysmsg_prefix('system'),
405			Command => ERR_ERRONEOUSNICKNAME,
406			Params => [$this->_runloop->current_nick,
407				   $rawnick,
408				   'Erroneous nickname']));
409		# これは鯖に送らない。
410		$msg = undef;
411	    }
412	} else {
413	    $this->send_message(
414		$this->construct_irc_message(
415		    Prefix => $this->_runloop->sysmsg_prefix('system'),
416		    Command => ERR_NONICKNAMEGIVEN,
417		    Params => [$this->_runloop->current_nick,
418			       'No nickname given']));
419	    # これは鯖に送らない。
420	    $msg = undef;
421	}
422    }
423    elsif ($command eq 'QUIT') {
424	my $quit_message = $msg->param(0);
425	$quit_message = '' unless defined $quit_message;
426
427	$this->send_message(
428	    $this->construct_irc_message(Command => 'ERROR',
429			   Param => 'Closing Link: '.$this->fullname('error').' ('.$quit_message.')'));
430	$this->disconnect_after_writing;
431
432	# 接続が切れた事にする。
433	$this->_runloop->notify_modules('client_detached',$this);
434
435	# これは鯖に送らない。
436	$msg = undef;
437    }
438    else {
439	$msg = LocalChannelManager->shared
440	    ->message_arrived($msg, $this);
441    }
442    return $msg;
443}
444
445sub do_namreply {
446    my ($this, $ch, $network, $max_length, $flush_func) = @_;
447
448    $max_length = 400 if !defined $max_length;
449    croak('$ch is not specified') if !defined $ch;
450    croak('$network is not specified') if !defined $network;
451    croak('$flush_func is not specified') if !defined $flush_func;
452    my $global_to_local = sub {
453	Multicast::global_to_local(shift, $network);
454    };
455    my $ch_property_char = do {
456	if ($ch->switches('s')) {
457	    '@';
458	}
459	elsif ($ch->switches('p')) {
460	    '*';
461	}
462	else {
463	    '=';
464	}
465    };
466    # 余裕を見てnickの列挙部が $max_length(デフォルト:400) バイトを越えたら行を分ける。
467    my $nick_enumeration = '';
468    my $flush_enum_buffer = sub {
469	if ($nick_enumeration ne '') {
470	    $flush_func->(
471		$this->construct_irc_message(
472		    Prefix => $this->_runloop->sysmsg_prefix('system'),
473		    Command => RPL_NAMREPLY,
474		    Params => [$this->_runloop->current_nick,
475			       $ch_property_char,
476			       Multicast::attach_for_client($ch->name, $network->network_name),
477			       $nick_enumeration]));
478	    $nick_enumeration = '';
479	}
480    };
481    my $append_to_enum_buffer = sub {
482	my $nick_to_append = shift;
483	if ($nick_enumeration eq '') {
484	    $nick_enumeration = $nick_to_append;
485	}
486	else {
487	    $nick_enumeration .= ' '.$nick_to_append;
488	}
489    };
490    map {
491	my $person = $_;
492	my $mode_char = do {
493	    if ($person->has_o) {
494		'@';
495	    }
496	    elsif ($person->has_v) {
497		'+';
498	    }
499	    else {
500		'';
501	    }
502	};
503	$append_to_enum_buffer->($mode_char . $global_to_local->($person->person->nick));
504	if (length($nick_enumeration) > $max_length) {
505	    $flush_enum_buffer->();
506	}
507    } values %{$ch->names};
508    $flush_enum_buffer->();
509
510    undef;
511}
512
513sub inform_joinning_channels {
514    my $this = shift;
515    my $local_nick = $this->_runloop->current_nick;
516
517    my $send_channelinfo = sub {
518	my ($network, $ch) = @_;
519	my $ch_name = Multicast::attach_for_client($ch->name, $network->network_name);
520
521	# まずJOIN
522	$this->send_message(
523	    $this->construct_irc_message(
524		Prefix => $this->fullname,
525		Command => 'JOIN',
526		Param => $ch_name));
527	# 次にRPL_TOPIC(あれば)
528	if ($ch->topic ne '') {
529	    $this->send_message(
530		$this->construct_irc_message(
531		    Prefix => $this->_runloop->sysmsg_prefix('system'),
532		    Command => RPL_TOPIC,
533		    Params => [$local_nick,$ch_name,$ch->topic]));
534	}
535	# 次にRPL_TOPICWHOTIME(あれば)
536	if (defined($ch->topic_who)) {
537	    $this->send_message(
538		$this->construct_irc_message(
539		    Prefix => $this->_runloop->sysmsg_prefix('system'),
540		    Command => RPL_TOPICWHOTIME,
541		    Params => [$local_nick,$ch_name,$ch->topic_who,$ch->topic_time]));
542	}
543	# 次にRPL_NAMREPLY
544	my $flush_namreply = sub {
545	    my $msg = shift;
546	    $this->send_message($msg);
547	};
548	$this->do_namreply($ch, $network, undef, $flush_namreply);
549	# 最後にRPL_ENDOFNAMES
550	$this->send_message(
551	    $this->construct_irc_message(
552		Prefix => $this->_runloop->sysmsg_prefix('system'),
553		Command => RPL_ENDOFNAMES,
554		Params => [$local_nick,$ch_name,'End of NAMES list']));
555
556	# channel-infoフックの引数は (IrcIO::Client, 送信用チャンネル名, ネットワーク, ChannelInfo)
557	eval {
558	    IrcIO::Client::HookTarget->shared->call(
559		'channel-info', $this, $ch_name, $network, $ch);
560	}; if ($@) {
561	    # エラーメッセージは表示するが、送信処理は続ける
562	    $this->_runloop->notify_error(__PACKAGE__." hook call error: $@");
563	}
564
565	# クライアントに出力。
566	$this->flush;
567    };
568
569    my %channels = map {
570	my $network = $_;
571	map {
572	    my $ch = $_;
573	    (Multicast::attach($ch->name, $network->network_name) =>
574		    [$network, $ch]);
575	} values %{$network->channels};
576    } values %{$this->_runloop->networks};
577
578    while (1) {
579	# Mask を使って、マッチしたものを出力
580	foreach ($this->_conf_networks->
581		     fixed_channels('block')->channel('all')) {
582	    my $mask = $_;
583	    foreach (keys %channels) {
584		my $ch_name = $_;
585		if (Mask::match($mask, $ch_name)) {
586		    $send_channelinfo->(@{$channels{$ch_name}});
587		    last unless $this->connected;
588		    delete $channels{$ch_name};
589		}
590	    }
591	}
592
593	# のこりを出力
594	foreach (values %channels) {
595	    $send_channelinfo->(@$_);
596	    last unless $this->connected;
597	}
598
599	last;
600    }
601}
602
603# -----------------------------------------------------------------------------
604# クライアントにチャンネル情報(JOIN,TOPIC,NAMES等)を渡した直後に呼ばれるフック。
605# チャンネル名(multi server modeならネットワーク名付き)を引数として、
606# チャンネル一つにつき一度ずつ呼ばれる。
607#
608# my $hook = IrcIO::Client::Hook->new(sub {
609#     my $hook_itself = shift;
610#     # 何らかの処理を行なう。
611# })->install('channel-info'); # チャンネル情報転送時にこのフックを呼ぶ。
612# -----------------------------------------------------------------------------
613package IrcIO::Client::Hook;
614use FunctionalVariable;
615use base 'Hook';
616
617our $HOOK_TARGET_NAME = 'IrcIO::Client::HookTarget';
618our @HOOK_NAME_CANDIDATES = qw/channel-info/;
619our $HOOK_NAME_DEFAULT = 'channel-info';
620our $HOOK_TARGET_DEFAULT;
621FunctionalVariable::tie(
622    \$HOOK_TARGET_DEFAULT,
623    FETCH => sub {
624	IrcIO::Client::HookTarget->shared;
625    },
626   ) unless defined $HOOK_TARGET_DEFAULT;
627
628# -----------------------------------------------------------------------------
629package IrcIO::Client::HookTarget;
630use Hook;
631our @ISA = 'HookTarget';
632use Tiarra::SharedMixin;
633
634sub _new {
635    return bless {} => shift;
636}
637
638sub call {
639    my ($this, $name, @args) = @_;
640    $this->call_hooks($name, @args);
641}
642
6431;
644