]> _ Git - remote-logging.git/commitdiff
server,client: Implement ssl/tls encryption master
authormivirl <>
Sun, 2 Jun 2024 17:55:26 +0000 (12:55 -0500)
committermivirl <>
Sun, 2 Jun 2024 17:55:26 +0000 (12:55 -0500)
The server and client now use ssl to communicate, with certificates
generated by `cert-server.pl`.

Clients connect to the cert-server to request a certificate using a
password. After receiving a certificate they can connect to the server
and start sending logs.

Rewrote the server in perl to facilitate use of encryption.

Removed use of actually portable perl due to the prebuilt binary not
including IO::Socket::SSL and Net::SSLeay. Rebuilding the perl binary
would be required to use encryption, so the system perl will be used
instead.

README.md
build.sh
checksums
src/cert-server.pl [new file with mode: 0644]
src/client.pl
src/server.pl [new file with mode: 0644]
src/server.sh [deleted file]
src/start-server.sh

index e7a90afaad51514dc4d59ac607d0f4b5c97f68e8..62885696afe239fa238e7f5dcfe825a1f1ea0891 100644 (file)
--- a/README.md
+++ b/README.md
@@ -40,7 +40,7 @@ To deploy on the server, you'll need to transfer the `build/_output/server`
 directory to the remote system, then `cd` to that directory and run:
 
 ```sh
-./busybox sh start_server.sh
+sh start_server.sh
 ```
 
 By default the server uses the ports 46515-46550. This can be changed in
@@ -52,7 +52,7 @@ To deploy on the client, you'll need to transfer the `build/_output/client`
 directory to the remote system, then `cd` to that directory and run:
 
 ```sh
-./perl.com client.pl
+perl client.pl
 ```
 
 ## Monitoring
index b0a25b74f63af4164026044814f3c32127949d95..222beaad3f5c1077fd095f46b27dfc9adc1abf44 100644 (file)
--- a/build.sh
+++ b/build.sh
@@ -28,10 +28,9 @@ if ! sha256sum -c ../checksums >/dev/null 2>&1; then
     wget https://github.com/inotify-tools/inotify-tools/archive/refs/tags/4.23.9.0.tar.gz
     mv 4.23.9.0.tar.gz inotify-tools-4.23.9.0.tar.gz
     wget https://github.com/tstack/lnav/releases/download/v0.11.2/lnav-0.11.2-x86_64-linux-musl.zip
-    wget https://github.com/G4Vi/Perl-Dist-APPerl/releases/download/v0.3.0/perl.com
 fi
 
-sha256sum -c ../checksums
+sha256sum -c ../checksums || exit 1
 
 # Compile busybox
 # Config file for busybox uses default settings except:
@@ -65,10 +64,10 @@ cp busybox-1.36.1/busybox _output/server/
 cp busybox-1.36.1/busybox _output/client/
 cp inotify-tools-4.23.9.0/src/inotifywait _output/client/
 cp pspy64 _output/client/
-cp perl.com _output/client/
 cp lnav-0.11.2/lnav _output/server/
