#!/usr/bin/perl # Box Backup web management interface (c) Chris Wilson, 2008 # # LICENSE: The Box Backup license applies to this code, with the following # additional conditions: # # If you make any changes to this code, except for changes to existing # variables in the Configuration section below, you must publish the changes # under the same license, whether or not you distribute copies of the # changed version. # # If you use any of this code in a derivative work, you must publish the # source code of the derivative work under the same or compatible license, # whether or not you distribute copies of the derivative work. # # The terms of the Box Backup license may be viewed here: # https://www.boxbackup.org/license.html # # If you require access to the code under a different license, this may # be negotiated with the copyright holder. use strict; use warnings; # Variables which you may need to change to match your installation # Changes to existing variables are NOT required to be published. my $box_dir = "/etc/box"; my $bbstored_dir = "$box_dir/bbstored"; my $ca_dir = "/mnt/backup/boxbackup/ca"; # You should not need to change these unless you have a non-standard installation my $bbstored_conf_file = "$box_dir/bbstored.conf"; my $bbstoreaccounts = "/usr/local/sbin/bbstoreaccounts"; my $accounts_db_file = undef; # my $accounts_db_file = "/etc/box/bbstored/accounts.txt"; my $raidfile_conf_file = undef; # my $raidfile_conf_file = "/etc/box/raidfile.conf"; my $sign_period = '5000'; # install Perl module with: # perl -MCPAN -e 'install Config::Scoped' # perl -MCPAN -e 'force install P/PT/PTHOMSEN/BoxBackup/BBConfig-0.03.tar.gz' # perl -MCPAN -e 'install Convert::ASN1' # download http://search.cpan.org/CPAN/authors/id/L/LE/LEO/Convert-X509-0.1.tar.gz, # unpack, and move the Convert folder to /usr/lib/perl5/site_perl/X.X.X # Check that SSL is being used. # DO NOT DISABLE THIS unless you really know what you're doing! die "This script requires an SSL web server" unless $ENV{HTTPS}; # Check that the script is protected by basic authentication. # DO NOT DISABLE THIS unless you really know what you're doing! die "This script requires HTTP Authentication" unless $ENV{REMOTE_USER}; # You should not need to change anything below this line. # If you do, you must publish your changes to comply with the license. use BoxBackup::Config::Accounts; use BoxBackup::Config::DiskSets; use CGI::Carp qw(fatalsToBrowser); use CGI::Pretty; use Config::Scoped; use Convert::X509::Request; use English; use Fcntl; use File::Temp; use URI; use URI::QueryParam; sub check_access($$) { my ($file,$desc) = @_; unless (-r $file) { open FILE, "< $file" and die "should have failed"; die "Failed to access $desc ($file): $!"; } } sub check_executable($$) { my ($file,$desc) = @_; unless (-x $file) { open FILE, "< $file" and die "should have failed"; die "$desc is not executable ($file): $!"; } } my $cgi = new CGI; if (my $download = $cgi->param("download")) { my ($filename, $acct_no); if ($download eq "cert") { $acct_no = $cgi->param("account"); $acct_no =~ tr/0-9a-fA-F//cd; $filename = "$acct_no-cert.pem"; } elsif ($download eq "cacert") { $filename = "serverCA.pem"; } else { die "No such download method $download"; } print $cgi->header(-type => "text/plain", -"content-disposition" => "attachment; filename=$filename"); my $send_file; if ($download eq "cert") { $send_file = "$ca_dir/clients/$filename"; } elsif ($download eq "cacert") { $send_file = "$ca_dir/roots/serverCA.pem"; } die "File does not exist: $send_file" unless -f $send_file; die "File is not readable by user " . getpwuid($UID) . ": $send_file" unless -r $send_file; open SENDFILE, "< $send_file" or die "Failed to open file " . "$send_file: $!"; while (my $line = ) { print $line; } close SENDFILE; exit 0; } print $cgi->header(), $cgi->start_html(-title=>"Box Backup Certificates", -style=>'bb.css'); print $cgi->h1("Box Backup Certificates"); check_access($bbstored_conf_file, "BBStoreD configuration file"); my $bbstored_conf = Config::Scoped->new(file => $bbstored_conf_file)->parse(); $accounts_db_file ||= $bbstored_conf->{'Server'}{'AccountDatabase'}; die "Missing AccountDatabase in $bbstored_conf_file" unless $accounts_db_file; check_access($accounts_db_file, "Accounts Database"); $raidfile_conf_file ||= $bbstored_conf->{'Server'}{'RaidFileConf'}; die "Missing RaidFileConf in $bbstored_conf_file" unless $raidfile_conf_file; check_access($raidfile_conf_file, "RaidFile configuration file"); my $accounts_db = BoxBackup::Config::Accounts->new($accounts_db_file); check_executable($bbstoreaccounts, "bbstoreaccounts program"); sub error($) { my ($message) = @_; unless ($message =~ /^p($message); } print $cgi->div({-class=>"error"}, $message); return 0; } sub url { my $cgi = shift @_; my %params = @_; my $uri = URI->new($cgi->url(-absolute=>1)); foreach my $param (keys %params) { $uri->query_param($param, $params{$param}); } return $uri; } sub create_account($) { my ($cgi) = @_; my $upload = $cgi->upload('req'); unless ($upload) { return error("Please attach a certificate request file."); } my $tempfile = File::Temp->new("bbaccount-certreq-XXXXXX.pem"); my $csr_data = ""; while (my $line = <$upload>) { print $tempfile $line; $csr_data .= $line; } my @accounts = $accounts_db->getAccountIDs(); my $new_acc_no = $cgi->param('account'); if (not $new_acc_no) { return error("Please enter an account number."); } foreach my $account_no (@accounts) { if ($account_no == $new_acc_no) { return error("The account number $new_acc_no " . "already exists, please use one which " . "does not."); } } my $req = Convert::X509::Request->new($csr_data); my $cn; foreach my $part ($req->subject) { if ($part =~ /^cn=(.*)/i) { $cn = $1; last; } } unless ($cn) { return error("The certificate request does not include a " . "common name, which should be BACKUP-$new_acc_no."); } unless ($cn eq "BACKUP-$new_acc_no") { return error("The certificate request includes the wrong " . "common name. Expected " . "BACKUP-$new_acc_no but found " . "$cn."); } my $out_cert_dir = "$ca_dir/clients"; unless (-w $out_cert_dir) { return error("Cannot write to certificate directory " . "$out_cert_dir as user " . "" . getpwuid($UID) . "."); } my $out_cert = "$out_cert_dir/$new_acc_no-cert.pem"; if (-f $out_cert and not -w $out_cert) { return error("The certificate file $out_cert " . "exists and is not writable as user " . "$out_cert_dir as user " . "" . getpwuid($UID) . "."); } my $client_ca_cert_file = "$ca_dir/roots/clientCA.pem"; unless (-r $client_ca_cert_file) { return error("The client CA certificate file " . "$client_ca_cert_file " . "is not readable by user " . "" . getpwuid($UID) . "."); } my $client_ca_key_file = "$ca_dir/keys/clientRootKey.pem"; unless (-r $client_ca_key_file) { return error("The client CA key file " . "$client_ca_key_file " . "is not readable by user " . "" . getpwuid($UID) . "."); } my $serial_file = "$ca_dir/roots/clientCA.srl"; unless (-w $serial_file) { return error("The certificate serial number file " . "$serial_file " . "is not writable by user " . "" . getpwuid($UID) . "."); } my $outputfile = File::Temp->new("bbaccounts-openssl-output-XXXXXX"); if (system("openssl x509 -req -in $tempfile -sha1 " . "-extensions usr_crt " . "-CA $client_ca_cert_file " . "-CAkey $client_ca_key_file " . "-out $out_cert -days $sign_period " . ">$outputfile 2>&1") != 0) { open ERR, "< $outputfile" or die "$outputfile: $!"; my $errors = join("", ); close ERR; return error($cgi->p("Failed to sign certificate:") . $cgi->pre($errors)); } my $cert_uri = url($cgi, download => "cert", account => $new_acc_no); my $ca_uri = url($cgi, download => "cacert"); print $cgi->div({-class=>"success"}, $cgi->p("Account created. Please download the following " . "files:") . $cgi->ul( $cgi->li($cgi->a({href=>$cert_uri}, "Client Certificate")), $cgi->li($cgi->a({href=>$ca_uri}, "CA Certificate")) ) ); return 1; } if ($cgi->param("create")) { print $cgi->h2("Account Creation"); create_account($cgi); } print $cgi->h2("Accounts"); print $cgi->start_table({-border=>0, -class=>"numbers"}); print $cgi->Tr( $cgi->th("Account"), $cgi->th('Used'), $cgi->th('%'), $cgi->th('Old files'), $cgi->th('%'), $cgi->th('Deleted files'), $cgi->th('%'), $cgi->th('Directories'), $cgi->th('%'), $cgi->th('Soft limit'), $cgi->th('%'), $cgi->th('Hard limit'), $cgi->th('Actions') ); sub human_format($) { my ($kb) = @_; die "bad format in value: expected number followed by kB, " . "found '$kb'" unless $kb =~ /^(\d+) (kB)$/; my $value = $1; my $units = $2; if ($value > 1024) { $value /= 1024; $units = "MB"; } if ($value > 1024) { $value /= 1024; $units = "GB"; } $value = sprintf("%.1f", $value); return "$value $units"; } sub bbstoreaccounts_format($) { my ($kb) = @_; die unless $kb =~ /^(\d+) (kB)$/; my $value = $1; my $units = "K"; unless ($value % 1024) { $value /= 1024; $units = "M"; } unless ($value % 1024) { $value /= 1024; $units = "G"; } return "$value$units"; } sub get_account_info($) { my ($account) = @_; open BBSA, "$bbstoreaccounts -c $bbstored_conf_file -m info $account |" or die "Failed to get account info for $account: $!"; my $account_info = {}; while (my $line = ) { unless ($line =~ m/([^:]*): (.*)/) { die "Bad format in bbstoreaccounts info output " . "for account $account: '$line'"; } my $name = $1; my $value = $2; if ($value =~ /(.*), (.*)/) { $account_info->{$name} = [$1, $2]; } else { $account_info->{$name} = $value; } } return $account_info; } sub format_account_info($) { my ($values) = @_; my $kb = $values->[0]; my $pc = $values->[1]; return $cgi->td(human_format($kb)), $cgi->td($values->[1]); } my %account_numbers; my @accounts = $accounts_db->getAccountIDs(); foreach my $i (@accounts) { die "Duplicate account number $i" if $account_numbers{hex($i)}; $account_numbers{hex($i)} = 1; # Find out what account is on what diskset. my $disk = $accounts_db->getDisk($i); # store limits my $account_info = get_account_info($i); print $cgi->Tr( $cgi->td($i), format_account_info($account_info->{'Used'}), format_account_info($account_info->{'Old files'}), format_account_info($account_info->{'Deleted files'}), format_account_info($account_info->{'Directories'}), format_account_info($account_info->{'Soft limit'}), $cgi->td(human_format($account_info->{'Hard limit'}[0])), $cgi->td($cgi->a({-href=>url($cgi, account=>$i)}, "Edit")) ); } print $cgi->end_table(); my $account_no = $cgi->param("account"); $account_no =~ tr/0-9a-fA-F//cd; if (not $cgi->param("showcreate")) { print $cgi->start_form, $cgi->submit(-name=>"showcreate", -value=>"Create Account"), $cgi->end_form(); } if ($account_no) { print $cgi->h2("Edit Account"); my $account_info = get_account_info($account_no); $cgi->param("account", $account_no); $cgi->param("soft_limit", bbstoreaccounts_format($account_info->{'Soft limit'}[0])); $cgi->param("hard_limit", bbstoreaccounts_format($account_info->{'Hard limit'}[0])); } elsif ($cgi->param("showcreate")) { print $cgi->h2("Create Account"); } if ($account_no or $cgi->param("showcreate")) { my $new_account_no = 1; while ($account_numbers{$new_account_no}) { $new_account_no++; } my $disksets_conf = BoxBackup::Config::DiskSets->new($raidfile_conf_file); my @disk_names = $disksets_conf->getListofDisks(); my @disk_numbers; my %disk_labels; foreach my $name (@disk_names) { my $num = $disksets_conf->getParamVal($name, "SetNumber"); push @disk_numbers, $num; $disk_labels{$num} = $name; } print $cgi->start_multipart_form(), $cgi->start_table(); if ($account_no) { print $cgi->Tr( $cgi->th("Account Number"), $cgi->td($account_no . $cgi->hidden("account", $account_no)) ); } else { print $cgi->Tr( $cgi->th("Account Number"), $account_no ? $account_no : $cgi->td($cgi->textfield(-name => "account", -default => sprintf("%x", $new_account_no))), ); } if (not $account_no) { print $cgi->Tr( $cgi->th("Disk Set"), $cgi->td($cgi->popup_menu(-name => "disk_set", -values => \@disk_numbers, -labels => \%disk_labels)) ); } print $cgi->Tr( $cgi->th("Soft Limit"), $cgi->td($cgi->textfield(-name => "soft_limit", -default => "10G")) ), $cgi->Tr( $cgi->th("Hard Limit"), $cgi->td($cgi->textfield(-name => "hard_limit", -default => "20G")) ), $cgi->Tr( $cgi->th("Certificate Request"), $cgi->td($cgi->filefield({ -name => "req", -default => "*.crt" })) ); if ($account_no) { print $cgi->Tr( $cgi->th(), $cgi->td($cgi->submit(-name => "update", -value => "Update Account")) ); } else { print $cgi->Tr( $cgi->th(), $cgi->td($cgi->submit(-name => "create", -value => "Create Account")) ); } print $cgi->end_table(), $cgi->end_form(); } print $cgi->end_html; exit 0;