~ubuntu-branches/debian/jessie/alice/jessie

« back to all changes in this revision

Viewing changes to lib/App/Alice/HTTPD.pm

  • Committer: Bazaar Package Importer
  • Author(s): Dave Walker (Daviey)
  • Date: 2011-07-29 22:17:12 UTC
  • Revision ID: james.westby@ubuntu.com-20110729221712-av9dbulzigsrx3n7
Tags: upstream-0.19
ImportĀ upstreamĀ versionĀ 0.19

Show diffs side-by-side

added added

removed removed

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