OSDN Git Service

This commit was manufactured by cvs2svn to create tag 'release_2_0'.
[keitairc/keitairc.git] / keitairc
1 #!/usr/bin/perl
2 # -*- mode: perl; coding: utf-8 -*-
3 # keitairc
4 # $Id: keitairc,v 1.82 2010-05-19 00:36:18 ishikawa Exp $
5 # $Source: /home/ishikawa/work/keitairc/tmp/keitairc/keitairc,v $
6 #
7 # Copyright (c) 2003-2008 Jun Morimoto <morimoto@mrmt.net>
8 # This program is covered by the GNU General Public License 2
9 #
10 # Depends: libpoe-component-irc-perl,
11 #   liburi-perl, libwww-perl, libappconfig-perl, libproc-daemon-perl,
12 #   libhtml-template-perl
13 #
14 # 00location_receiver plugin use XML::Simple, so if you want to use it
15 #    Depends: libxml-simple-perl
16
17 use Encode;
18 use POE;
19 use POE::Filter::HTTPD;
20 use POE::Component::IRC;
21 use POE::Component::Server::TCP;
22 use URI::Escape;
23 use HTML::Template;
24 use HTTP::Response;
25
26 use FindBin;
27 use lib ("$FindBin::Bin/lib", '/usr/share/keitairc/lib');
28 use Keitairc::Config;
29 use Keitairc::View;
30 use Keitairc::IrcBuffer;
31 use Keitairc::IrcCallback;
32 use Keitairc::ClientInfo;
33 use Keitairc::SessionManager;
34 use Keitairc::Plugins;
35 use Keitairc::Log;
36 use strict;
37 use warnings;
38
39 our $cf = new Keitairc::Config({version => '2.0', argv => \@ARGV});
40 our $ib = new Keitairc::IrcBuffer({history => $cf->web_lines()});
41 our $sm = new Keitairc::SessionManager({default_ttl => $cf->session_ttl()});
42 our $pl = new Keitairc::Plugins({config => $cf});
43
44 # daemonize
45 if($cf->daemonize()){
46         use Proc::Daemon;
47
48         Proc::Daemon::Init;
49         if(length $cf->pid_dir()){
50                 if (open(PID, '> ' . $cf->pid_dir() . '/' . $cf->pid_file())) {
51                         print PID $$, "\n";
52                         close(PID);
53                 }
54         }
55         $poe_kernel->has_forked if ($poe_kernel->can('has_forked'));
56 }
57
58 # create irc component
59 our $irc = POE::Component::IRC->spawn(
60         Alias => 'keitairc_irc',
61         Nick => $cf->irc_nick(),
62         Username => $cf->irc_username(),
63         Ircname => $cf->irc_desc(),
64         Server => $cf->irc_server(),
65         Port => $cf->irc_port(),
66         Password => $cf->irc_password());
67
68 # create POE session
69 POE::Session->create(
70         heap => {
71                 seen_traffic => 0,
72                 disconnect_msg => 1,
73                 Config => $cf,
74                 Irc => $irc,
75                 IrcBuffer => $ib,
76         },
77         inline_states => {
78                 _start => \&Keitairc::IrcCallback::irc_start,
79                 autoping => \&Keitairc::IrcCallback::irc_autoping,
80                 connect => \&Keitairc::IrcCallback::irc_connect,
81                 irc_registered => \&Keitairc::IrcCallback::irc_registered,
82                 irc_001 => \&Keitairc::IrcCallback::irc_001,
83                 irc_join => \&Keitairc::IrcCallback::irc_join,
84                 irc_part => \&Keitairc::IrcCallback::irc_part,
85                 irc_quit => \&Keitairc::IrcCallback::irc_quit,
86                 irc_public => \&Keitairc::IrcCallback::irc_public,
87                 irc_notice => \&Keitairc::IrcCallback::irc_notice,
88                 irc_mode => \&Keitairc::IrcCallback::irc_mode,
89                 irc_nick => \&Keitairc::IrcCallback::irc_nick,
90                 irc_msg => \&Keitairc::IrcCallback::irc_msg,
91                 irc_topic => \&Keitairc::IrcCallback::irc_topic,
92                 irc_332 => \&Keitairc::IrcCallback::irc_topicraw,
93                 irc_352 => \&Keitairc::IrcCallback::irc_whoreply,
94                 irc_ctcp_action => \&Keitairc::IrcCallback::irc_ctcp_action,
95                 irc_disconnected => \&Keitairc::IrcCallback::irc_reconnect,
96                 irc_error => \&Keitairc::IrcCallback::irc_reconnect,
97                 irc_socketerr => \&Keitairc::IrcCallback::irc_reconnect,
98         });
99
100 # create web server component
101 POE::Component::Server::TCP->new(
102         Alias => 'keitairc',
103         Port => $cf->web_listen_port(),
104         ClientFilter => 'POE::Filter::HTTPD',
105         ClientInput => \&http_request);
106
107 # fire up main loop
108 $poe_kernel->run();
109 exit 0;
110
111 ################################################################
112 sub http_request{
113         my ($kernel, $heap, $request) = @_[KERNEL, HEAP, ARG0];
114
115         # Filter::HTTPD sometimes generates HTTP::Response objects.
116         # They indicate (and contain the response for) errors that occur
117         # while parsing the client's HTTP request.  It's easiest to send
118         # the responses as they are and finish up.
119         if($request->isa('HTTP::Response')){
120                 $heap->{client}->put($request);
121         }elsif(my $response = dispatch($request)){
122                 $heap->{client}->put($response);
123         }
124
125         $kernel->yield('shutdown');
126 }
127
128 ################################################################
129 sub dispatch{
130         my $request = shift;
131         my $uri = $request->uri();
132         my $ci = new Keitairc::ClientInfo($request);
133
134         Keitairc::Log::log_debug("dispatch: $uri");
135
136         {
137                 # chop off $cf->web_root()
138                 my $root = $cf->web_root();
139                 $uri =~ s|$root|/|;
140         }
141
142         if($uri eq '/'){
143                 return action_root($request);
144         }
145
146         if($uri eq '/login'){
147                 return action_login($request);
148         }
149
150         if($uri eq '/login_icc'){
151                 return action_login_icc($request);
152         }
153
154         if($uri eq '/login_imodeid?guid=ON'){
155                 return action_login_imodeid($request);
156         }
157
158         for my $name ($pl->list_action_plugins()){
159                 if($uri =~ m|^/(S[a-zA-Z]{10})/$name/(.*)| ||
160                    $uri =~ m|^/(S[a-zA-Z]{10})/$name$|){
161                         if($sm->verify({session_id => $1, user_agent => $ci->user_agent()})){
162                                 return add_cookie($pl->{plugins}->{$name}->{action_imprementation}($request, $name, $1, $2), $1);
163                         }
164                         return action_redirect_root($request);
165                 }
166         }
167
168         return action_public($request, $uri) || action_404($request);
169 }
170
171 ################################################################
172 # adds session id cookie to http response object
173 sub add_cookie{
174         my $response = shift;
175         my $session_id = shift;
176
177         my ($sec, $min, $hour, $mday, $mon, $year, $wday) = localtime(time + $cf->cookie_ttl());
178         my $expiration =
179                 sprintf('%.3s, %.2d-%.3s-%.4s %.2d:%.2d:%.2d',
180                         qw(Sun Mon Tue Wed Thu Fri Sat)[$wday],
181                         $mday,
182                         qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec)[$mon],
183                         $year + 1900,
184                         $hour,
185                         $min,
186                         $sec);
187         my $content = sprintf("sid=%s; expires=%s; path=%s; \n", $session_id, $expiration, $cf->web_root());
188         $response->push_header('Set-Cookie', $content);
189         $response;
190 }
191
192 ################################################################
193 # 通常ログインのPOST先
194 # パスワードをチェックして
195 # 間違っていたら / へリンクして終わり
196 # 合っていたらセッションを発行し /{SESSION}/index へ
197 sub action_login{
198         my $request = shift;
199         my $ci = new Keitairc::ClientInfo($request);
200         my $content = $request->decoded_content();
201         my ($password) = ($content =~ /^password=(.*)/);
202
203         Keitairc::Log::log_debug("password [$password]");
204         Keitairc::Log::log_debug("web_password [" . $cf->web_password() . "]");
205
206         if($cf->web_password() eq $password){
207                 my $s = $sm->add($ci->user_agent(), $ci->serial_key());
208                 my $view = new Keitairc::View($cf, $ci, $s->{id});
209                 return $view->redirect("/$s->{id}/index");
210         }
211
212         # password mismatch
213         my $view = new Keitairc::View($cf, $ci);
214         return $view->redirect('/');
215 }
216
217 ################################################################
218 sub action_404{
219         my $request = shift;
220         my $ci = new Keitairc::ClientInfo($request);
221         my $view = new Keitairc::View($cf, $ci);
222         return $view->render('404.html', { action => $request->uri(), _http_status_code => 404 });
223 }
224
225 ################################################################
226 sub action_public{
227         my $request = shift;
228         my $uri = shift;        # such as '/favicon.ico'
229         my $ci = new Keitairc::ClientInfo($request);
230         my $view = new Keitairc::View($cf, $ci);
231         return $view->public($uri);
232 }
233
234 ################################################################
235 # かんたんログインのPOST先
236 # DoCoMoだったらiccが来ているはずなので, icc + user_agent でチェック。
237 # 合っていたらセッション復帰して /{SESSION}/index へ
238 sub action_login_icc{
239         my $request = shift;
240         my $ci = new Keitairc::ClientInfo($request);
241         if($ci->is_docomo()){
242                 my $docomo_foma_icc = $ci->docomo_foma_icc();
243                 if(length $docomo_foma_icc){
244                         if(my $s = $sm->verify({serial_key => $docomo_foma_icc,
245                                                 user_agent => $ci->user_agent()})){
246                                 Keitairc::Log::log_debug("redirect to /$s->{id}/index from docomo_foma_icc");
247                                 my $view = new Keitairc::View($cf, $ci, $s->{id});
248                                 return $view->redirect("/$s->{id}/index");
249                         }
250
251                         if($docomo_foma_icc eq $cf->docomo_foma_icc()){
252                                 my $s = $sm->add($ci->user_agent(), $docomo_foma_icc);
253                                 Keitairc::Log::log_debug("redirect to /$s->{id}/index from docomo_foma_icc");
254                                 my $view = new Keitairc::View($cf, $ci, $s->{id});
255                                 return $view->redirect("/$s->{id}/index");
256                         }
257
258                         my $view = new Keitairc::View($cf, $ci);
259                         return $view->render('login_icc.html', { icc => $docomo_foma_icc });
260                 }
261         }
262
263         my $view = new Keitairc::View($cf, $ci);
264         return $view->render('root.html', {
265                 docomo_foma_icc => $cf->docomo_foma_icc(),
266                 docomo_imodeid => $cf->docomo_imodeid(),
267                         });
268 }
269
270 ################################################################
271 # かんたんログインのPOST先
272 # DoCoMoだったらiモードIDが来ているはずなので, iモードID + user_agent でチェック。
273 # 合っていたらセッション復帰して /{SESSION}/index へ
274 sub action_login_imodeid{
275         my $request = shift;
276         my $ci = new Keitairc::ClientInfo($request);
277         if($ci->is_docomo()){
278                 my $docomo_imodeid = $ci->docomo_imodeid();
279                 if(length $docomo_imodeid){
280                         if(my $s = $sm->verify({serial_key => $docomo_imodeid,
281                                                 user_agent => $ci->user_agent()})){
282                                 Keitairc::Log::log_debug("redirect to /$s->{id}/index from docomo_imodeid");
283                                 my $view = new Keitairc::View($cf, $ci, $s->{id});
284                                 return $view->redirect("/$s->{id}/index");
285                         }
286
287                         if($docomo_imodeid eq $cf->docomo_imodeid()){
288                                 my $s = $sm->add($ci->user_agent(), $docomo_imodeid);
289                                 Keitairc::Log::log_debug("redirect to /$s->{id}/index from docomo_imodeid");
290                                 my $view = new Keitairc::View($cf, $ci, $s->{id});
291                                 return $view->redirect("/$s->{id}/index");
292                         }
293
294                         my $view = new Keitairc::View($cf, $ci);
295                         return $view->render('login_imodeid.html', { imodeid => $docomo_imodeid });
296                 }
297         }
298
299         my $view = new Keitairc::View($cf, $ci);
300         return $view->render('root.html', {
301                 docomo_foma_icc => $cf->docomo_foma_icc(),
302                 docomo_imodeid => $cf->docomo_imodeid(),
303                         });
304 }
305
306 ################################################################
307 sub action_root{
308         my $request = shift;
309         my $ci = new Keitairc::ClientInfo($request);
310
311         if($ci->cookie_available()){
312                 my $session_id = $ci->{cookie}->{sid};
313                 if(defined($session_id) && length($session_id)){
314                         if($sm->verify({session_id => $session_id,
315                                         user_agent => $ci->user_agent()})){
316                                 Keitairc::Log::log_debug("redirect to /$session_id/index from cookie");
317                                 my $view = new Keitairc::View($cf, $ci, $session_id);
318                                 return $view->redirect("/$session_id/index");
319                         }
320                 }
321         }
322
323         if($ci->is_ezweb()){
324                 my $subscriber_id = $ci->au_subscriber_id();
325                 if(length $subscriber_id){
326                         if(my $s = $sm->verify({serial_key => $subscriber_id,
327                                                 user_agent => $ci->user_agent()})){
328                                 Keitairc::Log::log_debug("redirect to /$s->{id}/index from subscriber_id");
329                                 my $view = new Keitairc::View($cf, $ci, $s->{id});
330                                 return $view->redirect("/$s->{id}/index");
331                         }
332
333                         if($subscriber_id eq $cf->au_subscriber_id()){
334                                 my $s = $sm->add($ci->user_agent(), $subscriber_id);
335                                 Keitairc::Log::log_debug("redirect to /$s->{id}/index from au_subscriber_id");
336                                 my $view = new Keitairc::View($cf, $ci, $s->{id});
337                                 return $view->redirect("/$s->{id}/index");
338                         }
339                 }
340         }
341
342         if($ci->is_softbank()){
343                 my $serial_key = $ci->softbank_serial();
344                 if(length $serial_key){
345                         if(my $s = $sm->verify({serial_key => $serial_key,
346                                                 user_agent => $ci->user_agent()})){
347                                 Keitairc::Log::log_debug("redirect to /$s->{id}/index from softbank serial_key");
348                                 my $view = new Keitairc::View($cf, $ci, $s->{id});
349                                 return $view->redirect("/$s->{id}/index");
350                         }
351                         if($serial_key eq $cf->softbank_serial_key()){
352                                 my $s = $sm->add($ci->user_agent(), $serial_key);
353                                 Keitairc::Log::log_debug("redirect to /$s->{id}/index from softbank_serial_key");
354                                 my $view = new Keitairc::View($cf, $ci, $s->{id});
355                                 return $view->redirect("/$s->{id}/index");
356                         }
357                 }
358         }
359
360         if($ci->is_emobile()){
361                 my $userid = $ci->emobile_userid();
362                 if(length $userid){
363                         if(my $s = $sm->verify({serial_key => $userid,
364                                                 user_agent => $ci->user_agent()})){
365                                 Keitairc::Log::log_debug("redirect to /$s->{id}/index from userid");
366                                 my $view = new Keitairc::View($cf, $ci, $s->{id});
367                                 return $view->redirect("/$s->{id}/index");
368                         }
369
370                         if($userid eq $cf->emobile_userid()){
371                                 my $s = $sm->add($ci->user_agent(), $userid);
372                                 Keitairc::Log::log_debug("redirect to /$s->{id}/index from emobile_userid");
373                                 my $view = new Keitairc::View($cf, $ci, $s->{id});
374                                 return $view->redirect("/$s->{id}/index");
375                         }
376                 }
377         }
378
379         my $view = new Keitairc::View($cf, $ci);
380         return $view->render('root.html', {
381                 docomo_foma_icc => $cf->docomo_foma_icc(),
382                 docomo_imodeid => $cf->docomo_imodeid(),
383                         });
384 }
385
386 ################################################################
387 sub action_redirect_root{
388         my $request = shift;
389         my $ci = new Keitairc::ClientInfo($request);
390         my $view = new Keitairc::View($cf, $ci);
391         return $view->redirect('/');
392 }
393
394 ################################################################
395 sub parse_message{
396         my $request = shift;
397         my $ci = new Keitairc::ClientInfo($request);
398         my $timestamp;
399
400         my $message = $request->content();
401
402         if(length($message)){
403                 ($message, $timestamp) = split(/&/, $message);
404                 $timestamp =~ s/^stamp=//g;
405
406                 $message =~ s/^m=//;
407                 $message =~ s/\+/ /g;
408                 $message = uri_unescape($message);
409                 if($ci->is_ipod()){
410                         $message = fix_iui_escape($message);
411                 }
412         }
413
414         return ($message, $timestamp);
415 }
416
417 sub send_message{
418         my $request = shift;
419         my $channel = shift;
420
421         my ($message, $timestamp) = parse_message($request);
422
423         if(length($message) && length($channel)){
424                 my $jis = $message;
425                 Encode::from_to($jis, $cf->web_charset(), $cf->irc_charset());
426                 my $euc = Encode::decode($cf->web_charset(), $message);
427                 if($ib->update_timestamp($timestamp)){
428                         $irc->yield(privmsg => $channel => $jis);
429                         my $cid = $ib->name2cid($channel);
430                         $ib->add_message($cid, $euc, $cf->irc_nick());
431                 }
432         }
433 }
434
435 sub send_command{
436         my $request = shift;
437
438         my ($message, $timestamp) = parse_message($request);
439
440         if(length($message)){
441                 Encode::from_to($message, $cf->web_charset(), $cf->irc_charset());
442                 if($message =~ s|^/||) {
443                         my ($params, $trailing) = split(/ :/, $message, 2);
444                         my @postcmd = split(/ /, $params);
445                         push @postcmd, $trailing if defined $trailing;
446                         # This parser may be incomplete.
447                         if($postcmd[0] =~ /join/i) {
448                                 if($postcmd[1] =~ /^\w/) {
449                                         $ib->join($postcmd[1]);
450                                         return;
451                                 }
452                         } elsif($postcmd[0] =~ /part/i) {
453                                 if($postcmd[1] =~ /^\w/) {
454                                         $ib->part($ib->name2cid($postcmd[1]));
455                                         return;
456                                 }
457                         }
458                         $irc->yield(@postcmd);
459                 }
460         }
461 }
462
463 ################################################################
464 # posted string from iPhone/iPod touch (with iui framework)
465 # contains escaped utf-8 in the form %uXXXX
466 # and may contains escaped Shift-JIS (web_charset) in the form \xXX
467 # when operated from Safari/Mac OS X
468 sub fix_iui_escape{
469         # charset: $cf->irc_charset()
470         my $in = shift;
471         $in =~ s/\\x([0-9A-F]{2})/pack('C',hex($1))/egi;
472         my $pi = Encode::decode($cf->web_charset(), $in);
473         $pi =~ s/%u([0-9A-F]{4})/pack('U',hex($1))/egi;
474         return Encode::encode($cf->web_charset(), $pi);
475 }
476
477 __END__