123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341 |
- #!/usr/bin/perl
- # mojolicious web handler
- # must set env var LOGBOT_CONFIG to config filename
- #
- # in production runs as a daemon behind a reverse proxy, which handles requests
- # for static assets (web/public/static)
- #
- # use `dev-make` to build static assets
- use local::lib;
- use v5.10;
- use strict;
- use warnings;
- use FindBin qw( $RealBin );
- use lib "$RealBin/lib";
- BEGIN { $ENV{TZ} = 'UTC' }
- use DateTime ();
- use IO::Compress::Gzip qw( gzip );
- use LogBot::Config qw( find_config load_all_configs load_config reload_config );
- use LogBot::MemCache ();
- use LogBot::Util qw( file_for file_time time_to_ymd );
- use LogBot::Web::Util qw( channel_from_param channel_topics linkify render_init rewrite_old_urls );
- use Mojo::ByteStream ();
- use Mojo::Log ();
- use Mojo::Util qw( dumper );
- use Mojolicious::Lite qw( app );
- # load networks
- my $networks = [];
- {
- if (my $config = $ENV{LOGBOT_CONFIG}) {
- # load specific networks (mostly for dev)
- foreach my $network (split(/,/, $config)) {
- push @{$networks}, load_config(find_config($network), web => 1);
- }
- } else {
- # load all networks
- foreach my $network (values %{ load_all_configs(web => 1) }) {
- push @{$networks}, $network;
- }
- }
- $networks = [sort { $a->{name} cmp $b->{name} } @{$networks}];
- }
- # configure mojo
- my $is_production = app->mode() eq 'production';
- app->secrets('!logbot!');
- app->renderer->paths([$RealBin . '/web/templates']);
- app->static->paths([$RealBin . '/web/public']);
- app->config(
- hypnotoad => {
- listen => ['http://127.0.0.1:' . ($ENV{LOGBOT_PORT} // 3001)],
- pid_file => ($ENV{LOGBOT_PID_FILE} // ($RealBin . '/logbot-web.pid')),
- },
- );
- app->log(Mojo::Log->new(path => $is_production ? '/var/log/logbot/mojo.log' : 'log/mojo.log'));
- plugin AccessLog => {
- log => ($is_production ? '/var/log/logbot/access.log' : 'log/access.log'),
- format => '%h %{X-Network}o - %t "%r" %>s %b "%{Referer}i" "%{User-Agent}i"',
- };
- plugin 'Status' => {
- route => app->routes->under(
- '_status' => sub {
- my ($c) = @_;
- my $password = $ENV{'LOGBOT_STATUS_PASSWORD'} // 'status';
- return 1 if $password eq '';
- return 1 if ($c->req->url->to_abs->userinfo // '') eq 'logbot:' . $password;
- $c->res->headers->www_authenticate('Basic');
- $c->render(text => 'Authentication required', status => 401);
- return undef;
- }
- )
- };
- my $memcache = LogBot::MemCache->new(binary => $is_production);
- render_init($RealBin);
- # per-request initialisation
- under sub {
- my ($c) = @_;
- # determine current config
- my $config_index = 0;
- if (scalar(@{$networks}) > 1) {
- my $host = lc($c->req->url->to_abs->host);
- my $dot_posn = index($host, '.');
- if ($dot_posn != -1) {
- $host = substr($host, 0, $dot_posn);
- for (my $i = 0; $i < scalar(@{$networks}); $i++) {
- next unless $networks->[$i]->{name} eq $host;
- $config_index = $i;
- last;
- }
- }
- }
- # reload config
- $networks->[$config_index] = reload_config($networks->[$config_index]);
- my $config = $networks->[$config_index];
- # store network name in response header for logging
- $c->res->headers->add('X-Network' => $config->{name});
- # for client-side list cache
- my $topics_lastmod = file_time(file_for($config, 'topics_lastmod')) // 0;
- $c->stash(
- config => $config,
- networks => $networks,
- network => $config->{name},
- channels => $config->{_derived}->{visible_channels},
- topics => channel_topics($config),
- channel => '',
- date => '',
- error => '',
- event_count => 0,
- bot_event_count => 0,
- page => '',
- today => DateTime->now()->truncate(to => 'day'),
- is_today => 0,
- cache_prefix => $config->{_derived}->{time} . '.' . $config->{name} . '.',
- channel_list_id => $config->{_derived}->{time} . '.' . $topics_lastmod,
- topics_lastmod => $topics_lastmod,
- );
- return 1;
- };
- #
- # default => about logbot || search
- get '/' => sub {
- my ($c) = @_;
- # redirect old urls
- if (my $url = rewrite_old_urls($c)) {
- # url was formed server-side, no need to bounce through js redirect
- if ($url !~ /#/) {
- return $c->redirect_to($url);
- }
- $c->stash(redirect_to => $url);
- return $c->render('redirect');
- }
- # search
- my $q = $c->req->query_params->param('q');
- if (defined($q)) {
- LogBot::Web::Search::render($c, $q);
- } else {
- # index
- LogBot::Web::Index::render($c);
- }
- };
- # config
- get '/_config' => sub {
- my ($c) = @_;
- LogBot::Web::Config::render($c);
- };
- # channel list
- get '/_channels' => sub {
- my ($c) = @_;
- LogBot::Web::List::render($c);
- };
- get '/_channels_body' => sub {
- my ($c) = @_;
- LogBot::Web::List::render($c, { body_only => 1 });
- };
- # network stats
- get '/_stats' => sub {
- my ($c) = @_;
- LogBot::Web::Stats::render($c);
- };
- get '/_stats/meta' => sub {
- my ($c) = @_;
- LogBot::Web::Stats::render_meta($c);
- };
- get '/_stats/hours' => sub {
- my ($c) = @_;
- LogBot::Web::Stats::render_hours($c);
- };
- # debugging
- if (!$is_production) {
- get '/_stash' => sub {
- my ($c) = @_;
- $c->stash(today => $c->stash('today')->ymd());
- $c->render(text => dumper($c->stash), format => 'txt');
- };
- }
- # robots.txt
- my $robots_txt = <<'EOF';
- # http://law.di.unimi.it/BUbiNG.html
- # 20% of my traffic was from this bot
- User-agent: BUbiNG
- Disallow: /
- # Marketing/SEO bot
- User-agent: SemrushBot
- Disallow: /
- User-agent: SemrushBot-SA
- Disallow: /
- EOF
- get '/robots.txt' => sub {
- my ($c) = @_;
- $c->render(text => $robots_txt, format => 'txt');
- };
- # /channel => redirect to current date
- get '/#channel' => sub {
- my ($c) = @_;
- my $channel = channel_from_param($c) // return;
- # redirect to current date
- my $path = $c->req->url->path;
- $path .= '/' unless substr($path, -1) eq '/';
- $c->redirect_to($path . time_to_ymd(time()));
- };
- # /channel/date => show logs
- get '/#channel/:date' => [date => qr/\d{8}/] => sub {
- my ($c) = @_;
- LogBot::Web::Channel::render_logs($c);
- };
- get '/#channel/:date/raw' => [date => qr/\d{8}/] => sub {
- my ($c) = @_;
- LogBot::Web::Channel::render_raw($c);
- };
- get '/#channel/link/:time/:nick' => [time => qr /\d+/] => sub {
- my ($c) = @_;
- LogBot::Web::Channel::redirect_to($c);
- };
- get '/#channel/stats' => sub {
- my ($c) = @_;
- LogBot::Web::Stats::render($c, require_channel => 1);
- };
- get '/#channel/stats/meta' => sub {
- my ($c) = @_;
- LogBot::Web::Stats::render_meta($c, require_channel => 1);
- };
- get '/#channel/stats/hours' => sub {
- my ($c) = @_;
- LogBot::Web::Stats::render_hours($c);
- };
- get '/#channel/stats/nicks' => sub {
- my ($c) = @_;
- LogBot::Web::Stats::render_nicks($c);
- };
- # 404 handler
- any '*' => sub {
- my ($c) = @_;
- $c->res->code(404);
- $c->res->message('Not Found');
- LogBot::Web::Index::render($c, { error => 'Not Found.' });
- };
- my %cache;
- # static file with timestamp
- helper static => sub {
- my ($c, $file) = @_;
- return $cache{static}->{$file} //= '/static/' . $file . '?' . file_time($RealBin . '/web/public/static/' . $file);
- };
- # inline svg
- helper svg => sub {
- my ($c, $file) = @_;
- return $cache{svg}->{$file} //= Mojo::ByteStream->new(slurp($RealBin . '/web/svg/' . $file . '.svg'));
- };
- # linkify text
- helper linkify => sub {
- my ($c, $text) = @_;
- return linkify($text);
- };
- # cache
- helper cached => sub {
- my ($c, $key, $callback) = @_;
- return $key eq ''
- ? $callback->()
- : Mojo::ByteStream->new($memcache->cached($c->stash('cache_prefix') . $key, $callback));
- };
- hook after_render => sub {
- my ($c, $output, $format) = @_;
- my $headers = $c->res->headers;
- # CSP
- state $csp = join(
- '; ',
- q{default-src 'self'},
- q{object-src 'none'},
- q{frame-ancestors 'none'},
- q{base-uri 'none'},
- q{style-src 'self' 'unsafe-inline'}, # unsafe-inline for chosen, top-nick graph
- q{img-src 'self' data:}, # data: for pikaday
- );
- $headers->header('Content-Security-Policy' => $csp);
- # preload fonts
- state $link = join(', ',
- map { '<' . $c->url_for($_)->to_abs . '>; rel=preload; as=font' }
- qw( /static/hind-regular.ttf /static/hind-medium.ttf /static/hind-bold.ttf ));
- $headers->header(Link => $link);
- # no need to expose this info
- $headers->remove('Server');
- # gzip compression
- if (($c->req->headers->accept_encoding // '') =~ /gzip/i) {
- $headers->append(Vary => 'Accept-Encoding');
- $headers->content_encoding('gzip');
- gzip($output, \my $compressed);
- ${$output} = $compressed;
- }
- };
- app->start;
|