serve static site, added modified-time and if-modified-since handling

This commit is contained in:
Rudis Muiznieks 2020-07-21 00:14:50 -05:00
parent b42c89441f
commit e84c0f397f
4 changed files with 78 additions and 30 deletions

View File

@ -1,14 +1,14 @@
from alpine:latest from alpine:latest
run apk add wget gnupg sqlite unzip build-base perl perl-dev perl-app-cpanminus 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 Crypt::Random 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 run mkdir -p /opt/data/plans
copy schema.sql /opt/data copy schema.sql /opt/data
run cat /opt/data/schema.sql | sqlite3 /opt/data/users.db run cat /opt/data/schema.sql | sqlite3 /opt/data/users.db
run rm /opt/data/schema.sql 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 copy server.pl /opt
workdir /opt workdir /opt

View File

@ -41,6 +41,8 @@ if (defined $ENV{'LOCAL_DOMAINS'}) {
package DotplanApi; package DotplanApi;
use base qw(HTTP::Server::Simple::CGI); use base qw(HTTP::Server::Simple::CGI);
sub net_server { 'Net::Server::Fork' } sub net_server { 'Net::Server::Fork' }
use HTTP::Server::Simple::Static;
my $webroot = './static';
# Caching DNS resolver # Caching DNS resolver
{ {
@ -62,7 +64,7 @@ if (defined $ENV{'LOCAL_DOMAINS'}) {
use MIME::Base64 qw(decode_base64); use MIME::Base64 qw(decode_base64);
use POSIX qw(strftime); use POSIX qw(strftime);
use JSON qw(encode_json decode_json); 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 HTML::Entities qw(encode_entities);
use File::Spec::Functions qw(catfile); use File::Spec::Functions qw(catfile);
@ -78,6 +80,7 @@ if (defined $ENV{'LOCAL_DOMAINS'}) {
my $resp_header = { my $resp_header = {
200 => 'OK', 200 => 'OK',
301 => 'Moved Permanently', 301 => 'Moved Permanently',
304 => 'Not Modified',
308 => 'Permanent Redirect', 308 => 'Permanent Redirect',
400 => 'Bad Request', 400 => 'Bad Request',
401 => 'Unauthorized', 401 => 'Unauthorized',
@ -98,6 +101,8 @@ if (defined $ENV{'LOCAL_DOMAINS'}) {
my $req_id = util_token(12); my $req_id = util_token(12);
$cgi->param('request_id', $req_id); $cgi->param('request_id', $req_id);
my $path = $cgi->path_info(); my $path = $cgi->path_info();
$path =~ s{^https?://([^/:]+)(:\d+)?/}{/};
$cgi->{'.path_info'} = '/index.html' if $path eq '/';
my $method = $cgi->request_method(); my $method = $cgi->request_method();
my $host = $cgi->http('X-Forwarded-For') || $cgi->remote_addr(); my $host = $cgi->http('X-Forwarded-For') || $cgi->remote_addr();
@ -110,7 +115,13 @@ if (defined $ENV{'LOCAL_DOMAINS'}) {
get_pwtoken($1, $cgi); get_pwtoken($1, $cgi);
} elsif ($path =~ /^\/plan\/([^\/]{$minimum_email_length,$maximum_email_length})$/) { } elsif ($path =~ /^\/plan\/([^\/]{$minimum_email_length,$maximum_email_length})$/) {
get_plan($1, $cgi); 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); print_response($cgi, 404, $not_found);
} }
} elsif ($method eq 'POST') { } elsif ($method eq 'POST') {
@ -152,7 +163,7 @@ if (defined $ENV{'LOCAL_DOMAINS'}) {
################## ##################
sub print_response { sub print_response {
my ($cgi, $code, $body, $type, $redirect) = @_; my ($cgi, $code, $body, $type, $redirect, $mtime) = @_;
my $req_id = $cgi->param('request_id'); my $req_id = $cgi->param('request_id');
my $path = $cgi->path_info(); my $path = $cgi->path_info();
my $method = $cgi->request_method(); my $method = $cgi->request_method();
@ -164,15 +175,21 @@ if (defined $ENV{'LOCAL_DOMAINS'}) {
$type = 'application/json'; $type = 'application/json';
} }
my $length = length($body); 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 = ''; my $redirect_header = '';
if (defined $redirect) { if (defined $redirect) {
$redirect_header = "\nLocation: $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 <<EOF; print <<EOF;
HTTP/1.1 $code $header HTTP/1.1 $code $header
Server: DotplanApi Server: DotplanApi
Date: $date Date: $date$mtime_header
Content-Type: $type Content-Type: $type
Content-Length: $length$redirect_header Content-Length: $length$redirect_header
EOF EOF
@ -215,7 +232,7 @@ EOF
util_sendmail($email, '[DOTPLAN] Verify your email', util_sendmail($email, '[DOTPLAN] Verify your email',
"Please verify your email address.\n" . "Please verify your email address.\n" .
"Click the following link or copy it into your browser:\n" . "Click the following link or copy it into your browser:\n" .
"https://$hostname/static/verify?token=$token"); "https://$hostname/verify.html?token=$token");
print_json_response($cgi, 200, {email => $email}); print_json_response($cgi, 200, {email => $email});
} }
} }
@ -297,7 +314,7 @@ EOF
"Someone (hopefully you) has requested to change your password.\n" . "Someone (hopefully you) has requested to change your password.\n" .
"If it wasn't you, you can ignore and delete this email.\n\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" . "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}); print_json_response($cgi, 200, {success => 1});
} }
} }
@ -361,17 +378,29 @@ EOF
# found external plan service, redirect request # found external plan service, redirect request
print_response($cgi, 301, encode_json({location => $plan->{'redirect'}}), 'application/json', $plan->{'redirect'}); print_response($cgi, 301, encode_json({location => $plan->{'redirect'}}), 'application/json', $plan->{'redirect'});
} elsif (defined $plan) { } elsif (defined $plan) {
# found local plan, render response # found local plan, check modified
my $body; my $now = time;
if ($format eq 'application/json') { my $mtime = $plan->{'mtime'};
$body = encode_json($plan); my $ifmod = $cgi->http('If-Modified-Since');
} elsif ($format eq 'text/html') { my $ifmtime = HTTP::Date::str2time($ifmod) if defined $ifmod;
$body = encode_entities($plan->{'plan'}); if (defined $mtime && defined $ifmtime && $ifmtime <= $now && $mtime <= $ifmtime) {
$body =~ s/\n/<br>\n/g; print_response($cgi, 304, '', $format, undef, $mtime);
} else { } 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/<br>\n/g;
} else {
$body = $plan->{'plan'};
}
}
print_response($cgi, 200, $body, $format, undef, $mtime);
} }
print_response($cgi, 200, $body, $format);
} else { } else {
if ($format eq 'application/json') { if ($format eq 'application/json') {
print_response($cgi, 404, $not_found); print_response($cgi, 404, $not_found);
@ -588,7 +617,8 @@ EOF
open(my $plan_file, '<', "$basename.plan") or die $!; open(my $plan_file, '<', "$basename.plan") or die $!;
flock($plan_file, LOCK_SH); flock($plan_file, LOCK_SH);
my $mtime = (stat($plan_file))[9]; 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; $details->{'timestamp'} = $timestamp;
local $/; local $/;
$details->{'plan'} = <$plan_file>; $details->{'plan'} = <$plan_file>;

18
static/index.html Normal file
View File

@ -0,0 +1,18 @@
<!doctype html>
<html lang='en'>
<head>
<title>Dotplan Online</title>
<meta charset='utf-8'>
</head>
<body>
<article>
<h1>Dotplan Online</h1>
<h2>What is Dotplan?</h2>
<p>Dotplan is a modern re-imagining of the <a href='https://en.wikipedia.org/wiki/Finger_protocol#Finger_user_information_protocol'>.plan</a> 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.</p>
<h2>What is Dotplan Online?</h2>
<p><a href='https://dotplan.online'>Dotplan Online</a> is a free service running the <a href='https://github.com/rudism/dotplan-online'>reference implementation</a> Dotplan API server.</p>
</article>
</body>
</html>

View File

@ -55,11 +55,11 @@ assert_equal() {
actual=$1;shift actual=$1;shift
expected=$1;shift expected=$1;shift
if [ "$actual" != "$expected" ]; then 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)) ((++FAILED))
return 1 return 1
fi fi
printf "${GREEN} CHECK${NC} ${BOLD}$check_name${NC}\n" printf "${GREEN}CHECK${NC} ${BOLD}$check_name${NC}\n"
return 0; return 0;
} }
@ -68,11 +68,11 @@ assert_equal_jq() {
expected=$1;shift expected=$1;shift
actual=$(echo "$TEST_CONTENT" | jq -r "$selector") actual=$(echo "$TEST_CONTENT" | jq -r "$selector")
if [ "$actual" != "$expected" ]; then 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)) ((++FAILED))
return 1 return 1
fi fi
printf "${GREEN} CHECK${NC} ${BOLD}$selector${NC}\n" printf "${GREEN}CHECK${NC} ${BOLD}$selector${NC}\n"
return 0; return 0;
} }
@ -81,11 +81,11 @@ assert_notequal_jq() {
expected=$1;shift expected=$1;shift
actual=$(echo "$TEST_CONTENT" | jq -r "$selector") actual=$(echo "$TEST_CONTENT" | jq -r "$selector")
if [ "$actual" == "$expected" ]; then 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)) ((++FAILED))
return 1 return 1
fi fi
printf "${GREEN} CHECK${NC} ${BOLD}$selector${NC}\n" printf "${GREEN}CHECK${NC} ${BOLD}$selector${NC}\n"
return 0; return 0;
} }
@ -94,11 +94,11 @@ assert_exists() {
dir=$1;shift dir=$1;shift
file=$1;shift file=$1;shift
if [ ! -e "$BASEDIR/$dir/$file" ]; then 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)) ((++FAILED))
return 1 return 1
fi fi
printf "${GREEN} CHECK${NC} ${BOLD}$check_name${NC}\n" printf "${GREEN}CHECK${NC} ${BOLD}$check_name${NC}\n"
return 0; return 0;
} }
@ -107,11 +107,11 @@ assert_not_exists() {
dir=$1;shift dir=$1;shift
file=$1;shift file=$1;shift
if [ -e "$BASEDIR/$dir/$file" ]; then 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)) ((++FAILED))
return 1 return 1
fi fi
printf "${GREEN} CHECK${NC} ${BOLD}$check_name${NC}\n" printf "${GREEN}CHECK${NC} ${BOLD}$check_name${NC}\n"
return 0; return 0;
} }