From 5dde16db1463aa520d256ad34e1f7e3feb0b3211 Mon Sep 17 00:00:00 2001 From: "B. Watson" Date: Sun, 26 Aug 2018 03:29:48 -0400 Subject: slacktopic.pl: rewrite, add paranoia, no more external script --- slack_last_update.sh | 15 ++-- slacktopic.pl | 224 ++++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 187 insertions(+), 52 deletions(-) diff --git a/slack_last_update.sh b/slack_last_update.sh index 696bed5..1f04cb6 100755 --- a/slack_last_update.sh +++ b/slack_last_update.sh @@ -15,21 +15,26 @@ # it depends on what the error actually was. # This script can be tested from the command line, but in production it -# will be executed from an irssi script, which will use the exit status -# to decide whether the channel /topic should be updated. +# will be executed from an irssi script, so it's got no need for options +# or verbose output. Actually, the irssi script currently doesn't even +# look at the exit status (other than to check for an error), so even +# that could be removed to simplify things. # For the morbidly curious: I intend this to be executed by bash, # but it also works with Slackware 14.2's ash and ksh, and dash from -# SBo. But *not* zsh: somehow $RANGE gets expanded to a quoted string, +# SBo. But *not* zsh: $RANGE gets expanded to a quoted string, # so curl gets executed as: # curl --silent '--range 0-28' http://... # and quite properly complains: # curl: option --range 0-28: is unknown -# I'm not a zsh guy, if you are & have a fix, let me know. +# I'm not a zsh guy, but I did some research... either have to use the +# syntax $=range (zsh-specific) or check for ZSH_* in the environment +# and do 'setopt shwordsplit'. Pretty sure this means I shall never +# again care whether one of my shell scripts fails on zsh. ### Config stuff. -# Where the date get stored +# Where the date gets stored. DATEFILE="$HOME/.slack_last_update" # Use the primary site, not a mirror. diff --git a/slacktopic.pl b/slacktopic.pl index 12fe27b..ea450f1 100644 --- a/slacktopic.pl +++ b/slacktopic.pl @@ -1,23 +1,35 @@ #!/usr/bin/perl # irssi script. updates the ##slackware topic any time there's a security -# update in Pat's ChangeLog. Depends on an external shell script, -# slack_last_update.sh. +# update in the Slackware ChangeLog. -# Plan of action: -# - When this script is loaded, it uses an irssi timer to check -# for updates periodically. -# - When there's an update, it asks for ops in the channel if not already -# opped, changes the topic (replacing only the [YYYY-MM-DD] part), then -# if we weren't already opped, it deops. If we *were* already opped, -# it doesn't deop (that would be *annoying*). +# At script startup, and again every $update_frequency seconds, we +# check for updates like so: +# - Exec a curl process to get the first part of the ChangeLog +# and extract the date from it. +# - Extract the date from the current /topic. +# - If the /topic has a date, and if it's different, update the +# /topic. Only the date (inside []) is changed, all the other +# stuff is left as-is. # Assumptions made: -# - By the time the first check happens ($update_frequency sec after -# script is loaded, or whenever it's run manually), we will be logged -# into services. +# - Every new ChangeLog entry is a security fix. This is almost +# 100% true for stable releases (which is what we track), and +# the only other updates will be fixes for major regressions... +# - Client is set to autojoin ##slackware, or else the user will +# always manually join it. Script doesn't do anything until +# this happens. +# - At some point we'll successfully log in to services. If the +# script tries to check for updates before that happens, it +# won't hurt anything, but the topic won't get updated either. # - We have enough ChanServ access on the channel to set the topic via -# ChanServ's topic command (flag +t in access list). +# ChanServ's topic command (flag +t in access list). Again, no +# harm done, but no topic updates either. + +# TODO: send debug/error/status messages to window #1, including +# successful topic updates. Otherwise, I'll never know when this script +# is working (the topic gets changed by ChanServ, I can't tell if that +# was my script doing it or another op doing it manually...) # References: # https://raw.githubusercontent.com/irssi/irssi/master/docs/perl.txt @@ -34,8 +46,11 @@ use Irssi qw/ window_find_name window_find_item windows + window_find_refnum channel_find command_bind + pidwait_add + signal_add_last /; our $VERSION = "0.1"; @@ -49,26 +64,68 @@ our %IRSSI = ( url => 'none', ); -# External script. Assume it's in $PATH. Could hardcode the full -# path instead. -our $update_script = "slack_last_update.sh"; +# TODO: make some or all of these config variables into irssi +# settings. Probably overkill, this script is niche-market (probably +# nobody but the author will ever run it...) + +# Print verbose debugging messages in local irssi window? +our $DEBUG = 1; + +# For testing, fake the date. 0 = use the real ChangeLog date. If you +# set this, it *must* match /\d\d\d\d-\d\d-\d\d/. +#our $FAKE = '9999-99-99'; +our $FAKE = 0; + +# Slackware ChangeLog URL. Version number is hardcoded here. +our $changelog_url = + "http://ftp.slackware.com/pub/slackware/slackware64-14.2/ChangeLog.txt"; + +# Max time curl will spend trying to do its thing. It'll give up after +# this many seconds, if it can't download the ChangeLog. +our $cmd_timeout = 60; -# Where the script stores the last update date. -our $update_file = "$ENV{HOME}/.slack_last_update"; +# $update_cmd will write its output here. It'll be in YYYY-MM-DD form, +# which is just what's needed for the /topic. +our $cmd_outfile = "$ENV{HOME}/.irssi/slack_update.txt"; + +# We /exec this to get the first line of the ChangeLog. So long as Pat +# follows his standard conventions, bytes 0-28 are the first line of +# the ChangeLog. +our $curl_args = "--silent --range 0-28 --max-time $cmd_timeout"; +our $update_cmd = "rm -f $cmd_outfile ; " . + "curl $curl_args $changelog_url |" . + "date -u -f- '+%F' > $cmd_outfile.new ; " . + "mv $cmd_outfile.new $cmd_outfile"; + +if($FAKE) { $update_cmd = "echo '$FAKE' > $cmd_outfile"; } # What channel's /topic are we updating? our $update_channel = "##slackware"; +# What server is $update_channel supposed to be on? This is paranoid +# maybe, AFAIK no other network uses the ## like freenode does, so +# the channel name ##slackware should be enough to identify it. But, +# ehhh, a little paranoia goes a long way... +our $server_regex = qr/\.freenode\.(org|net)$/; + # Seconds between update checks. Every check executes $update_script, which # talks to ftp.slackware.com, so be polite here. -# TODO: make this an irssi setting? our $update_frequency = 600; -# Print debugging message in local irssi window? -our $DEBUG = 1; +# Bookkeeping stuffs. +our $timeout_tag; +our $child_proc; +our $log_window; +# Print a message to the status window, if there is one. Otherwise print +# it to whatever the active window happens to be. Use this or one of +# (err|debug|log)msg for all output, don't use regular print or warn. sub echo { - command("/echo $_") for @_; + if($log_window) { + $log_window->print($_) for @_; + } else { + command("/echo $_") for @_; + } } sub errmsg { @@ -82,44 +139,102 @@ sub debugmsg { echo("$file:$line: $_") for @_; } +sub logmsg { + echo("$IRSSI{name}: $_") for @_; +} + +# Called once at script load. sub init { + $log_window = window_find_name("(msgs)"); # should be status window + if($log_window) { + logmsg("Logging to status window"); + } else { + logmsg("Logging to active window"); + } + debugmsg("init() called"); + # This gets called any time an /exec finished. + signal_add_last("exec remove", "finish_update"); + # Command for manual updates. - command_bind("slacktopic", "update_check"); + command_bind("slacktopic", "start_update"); # Check once at script load. - update_check(); + start_update(); # Also, automatically run it on a timer. 3rd argument unused here. - timeout_add($update_frequency * 1000, "update_check", 0); + timeout_add($update_frequency * 1000, "start_update", 0); } -sub update_check { - debugmsg("update_check() called"); +# Start the update process. +sub start_update { + debugmsg("start_update() called"); # Don't do anything if we're not joined to the channel already. my $chan = channel_find($update_channel); - debugmsg("not joined to $update_channel") unless $chan;; - return unless $chan; + if(!$chan) { + errmsg("not joined to $update_channel"); + return; + } - # Get the date of the last update. - my $new_date = exec_update(); - if(not defined $new_date) { - errmsg("couldn't get new date, not updating topic"); + my $server = $chan->{server}->{address}; + if($server !~ $server_regex) { + errmsg("channel $update_channel server is wrong: " . + $chan->{server}->{address}); + return; + } + + exec_update(); +} + +# Called when an /exec finishes. +sub finish_update { + debugmsg("finish_update() called"); + + my ($proc, $status) = @_; + + # We get called for *every* /exec. Make sure we only respond to + # the right one. + ## debugmsg("$proc->{name}: $status"); + return unless $proc->{name} eq 'slacktopic_update'; + + if(defined($timeout_tag)) { + timeout_remove($timeout_tag); + undef $timeout_tag; + undef $child_proc; + } + + my $chan = channel_find($update_channel); + if(!$chan) { + debugmsg("not joined to $update_channel"); return; } + # Get the date of the last update. + my $new_date; + open my $fh, "<$cmd_outfile" or do { + errmsg("$cmd_outfile not found, update command failed"); + return; + }; + + chomp($new_date = <$fh>); + close $fh; + $new_date ||= ""; + # This should never happen, but... - if($new_date !~ /^\d\d\d\d-\d\d-\d\d/) { - errmsg("$update_script result isn't a valid date: $new_date"); + if($new_date !~ /^\d\d\d\d-\d\d-\d\d$/) { + errmsg("$cmd_outfile content isn't a valid date: '$new_date'"); return; } # Get old topic, replace the date with the new one. debugmsg("\$new_date is: $new_date"); my $t = $chan->{topic}; - $t =~ s,\[\d\d\d\d-\d\d-\d\d\],[$new_date],; + unless($t =~ s,\[\d\d\d\d-\d\d-\d\d\],[$new_date],) { + errmsg("topic doesn't contain [yyyy-mm-dd] date, fix it manually"); + return; + } # Don't do anything if the topic's already correct. if($t eq $chan->{topic}) { @@ -129,23 +244,38 @@ sub update_check { # Ask ChanServ to change the topic for us. We don't need +o in # the channel, so long as we're logged in to services and have +t. - debugmsg("asking ChanServ to update the topic"); - $chan->{server}->send_raw("ChanServ topic $update_channel :$t"); + logmsg("ChangeLog updated [$new_date], asking ChanServ to update topic"); + $chan->{server}->send_raw("ChanServ topic $update_channel $t"); +} + +# Called if the child process times out ($cmd_timeout + 2 sec). +sub update_timed_out { + errmsg("child process timed out, killing it"); + undef $timeout_tag; + if(defined($child_proc) && defined($child_proc->{pid})) { + kill 'KILL', $child_proc->{pid}; + } + undef $child_proc; } +# Spawn $update_cmd. It'll either complete (in which case finish_update() +# gets called) or time out (in which case, update_timed_out()). sub exec_update { debugmsg("exec_update() called"); - open my $fh, "$update_script|" or do { - errmsg("couldn't execute '$update_script': $!"); - return undef; - }; - chomp(my $result = <$fh>); - close $fh; - my $status = $? >> 8; + if($timeout_tag) { + errmsg("Timeout still active, not spawning new process"); + return; + } + + $child_proc = command("/exec - -name slacktopic_update $update_cmd"); + $timeout_tag = timeout_add_once( + 1000 * ($cmd_timeout + 2), + 'update_timed_out', + 0); # last arg is unused - debugmsg("$update_script exit status: $status, result '$result'"); - return $result; + # is this really necessary? seems it isn't. + #pidwait_add($child_proc->{pid}); } # main() -- cgit v1.2.3