Three tiny CGI projects that would be miserable in anything else
The Perl Community Conference
Summer 2026
dhcp-manager mega home-interface
Demo
REST API in front of isc-dhcp-server.
Parses dhcpd.leases, blocks/unblocks clients at the firewall.
Web UI on top of the MegaCLI binary
for LSI RAID controllers. Dashboard, drives, BBU, event log.
The umbrella dashboard. Aggregates status across every server — storage, RAID, DHCP, traffic.
A REST API in front of isc-dhcp-server
One file, one script, one process per request. No daemon to babysit.
#!/usr/bin/perl -T
use strict;
use warnings;
use CGI;
use JSON::XS;
use Fcntl qw(:flock);
# The -T flag makes Perl refuse to use any data that came from
# outside the program (query strings, environment, file contents)
# in a "dangerous" context -- shelling out, opening a file, etc. --
# until we've explicitly untainted it with a regex capture.
sudo manage_client.sh <ip>. Getting user input into that
command line without taint mode is a CVE waiting to happen. Perl gives you a runtime
safety net that no other scripting language bundles by default.
sub validate_ip {
my ($raw) = @_;
return undef unless defined $raw;
if ($raw =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/) {
my $ip = $1; # <-- untainted by the capture
my @octets = split /\./, $ip;
for my $o (@octets) {
return undef if $o > 255;
}
return undef if $octets[0] == 0; # 0.x.x.x
return undef if $octets[0] == 127; # loopback
return undef if $octets[0] >= 224; # multicast / reserved
return $ip;
}
return undef;
}
The regex capture is what makes $1 safe to pass to a shell.
“Validate” and “untaint” become the same operation.
open(my $fh, '<', $LEASES_FILE) or return {};
flock($fh, LOCK_SH);
my ($in_lease, $current_ip);
my %leases;
while (my $line = <$fh>) {
chomp $line; $line =~ s/^\s+//;
if ($line =~ /^lease\s+(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\s*\{/) {
$in_lease = 1;
$current_ip = $1;
}
elsif ($in_lease && $line =~ /^hardware\s+ethernet\s+([0-9a-fA-F:]+)\s*;/) {
$leases{$current_ip}{mac} = validate_mac($1) // '';
}
elsif ($in_lease && $line =~ /^client-hostname\s+"([^"]*)"\s*;/) {
$leases{$current_ip}{hostname} = $1;
}
elsif ($line =~ /^\}/) {
$in_lease = 0;
}
}
Six regexes, a state flag, a hash. This is the whole parser. Try writing that in Go without missing lunch.
-T taint mode. Nothing else ships this.
Python has ast-grep, Ruby has $SAFE (deprecated). Perl gives me a hard runtime failure
the moment I shell out with untrusted data.CGI + JSON::XS.
One apt-get install libjson-xs-perl and you’re done.A browser UI for LSI MegaRAID controllers
megacli, parse the text,
emit JSON. That’s it. That’s the whole product.
Same shape as dhcp-manager. Different tool being wrapped.
my %dispatch = (
'adapters' => \&get_adapters,
'adapter_info' => \&get_adapter_info,
'summary' => \&get_summary,
'vdrives' => \&get_virtual_drives,
'pdrives' => \&get_physical_drives,
'pdrive_info' => \&get_physical_drive_info,
'bbu' => \&get_bbu_status,
'rebuild' => \&get_rebuild_progress,
'enclosures' => \&get_enclosures,
'event_log' => \&get_event_log,
);
my $action = param_word('action', 'summary');
if (exists $dispatch{$action}) {
my $result = eval { $dispatch{$action}->() };
print $json->encode($@ ? { error => "$@" } : $result);
} else {
print $json->encode({ error => "unknown action: $action" });
}
A hash of coderefs is your router. No framework, no annotations, no reflection. Perl treats functions as data — use it.
sub untaint {
my ($val, $re) = @_;
return undef unless defined $val;
return ($val =~ /($re)/) ? $1 : undef;
}
sub param_digits {
my ($name, $default) = @_;
my $raw = $cgi->param($name) // $default;
my $clean = untaint($raw, qr/\d+/);
return defined $clean ? $clean : $default;
}
# Later, when building the command line:
my $adapter = param_digits('adapter', 0);
my $out = `sudo $MEGACLI -LDInfo -Lall -a$adapter -NoLog`;
qr/\d+/
gives you a regex object you can pass around, cache, and compose.
This is why the untaint helper is three lines.
# MegaCLI emits blocks like:
#
# Virtual Drive: 0 (Target Id: 0)
# Name :
# RAID Level : Primary-1, Secondary-0, RAID Level Qualifier-0
# Size : 1.818 TB
# State : Optimal
# Number Of Drives : 2
sub parse_kv_block {
my ($text) = @_;
my %data;
for my $line (split /\n/, $text) {
if ($line =~ /^\s*(.+?)\s*:\s*(.+?)\s*$/) {
my ($k, $v) = ($1, $2);
$k =~ s/\s+/_/g; # "RAID Level" -> "RAID_Level"
$data{$k} = $v;
}
}
return \%data;
}
Twelve lines. That’s the whole parser for a text format that has actively resisted standardization for two decades.
JSON::encode
turns it into a browser response. One data structure end-to-end.my $out = `sudo megacli ...`; — combined with taint mode —
is a perfectly reasonable subprocess API when the input has been untainted through a regex.cp. One .pl file
into the CGI directory. No virtualenv, no go build,
no container. The script is the artifact.The umbrella dashboard for the whole lab
dashboard, storage,
raid, dhcp, traffic,
toolsservers.js declaring six named servers:
Templar, Mage, Paladin, Druid, Ranger, Knight
#!/usr/bin/perl
use strict;
use warnings;
my $STORCLI = "/opt/MegaRAID/storcli/storcli64";
print "Content-Type: application/json; charset=utf-8\r\n";
print "Access-Control-Allow-Origin: *\r\n";
print "Cache-Control: no-store\r\n";
print "\r\n";
if (! -x $STORCLI) {
print qq({"error":"storcli not found at $STORCLI"});
exit 0;
}
my $cmd = "sudo -n $STORCLI /call show all J 2>&1";
my $out = `$cmd`;
my $rc = $? >> 8;
if ($rc != 0) {
my $msg = $out;
$msg =~ s/\\/\\\\/g;
$msg =~ s/"/\\"/g;
$msg =~ s/[\r\n]+/\\n/g;
print qq({"error":"storcli exited with $rc","output":"$msg"});
exit 0;
}
print $out; # storcli already emits JSON with the "J" flag
import express.print. CORS included.print $out.cgi-bin/.
.pl into
cgi-bin/.raid.pl hangs,
it doesn’t take lvs.pl with it. Each request is a fresh process.
That’s a feature I’m paying for elsewhere in every long-running server.megacli, lvs,
storcli, dhcpd.leases.
You’re not going to change that.CGI.pm + JSON.
Under 100 lines per endpoint.$line =~ /pattern/ is the language.scp file.pl. Done.Questions?
dhcp-manager · mega · home-interface
The Perl Community Conference · Summer 2026