serve static site, added modified-time and if-modified-since handling
This commit is contained in:
parent
b42c89441f
commit
e84c0f397f
|
@ -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
|
||||||
|
|
64
server.pl
64
server.pl
|
@ -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>;
|
||||||
|
|
|
@ -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>
|
20
test/run.sh
20
test/run.sh
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Reference in New Issue