1
package App::Alice::HTTPD;
9
use Plack::Middleware::Static;
10
use Plack::Session::Store::File;
12
use IRC::Formatting::HTML qw/html_to_irc/;
14
use App::Alice::Stream;
15
use App::Alice::Commands;
29
has 'httpd' => (is => 'rw');
30
has 'ping_timer' => (is => 'rw');
34
isa => 'App::Alice::Config',
36
default => sub {shift->app->config},
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 ],
57
sub url_handlers { return $url_handlers }
62
isa => 'ArrayRef[App::Alice::Stream]',
66
sub add_stream {push @{shift->streams}, @_}
67
sub no_streams {@{$_[0]->streams} == 0}
68
sub stream_count {scalar @{$_[0]->streams}}
72
my $httpd = Twiggy::Server->new(
73
host => $self->config->http_address,
74
port => $self->config->http_port,
76
$httpd->register_service(
78
if ($self->app->auth_enabled) {
79
mkdir $self->config->path."/sessions"
80
unless -d $self->config->path."/sessions";
82
store => Plack::Session::Store::File->new(dir => $self->config->path),
85
enable "Static", path => qr{^/static/}, root => $self->config->assetdir;
86
sub {$self->dispatch(shift)}
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;
103
for my $handler (@{$self->url_handlers}) {
104
my $re = $handler->[0];
105
if ($req->path_info =~ /$re/) {
106
return $handler->[1]->($self, $req);
109
return $self->not_found($req);
113
my ($self, $req) = @_;
114
my $session = $req->env->{"psgix.session"};
115
return $session->{is_logged_in};
119
my ($self, $req) = @_;
120
my $res = $req->new_response;
121
if (!$self->app->auth_enabled or $self->is_logged_in($req)) {
123
return $res->finalize;
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;
130
return $res->finalize;
132
$res->body($self->app->render("login", "bad username or password"));
135
$res->body($self->app->render("login"));
138
return $res->finalize;
142
my ($self, $req) = @_;
143
my $res = $req->new_response;
144
if (!$self->app->auth_enabled) {
147
$req->env->{"psgix.session"}{is_logged_in} = 0;
148
$req->env->{"psgix.session.options"}{expire} = 1;
149
$res->redirect("/login");
151
return $res->finalize;
156
$self->ping_timer(AnyEvent->timer(
170
$_->close for $self->streams;
172
$self->ping_timer(undef);
177
my ($self, $req) = @_;
178
my $url = $req->request_uri;
179
$url =~ s/^\/get\///;
183
my ($data, $headers) = @_;
184
my $res = $req->new_response($headers->{Status});
185
$res->headers($headers);
187
$respond->($res->finalize);
193
my ($self, @data) = @_;
194
return if $self->no_streams or !@data;
196
for my $stream ($self->streams) {
198
$stream->send(@data);
204
$self->purge_disconnects if $purge;
208
my ($self, $req) = @_;
209
$self->app->log(info => "opening new stream");
210
my $min = $req->param('msgid') || 0;
213
my $stream = App::Alice::Stream->new(
214
queue => [ map({$_->join_action} $self->app->windows) ],
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,
220
$self->add_stream($stream);
221
$self->app->with_messages(sub {
224
map {$_->{buffered} = 1; $_}
225
grep {$_->{msgid} > $min}
233
sub purge_disconnects {
235
$self->app->log(debug => "removing broken streams");
236
$self->streams([grep {!$_->closed} $self->streams]);
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);
248
for (split /\n/, $msg) {
250
$self->app->handle_command($_, $window) if length $_
252
$self->app->log(info => $_);
256
my $res = $req->new_response(200);
257
$res->content_type('text/plain');
258
$res->content_length(2);
260
return $res->finalize;
264
my ($self, $req) = @_;
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));
274
delete $_->{active} for @windows;
280
my ($self, $writer, $cb, @windows) = @_;
285
my $window = pop @windows;
286
$writer->write(encode_utf8 $self->app->render('window_head', $window));
287
$window->buffer->with_messages(sub {
289
$writer->write(encode_utf8 $_->{html}) for @messages;
291
$writer->write(encode_utf8 $self->app->render('window_footer', $window));
292
$self->send_windows($writer, $cb, @windows);
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;
306
my ($self, $req) = @_;
309
$self->app->history->search(
310
user => $self->app->user, %{$req->parameters}, sub {
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);
321
my ($self, $req) = @_;
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);
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);
343
return $res->finalize;
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);
352
return $res->finalize;
356
my ($self, $req) = @_;
357
$self->app->log(info => "serving blank server config");
359
my $name = $req->param('name');
361
my $config = $self->app->render('new_server', $name);
362
my $listitem = $self->app->render('server_listitem', $name);
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;
371
my ($self, $req) = @_;
372
$self->app->log(info => "saving config");
375
if ($req->parameters->{has_servers}) {
376
$new_config->{servers} = {};
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)];
385
$new_config->{servers}{$1}{$2} = $req->param($name);
388
elsif ($name eq "highlights") {
389
$new_config->{$name} = [$req->parameters->get_all($name)];
392
$new_config->{$name} = $req->param($name);
395
$self->app->reload_config($new_config);
397
$self->app->broadcast(
398
$self->app->format_info("config", "saved")
401
my $res = $req->new_response(200);
402
$res->content_type('text/plain');
403
$res->content_length(2);
405
return $res->finalize;
409
my ($self, $req) = @_;
410
$self->app->log(debug => "updating tab order");
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);
417
return $res->finalize;
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;
427
__PACKAGE__->meta->make_immutable;