File Coverage

blib/lib/App/Alice/HTTPD.pm
Criterion Covered Total %
statement 61 267 22.8
branch 2 50 4.0
condition 1 20 5.0
subroutine 21 55 38.1
pod 1 27 3.7
total 86 419 20.5


line stmt bran cond sub pod time code
1             package App::Alice::HTTPD;
2              
3 4     4   6503 use AnyEvent;
  4         22254  
  4         118  
4 4     4   4299 use AnyEvent::HTTP;
  4         153293  
  4         399  
5              
6 4     4   4257 use Twiggy::Server;
  4         214546  
  4         199  
7 4     4   4131 use Plack::Request;
  4         268112  
  4         173  
8 4     4   3777 use Plack::Builder;
  4         28494  
  4         349  
9 4     4   3275 use Plack::Middleware::Static;
  4         36699  
  4         110  
10 4     4   3436 use Plack::Session::Store::File;
  4         5738  
  4         97  
11              
12 4     4   22 use IRC::Formatting::HTML qw/html_to_irc/;
  4         6  
  4         262  
13              
14 4     4   2051 use App::Alice::Stream;
  4         15  
  4         148  
15 4     4   2394 use App::Alice::Commands;
  4         10  
  4         112  
16              
17 4     4   26 use JSON;
  4         8  
  4         37  
18 4     4   500 use Encode;
  4         9  
  4         364  
19 4     4   20 use utf8;
  4         7  
  4         36  
20 4     4   90 use Any::Moose;
  4         10  
  4         16  
21 4     4   1792 use Try::Tiny;
  4         9  
  4         16577  
22              
23             has 'app' => (
24             is => 'ro',
25             isa => 'App::Alice',
26             required => 1,
27             );
28              
29             has 'httpd' => (is => 'rw');
30             has 'ping_timer' => (is => 'rw');
31              
32             has 'config' => (
33             is => 'ro',
34             isa => 'App::Alice::Config',
35             lazy => 1,
36             default => sub {shift->app->config},
37             );
38              
39             my $url_handlers = [
40             [ qr{^/$} => \&send_index ],
41             [ qr{^/say/?$} => \&handle_message ],
42             [ qr{^/stream/?$} => \&setup_stream ],
43             [ qr{^/config/?$} => \&send_config ],
44             [ qr{^/prefs/?$} => \&send_prefs ],
45             [ qr{^/serverconfig/?$} => \&server_config ],
46             [ qr{^/save/?$} => \&save_config ],
47             [ qr{^/tabs/?$} => \&tab_order ],
48             [ qr{^/login/?$} => \&login ],
49             [ qr{^/logout/?$} => \&logout ],
50             [ qr{^/logs/?$} => \&send_logs ],
51             [ qr{^/search/?$} => \&send_search ],
52             [ qr{^/range/?$} => \&send_range ],
53             [ qr{^/view/?$} => \&send_index ],
54             [ qr{^/get} => \&image_proxy ],
55             ];
56              
57 0     0 0 0 sub url_handlers { return $url_handlers }
58              
59             has 'streams' => (
60             is => 'rw',
61             auto_deref => 1,
62             isa => 'ArrayRef[App::Alice::Stream]',
63             default => sub {[]},
64             );
65              
66 0     0 0 0 sub add_stream {push @{shift->streams}, @_}
  0         0  
67 22     22 0 43 sub no_streams {@{$_[0]->streams} == 0}
  22         191  
68 2     2 0 4 sub stream_count {scalar @{$_[0]->streams}}
  2         15  
