diff --git a/Dockerfile b/Dockerfile index a39ffb4..9bbda9e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,14 +1,14 @@ from alpine:latest -run apk add wget gnupg sqlite unzip build-base perl perl-dev perl-app-cpanminus -run cpanm --notest IPC::Run DBD::SQLite Net::DNS::Resolver Crypt::Eksblowfish::Bcrypt JSON URI::Escape HTML::Entities Net::Server HTTP::Server::Simple Crypt::Random +run apk add wget gnupg sqlite unzip build-base libmagic file-dev perl perl-dev perl-app-cpanminus +run cpanm --notest IPC::Run DBD::SQLite Net::DNS::Resolver Crypt::Eksblowfish::Bcrypt JSON URI::Escape HTML::Entities Net::Server HTTP::Server::Simple HTTP::Server::Simple::Static Crypt::Random run mkdir -p /opt/data/plans copy schema.sql /opt/data run cat /opt/data/schema.sql | sqlite3 /opt/data/users.db run rm /opt/data/schema.sql -run apk del build-base perl-dev perl-app-cpanminus wget sqlite unzip +run apk del build-base perl-dev perl-app-cpanminus wget sqlite unzip file-dev copy server.pl /opt workdir /opt diff --git a/server.pl b/server.pl index 8debf68..c3dfa32 100644 --- a/server.pl +++ b/server.pl @@ -41,6 +41,8 @@ if (defined $ENV{'LOCAL_DOMAINS'}) { package DotplanApi; use base qw(HTTP::Server::Simple::CGI); sub net_server { 'Net::Server::Fork' } + use HTTP::Server::Simple::Static; + my $webroot = './static'; # Caching DNS resolver { @@ -62,7 +64,7 @@ if (defined $ENV{'LOCAL_DOMAINS'}) { use MIME::Base64 qw(decode_base64); use POSIX qw(strftime); use JSON qw(encode_json decode_json); - use URI::Escape qw(uri_escape); + use URI::Escape qw(uri_escape uri_unescape); use HTML::Entities qw(encode_entities); use File::Spec::Functions qw(catfile); @@ -78,6 +80,7 @@ if (defined $ENV{'LOCAL_DOMAINS'}) { my $resp_header = { 200 => 'OK', 301 => 'Moved Permanently', + 304 => 'Not Modified', 308 => 'Permanent Redirect', 400 => 'Bad Request', 401 => 'Unauthorized', @@ -98,6 +101,8 @@ if (defined $ENV{'LOCAL_DOMAINS'}) { my $req_id = util_token(12); $cgi->param('request_id', $req_id); my $path = $cgi->path_info(); + $path =~ s{^https?://([^/:]+)(:\d+)?/}{/}; + $cgi->{'.path_info'} = '/index.html' if $path eq '/'; my $method = $cgi->request_method(); my $host = $cgi->http('X-Forwarded-For') || $cgi->remote_addr(); @@ -110,7 +115,13 @@ if (defined $ENV{'LOCAL_DOMAINS'}) { get_pwtoken($1, $cgi); } elsif ($path =~ /^\/plan\/([^\/]{$minimum_email_length,$maximum_email_length})$/) { get_plan($1, $cgi); - } else { + } elsif (!$self->serve_static($cgi, $webroot)) { + print_response($cgi, 404, $not_found); + } + } elsif ($method eq 'HEAD') { + if ($path =~ /^\/plan\/([^\/]{$minimum_email_length,$maximum_email_length})$/) { + get_plan($1, $cgi); + } elsif (!$self->serve_static($cgi, $webroot)) { print_response($cgi, 404, $not_found); } } elsif ($method eq 'POST') { @@ -152,7 +163,7 @@ if (defined $ENV{'LOCAL_DOMAINS'}) { ################## sub print_response { - my ($cgi, $code, $body, $type, $redirect) = @_; + my ($cgi, $code, $body, $type, $redirect, $mtime) = @_; my $req_id = $cgi->param('request_id'); my $path = $cgi->path_info(); my $method = $cgi->request_method(); @@ -164,15 +175,21 @@ if (defined $ENV{'LOCAL_DOMAINS'}) { $type = 'application/json'; } my $length = length($body); - my $date = strftime("%a, %d %b %Y %H:%M:%S %z", localtime(time())); + my $now = time; + my $date = HTTP::Date::time2str($now); my $redirect_header = ''; if (defined $redirect) { $redirect_header = "\nLocation: $redirect"; } + my $mtime_header = ''; + if (defined $mtime) { + $mtime = $now if $mtime > $now; + $mtime_header = "\nModified-Date: " . HTTP::Date::time2str($mtime); + } print < $email}); } } @@ -297,7 +314,7 @@ EOF "Someone (hopefully you) has requested to change your password.\n" . "If it wasn't you, you can ignore and delete this email.\n\n" . "Otherwise, click the following link or copy it into your browser:\n" . - "https://$hostname/static/change-password?token=$token"); + "https://$hostname/change-password.html?token=$token"); print_json_response($cgi, 200, {success => 1}); } } @@ -361,17 +378,29 @@ EOF # found external plan service, redirect request print_response($cgi, 301, encode_json({location => $plan->{'redirect'}}), 'application/json', $plan->{'redirect'}); } elsif (defined $plan) { - # found local plan, render response - my $body; - if ($format eq 'application/json') { - $body = encode_json($plan); - } elsif ($format eq 'text/html') { - $body = encode_entities($plan->{'plan'}); - $body =~ s/\n/
\n/g; + # found local plan, check modified + my $now = time; + my $mtime = $plan->{'mtime'}; + my $ifmod = $cgi->http('If-Modified-Since'); + my $ifmtime = HTTP::Date::str2time($ifmod) if defined $ifmod; + if (defined $mtime && defined $ifmtime && $ifmtime <= $now && $mtime <= $ifmtime) { + print_response($cgi, 304, '', $format, undef, $mtime); } else { - $body = $plan->{'plan'}; + # render response + my $body = ''; + delete $plan->{'mtime'}; + if ($cgi->request_method() ne 'HEAD') { + if ($format eq 'application/json') { + $body = encode_json($plan); + } elsif ($format eq 'text/html') { + $body = encode_entities($plan->{'plan'}); + $body =~ s/\n/
\n/g; + } else { + $body = $plan->{'plan'}; + } + } + print_response($cgi, 200, $body, $format, undef, $mtime); } - print_response($cgi, 200, $body, $format); } else { if ($format eq 'application/json') { print_response($cgi, 404, $not_found); @@ -588,7 +617,8 @@ EOF open(my $plan_file, '<', "$basename.plan") or die $!; flock($plan_file, LOCK_SH); my $mtime = (stat($plan_file))[9]; - my $timestamp = strftime("%a, %d %b %Y %H:%M:%S %z", localtime($mtime)); + my $timestamp = HTTP::Date::time2str(localtime($mtime)); + $details->{'mtime'} = $mtime; $details->{'timestamp'} = $timestamp; local $/; $details->{'plan'} = <$plan_file>; diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..88097f2 --- /dev/null +++ b/static/index.html @@ -0,0 +1,18 @@ + + + + Dotplan Online + + + +
+

Dotplan Online

+ +

What is Dotplan?

+

Dotplan is a modern re-imagining of the .plan file. It is intended to give the internet elite a free and open platform through which they may express their personalities (or lack thereof) with one another.

+ +

What is Dotplan Online?

+

Dotplan Online is a free service running the reference implementation Dotplan API server.

+
+ + diff --git a/test/run.sh b/test/run.sh index c0d5dbd..e0af33b 100755 --- a/test/run.sh +++ b/test/run.sh @@ -55,11 +55,11 @@ assert_equal() { actual=$1;shift expected=$1;shift if [ "$actual" != "$expected" ]; then - printf "${RED}✗ CHECK${NC} ${BOLD}$check_name${NC}\n\n\"${YELLOW}"; echo -n "$actual"; printf "${NC}\" != \"${YELLOW}"; echo -n "$expected"; printf "${NC}\"\n\n" + printf "${RED}✗ CHECK${NC} ${BOLD}$check_name${NC}\n\n\"${YELLOW}"; echo -n "$actual"; printf "${NC}\" != \"${YELLOW}"; echo -n "$expected"; printf "${NC}\"\n\n" ((++FAILED)) return 1 fi - printf "${GREEN}✓ CHECK${NC} ${BOLD}$check_name${NC}\n" + printf "${GREEN}✓ CHECK${NC} ${BOLD}$check_name${NC}\n" return 0; } @@ -68,11 +68,11 @@ assert_equal_jq() { expected=$1;shift actual=$(echo "$TEST_CONTENT" | jq -r "$selector") if [ "$actual" != "$expected" ]; then - printf "${RED}✗ CHECK${NC} ${BOLD}$selector${NC}\n\n\"${YELLOW}"; echo -n "$actual"; printf "${NC}\" != \"${YELLOW}"; echo -n "$expected"; printf "${NC}\"\n\n" + printf "${RED}✗ CHECK${NC} ${BOLD}$selector${NC}\n\n\"${YELLOW}"; echo -n "$actual"; printf "${NC}\" != \"${YELLOW}"; echo -n "$expected"; printf "${NC}\"\n\n" ((++FAILED)) return 1 fi - printf "${GREEN}✓ CHECK${NC} ${BOLD}$selector${NC}\n" + printf "${GREEN}✓ CHECK${NC} ${BOLD}$selector${NC}\n" return 0; } @@ -81,11 +81,11 @@ assert_notequal_jq() { expected=$1;shift actual=$(echo "$TEST_CONTENT" | jq -r "$selector") if [ "$actual" == "$expected" ]; then - printf "${RED}✗ CHECK${NC} ${BOLD}$selector${NC}\n\n\"${YELLOW}"; echo -n "$selector"; printf "${NC}\" = \"${YELLOW}"; echo -n "$expected"; printf "${NC}\"\n\n" + printf "${RED}✗ CHECK${NC} ${BOLD}$selector${NC}\n\n\"${YELLOW}"; echo -n "$selector"; printf "${NC}\" = \"${YELLOW}"; echo -n "$expected"; printf "${NC}\"\n\n" ((++FAILED)) return 1 fi - printf "${GREEN}✓ CHECK${NC} ${BOLD}$selector${NC}\n" + printf "${GREEN}✓ CHECK${NC} ${BOLD}$selector${NC}\n" return 0; } @@ -94,11 +94,11 @@ assert_exists() { dir=$1;shift file=$1;shift if [ ! -e "$BASEDIR/$dir/$file" ]; then - printf "${RED}✗ CHECK${NC} ${BOLD}$check_name${NC}\n\n\"${YELLOW}"; echo -n "$BASEDIR/$dir/$file"; printf "${NC}\" does not exist\n\n" + printf "${RED}✗ CHECK${NC} ${BOLD}$check_name${NC}\n\n\"${YELLOW}"; echo -n "$BASEDIR/$dir/$file"; printf "${NC}\" does not exist\n\n" ((++FAILED)) return 1 fi - printf "${GREEN}✓ CHECK${NC} ${BOLD}$check_name${NC}\n" + printf "${GREEN}✓ CHECK${NC} ${BOLD}$check_name${NC}\n" return 0; } @@ -107,11 +107,11 @@ assert_not_exists() { dir=$1;shift file=$1;shift if [ -e "$BASEDIR/$dir/$file" ]; then - printf "${RED}✗ CHECK${NC} ${BOLD}$check_name${NC}\n\n\"${YELLOW}"; echo -n "$BASEDIR/$dir/$file"; printf "${NC}\" exists\n\n" + printf "${RED}✗ CHECK${NC} ${BOLD}$check_name${NC}\n\n\"${YELLOW}"; echo -n "$BASEDIR/$dir/$file"; printf "${NC}\" exists\n\n" ((++FAILED)) return 1 fi - printf "${GREEN}✓ CHECK${NC} ${BOLD}$check_name${NC}\n" + printf "${GREEN}✓ CHECK${NC} ${BOLD}$check_name${NC}\n" return 0; }