finished pubkey validation service
This commit is contained in:
parent
051716c095
commit
fc16e6d621
|
@ -43,7 +43,7 @@
|
||||||
- `404` if no plan found
|
- `404` if no plan found
|
||||||
- `301` redirect if plan is on a different provider
|
- `301` redirect if plan is on a different provider
|
||||||
- `POST /verify/{email}` - verify PGP signature of a plan
|
- `POST /verify/{email}` - verify PGP signature of a plan
|
||||||
- request data: `{"pgpkey":"ascii public key"}`
|
- request data: `{"pubkey":"ascii public key"}`
|
||||||
- response data: `{"plan":"whatever","verified":1}` or `{"verified":0}`
|
- response data: `{"plan":"whatever","verified":1}` or `{"verified":0}`
|
||||||
- `404` if no plan found
|
- `404` if no plan found
|
||||||
- `308` redirect if plan is on a different provider
|
- `308` redirect if plan is on a different provider
|
||||||
|
|
95
server.pl
95
server.pl
|
@ -10,10 +10,10 @@ use open qw(:std :utf8);
|
||||||
######################
|
######################
|
||||||
|
|
||||||
my $server_port = $ENV{'PORT'} || 4227;
|
my $server_port = $ENV{'PORT'} || 4227;
|
||||||
my $pid_file = $ENV{'PID_FILE'} || './dotplan.pid';
|
my $pid_file = $ENV{'PID_FILE'} || './data/dotplan.pid';
|
||||||
my $log_file = $ENV{'LOG_FILE'} || './dotplan.log';
|
my $log_file = $ENV{'LOG_FILE'} || './data/dotplan.log';
|
||||||
my $database = $ENV{'DATABASE'} || './users.db';
|
my $database = $ENV{'DATABASE'} || './data/users.db';
|
||||||
my $plan_dir = $ENV{'PLAN_DIR'} || './plans';
|
my $plan_dir = $ENV{'PLAN_DIR'} || './data/plans';
|
||||||
my $sendmail = $ENV{'SENDMAIL'} || '/usr/bin/sendmail';
|
my $sendmail = $ENV{'SENDMAIL'} || '/usr/bin/sendmail';
|
||||||
|
|
||||||
my $pw_token_expiration_minutes = $ENV{'PW_TOKEN_EXPIRATION_MINUTES'} || 10;
|
my $pw_token_expiration_minutes = $ENV{'PW_TOKEN_EXPIRATION_MINUTES'} || 10;
|
||||||
|
@ -38,6 +38,7 @@ 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' }
|
||||||
|
|
||||||
# Caching DNS resolver
|
# Caching DNS resolver
|
||||||
{
|
{
|
||||||
|
@ -49,6 +50,7 @@ if (defined $ENV{'LOCAL_DOMAINS'}) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
use IPC::Run;
|
||||||
use DBI;
|
use DBI;
|
||||||
use File::Temp qw(tempfile);
|
use File::Temp qw(tempfile);
|
||||||
use Fcntl qw(:flock);
|
use Fcntl qw(:flock);
|
||||||
|
@ -83,8 +85,6 @@ if (defined $ENV{'LOCAL_DOMAINS'}) {
|
||||||
500 => 'Internal Server Error'
|
500 => 'Internal Server Error'
|
||||||
};
|
};
|
||||||
|
|
||||||
sub net_server { 'Net::Server::Fork' }
|
|
||||||
|
|
||||||
#################
|
#################
|
||||||
# Request Routing
|
# Request Routing
|
||||||
#################
|
#################
|
||||||
|
@ -212,7 +212,7 @@ EOF
|
||||||
my $user = util_get_user($email);
|
my $user = util_get_user($email);
|
||||||
if (defined $user && $user->{'verified'}) {
|
if (defined $user && $user->{'verified'}) {
|
||||||
print_json_response($cgi, 400, {error => 'User already exists.'});
|
print_json_response($cgi, 400, {error => 'User already exists.'});
|
||||||
} elsif (defined $user->{'pw_token_expires'} && $user->{'pw_token_expires'} >= time) {
|
} elsif (defined $user && defined $user->{'pw_token_expires'} && $user->{'pw_token_expires'} >= time) {
|
||||||
print_json_response($cgi, 429, {error => "Please wait up to $pw_token_expiration_minutes minutes and try again."});
|
print_json_response($cgi, 429, {error => "Please wait up to $pw_token_expiration_minutes minutes and try again."});
|
||||||
} else {
|
} else {
|
||||||
my $password = util_json_body($cgi)->{'password'};
|
my $password = util_json_body($cgi)->{'password'};
|
||||||
|
@ -222,9 +222,10 @@ EOF
|
||||||
my $query = (defined $user)
|
my $query = (defined $user)
|
||||||
? "UPDATE users SET password=?, pw_token=?, pw_token_expires=datetime('now', '+$pw_token_expiration_minutes minutes') WHERE email=?"
|
? "UPDATE users SET password=?, pw_token=?, pw_token_expires=datetime('now', '+$pw_token_expiration_minutes minutes') WHERE email=?"
|
||||||
: "INSERT INTO users (password, pw_token, pw_token_expires, email) values (?, ?, datetime('now', '+$pw_token_expiration_minutes minutes'), ?)";
|
: "INSERT INTO users (password, pw_token, pw_token_expires, email) values (?, ?, datetime('now', '+$pw_token_expiration_minutes minutes'), ?)";
|
||||||
my $sth = util_get_dbh()->prepare($query);
|
|
||||||
my $crypted = util_bcrypt($password);
|
my $crypted = util_bcrypt($password);
|
||||||
|
my $sth = util_get_dbh()->prepare($query);
|
||||||
$sth->execute($crypted, util_token(), $email);
|
$sth->execute($crypted, util_token(), $email);
|
||||||
|
die $sth->errstr if $sth->err;
|
||||||
# TODO: send email
|
# TODO: send email
|
||||||
print_json_response($cgi, 200, {email => $email});
|
print_json_response($cgi, 200, {email => $email});
|
||||||
}
|
}
|
||||||
|
@ -237,17 +238,18 @@ EOF
|
||||||
my ($email, $cgi) = @_;
|
my ($email, $cgi) = @_;
|
||||||
my $token = $cgi->param('token');
|
my $token = $cgi->param('token');
|
||||||
if (!defined $token) {
|
if (!defined $token) {
|
||||||
print_html_response(400, 'No token found in request.');
|
print_html_response($cgi, 400, 'No token found in request.');
|
||||||
} else {
|
} else {
|
||||||
my $user = util_get_user($email);
|
my $user = util_get_user($email);
|
||||||
if (!defined $user || $user->{'verified'}) {
|
if (!defined $user || $user->{'verified'}) {
|
||||||
print_html_response(404, 'User not found.');
|
print_html_response($cgi, 404, 'User not found.');
|
||||||
} elsif ($user->{'pw_token'} ne $token) {
|
} elsif ($user->{'pw_token'} ne $token) {
|
||||||
print_html_response(400, 'Bad or expired token.');
|
print_html_response($cgi, 400, 'Bad or expired token.');
|
||||||
} else {
|
} else {
|
||||||
my $sth = util_get_dbh()->prepare('UPDATE users SET verified=1, pw_token=null, pw_token_expires=null WHERE email=?');
|
my $sth = util_get_dbh()->prepare('UPDATE users SET verified=1, pw_token=null, pw_token_expires=null WHERE email=?');
|
||||||
$sth->execute($email);
|
$sth->execute($email);
|
||||||
print_html_response(200, 'Your email address has been verified.');
|
die $sth->errstr if $sth->err;
|
||||||
|
print_html_response($cgi, 200, 'Your email address has been verified.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -270,6 +272,7 @@ EOF
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$sth->execute($token, "+$minutes minutes", $user->{'email'});
|
$sth->execute($token, "+$minutes minutes", $user->{'email'});
|
||||||
|
die $sth->errstr if $sth->err;
|
||||||
print_json_response($cgi, 200, {token => $token});
|
print_json_response($cgi, 200, {token => $token});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -283,6 +286,7 @@ EOF
|
||||||
} else {
|
} else {
|
||||||
my $sth = util_get_dbh()->prepare('UPDATE users SET token=null, token_expires=null WHERE email=?');
|
my $sth = util_get_dbh()->prepare('UPDATE users SET token=null, token_expires=null WHERE email=?');
|
||||||
$sth->execute($user->{'email'});
|
$sth->execute($user->{'email'});
|
||||||
|
die $sth->errstr if $sth->err;
|
||||||
print_json_response($cgi, 200, {success => 1});
|
print_json_response($cgi, 200, {success => 1});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -299,6 +303,7 @@ EOF
|
||||||
my $token = util_token();
|
my $token = util_token();
|
||||||
my $sth = util_get_dbh()->prepare("UPDATE users SET pw_token=?, pw_token_expires=datetime('now', '+10 minutes') WHERE email=?");
|
my $sth = util_get_dbh()->prepare("UPDATE users SET pw_token=?, pw_token_expires=datetime('now', '+10 minutes') WHERE email=?");
|
||||||
$sth->execute($token, $email);
|
$sth->execute($token, $email);
|
||||||
|
die $sth->errstr if $sth->err;
|
||||||
# TODO: send email
|
# TODO: send email
|
||||||
print_html_response($cgi, 200, 'Check your email and follow the instructions to change your password.');
|
print_html_response($cgi, 200, 'Check your email and follow the instructions to change your password.');
|
||||||
}
|
}
|
||||||
|
@ -320,8 +325,9 @@ EOF
|
||||||
print_json_response($cgi, 400, {error => "Password must be at least $minimum_password_length characters long."});
|
print_json_response($cgi, 400, {error => "Password must be at least $minimum_password_length characters long."});
|
||||||
} else {
|
} else {
|
||||||
my $crypted = util_bcrypt($password);
|
my $crypted = util_bcrypt($password);
|
||||||
my $sth = util_get_dbh()->prepare('UPDATE users SET password=?, pw_token=null, pw_token_expires=null WHERE email=?');
|
my $sth = util_get_dbh()->prepare('UPDATE users SET password=?, pw_token=null, pw_token_expires=null, token=null, token_expires=null WHERE email=?');
|
||||||
$sth->execute($crypted, $email);
|
$sth->execute($crypted, $email);
|
||||||
|
die $sth->errstr if $sth->err;
|
||||||
print_json_response($cgi, 200, {success => 1});
|
print_json_response($cgi, 200, {success => 1});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -355,6 +361,7 @@ EOF
|
||||||
sub get_plan {
|
sub get_plan {
|
||||||
my ($email, $cgi) = @_;
|
my ($email, $cgi) = @_;
|
||||||
|
|
||||||
|
my $format = util_get_response_format($cgi);
|
||||||
my $plan = util_get_plan($email);
|
my $plan = util_get_plan($email);
|
||||||
|
|
||||||
if (defined $plan && defined $plan->{'redirect'}) {
|
if (defined $plan && defined $plan->{'redirect'}) {
|
||||||
|
@ -362,25 +369,24 @@ EOF
|
||||||
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, render response
|
||||||
|
|
||||||
my $accept = $cgi->http('Accept');
|
|
||||||
my $format = lc($cgi->param('format') || $cgi->http('Accept'));
|
|
||||||
my $body;
|
my $body;
|
||||||
if ($format eq 'json' || $format eq 'application/json') {
|
if ($format eq 'application/json') {
|
||||||
$format = 'application/json';
|
|
||||||
$body = encode_json($plan);
|
$body = encode_json($plan);
|
||||||
} elsif ($format eq 'html' || $format eq 'text/html') {
|
} elsif ($format eq 'text/html') {
|
||||||
$format = 'text/html';
|
|
||||||
$body = encode_entities($plan->{'plan'});
|
$body = encode_entities($plan->{'plan'});
|
||||||
$body =~ s/\n/<br>\n/g;
|
$body =~ s/\n/<br>\n/g;
|
||||||
} else {
|
} else {
|
||||||
$format = 'text/plain';
|
|
||||||
$body = $plan->{'plan'};
|
$body = $plan->{'plan'};
|
||||||
}
|
}
|
||||||
|
|
||||||
print_response($cgi, 200, $body, $format);
|
print_response($cgi, 200, $body, $format);
|
||||||
} else {
|
} else {
|
||||||
|
if ($format eq 'application/json') {
|
||||||
print_response($cgi, 404, $not_found);
|
print_response($cgi, 404, $not_found);
|
||||||
|
} elsif ($format eq 'text/html') {
|
||||||
|
print_html_response($cgi, 404, 'No plan found.');
|
||||||
|
} else {
|
||||||
|
print_response($cgi, 404, '', 'text/plain');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -394,7 +400,7 @@ EOF
|
||||||
# found external plan service, redirect request
|
# found external plan service, redirect request
|
||||||
print_response($cgi, 308, encode_json({location => $plan->{'redirect'}}), 'application/json', $plan->{'redirect'});
|
print_response($cgi, 308, encode_json({location => $plan->{'redirect'}}), 'application/json', $plan->{'redirect'});
|
||||||
} elsif (defined $plan) {
|
} elsif (defined $plan) {
|
||||||
my $pubkey = util_json_body($cgi)->{'pgpkey'};
|
my $pubkey = util_json_body($cgi)->{'pubkey'};
|
||||||
if (!defined $pubkey || !defined $plan->{'signature'}) {
|
if (!defined $pubkey || !defined $plan->{'signature'}) {
|
||||||
print_json_response($cgi, 200, {verified => 0});
|
print_json_response($cgi, 200, {verified => 0});
|
||||||
} elsif (length($pubkey) > $maximum_pubkey_length) {
|
} elsif (length($pubkey) > $maximum_pubkey_length) {
|
||||||
|
@ -403,17 +409,17 @@ EOF
|
||||||
my ($keyfh, $keyfile) = tempfile('tmpXXXXXX', TMPDIR => 1);
|
my ($keyfh, $keyfile) = tempfile('tmpXXXXXX', TMPDIR => 1);
|
||||||
print $keyfh $pubkey;
|
print $keyfh $pubkey;
|
||||||
close($keyfh);
|
close($keyfh);
|
||||||
util_log("saved key file to $keyfile");
|
|
||||||
my $basename = "$plan_dir/" . shell_quote($email);
|
my $basename = "$plan_dir/" . shell_quote($email);
|
||||||
my $convert = system("gpg2 --dearmor $keyfile > $keyfile.gpg");
|
if(
|
||||||
my $valid = system("gpg2 --no-default-keyring --keyring $keyfile.gpg --verify $basename.sig $basename.plan");
|
(IPC::Run::run ['gpg2', '--dearmor'], '<', $keyfile, '>', "$keyfile.gpg", '2>>', '/dev/null') &&
|
||||||
if ($convert != 0 || $valid != 0) {
|
(IPC::Run::run ['gpg2', '--no-default-keyring', '--keyring', "$keyfile.gpg", '--verify', "$basename.asc", "$basename.plan"], '>', '/dev/null', '2>>', '/dev/null')
|
||||||
print_json_response($cgi, 200, {verified => 0});
|
) {
|
||||||
} else {
|
|
||||||
print_json_response($cgi, 200, {
|
print_json_response($cgi, 200, {
|
||||||
plan => $plan->{'plan'},
|
plan => $plan->{'plan'},
|
||||||
verified => 1
|
verified => 1
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
print_json_response($cgi, 200, {verified => 0});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -446,6 +452,18 @@ EOF
|
||||||
print $_log "$timestamp $msg\n";
|
print $_log "$timestamp $msg\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sub util_get_response_format {
|
||||||
|
my $cgi = shift;
|
||||||
|
my $accept = $cgi->http('Accept');
|
||||||
|
my $format = lc($cgi->param('format') || $cgi->http('Accept'));
|
||||||
|
if ($format eq 'json' || $format eq 'application/json') {
|
||||||
|
return 'application/json';
|
||||||
|
} elsif ($format eq 'html' || $format eq 'text/html') {
|
||||||
|
return 'text/html';
|
||||||
|
}
|
||||||
|
return 'text/plain';
|
||||||
|
}
|
||||||
|
|
||||||
# encrypt a password with a provided or random salt
|
# encrypt a password with a provided or random salt
|
||||||
sub util_bcrypt {
|
sub util_bcrypt {
|
||||||
my ($password, $salt) = @_;
|
my ($password, $salt) = @_;
|
||||||
|
@ -525,7 +543,9 @@ EOF
|
||||||
my $email = shift;
|
my $email = shift;
|
||||||
my $sth = util_get_dbh()->prepare("SELECT email, password, token, strftime('%s', token_expires) AS token_expires, pw_token, strftime('%s', pw_token_expires) AS pw_token_expires, verified, strftime('%s', created) AS created, strftime('%s', updated) AS updated FROM users WHERE email=?");
|
my $sth = util_get_dbh()->prepare("SELECT email, password, token, strftime('%s', token_expires) AS token_expires, pw_token, strftime('%s', pw_token_expires) AS pw_token_expires, verified, strftime('%s', created) AS created, strftime('%s', updated) AS updated FROM users WHERE email=?");
|
||||||
$sth->execute($email);
|
$sth->execute($email);
|
||||||
return $sth->fetchrow_hashref;
|
die $sth->errstr if $sth->err;
|
||||||
|
my $user = $sth->fetchrow_hashref;
|
||||||
|
return (keys %$user > 0) ? $user : undef;
|
||||||
}
|
}
|
||||||
|
|
||||||
# save a plan by email
|
# save a plan by email
|
||||||
|
@ -544,13 +564,12 @@ EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
if (defined $plan && defined $signature) {
|
if (defined $plan && defined $signature) {
|
||||||
open(my $sig_file, '>', "$basename.sig");
|
open(my $sig_file, '>', "$basename.asc");
|
||||||
flock($sig_file, LOCK_EX);
|
flock($sig_file, LOCK_EX);
|
||||||
binmode $sig_file;
|
print $sig_file $signature;
|
||||||
print $sig_file decode_base64($signature);
|
|
||||||
close($sig_file);
|
close($sig_file);
|
||||||
} elsif (-f "$basename.sig") {
|
} elsif (-f "$basename.asc") {
|
||||||
unlink "$basename.sig";
|
unlink "$basename.asc";
|
||||||
}
|
}
|
||||||
|
|
||||||
# invalidate cache
|
# invalidate cache
|
||||||
|
@ -571,8 +590,8 @@ EOF
|
||||||
$details->{'plan'} = <$plan_file>;
|
$details->{'plan'} = <$plan_file>;
|
||||||
close($plan_file);
|
close($plan_file);
|
||||||
|
|
||||||
if (-f "$basename.sig") {
|
if (-f "$basename.asc") {
|
||||||
open(my $sig_file, '<', "$basename.sig");
|
open(my $sig_file, '<', "$basename.asc");
|
||||||
flock($sig_file, LOCK_SH);
|
flock($sig_file, LOCK_SH);
|
||||||
local $/;
|
local $/;
|
||||||
$details->{'signature'} = <$sig_file>;
|
$details->{'signature'} = <$sig_file>;
|
||||||
|
@ -625,7 +644,7 @@ EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
# only supports one optional argument -d to daemonize
|
# only supports one optional argument -d to daemonize
|
||||||
my $daemonize = $ARGV[0] == '-d' if @ARGV == 1;
|
my $daemonize = $ARGV[0] eq '-d' if @ARGV == 1;
|
||||||
|
|
||||||
# start server and fork process as current user
|
# start server and fork process as current user
|
||||||
my ($user, $passwd, $uid, $gid) = getpwuid $<;
|
my ($user, $passwd, $uid, $gid) = getpwuid $<;
|
||||||
|
|
Reference in New Issue