69              
70             sub BUILD {
71 2     2 1 4 my $self = shift;
72 2         14 my $httpd = Twiggy::Server->new(
73             host => $self->config->http_address,
74             port => $self->config->http_port,
75             );
76             $httpd->register_service(
77             builder {
78 2 50   2   107 if ($self->app->auth_enabled) {
79 0 0       0 mkdir $self->config->path."/sessions"
80             unless -d $self->config->path."/sessions";
81 0         0 enable "Session",
82             store => Plack::Session::Store::File->new(dir => $self->config->path),
83             expires => "24h";
84             }
85 2         23 enable "Static", path => qr{^/static/}, root => $self->config->assetdir;
86 0         0 sub {$self->dispatch(shift)}
87 2         125 }
88 2         42 );
89 2         11944 $self->httpd($httpd);
90 2         9 $self->ping;
91             }
92              
93             sub dispatch {
94 0     0 0 0 my ($self, $env) = @_;
95 0         0 my $req = Plack::Request->new($env);
96 0 0       0 if ($self->app->auth_enabled) {
97 0 0 0     0 unless ($req->path eq "/login" or $self->is_logged_in($req)) {
98 0         0 my $res = $req->new_response;
99 0         0 $res->redirect("/login");
100 0         0 return $res->finalize;
101             }
102             }
103 0         0 for my $handler (@{$self->url_handlers}) {
  0         0  
104 0         0 my $re = $handler->[0];
105 0 0       0 if ($req->path_info =~ /$re/) {
106 0         0 return $handler->[1]->($self, $req);
107             }
108             }
109 0         0 return $self->not_found($req);
110             }
111              
112             sub is_logged_in {
113 0     0 0 0 my ($self, $req) = @_;
114 0         0 my $session = $req->env->{"psgix.session"};
115 0         0 return $session->{is_logged_in};
116             }
117              
118             sub login {
119 0     0 0 0 my ($self, $req) = @_;
120 0         0 my $res = $req->new_response;
121 0 0 0     0 if (!$self->app->auth_enabled or $self->is_logged_in($req)) {
    0 0        
122 0         0 $res->redirect("/");
123 0         0 return $res->finalize;
124             }
125             elsif (my $user = $req->param("username")
126             and my $pass = $req->param("password")) {
127 0 0       0 if ($self->app->authenticate($user, $pass)) {
128 0         0 $req->env->{"psgix.session"}->{is_logged_in} = 1;
129 0         0 $res->redirect("/");
130 0         0 return $res->finalize;
131             }
132 0         0 $res->body($self->app->render("login", "bad username or password"));
133             }
134             else {
135 0         0 $res->body($self->app->render("login"));
136             }
137 0         0 $res->status(200);
138 0         0 return $res->finalize;
139             }
140              
141             sub logout {
142 0     0 0 0 my ($self, $req) = @_;
143 0         0 my $res = $req->new_response;
144 0 0       0 if (!$self->app->auth_enabled) {
145 0         0 $res->redirect("/");
146             } else {
147 0         0 $req->env->{"psgix.session"}{is_logged_in} = 0;
148 0         0 $req->env->{"psgix.session.options"}{expire} = 1;
149 0         0 $res->redirect("/login");
150             }
151 0         0 return $res->finalize;
152             }
153              
154             sub ping {
155 2     2 0 4 my $self = shift;
156             $self->ping_timer(AnyEvent->timer(
157             after => 5,
158             interval => 10,
159             cb => sub {
160 0     0   0 $self->broadcast({
161             type => "action",
162             event => "ping",
163             });
164             }
165 2         21 ));
166             }
167              
168             sub shutdown {
169 0     0 0 0 my $self = shift;
170 0         0 $_->close for $self->streams;
171 0         0 $self->streams([]);
172 0         0 $self->ping_timer(undef);
173 0         0 $self->httpd(undef);
174             }
175              
176             sub image_proxy {
177 0     0 0 0 my ($self, $req) = @_;
178 0         0 my $url = $req->request_uri;
179 0         0 $url =~ s/^\/get\///;
180             return sub {
181 0     0   0 my $respond = shift;
182             http_get $url, sub {
183 0         0 my ($data, $headers) = @_;
184 0         0 my $res = $req->new_response($headers->{Status});
185 0         0 $res->headers($headers);
186 0         0 $res->body($data);
187 0         0 $respond->($res->finalize);
188 0         0 };
189             }
190 0         0 }
191              
192             sub broadcast {
193 22     22 0 122 my ($self, @data) = @_;
194 22 50 33     74 return if $self->no_streams or !@data;
195 0           my $purge = 0;
196 0           for my $stream ($self->streams) {
197             try {
198 0     0     $stream->send(@data);
199             } catch {
200 0     0     $stream->close;
201 0           $purge = 1;
202 0           };
203             }
204 0 0         $self->purge_disconnects if $purge;
205             };
206              
207             sub setup_stream {
208 0     0 0   my ($self, $req) = @_;
209 0           $self->app->log(info => "opening new stream");
210 0   0       my $min = $req->param('msgid') || 0;
211             return sub {
212 0     0     my $respond = shift;
213 0           my $stream = App::Alice::Stream->new(
214 0 0         queue => [ map({$_->join_action} $self->app->windows) ],
215             writer => $respond,
216             start_time => $req->param('t'),
217             # android requires 4K updates to trigger loading event
218             min_bytes => $req->user_agent =~ /android/i ? 4096 : 0,
219             );
220 0           $self->add_stream($stream);
221             $self->app->with_messages(sub {
222 0 0         return unless @_;
223 0           $stream->enqueue(
224 0           map {$_->{buffered} = 1; $_}
  0            
225 0           grep {$_->{msgid} > $min}
226             @_
227             );
228 0           $stream->send;
229 0           });
230             }
231 0           }
232              
233             sub purge_disconnects {
234 0     0 0   my ($self) = @_;
235 0           $self->app->log(debug => "removing broken streams");
236 0           $self->streams([grep {!$_->closed} $self->streams]);
  0            
237             }
238              
239             sub handle_message {
240 0     0 0   my ($self, $req) = @_;
241 0           my $msg = $req->param('msg');
242 0           my $is_html = $req->param('html');
243 0 0         utf8::decode($msg) unless utf8::is_utf8($msg);
244 0 0         $msg = html_to_irc($msg) if $is_html;
245 0           my $source = $req->param('source');
246 0           my $window = $self->app->get_window($source);
247 0 0         if ($window) {
248 0           for (split /\n/, $msg) {
249             try {
250 0 0   0     $self->app->handle_command($_, $window) if length $_
251             } catch {
252 0     0     $self->app->log(info => $_);
253             }
254 0           }
255             }
256 0           my $res = $req->new_response(200);
257 0           $res->content_type('text/plain');
258 0           $res->content_length(2);
259 0           $res->body('ok');
260 0           return $res->finalize;
261             }
262              
263             sub send_index {
264 0     0 0   my ($self, $req) = @_;
265             return sub {
266 0     0     my $respond = shift;
267 0           my $writer = $respond->([200, ["Content-type" => "text/html; charset=utf-8"]]);
268 0           my @windows = $self->app->sorted_windows;
269 0 0         @windows > 1 ? $windows[1]->{active} = 1 : $windows[0]->{active} = 1;
270 0           $writer->write(encode_utf8 $self->app->render('index_head', @windows));
271             $self->send_windows($writer, sub {
272 0           $writer->write(encode_utf8 $self->app->render('index_footer', @windows));
273 0           $writer->close;
274 0           delete $_->{active} for @windows;
275 0           }, @windows);
276             }
277 0           }
278              
279             sub send_windows {
280 0     0 0   my ($self, $writer, $cb, @windows) = @_;
281 0 0         if (!@windows) {
282 0           $cb->();
283             }
284             else {
285 0           my $window = pop @windows;
286 0           $writer->write(encode_utf8 $self->app->render('window_head', $window));
287             $window->buffer->with_messages(sub {
288 0     0     my @messages = @_;
289 0           $writer->write(encode_utf8 $_->{html}) for @messages;
290             }, 0, sub {
291 0     0     $writer->write(encode_utf8 $self->app->render('window_footer', $window));
292 0           $self->send_windows($writer, $cb, @windows);
293 0           });
294             }
295             }
296              
297             sub send_logs {
298 0     0 0   my ($self, $req) = @_;
299 0           my $output = $self->app->render('logs');
300 0           my $res = $req->new_response(200);
301 0           $res->body(encode_utf8 $output);
302 0           return $res->finalize;
303             }
304              
305             sub send_search {
306 0     0 0   my ($self, $req) = @_;
307             return sub {
308 0     0     my $respond = shift;
309 0           $self->app->history->search(
310             user => $self->app->user, %{$req->parameters}, sub {
311 0           my $rows = shift;
312 0           my $content = $self->app->render('results', $rows);
313 0           my $res = $req->new_response(200);
314 0           $res->body(encode_utf8 $content);
315 0           $respond->($res->finalize);
316 0           });
317             }
318 0           }
319              
320             sub send_range {
321 0     0 0   my ($self, $req) = @_;
322             return sub {
323 0     0     my $respond = shift;
324             $self->app->history->range(
325             $self->app->user, $req->param('channel'), $req->param('id'), sub {
326 0           my ($before, $after) = @_;
327 0           $before = $self->app->render('range', $before, 'before');
328 0           $after = $self->app->render('range', $after, 'after');
329 0           my $res = $req->new_response(200);
330 0           $res->body(to_json [$before, $after]);
331 0           $respond->($res->finalize);
332             }
333 0           );
334             }
335 0           }
336              
337             sub send_config {
338 0     0 0   my ($self, $req) = @_;
339 0           $self->app->log(info => "serving config");
340 0           my $output = $self->app->render('servers');
341 0           my $res = $req->new_response(200);
342 0           $res->body($output);
343 0           return $res->finalize;
344             }
345              
346             sub send_prefs {
347 0     0 0   my ($self, $req) = @_;
348 0           $self->app->log(info => "serving prefs");
349 0           my $output = $self->app->render('prefs');
350 0           my $res = $req->new_response(200);
351 0           $res->body($output);
352 0           return $res->finalize;
353             }
354              
355             sub server_config {
356 0     0 0   my ($self, $req) = @_;
357 0           $self->app->log(info => "serving blank server config");
358            
359 0           my $name = $req->param('name');
360 0           $name =~ s/\s+//g;
361 0           my $config = $self->app->render('new_server', $name);
362 0           my $listitem = $self->app->render('server_listitem', $name);
363            
364 0           my $res = $req->new_response(200);
365 0           $res->body(to_json({config => $config, listitem => $listitem}));
366 0           $res->header("Cache-control" => "no-cache");
367 0           return $res->finalize;
368             }
369              
370             sub save_config {
371 0     0 0   my ($self, $req) = @_;
372 0           $self->app->log(info => "saving config");
373            
374 0           my $new_config = {};
375 0 0         if ($req->parameters->{has_servers}) {
376 0           $new_config->{servers} = {};
377             }
378 0           for my $name (keys %{$req->parameters}) {
  0            
379 0 0         next unless $req->parameters->{$name};
380 0 0         next if $name eq "has_servers";
381 0 0 0       if ($name =~ /^(.+?)_(.+)/ and exists $new_config->{servers}) {
    0          
382 0 0 0       if ($2 eq "channels" or $2 eq "on_connect") {
383 0           $new_config->{servers}{$1}{$2} = [$req->parameters->get_all($name)];
384             } else {
385 0           $new_config->{servers}{$1}{$2} = $req->param($name);
386             }
387             }
388             elsif ($name eq "highlights") {
389 0           $new_config->{$name} = [$req->parameters->get_all($name)];
390             }
391             else {
392 0           $new_config->{$name} = $req->param($name);
393             }
394             }
395 0           $self->app->reload_config($new_config);
396              
397 0           $self->app->broadcast(
398             $self->app->format_info("config", "saved")
399             );
400              
401 0           my $res = $req->new_response(200);
402 0           $res->content_type('text/plain');
403 0           $res->content_length(2);
404 0           $res->body('ok');
405 0           return $res->finalize;
406             }
407              
408             sub tab_order {
409 0     0 0   my ($self, $req) = @_;
410 0           $self->app->log(debug => "updating tab order");
411            
412 0           $self->app->tab_order([grep {defined $_} $req->parameters->get_all('tabs')]);
  0            
413 0           my $res = $req->new_response(200);
414 0           $res->content_type('text/plain');
415 0           $res->content_length(2);
416 0           $res->body('ok');
417 0           return $res->finalize;
418             }
419              
420             sub not_found {
421 0     0 0   my ($self, $req) = @_;
422 0           $self->app->log(debug => "sending 404 " . $req->path_info);
423 0           my $res = $req->new_response(404);
424 0           return $res->finalize;
425             }
426              
427             __PACKAGE__->meta->make_immutable;
428             1;