-chmod u+x _output/server/*
-chmod u+x _output/client/*
 
 cp ../src/*server* _output/server/
 cp ../src/*client* _output/client/
+
+chmod u+x _output/server/*
+chmod u+x _output/client/*
index 05bc84d47ae31b1479f6857be0a41ba430e16aad..331886d41c02d128079742b2d45ff70b29e581c3 100644 (file)
--- a/checksums
+++ b/checksums
@@ -1,5 +1,4 @@
 b8cc24c9574d809e7279c3be349795c5d5ceb6fdf19ca709f80cde50e47de314  busybox-1.36.1.tar.bz2
 1dfa33f80b6797ce2f6c01f454fd486d30be4dca1b0c5c2ea9ba3c30a5c39855  inotify-tools-4.23.9.0.tar.gz
 6515598ca4985ec42daeafbae7eb5c7c6d7e94a11f88666d157b3d363aa391ff  lnav-0.11.2-x86_64-linux-musl.zip
-b5997a2683ea993aaa3a0bac08c2172afd94785b22219fddc6876f3740364d59  perl.com
 c93f29a5cc1347bdb90e14a12424e6469c8cfea9a20b800bc249755f0043a3bb  pspy64
diff --git a/src/cert-server.pl b/src/cert-server.pl
new file mode 100644 (file)
index 0000000..36af4a4
--- /dev/null
@@ -0,0 +1,229 @@
+#!/usr/bin/perl
+
+# Clients connect to this server over ssl to request a certificate
+# The provided certificate is signed by a CA ca cert to allow using
+# client certificates for authentication
+
+use strict;
+use warnings;
+use IO::Socket::INET;
+use IO::Socket::SSL;
+use Digest::SHA "sha256";
+use POSIX "strftime";
+
+umask 0077;
+
+$ENV{PATH} = '/bin:/usr/bin';
+
+my %hostnames;
+
+my ($clientname, $clientip);
+open(my $logfh, '>>', '_cert_provider_log.txt');
+sub log_client {
+    my ($message) = @_;
+    $clientname = 'unknown' if ! defined $clientname;
+    $clientip = 'unknown' if ! defined $clientip;
+    $message =~ s/"/\\"/g;
+    my $timestamp = strftime "%Y-%m-%d %H:%M:%S", localtime;
+    my $logmsg = "ts=\"$timestamp\" client=\"$clientname\" ip=\"$clientip\" message=\"$message\"\n";
+    print $logmsg;
+    print $logfh $logmsg;
+}
+
+sub log_client_die {
+    my ($message) = @_;
+    log_client $message;
+    exit 1;
+}
+
+sub find_existing_names {
+    opendir(my $dir, '.');
+    while (readdir($dir)) {
+        my ($name) = $_ =~ /client-([a-zA-Z0-9-]+)-cert.pem/;
+        $hostnames{"$name.c.logs"}++ if defined $name;
+    }
+}
+
+sub generate_ca_cert {
+    print "Generating CA private key and certificate...\n";
+    my $pid = fork();
+    if (!$pid) {
+        open STDOUT, '>', undef;
+        open STDERR, '>', undef;
+        exec 'openssl', 'req', '-x509', '-new', '-noenc', '-newkey', 'rsa:4096',
+            '-sha256', '-days', '3650', '-subj',
+            '/C=AU/ST=none/L=none/O=none/OU=none/CN=ca.logs', '-keyout',
+            'ca-key.pem', '-out', 'ca-cert.pem' || die "Failed to exec openssl";
+    }
+    waitpid ($pid, 0);
+
+    chmod 0644, 'ca-cert.pem';
+
+    return ('ca-key.pem', 'ca-cert.pem');
+}
+
+sub generate_client_cert {
+    my ($cakey, $cacert, $name, $postfix) = @_;
+
+    $clientname = "${name}${postfix}.logs";
+    my $fn;
+    if ($postfix ne '') {
+        $fn = "client-${name}";
+    } else {
+        $fn = "${name}";
+    }
+
+    if (exists $hostnames{$clientname}) {
+        return (undef, undef);
+    }
+    $hostnames{$clientname}++;
+
+    log_client "Generating private key and certificate for ${clientname}";
+    my $pid = fork();
+    if (!$pid) {
+        # Don't show output
+        open STDOUT, '>', undef;
+        open STDERR, '>', undef;
+        exec 'openssl', 'req', '-x509', '-noenc', '-newkey', 'rsa:4096', '-sha256',
+            '-days', '3650', '-subj', "/C=AU/ST=none/L=none/O=none/OU=none/CN=${clientname}",
+            '-CA', $cacert, '-CAkey', $cakey, '-keyout', "${fn}-key.pem",
+            '-out', "${fn}-cert.pem" || die "Failed to exec openssl";
+    }
+    waitpid($pid, 0);
+
+    return ("${fn}-key.pem", "${fn}-cert.pem", $clientname), 
+};
+
+sub fingerprint_cert {
+    my ($cert) = @_;
+    print "$cert : ";
+    my $pid = fork();
+    if (!$pid) {
+        exec 'openssl', 'x509', '-in', $cert, '-noout', '-fingerprint', '-sha256'
+            || die "Failed to exec openssl";
+    }
+    waitpid ($pid, 0);
+
+    return;
+}
+
+sub handle_client {
+    my ($srvkey, $srvcert, $cakey, $cacert, $pw, $client) = @_;
+    my $pid = fork();
+    return if $pid;
+
+    $clientip = $client->peerhost;
+    log_client "Client connected";
+
+    # Upgrade to SSL after fork to not slow down accepting connections
+    IO::Socket::SSL->start_SSL(
+        $client,
+        SSL_server => 1,
+        SSL_cert_file => 'server-cert.pem',
+        SSL_key_file => 'server-key.pem',
+    ) || log_client_die "Failed ssl handshake: $SSL_ERROR";
+
+    my $pw_attempt = "";
+    my $char = "";
+    while ($char ne "\n") {
+        $client->sysread($char, 1) || log_client_die "Client disconnected while providing password";
+        $pw_attempt = "$pw_attempt$char";
+    }
+    chomp $pw_attempt;
+
+    my $sha_pw = sha256($pw);
+    my $sha_pw_attempt = sha256($pw_attempt);
+
+    if ($sha_pw ne $sha_pw_attempt) {
+        log_client_die "Authentication failure";
+    }
+    log_client "Authentication success";
+
+    # Read client name (will be used for the certificate)
+    my $name = "";
+    $char = "";
+    while ($char ne "\n") {
+        $client->sysread($char, 1) || log_client_die "Client disconnected while providing name";
+        if ($char =~ /[a-zA-Z0-9-]/) {
+            $name = "$name$char";
+        }
+    }
+    chomp $name;
+
+    if ($name eq '') {
+        log_client_die "Client provided blank name, no certificate generated";
+    }
+
+    my ($clientkey, $clientcert, $hn) = generate_client_cert($cakey, $cacert, $name, '.c');
+    if (!defined $clientkey || !defined $clientcert) {
+        log_client_die "Client [$clientip] requested existing certificate, no certificate generated";
+    }
+
+    # Send certificate and key to client
+    log_client "Sending client private key and certificate for ${hn}";
+    open(my $ckfh, '<', $clientkey);
+    while (<$ckfh>) {
+        $client->print($_);
+    }
+    open(my $ccfh, '<', $clientcert);
+    while (<$ccfh>) {
+        $client->print($_);
+    }
+    close($ckfh);
+    close($ccfh);
+
+    # Deleting the client's private key since the server doesn't need it
+    unlink($clientkey);
+    exit;
+}
+
+sub help {
+        print STDERR "Usage: $0 port [ca_key ca_certificate [server_key server_certificate]]\n";
+        exit 1;
+}
+
+sub main {
+    my $argcount = scalar @ARGV;
+    if (($argcount != 1 && $argcount != 3 && $argcount != 5) || $ARGV[0] eq '-h') {
+        help;
+    }
+    my ($cakey, $cacert, $srvkey, $srvcert);
+    if (scalar @ARGV == 1) {
+        ($cakey, $cacert) = generate_ca_cert();
+        ($srvkey, $srvcert) = generate_client_cert($cakey, $cacert, 'server', '');
+    } elsif (scalar @ARGV == 1) {
+        $cakey = $ARGV[1];
+        $cacert = $ARGV[2];
+        ($srvkey, $srvcert) = generate_client_cert($cakey, $cacert, 'server', '');
+    } else {
+        $cakey = $ARGV[1];
+        $cacert = $ARGV[2];
+        $srvkey = $ARGV[3];
+        $srvcert = $ARGV[4];
+    }
+
+    print "\n";
+    fingerprint_cert($srvcert);
+
+    print "\nCreating password. Clients will need to provide this to request a certificate\n";
+    print "Set password: ";
+    my $password = <STDIN>;
+    chomp $password;
+
+    find_existing_names;
+
+    my $srv = IO::Socket::INET->new(
+        LocalAddr => '0.0.0.0',
+        LocalPort => $ARGV[0],
+        Listen => 1024,
+    ) || die "Failed to listen: $!";
+
+    $clientname = undef;
+    $clientip = undef;
+    while (1) {
+        my $client = $srv->accept;
+        handle_client($srvkey, $srvcert, $cakey, $cacert, $password, $client);
+    }
+}
+
+main;
index 27b4cacf14429817b63aacdc9aabb39211a9a9b8..8b165aefcb875392f4c36357e4604eb71eaacb7b 100644 (file)
@@ -1,18 +1,31 @@
 #!/usr/bin/env perl
+
+# Client connects to certificate provider to request a client certificate
+# Sends logs and changed files to the server over ssl using the provided
+# client certificate to authenticate
+
 use strict;
 use warnings;
 use IO::Socket::INET;
+use IO::Socket::SSL;
 use POSIX "strftime";
 
+umask 0077;
+
 # Change this to the IP of the server
 my $server_ip = "127.0.0.1";
 my $server_port = 46515;
+my $cert_provider_ip = $server_ip;
+my $cert_provider_port = 46516;
+
+my $server_fingerprint; # Trust on first use if not defined. Will be saved to server_fingerprint.txt
 
 # See what's sent and monitored at the bottom of the script
 
 # Handle SIGINT
 my @child_processes;
 sub stop_child_processes {
+    return if scalar(@child_processes) == 0;
     kill 'INT', @child_processes;
 }
 $SIG{'INT'} = 'stop_child_processes';
@@ -20,8 +33,14 @@ $SIG{'INT'} = 'stop_child_processes';
 
 # Register client with server
 my ($hostname) = ns_system('./busybox', 'hostname');
-my ($clientName, $clientKey) = register($hostname);
+get_server_fingerprint();
 
+if (! -f 'client-cert.pem') {
+    printf "Enter password for certificate: ";
+    my $password = <STDIN>;
+    chomp $password;
+    request_certificate($cert_provider_ip, $cert_provider_port, $password, $hostname);
+}
 
 # ------------------------------------------------------------------------------
 
@@ -69,13 +88,56 @@ sub get_files_recursively {
     return @files;
 }
 
+sub request_certificate {
+    my ($cert_provider_ip, $cert_provider_port, $password, $name) = @_;
+    return if (-f 'client-cert.pem');
+
+    my $socket = IO::Socket::SSL->new(
+      PeerAddr => $cert_provider_ip,
+      PeerPort => $cert_provider_port,
+      SSL_fingerprint => $server_fingerprint,
+    ) || die "Failed to connect to certificate provider: $SSL_ERROR";
+    $socket->write("$password\n$name\n");
+    open(my $fh, '>', 'client-cert.pem');
+    my $buffer = "";
+    while (1) {
+        $socket->sysread($buffer, 1024) || last;
+        print $fh $buffer;
+    }
+    $socket->shutdown(SHUT_RD);
+    close $fh;
+}
+
+sub get_server_fingerprint {
+    if (-e 'server_fingerprint.txt') {
+        open(my $fh, '<', 'server_fingerprint.txt');
+        $server_fingerprint = <$fh>;
+        close $fh;
+        return;
+    }
+    my $socket = IO::Socket::SSL->new(
+        PeerAddr => $server_ip,
+        PeerPort => $server_port,
+        SSL_verify_mode => SSL_VERIFY_NONE
+    );
+    $server_fingerprint = $socket->get_fingerprint('sha256');
+    print "Trusting server fingerprint: ", $server_fingerprint, "\n";
+    open(my $fh, '>', 'server_fingerprint.txt');
+    print $fh $server_fingerprint;
+    close $fh;
+}
+
 sub connect_to_server {
-    my ($port) = @_;
-    $port = $server_port if (!defined $port);
-    my $socket = IO::Socket::INET->new(
+
+    get_server_fingerprint if !defined $server_fingerprint;
+
+    my $socket = IO::Socket::SSL->new(
       PeerAddr => $server_ip,
-      PeerPort => $port,
-      Proto    => 'tcp'
+      PeerPort => $server_port,
+      SSL_cert_file => 'client-cert.pem',
+      SSL_key_file => 'client-cert.pem',
+      SSL_fingerprint => $server_fingerprint,
+      SSL_fast_shutdown => 0,
     );
     my $wait = 1;
     while ((! $socket) && $wait < 16) {
@@ -83,67 +145,27 @@ sub connect_to_server {
         $wait *= 2;
     }
     if ($wait >= 16) {
-        die "Failed to connect to $server_ip:$port : $!";
+        die "Failed to connect to $server_ip:$server_port : $!";
     }
     $socket->autoflush(1);
     return $socket;
 }
 
-sub register {
-    my ($hostname) = @_;
-    my $socket = connect_to_server;
-    $socket->send("register\n");
-    $socket->send("$hostname\n");
-
-    # Wait for connection to be established, try up to 5 times
-    my $response;
-    foreach (1..5) {
-        sleep $_;
-        $socket->recv($response, 80);
-        last if ($response =~ m/Key/);
-    }
-    (my $clientName, my $clientKey) = $response =~ m/Name: (client_[a-zA-Z]+_\d+)\nKey: (\d+)\n/;
-
-    if (defined $clientName && defined $clientKey) {
-        print_log "Register: success";
-    } else {
-        print_log "Register: failure";
+sub send_info {
+    my $pid = fork;
+    if ($pid) {
+        push @child_processes, $pid;
+        return;
     }
-    $socket->close();
-    return ($clientName, $clientKey);
-}
 
-sub login {
     my $socket = connect_to_server;
-
-    # Wait for connection to be established, try up to 5 times
-    my $response;
-    $socket->send("login\n");
-    $socket->send("$clientName\n");
-    $socket->send("$clientKey\n");
-    foreach (1..5) {
-        sleep $_;
-        $socket->recv($response, 80);
-        last if ($response =~ m/Auth/);
-    }
-
-    if ($response =~ m/Auth success/) {
-        print_log "Login: success";
-    } elsif ($response =~ m/Auth failure/) {
-        print_log "Login: failure";
-    }
-    return $socket;
-}
-
-sub send_info {
-    my $socket = login($clientName, $clientKey);
-
     my $info = join "", ns_system('./busybox',  'sh', '-c', 'hostname; date; uname -a; cat /etc/os-release; lspci; lsusb; ifconfig');
 
-    $socket->send("info\n");
-    $socket->send($info);
-    $socket->send("⟃---EOF---⟄\n");
-    return;
+    $socket->write("info\n...\n");
+    $socket->write($info);
+
+    $socket->shutdown(SHUT_WR);
+    exit;
 }
 
 sub send_log {
@@ -157,23 +179,20 @@ sub send_log {
     # Check that log exists and is readable by current user
     exit if (! -e $file || ! -r _);
 
-    my $socket = login($clientName, $clientKey);
-
-    # Replace / character with similar-looking character that is valid
-    # for filenames. Used to show full path to file
-    my $fileName = $file =~ s/\////gr;
+    my $socket = connect_to_server;
 
     # Upload tailed log continuously
-    $socket->send("log\n");
-    $socket->send("$fileName\n");
+    $socket->write("logs\n");
+    $socket->write("$file\n");
     print_log "Log: Uploading $file";
     my $tailLog = ns_systemFH('./busybox', './busybox', 'tail', '-F', "$file");
     while (<$tailLog>) {
-        $socket->send($_);
+        $socket->write($_);
     }
     print_log "Log: Closing $file";
     close($tailLog);
-    $socket->send("⟃---EOF---⟄\n");
+
+    $socket->shutdown(SHUT_WR);
     exit;
 }
 
@@ -184,19 +203,8 @@ sub send_processes {
         return;
     }
 
-    my $socket = login($clientName, $clientKey);
-
-    # Upload process log continuously
-    $socket->send("processes\n");
-    print_log "Processes: Started";
-    my $commandLog = ns_systemFH('./pspy64', '-f');
-    while (<$commandLog>) {
-        $socket->send($_);
-    }
-    print_log "Processes: Finished";
-    close($commandLog);
-    $socket->send("⟃---EOF---⟄\n");
-    exit;
+    my $socket = connect_to_server;
+    ...
 }
 
 sub send_command_output {
@@ -207,20 +215,21 @@ sub send_command_output {
         return;
     }
 
-    my $socket = login($clientName, $clientKey);
+    my $socket = connect_to_server;
 
     # Upload command output continously with provided filename
     my ($fileName) = $name;
-    $socket->send("command\n");
-    $socket->send("$fileName\n");
+    $socket->write("cmds\n");
+    $socket->write("$fileName\n");
     print_log "Command: Started @command";
     my $commandLog = ns_systemFH(@command);
     while (<$commandLog>) {
-        $socket->send($_);
+        $socket->write($_);
     }
     print_log "Command: Completed @command";
     close($commandLog);
-    $socket->send("⟃---EOF---⟄\n");
+
+    $socket->shutdown(SHUT_WR);
     exit;
 }
 
@@ -235,56 +244,24 @@ sub send_file {
     # Check that log exists and is readable by current user
     exit if (! -e $file || ! -r _);
 
-    # Replace / character with similar-looking character that is valid
-    # for filenames. Used to show full path to file
-    my $fileName = $file =~ s/\////gr;
-    my ($fileHash) = ns_system('./busybox', 'md5sum', "$file");
-    chomp $fileName; chomp $fileHash;
-    ($fileHash) = $fileHash =~ m/([0-9a-f]+)/;
-
-    my $socket = login($clientName, $clientKey);
+    my $socket = connect_to_server;
 
     # Send filename and hash to server, wait for a response with the port to
     # upload the file to
-    $socket->send("file\n");
-    $socket->send("$fileName\n");
-    $socket->send("$fileHash\n");
-    $socket->recv(my $ignored, 128);
-    my $port = undef;
-    my $r;
-    my $sleeptime = 2;
-    my $attemptcount = 0;
-    while (! defined $port) {
-        sleep $sleeptime;
-        $sleeptime *= 2;
-        $sleeptime = 10 if ($sleeptime > 10);
-        $socket->recv($r, 128);
-        ($port) = $r =~ m/(\d+)/;
-        $attemptcount += 1;
-        if ($attemptcount >= 5) {
-            print_log "File: upload failure ($file)";
-            exit;
-        }
-    }
+    $socket->write("file\n");
+    $socket->write("$file\n");
 
-    # Send file once
-    print_log "File: upload port is $port ($file)";
+    ## Send file once
+    print_log "File: Uploading $file";
     open(my $fileFH, '<', "$file") || die "Failed to open $file";
-    my $fileSocket = connect_to_server $port;
     while (<$fileFH>) {
-        $fileSocket->send($_);
+        $socket->write($_);
     }
+    print_log "File: Closing $file";
     close($fileFH);
-    close($fileSocket);
+    $socket->flush;
 
-    # Server checks that the uploaded hash matches and informs on error
-    # No retry is attempted on failed upload
-    $socket->recv(my $response, 128);
-    if ($response =~ m/Transfer success/) {
-        print_log "File: upload success ($file)";
-    } else {
-        print_log "File: upload failure ($file)";
-    }
+    $socket->shutdown(SHUT_WR);
     exit;
 }
 
diff --git a/src/server.pl b/src/server.pl
new file mode 100644 (file)
index 0000000..e9454c6
--- /dev/null
@@ -0,0 +1,221 @@
+#!/usr/bin/perl
+
+# Client connects to this server over ssl and sends logs, files, and command
+# output which are stored under the following directories according to their
+# type:
+#     clients
+#     └── development.c.logs
+#         ├── _clientinfo.txt
+#         ├── _serverlog
+#         ├── commands
+#         ├── files
+#         └── logs
+
+use strict;
+use warnings;
+use IO::Socket::INET;
+use IO::Socket::SSL;
+use POSIX "strftime";
+
+my $perl_binary = '/usr/bin/perl';
+
+umask 0077;
+
+binmode(STDOUT, ":encoding(UTF-8)");
+
+my ($clientname, $clientip);
+my $clientlogfh;
+sub log_client {
+    my ($message) = @_;
+    $clientname = 'unknown' if ! defined $clientname;
+    $clientip = 'unknown' if ! defined $clientip;
+    $message =~ s/"/\\"/g;
+    my $timestamp = strftime "%Y-%m-%d %H:%M:%S", localtime;
+    my $logmsg = "ts=\"$timestamp\" client=\"$clientname\" ip=\"$clientip\" message=\"$message\"\n";
+    print $logmsg;
+    print $clientlogfh $logmsg if defined $clientlogfh;
+}
+
+sub log_client_die {
+    my ($message) = @_;
+    log_client $message;
+    exit 1;
+}
+
+sub write_to_file {
+    my ($client, $filetype, $filename) = @_;
+
+    # Add suffix if file already exists
+    if ($filetype eq 'file' || $filetype eq 'info') {
+        if (-e $filename) {
+            my $suffix = 1;
+            while (-e "${filename}_${suffix}") {
+                ++$suffix;
+            }
+            $filename = "${filename}_${suffix}";
+        }
+    }
+
+    log_client "Started writing to $filename";
+
+    open(my $fh, '>>', $filename);
+    select $fh;
+    $| = 1; # flush stream automatically
+    select STDOUT;
+
+    my $char = "";
+    while (1) {
+        $client->sysread($char, 1024) || last;
+        print $fh $char;
+    }
+    log_client "Finished writing to $filename";
+    $fh->flush;
+    close($fh);
+    $client->shutdown(SHUT_RD);
+    exit;
+}
+
+sub handle_client {
+    my ($client) = @_;
+    if (fork()) {
+        $client->close;
+        return;
+    }
+
+    # Upgrade to SSL after fork to not slow down accepting connections
+    IO::Socket::SSL->start_SSL(
+        $client,
+        SSL_server => 1,
+        SSL_cert_file => 'server-cert.pem',
+        SSL_key_file => 'server-key.pem',
+        SSL_verify_mode => SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT,
+        SSL_ca_file => 'ca-cert.pem',
+        SSL_fast_shutdown => 0,
+    ) || log_client_die "Failed ssl handshake: $SSL_ERROR";
+
+    $clientname = $client->peer_certificate('commonName');
+    $clientip = $client->peerhost;
+
+    my $dirname = "clients/$clientname";
+
+    # Create skeleton directories if not present
+    if (! -d $dirname) {
+        mkdir($dirname, 0700);
+        mkdir("$dirname/clients", 0700);
+        mkdir("$dirname/files", 0700);
+        mkdir("$dirname/commands", 0700);
+    }
+    open($clientlogfh, '>>:encoding(UTF-8)', "$dirname/_serverlog");
+    select $clientlogfh;
+    $| = 1; # flush stream automatically
+    select STDOUT;
+
+    log_client 'connected';
+
+    # Read source of data, choose filename prefix accordingly
+    my $filename;
+    my $filetype;
+    my $buffer = "";
+    $client->read($buffer, 5);
+    $buffer =~ s/\n//g;
+
+    if ($buffer eq 'info') {
+        $filetype = 'info';
+        $filename = "$dirname/_clientinfo.txt";
+    } elsif ($buffer eq 'logs') {
+        $filetype = 'logs';
+        $filename = "$dirname/logs/L__";
+    } elsif ($buffer eq 'cmds') {
+        $filetype = 'cmds';
+        $filename = "$dirname/commands/C__";
+    } elsif ($buffer eq 'file') {
+        $filetype = 'file';
+        $filename = "$dirname/files/F__";
+    } else {
+        log_client "provided invalid filetype ($buffer), ignoring";
+        exit;
+    }
+
+    # Read name for logfile, strip illegal characters for windows filenames
+    my $name = "";
+    my $char = "";
+    while ($char ne "\n") {
+        $client->read($char, 1) || log_client_die 'disconnected';
+        if ($char =~ /[^<>:"\/\\|?*]/) {
+            $name = "$name$char";
+        } else {
+            if ($char ne '/') {
+                # Use unicode replacement character
+                $name = "$name\N{REPLACEMENT CHARACTER}"; 
+            } else {
+                # Use unicode lookalike for /
+                $name = "$name\N{BIG SOLIDUS}"; 
+            }
+        }
+    }
+    chomp $name;
+
+    if ($name eq '') {
+        log_client 'provided blank filename, ignoring';
+        exit;
+    }
+
+    if ($filetype ne 'info') {
+        $filename = "$filename$name";
+    }
+
+    write_to_file($client, $filetype, $filename);
+
+    print $buffer;
+}
+
+sub help {
+    print STDERR "Usage: $0 port [-c port [-s]]\n";
+    print STDERR "\t-c port\tStart cert-server on port\n";
+    print STDERR "\t-s\tUse saved certificates for cert-server if they exist\n";
+    exit 1;
+}
+
+sub main {
+    my $argcount = scalar @ARGV;
+    if (($argcount != 1 && $argcount != 3 && $argcount != 4)
+        || $ARGV[0] eq '-h'
+        || ($argcount >= 3 && $ARGV[1] ne '-c')
+        || ($argcount == 4 && $ARGV[3] ne '-s')) {
+        help;
+    }
+
+    if ($argcount >= 3) {
+        my @cert_server_args = ('../cert-server.pl', $ARGV[2]);
+        if ($argcount == 4 && (-f 'ca-key.pem' && -f 'ca-cert.pem')) {
+            push @cert_server_args, 'ca-key.pem', 'ca-cert.pem';
+            if (-f 'server-key.pem' && -f 'server-cert.pem') {
+                push @cert_server_args, 'server-key.pem', 'server-cert.pem';
+            }
+        }
+        if (!fork()) {
+            exec $perl_binary, @cert_server_args;
+        }
+
+        while (!(-f 'server-key.pem' && -f 'server-cert.pem')) {
+            sleep 1;
+        }
+    }
+
+    mkdir('clients', 0700);
+
+    my $srv = IO::Socket::INET->new(
+        LocalAddr => '0.0.0.0',
+        LocalPort => $ARGV[0],
+        Listen => 16384,
+    ) || die "Failed to listen: $!";
+
+    print "Server is listening for clients\n";
+    while (1) {
+        my $client = $srv->accept;
+        handle_client($client);
+        $client->close();
+    }
+}
+
+main;
diff --git a/src/server.sh b/src/server.sh
deleted file mode 100644 (file)
index 7fbd00e..0000000
+++ /dev/null
@@ -1,188 +0,0 @@
-#!./busybox sh
-
-if [ $# -ne 2 ]; then
-    echo "Usage: $0 file_port_start file_port_num"
-    exit 1
-fi
-
-FILE_PORT_START=$1
-FILE_PORT_NUM=$2
-
-SRVDIR=$(pwd)
-
-verify_input() {
-    case "$@" in
-        "")    echo "Input error"
-               exit
-               ;;
-        *"/"*) echo "Input error"
-               exit
-               ;;
-      esac
-}
-
-write_to_file() {
-    if [ -n "$1" ]; then
-        touch "$1"
-        while IFS= read -r INPUT; do
-            if [ "$INPUT" != "⟃---EOF---⟄" ]; then
-                printf "%s" "$INPUT" >> "$1"
-            else
-                break
-            fi
-        done
-    fi
-}
-
-print_status() {
-    echo "Acknowledged"
-    "$@"
-    echo "Done."
-}
-
-
-# -----------------------------------------------------------------------------
-# Identify/authenticate client
-
-read -r COMMAND
-echo "$COMMAND" >&2
-
-# Existing client
-if [ "$COMMAND" = "login" ]; then
-    echo -n "Client name: "
-    read -r TMPNAME
-    echo -n "Client key: "
-    read -r TMPKEY
-
-    verify_input "$TMPNAME"
-
-    # Check if provided credentials are accurate
-    if [ -e "$SRVDIR/clients/$TMPNAME" ]; then
-        KEY=$(cat "$SRVDIR/clients/$TMPNAME/_auth-key")
-        if [ "$TMPKEY" = "$KEY" ]; then
-            CLIENTNAME="$TMPNAME"
-            CLIENTDIR="$SRVDIR/clients/$TMPNAME"
-            echo "Auth success"
-        else
-            echo "Auth failure"; exit
-        fi
-    else
-        echo "Auth failure"; exit
-    fi
-    unset TMPNAME
-    unset TMPKEY
-
-# New client
-elif [ "$COMMAND" = "register" ]; then
-    echo -n "Hostname: "
-    read -r CLIENTHOSTNAME
-    # Use shortened hostname and add random suffix to prevent collisions
-    CLIENTHOSTNAME=$(echo "$CLIENTHOSTNAME" | sed 's/[^a-zA-Z]//g' | cut -c -16)
-    CLIENTNAME="client_${CLIENTHOSTNAME}_$(( RANDOM * 2**30 + RANDOM * 2**15 + RANDOM ))"
-    CLIENTDIR="$SRVDIR/clients/$CLIENTNAME"
-    CLIENTKEY=$(( RANDOM * 2**30 + RANDOM * 2**15 + RANDOM ))
-    mkdir -p "$CLIENTDIR"
-    echo "$CLIENTKEY" > "$CLIENTDIR/_auth-key"
-    echo "Name: $CLIENTNAME"
-    echo "Key: $CLIENTKEY"
-    unset CLIENTKEY
-else
-    echo "Command not found."
-    exit
-fi
-
-cd "$CLIENTDIR"
-
-
-# -----------------------------------------------------------------------------
-# Client communication
-
-while read -r COMMAND; do
-    echo "$CLIENTNAME: $COMMAND" >&2
-
-    if [ "$COMMAND" = "info" ]; then
-        print_status write_to_file _info.txt
-
-
-    elif [ "$COMMAND" = "processes" ]; then
-        print_status write_to_file _processes.log
-
-
-    elif [ "$COMMAND" = "file" ]; then
-        echo -n "Filename: "
-        read -r TMPFILENAME
-        verify_input "F__${TMPFILENAME}"
-        echo -n "Hash: "
-        read -r TMPHASH
-
-        FILENAME="F__${TMPFILENAME}"
-
-        # Add suffix if file already exists
-        if [ -e "$FILENAME" ]; then
-            SUFFIX=1
-            while [ -e "${FILENAME}_${SUFFIX}" ]; do
-                SUFFIX=$(( SUFFIX + 1 ))
-            done
-            FILENAME="${FILENAME}_${SUFFIX}"
-        fi
-
-        # Grab an open port and listen for file. Use netcat since sh can't
-        # handle binary data well
-        ATTEMPTCOUNT=0
-        SLEEPTIME=0
-        while true; do
-            sleep $(( RANDOM % (SLEEPTIME + 5) + 1 ))
-            PORT=$(( (RANDOM * 2 + RANDOM % 2) % FILE_PORT_NUM + FILE_PORT_START ))
-            nc -w 7 -l -p "$PORT" > "$FILENAME" 2>/dev/null &
-            NC_PID=$!
-            # Wait for nc to fail. There seems to be a bug with busybox sh where
-            # using the builtin sleep when following the backgrounded nc
-            # doesn't sleep, so calling the binary again
-            ../../../busybox sleep 1
-            if ps -o pid | grep -q -e $NC_PID ; then
-                break
-            fi
-            ATTEMPTCOUNT=$(( ATTEMPTCOUNT + 1 ))
-            SLEEPTIME=$(( SLEEPTIME + 2 ))
-            if [ $ATTEMPTCOUNT -gt 5 ]; then
-                echo "$CLIENTNAME: ($FILENAME) Failed to bind open port for file transfer, giving up" >&2
-                echo "[${FILERECVTIME}] File transfer failed: ${FILENAME}" >> _files.log
-                exit
-            fi
-        done
-        echo "$PORT"
-        wait $NC_PID
-
-        FILERECVTIME="$(date '+%Y-%m-%d %H:%M:%S')"
-        echo "[${FILERECVTIME}] File received: ${FILENAME}" >> _files.log
-
-        HASH=$(md5sum "$FILENAME" | cut -d' ' -f1)
-        if [ "$HASH" != "$TMPHASH" ]; then
-            echo "Checksum error"
-        else
-            echo "Transfer success"
-        fi
-
-        unset TMPFILENAME
-
-
-    elif [ "$COMMAND" = "log" ]; then
-        echo -n "Filename: "
-        read -r TMPFILENAME
-        verify_input "L__${TMPFILENAME}"
-        print_status write_to_file "L__${TMPFILENAME}"
-        unset TMPFILENAME
-
-
-    elif [ "$COMMAND" = "command" ]; then
-        echo -n "Filename: "
-        read -r TMPFILENAME
-        verify_input "C__${TMPFILENAME}"
-        print_status write_to_file "C__${TMPFILENAME}"
-        unset TMPFILENAME
-
-
-    else
-        echo "Command not found."
-    fi
-done
index b1502105f7e5f3922e667a32884501c3bd46b1b1..2745eb3066de0ca4e19009fc24245db2a61e4155 100644 (file)
@@ -1,13 +1,12 @@
 #!./busybox sh
 
 SRVPORT=46515
-FILE_PORT_START=46516
-FILE_PORT_NUM=45
+CRTPORT=46516
 CWD=$(pwd)
 
 # -----------------------------------------------------------------------------
 # Start listening for clients
 
-mkdir -p "$CWD/srv/clients"
+mkdir -p "$CWD/srv"
 cd "$CWD/srv"
-tcpsvd -c 4096 0.0.0.0 "$SRVPORT" ../busybox sh ../server.sh "$FILE_PORT_START" "$FILE_PORT_NUM"
+perl ../server.pl "$SRVPORT" -c "$CRTPORT" -s