diff options
Diffstat (limited to 'examples')
-rw-r--r-- | examples/README | 2 | ||||
-rw-r--r-- | examples/README.unsubscribe | 40 | ||||
-rw-r--r-- | examples/logrotate.conf | 24 | ||||
-rw-r--r-- | examples/public-inbox-config | 1 | ||||
-rw-r--r-- | examples/public-inbox-httpd.socket | 10 | ||||
-rw-r--r-- | examples/public-inbox-httpd@.service | 32 | ||||
-rw-r--r-- | examples/public-inbox-nntpd.socket | 10 | ||||
-rw-r--r-- | examples/public-inbox-nntpd@.service | 32 | ||||
-rw-r--r-- | examples/public-inbox.psgi | 21 | ||||
-rw-r--r-- | examples/unsubscribe-milter.socket | 10 | ||||
-rw-r--r-- | examples/unsubscribe-milter@.service | 24 | ||||
-rw-r--r-- | examples/unsubscribe-psgi.socket | 11 | ||||
-rw-r--r-- | examples/unsubscribe-psgi@.service | 21 | ||||
-rw-r--r-- | examples/unsubscribe.milter | 134 | ||||
-rw-r--r-- | examples/unsubscribe.psgi | 69 | ||||
-rw-r--r-- | examples/varnish-4.vcl | 68 |
16 files changed, 502 insertions, 7 deletions
diff --git a/examples/README b/examples/README index 1244cb2c..1d5dcd34 100644 --- a/examples/README +++ b/examples/README @@ -16,4 +16,4 @@ apache2_perl.conf - intended to be the basis of a production config Contact ------- Please send any related feedback to public-inbox: meta@public-inbox.org -Our public-inbox is: git://public-inbox.org/meta +Our public-inbox is: https://public-inbox.org/meta/ diff --git a/examples/README.unsubscribe b/examples/README.unsubscribe new file mode 100644 index 00000000..7c41067c --- /dev/null +++ b/examples/README.unsubscribe @@ -0,0 +1,40 @@ +Unsubscribe endpoints for mlmmj users (and possibly Mailman, too) + +* examples/unsubscribe.milter filters outgoing messages + and appends an HTTPS URL to the List-Unsubscribe header. + This List-Unsubscribe header should point to the PSGI + described below. + Currently, this is only active for a whitelist of test + addresses in /etc/unsubscribe-milter.whitelist + with one email address per line. + +* examples/unsubscribe.psgi is a PSGI which needs to run + as the mlmmj user with permission to run mlmmj-unsub. + This depends on the PublicInbox::Unsubscribe module + which may be extracted from the rest of public-inbox. + It is strongly recommended to NOT run the rest of the + public-inbox WWW code in the same process as this PSGI. + (The public-inbox WWW code will never need write + permissions to anything besides stderr). + +* Both the .milter and .psgi examples are bundled with + systemd service and socket activation examples. + AFAIK no other PSGI server besides public-inbox-httpd + supports systemd socket activation. + +To wire up the milter for postfix, I use the following +in /etc/postfix/main.cf: + + # Milter configuration + milter_default_action = accept + milter_protocol = 2 + + # other milters may be chained here (e.g. opendkim) + # chroot users will need to adjust this path + smtpd_milters = local:/var/spool/postfix/unsubscribe/unsubscribe.sock + + # This is not needed for mlmmj since mlmmj uses SMTP: + # non_smtpd_milters = local:/var/spool/postfix/unsubscribe/unsubscribe.sock + +Copyright (C) 2016 all contributors <meta@public-inbox.org> +License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt> diff --git a/examples/logrotate.conf b/examples/logrotate.conf new file mode 100644 index 00000000..4ce08843 --- /dev/null +++ b/examples/logrotate.conf @@ -0,0 +1,24 @@ +# ==> /etc/logrotate.d/public-inbox <== +# +# See the logrotate(8) manpage for more information: +# http://linux.die.net/man/8/logrotate +/var/log/public-inbox/*.log { + weekly + missingok + rotate 52 + compress + delaycompress + notifempty + sharedscripts + dateext + # note the lack of the racy "copytruncate" option in this + # config. public-inbox-*d supports the USR1 signal and + # we send it as our "lastaction": + lastaction + # systemd users do not need PID files, + # only signal the @1 process since the @2 is short-lived + # For systemd users, assuming you use two services + systemctl kill -s SIGUSR1 public-inbox-httpd@1.service + systemctl kill -s SIGUSR1 public-inbox-nntpd@1.service + endscript +} diff --git a/examples/public-inbox-config b/examples/public-inbox-config index 0c1db118..7fcbe0ba 100644 --- a/examples/public-inbox-config +++ b/examples/public-inbox-config @@ -10,4 +10,3 @@ address = meta@public-inbox.org mainrepo = /home/pi/meta-main.git url = http://example.com/meta - atomUrl = http://example.com/meta diff --git a/examples/public-inbox-httpd.socket b/examples/public-inbox-httpd.socket new file mode 100644 index 00000000..1a1ed735 --- /dev/null +++ b/examples/public-inbox-httpd.socket @@ -0,0 +1,10 @@ +# ==> /etc/systemd/system/public-inbox-httpd.socket <== +[Unit] +Description = public-inbox-httpd socket + +[Socket] +ListenStream = 80 +Service = public-inbox-httpd@1.service + +[Install] +WantedBy = sockets.target diff --git a/examples/public-inbox-httpd@.service b/examples/public-inbox-httpd@.service new file mode 100644 index 00000000..56117ef0 --- /dev/null +++ b/examples/public-inbox-httpd@.service @@ -0,0 +1,32 @@ +# ==> /etc/systemd/system/public-inbox-httpd@.service <== +# Since SIGUSR2 upgrades do not work under systemd, this service file +# allows starting two simultaneous services during upgrade time +# (e.g. public-inbox-httpd@1 public-inbox-httpd@2) with the intention +# that they take turns running in-between upgrades. This should +# allow upgrading without downtime. + +[Unit] +Description = public-inbox PSGI server %i +Wants = public-inbox-httpd.socket +After = public-inbox-httpd.socket + +[Service] +Environment = PI_CONFIG=/home/pi/.public-inbox/config \ +PATH=/usr/local/bin:/usr/bin:/bin \ +PERL_INLINE_DIRECTORY=/tmp/.pub-inline + +LimitNOFILE = 30000 +ExecStartPre = /bin/mkdir -p -m 1777 /tmp/.pub-inline +ExecStart = /usr/local/bin/public-inbox-httpd \ +-1 /var/log/public-inbox/httpd.out.log +StandardError = syslog +Sockets = public-inbox-httpd.socket +KillSignal = SIGQUIT +User = nobody +Group = nogroup +ExecReload = /bin/kill -HUP $MAINPID +TimeoutStopSec = 86400 +KillMode = process + +[Install] +WantedBy = multi-user.target diff --git a/examples/public-inbox-nntpd.socket b/examples/public-inbox-nntpd.socket new file mode 100644 index 00000000..eeddf343 --- /dev/null +++ b/examples/public-inbox-nntpd.socket @@ -0,0 +1,10 @@ +# ==> /etc/systemd/system/public-inbox-nntpd.socket <== +[Unit] +Description = public-inbox-nntpd socket + +[Socket] +ListenStream = 119 +Service = public-inbox-nntpd@1.service + +[Install] +WantedBy = sockets.target diff --git a/examples/public-inbox-nntpd@.service b/examples/public-inbox-nntpd@.service new file mode 100644 index 00000000..62202c2f --- /dev/null +++ b/examples/public-inbox-nntpd@.service @@ -0,0 +1,32 @@ +# ==> /etc/systemd/system/public-inbox-nntpd@.service <== +# Since SIGUSR2 upgrades do not work under systemd, this service file +# allows starting two simultaneous services during upgrade time +# (e.g. public-inbox-nntpd@1 public-inbox-nntpd@2) with the intention +# that they take turns running in-between upgrades. This should +# allow upgrading without downtime. + +[Unit] +Description = public-inbox NNTP server %i +Wants = public-inbox-nntpd.socket +After = public-inbox-nntpd.socket + +[Service] +Environment = PI_CONFIG=/home/pi/.public-inbox/config \ +PATH=/usr/local/bin:/usr/bin:/bin \ +PERL_INLINE_DIRECTORY=/tmp/.pub-inline + +LimitNOFILE = 30000 +ExecStartPre = /bin/mkdir -p -m 1777 /tmp/.pub-inline +ExecStart = /usr/local/bin/public-inbox-nntpd \ +-1 /var/log/public-inbox/nntpd.out.log +StandardError = syslog +Sockets = public-inbox-nntpd.socket +KillSignal = SIGQUIT +User = nobody +Group = nogroup +ExecReload = /bin/kill -HUP $MAINPID +TimeoutStopSec = 86400 +KillMode = process + +[Install] +WantedBy = multi-user.target diff --git a/examples/public-inbox.psgi b/examples/public-inbox.psgi index 71592a7a..e97f917f 100644 --- a/examples/public-inbox.psgi +++ b/examples/public-inbox.psgi @@ -11,10 +11,11 @@ use PublicInbox::WWW; PublicInbox::WWW->preload; use Plack::Builder; my $www = PublicInbox::WWW->new; + +# share the public-inbox code itself: +my $src = $ENV{SRC_GIT_DIR}; # '/path/to/public-inbox.git' + builder { - # Chunked middleware conflicts with Starman: - # https://github.com/miyagawa/Starman/issues/23 - # enable 'Chunked'; eval { enable 'Deflater', content_type => [ qw( @@ -28,7 +29,7 @@ builder { # Enable to ensure redirects and Atom feed URLs are generated # properly when running behind a reverse proxy server which - # sets X-Forwarded-For and X-Forwarded-Proto request headers. + # sets the X-Forwarded-Proto request header. # See Plack::Middleware::ReverseProxy documentation for details eval { enable 'ReverseProxy' }; $@ and warn @@ -43,5 +44,15 @@ builder { # format => '%t "%r" %>s %b %D'; enable 'Head'; - sub { $www->call(@_) }; + sub { + my ($env) = @_; + # share public-inbox.git code! + if ($src && $env->{PATH_INFO} =~ + m!\A/(?:public-inbox(?:\.git)?/)? + ($PublicInbox::GitHTTPBackend::ANY)\z!xo) { + PublicInbox::GitHTTPBackend::serve($env, $src, $1); + } else { + $www->call($env); + } + }; } diff --git a/examples/unsubscribe-milter.socket b/examples/unsubscribe-milter.socket new file mode 100644 index 00000000..bfaa97a1 --- /dev/null +++ b/examples/unsubscribe-milter.socket @@ -0,0 +1,10 @@ +# ==> /etc/systemd/system/unsubscribe-milter.socket <== +[Unit] +Description = unsubscribe.milter socket + +[Socket] +ListenStream = /var/spool/postfix/unsubscribe/unsubscribe.sock +Service = unsubscribe-milter@1.service + +[Install] +WantedBy = sockets.target diff --git a/examples/unsubscribe-milter@.service b/examples/unsubscribe-milter@.service new file mode 100644 index 00000000..98e3d478 --- /dev/null +++ b/examples/unsubscribe-milter@.service @@ -0,0 +1,24 @@ +# ==> /etc/systemd/system/unsubscribe-milter@.service <== +# The '@' is to allow multiple simultaneous services to start +# and share the same socket so new code can be cycled in +# without downtime + +[Unit] +Description = unsubscribe milter %i +Wants = unsubscribe-milter.socket +After = unsubscribe-milter.socket + +[Service] +# First 8 bytes is for the key, next 8 bytes is for the IV +# using Blowfish. We want as short URLs as possible to avoid +# copy+paste errors +# umask 077 && dd if=/dev/urandom bs=16 count=1 of=.unsubscribe.key +ExecStart = /usr/local/sbin/unsubscribe.milter /home/mlmmj/.unsubscribe.key +Sockets = unsubscribe-milter.socket + +# the corresponding PSGI app needs permissions to modify the +# mlmmj spool, so we might as well use the same user since +User = mlmmj + +[Install] +WantedBy = multi-user.target diff --git a/examples/unsubscribe-psgi.socket b/examples/unsubscribe-psgi.socket new file mode 100644 index 00000000..e7ab797b --- /dev/null +++ b/examples/unsubscribe-psgi.socket @@ -0,0 +1,11 @@ +# ==> /etc/systemd/system/unsubscribe-psgi.socket <== +[Unit] +Description = unsubscribe PSGI socket + +[Socket] +# Forward to the PSGI using nginx or similar +ListenStream = /run/unsubscribe-psgi.sock +Service = unsubscribe-psgi@1.service + +[Install] +WantedBy = sockets.target diff --git a/examples/unsubscribe-psgi@.service b/examples/unsubscribe-psgi@.service new file mode 100644 index 00000000..acc29e8e --- /dev/null +++ b/examples/unsubscribe-psgi@.service @@ -0,0 +1,21 @@ +# ==> /etc/systemd/system/unsubscribe-psgi@.service <== +# The '@' is to allow multiple simultaneous services to start +# and share the same socket so new code can be cycled in +# without downtime + +[Unit] +Description = unsubscribe PSGI %i +Wants = unsubscribe-psgi.socket +After = unsubscribe-psgi.socket + +[Service] +# any PSGI server ought to work, +# but public-inbox-httpd supports socket activation like unsubscribe.milter +ExecStart = /usr/local/bin/public-inbox-httpd -W0 /etc/unsubscribe.psgi +Sockets = unsubscribe-psgi.socket +# we need to modify the mlmmj spool +User = mlmmj +KillMode = process + +[Install] +WantedBy = multi-user.target diff --git a/examples/unsubscribe.milter b/examples/unsubscribe.milter new file mode 100644 index 00000000..c245a5b8 --- /dev/null +++ b/examples/unsubscribe.milter @@ -0,0 +1,134 @@ +#!/usr/bin/perl -w +# Copyright (C) 2016 all contributors <meta@public-inbox.org> +# License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt> +use strict; +use warnings; +use Sendmail::PMilter qw(:all); +use IO::Socket; +use Crypt::CBC; +use MIME::Base64 qw(encode_base64url); + +my $key_file = shift @ARGV or die "Usage: $0 KEY_FILE\n"; +open my $fh, '<', $key_file or die "failed to open $key_file\n"; +my ($key, $iv); +if (read($fh, $key, 8) != 8 || read($fh, $iv, 8) != 8 || + read($fh, my $end, 8) != 0) { + die "KEY_FILE must be 16 bytes\n"; +} + +# these parameters were chosen to generate shorter parameters +# to reduce the possibility of copy+paste errors +my $crypt = Crypt::CBC->new(-key => $key, + -iv => $iv, + -header => 'none', + -cipher => 'Blowfish'); +$fh = $iv = $key = undef; + +my %cbs; +$cbs{connect} = sub { + my ($ctx) = @_; + eval { $ctx->setpriv({ header => {}, envrcpt => {} }) }; + warn $@ if $@; + SMFIS_CONTINUE; +}; + +$cbs{envrcpt} = sub { + my ($ctx, $addr) = @_; + eval { + $addr =~ tr!<>!!d; + $ctx->getpriv->{envrcpt}->{$addr} = 1; + }; + warn $@ if $@; + SMFIS_CONTINUE; +}; + +$cbs{header} = sub { + my ($ctx, $k, $v) = @_; + eval { + my $k_ = lc $k; + if ($k_ eq 'list-unsubscribe') { + my $header = $ctx->getpriv->{header} ||= {}; + my $ary = $header->{$k_} ||= []; + + # we create placeholders in case there are + # multiple headers of the same name + my $cur = []; + push @$ary, $cur; + + # This relies on mlmmj convention: + # $LIST+unsubscribe@$DOMAIN + if ($v =~ /\A<mailto:([^@]+)\+unsubscribe@([^>]+)>\z/) { + @$cur = ($k, $v, $1, $2); + + # Mailman convention: + # $LIST-request@$DOMAIN?subject=unsubscribe + } elsif ($v =~ /\A<mailto:([^@]+)-request@ + ([^\?]+)\?subject=unsubscribe>\z/x) { + # @$cur = ($k, $v, $1, $2); + } + } + }; + warn $@ if $@; + SMFIS_CONTINUE; +}; + +# We don't want people unsubscribing archivers: +sub archive_addr { + my ($addr) = @_; + return 1 if ($addr =~ /\@m\.gmane\.org\z/); + return 1 if ($addr eq 'archive@mail-archive.com'); + 0 +} + +$cbs{eom} = sub { + my ($ctx) = @_; + eval { + my $priv = $ctx->getpriv; + $ctx->setpriv({ header => {}, envrcpt => {} }); + my @rcpt = keys %{$priv->{envrcpt}}; + + # one recipient, one unique HTTP(S) URL + return SMFIS_CONTINUE if @rcpt != 1; + return SMFIS_CONTINUE if archive_addr(lc($rcpt[0])); + + my $unsub = $priv->{header}->{'list-unsubscribe'} || []; + my $n = 0; + foreach my $u (@$unsub) { + # Milter indices are 1-based, + # not 0-based like Perl arrays + my $index = ++$n; + my ($k, $v, $list, $domain) = @$u; + + next unless $k && $v && $list && $domain; + my $u = $crypt->encrypt($rcpt[0]); + $u = encode_base64url($u); + $v .= ",\n <https://$domain/u/$u/$list>"; + + $ctx->chgheader($k, $index, $v); + } + }; + warn $@ if $@; + SMFIS_CONTINUE; +}; + +my $milter = Sendmail::PMilter->new; + +# Try to inherit a socket from systemd or similar: +my $fds = $ENV{LISTEN_FDS}; +if ($fds && (($ENV{LISTEN_PID} || 0) == $$)) { + die "$0 can only listen on one FD\n" if $fds != 1; + my $start_fd = 3; + my $s = IO::Socket->new_from_fd($start_fd, 'r') or + die "inherited bad FD from LISTEN_FDS: $!\n"; + $milter->set_socket($s); +} else { + # fall back to binding a socket: + my $sock = 'unix:/var/spool/postfix/unsubscribe/unsubscribe.sock'; + $milter->set_listen(1024); + my $umask = umask 0000; + $milter->setconn($sock); + umask $umask; +} + +$milter->register('unsubscribe', \%cbs, SMFI_CURR_ACTS); +$milter->main(); diff --git a/examples/unsubscribe.psgi b/examples/unsubscribe.psgi new file mode 100644 index 00000000..5b9b16cc --- /dev/null +++ b/examples/unsubscribe.psgi @@ -0,0 +1,69 @@ +#!/usr/bin/perl -w +# Copyright (C) 2016 all contributors <meta@public-inbox.org> +# License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt> +# This should not require any other PublicInbox code, but may use +# PublicInbox::Config if ~/.public-inbox/config exists or +# PI_CONFIG is pointed to an appropriate location +use strict; +use Plack::Builder; +use PublicInbox::Unsubscribe; +my $app = PublicInbox::Unsubscribe->new( + pi_config => eval { # optional, for pointing out archives + require PublicInbox::Config; + # uses ~/.public-inbox/config by default, + # can override with PI_CONFIG or here since + # I run this .psgi as the mlmmj user while the + # public-inbox-mda code which actually writes to + # the archives runs as a different user. + PublicInbox::Config->new('/home/pi/.public-inbox/config') + }, + # change if you fork + code_url => 'https://public-inbox.org/public-inbox.git', + owner_email => 'BOFH@example.com', + confirm => 0, + + # First 8 bytes is for the key, next 8 bytes is for the IV + # using Blowfish. We want as short URLs as possible to avoid + # copy+paste errors + # umask 077 && dd if=/dev/urandom bs=16 count=1 of=.unsubscribe.key + key_file => '/home/mlmmj/.unsubscribe.key', + + # this runs as whatever user has perms to run /usr/bin/mlmmj-unsub + # users of other mailing lists. Returns '' on success. + unsubscribe => sub { + my ($user_addr, $list_addr) = @_; + + # map list_addr to mlmmj spool, I use: + # /home/mlmmj/spool/$LIST here + my ($list, $domain) = split('@', $list_addr, 2); + my $spool = "/home/mlmmj/spool/$list"; + + return "Invalid list: $list" unless -d $spool; + + # -c to send a confirmation email, -s is important + # in case a user is click-happy and clicks twice. + my @cmd = (qw(/usr/bin/mlmmj-unsub -c -s), + '-L', $spool, '-a', $user_addr); + + # we don't know which version they're subscribed to, + # try both non-digest and digest + my $normal = system(@cmd); + my $digest = system(@cmd, '-d'); + + # success if either succeeds: + return '' if ($normal == 0 || $digest == 0); + + # missing executable or FS error, + # otherwise -s always succeeds, right? + return 'Unknown error, contact admin'; + }, +); + +builder { + mount '/u' => builder { + eval { enable 'Deflater' }; # optional + eval { enable 'ReverseProxy' }; # optional + enable 'Head'; + sub { $app->call(@_) }; + }; +}; diff --git a/examples/varnish-4.vcl b/examples/varnish-4.vcl new file mode 100644 index 00000000..24296032 --- /dev/null +++ b/examples/varnish-4.vcl @@ -0,0 +1,68 @@ +# Example VCL for Varnish 4.0 with public-inbox WWW code +# This is based on what shipped for 3.x a long time ago (I think) +# and I'm hardly an expert in VCL (nor should we expect anybody +# who maintains a public-inbox HTTP interface to be). +# +# It seems to work for providing some protection from traffic +# bursts; but perhaps the public-inbox WWW interface can someday +# provide enough out-of-the-box performance that configuration +# of an extra component is pointless. + +vcl 4.0; +backend default { + # this is where public-inbox-http listens + .host = "127.0.0.1"; + .port = "280"; +} + +sub vcl_recv { + /* pipe POST and any other weird methods directly to backend */ + if (req.method != "GET" && req.method != "HEAD") { + return (pipe); + } + if (req.http.Authorization || req.http.Cookie) { + /* Not cacheable by default */ + return (pass); + } + return (hash); +} + +sub vcl_pipe { + # By default Connection: close is set on all piped requests by varnish, + # but public-inbox-httpd supports persistent connections well :) + unset bereq.http.connection; + return (pipe); +} + +sub vcl_hash { + hash_data(req.url); + if (req.http.host) { + hash_data(req.http.host); + } else { + hash_data(server.ip); + } + /* we generate fully-qualified URLs for Atom feeds and redirects */ + if (req.http.X-Forwarded-Proto) { + hash_data(req.http.X-Forwarded-Proto); + } + return (lookup); +} + +sub vcl_backend_response { + set beresp.grace = 60s; + set beresp.do_stream = true; + if (beresp.ttl <= 0s || + /* no point in caching stuff git already stores on disk */ + beresp.http.Content-Type ~ "application/x-git" || + beresp.http.Set-Cookie || + beresp.http.Vary == "*") { + /* Mark as "Hit-For-Pass" for the next 2 minutes */ + set beresp.ttl = 120 s; + set beresp.uncacheable = true; + return (deliver); + } else { + /* short TTL for up-to-dateness, our PSGI is not that slow */ + set beresp.ttl = 10s; + } + return (deliver); +} |