ctl script

This commit is contained in:
Rudis Muiznieks 2020-07-16 23:55:59 -05:00
parent 5386a3fd0d
commit 2833b882d2
5 changed files with 170 additions and 58 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
dotplan.pid
*.db

View File

@ -5,26 +5,46 @@
- User-provided content tied to an email address. - User-provided content tied to an email address.
- Text only, limited to 4kb. - Text only, limited to 4kb.
- No retweets, shares, @s, likes, or boosting of any kind. - No retweets, shares, @s, likes, or boosting of any kind.
- Authenticity verified by public PGP keys. - Authenticity optionally verified by clients using public PGP keys.
- Accessed via public APIs. - Accessed via public APIs.
- Self-hostable, discovery via SRV records. - Open source.
- Self-hostable, discovery via domain SRV records.
- Single giant Perl script because PERL IS AWESOME!
## API ## API
- `POST /users` to register an email address. ### Authentication
- Request data: `{"email":"whatever","password":"whatever"}`
- Will require validation. Email with token link will be sent. - `POST /users/{email}` - request new account
- `GET /users/{email}?token={token}` to validate an email. - request data: `{"password":"whatever"}`
- Token-based authentication. - email with validation token will be sent
- `GET /users/{email}/token` with basic auth validation to get a token. - `GET /users/{email}?token={token}` - validate new account
- `DELETE /users/{email}/token` to manually invalidate any token. - `GET /users/{email}/token` - retrieve auth token
- `PUT /plan/{email}` to update a .plan - http basic auth
- Request data: `{"plan":"whatever","signature":"whatever"}` - `?expires={date}` sets an explicit expiration, default is 300 seconds from creation
- Signature is optional PGP digital signature for the plan. - response data: `{"token":"whatever"}`
- `GET /plan/{email}` to retrieve a .plan without verification - `DELETE /users/{email}/token` - invalidate current auth token
- Plain text by default, or based on `accept` header, or force: - http basic auth
- `?format=html` will html-escape special characters. - `GET /users/{email}/pwtoken` - get password change token
- `?format=json` response data: `{"plan":"whatever","signature":"whatever"}` - email with password change token will be sent
- `POST /verify/{email}` to retrieve and verify the signature of a .plan - `PUT /users/{email}` - update password
- Request data: `{"pgpkey":"public key"}` - request data: `{"password":"whatever","pwtoken":"whatever"}`
- Response data: `{"plan":"whatever","verified":(true|false)}` - token expires 600 seconds from creation
### Plans
- `PUT /plan/{email}` - update a plan
- request data: `{"plan":"whatever","signature":"whatever"}`
- `GET /plan/{email}` - retrieve a plan
- `text/plain` by default - raw plan content
- `?format=html` or `Accept: text/html` - plan content with html entity encoding for special characters
- `?format=json` or `Accept: application/json` - response data: `{"plan":"whatever","signature":"whatever"}`
- `404` if no plan found
- `POST /verify/{email}` - verify PGP signature of a plan
- request data: `{"pgpkey":"public key"}`
- response data: `{"plan":"whatever","verified":true}` or `{"verified":false}`
- 404 if no plan found
- `POST /multi` - retrieve multiple plans
- request data: `{"plans":["user1@email.dom","user2@email.dom"],"pgpkeys":{"user1@email.dom":"public key"}}`
- response data: `{"user1@email.dom":{"plan":"whatever","verified":true},"user2@email.dom":{"plan":"whatever","signature":"whatever"}}`
- emails with no plan found excluded from response

29
ctl Executable file
View File

@ -0,0 +1,29 @@
#!/usr/bin/env sh
cmd=$1
killserver() {
if [ -f "dotplan.pid" ]; then
kill -9 $(cat dotplan.pid)
rm dotplan.pid
fi
}
if [ "$cmd" = "run" ]; then
killserver
perl server.pl
elif [ "$cmd" = "kill" ]; then
killserver
elif [ "$cmd" = "initdb" ]; then
if [ -f "users.db" ]; then
rm users.db
fi
cat schema.sql | sqlite3 users.db
else
echo 'Usage: ctl [command]'
echo
echo 'Commands:'
echo ' run'
echo ' kill'
echo ' initdb'
fi

11
schema.sql Normal file
View File

@ -0,0 +1,11 @@
create table users (
email text primary key,
password text not null,
token text,
token_expires timestamp,
pw_token text,
pw_token_expires timestamp,
verified boolean not null default false,
created timestamp not null default current_timestamp,
updated timestamp
);

View File

@ -10,6 +10,7 @@ use open qw(:std :utf8);
###################### ######################
my $server_port = 4227; my $server_port = 4227;
my $pid_file = './dotplan.pid';
######################################### #########################################
# dotplan.online Reference Implementation # dotplan.online Reference Implementation
@ -18,6 +19,9 @@ my $server_port = 4227;
{ {
package DotplanApi; package DotplanApi;
use base qw(HTTP::Server::Simple::CGI); use base qw(HTTP::Server::Simple::CGI);
use DBD::SQLite;
use Crypt::Eksblowfish::Bcrypt qw(bcrypt_hash en_base64 de_base64);
use POSIX qw(strftime); use POSIX qw(strftime);
use JSON qw(encode_json decode_json); use JSON qw(encode_json decode_json);
use HTML::Entities qw(encode_entities); use HTML::Entities qw(encode_entities);
@ -28,6 +32,14 @@ my $server_port = 4227;
my $not_found = encode_json({error => 'Not found.'}); my $not_found = encode_json({error => 'Not found.'});
my $not_implemented = encode_json({error => 'Not implemented yet.'}); my $not_implemented = encode_json({error => 'Not implemented yet.'});
my $not_allowed = encode_json({error => 'HTTP method not supported.'});
my $resp_header = {
200 => 'OK',
404 => 'Not Found',
501 => 'Not Implemented',
405 => 'Method Not Allowed'
};
################# #################
# Request Routing # Request Routing
@ -39,24 +51,32 @@ my $server_port = 4227;
my $method = $cgi->request_method(); my $method = $cgi->request_method();
if ($method eq 'GET') { if ($method eq 'GET') {
if ($path =~ /^\/plan\/(.*)$/) { if ($path =~ /^\/users\/([^\/]*)$/) {
get_plan($1, $cgi); validate_email($1, $cgi);
} elsif ($path =~ /^\/users\/([^\/]*)$/) {
verify_email($1, $cgi);
} elsif ($path =~ /^\/users\/([^\/]*)\/token$/) { } elsif ($path =~ /^\/users\/([^\/]*)\/token$/) {
get_token($1, $cgi); get_token($1, $cgi);
} elsif ($path =~ /^\/users\/([^\/]*)\/pwtoken$/) {
get_pwtoken($1, $cgi);
} elsif ($path =~ /^\/plan\/(.*)$/) {
get_plan($1, $cgi);
} else { } else {
print_response(404, $not_found); print_response(404, $not_found);
} }
} elsif ($method eq 'POST') { } elsif ($method eq 'POST') {
if ($path =~ /^\/users\/?$/) { if ($path =~ /^\/users\/([^\/]*)$/) {
create_user($cgi); create_user($1, $cgi);
} elsif ($path =~ /^\/verify\/([^\/]*)$/) {
verify_plan($1, $cgi);
} elsif ($path =~ /^\/multi$/) {
multi_plan($cgi);
} else { } else {
print_response(404, $not_found); print_response(404, $not_found);
} }
} elsif ($method eq 'PUT') { } elsif ($method eq 'PUT') {
if ($path =~ /^\/plan\/(.*)$/) { if ($path =~ /^\/users\/([^\/]*)$/) {
update_plan($cgi); update_password($1, $cgi);
} elsif ($path =~ /^\/plan\/(.*)$/) {
update_plan($1, $cgi);
} else { } else {
print_response(404, $not_found); print_response(404, $not_found);
} }
@ -67,7 +87,7 @@ my $server_port = 4227;
print_response(404, $not_found); print_response(404, $not_found);
} }
} else { } else {
print_response(405, encode_json({error => 'Not supported.'})); print_response(405, $not_allowed);
} }
} }
@ -77,13 +97,14 @@ my $server_port = 4227;
sub print_response { sub print_response {
my ($code, $body, $type) = @_; my ($code, $body, $type) = @_;
my $header = $resp_header->{$code};
if (!defined $type) { if (!defined $type) {
$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 $date = strftime("%a, %d %b %Y %H:%M:%S %z", localtime(time()));
print <<EOF; print <<EOF;
HTTP/1.0 200 OK HTTP/1.0 $code $header
Server: DotplanApi Server: DotplanApi
Date: $date Date: $date
Content-Type: $type Content-Type: $type
@ -96,12 +117,34 @@ EOF
# API Implementation # API Implementation
#################### ####################
##### POST /users/{email}
sub create_user { print_response(501, $not_implemented); }
##### GET /users/{email}?token={token}
sub validate_email { print_response(501, $not_implemented); }
##### GET /users/{email}/token
sub get_token { print_response(501, $not_implemented); }
##### DELETE /users/{email}/token
sub delete_token { print_response(501, $not_implemented); }
##### GET /users/{email}/pwtoken
sub get_pwtoken { print_response(501, $not_implemented); }
##### PUT /users/{email}
sub update_password { print_response(501, $not_implemented); }
##### PUT /plan/{email}
sub update_plan { print_response(501, $not_implemented); }
##### GET /plan/{email} ##### GET /plan/{email}
sub get_plan { sub get_plan {
my ($email, $cgi) = @_; my ($email, $cgi) = @_;
my $plan = {plan => 'my plan & goals in life </sarcasm>'}; my $plan = util_get_plan($email);
if (defined $plan) {
# found plan, render response # found plan, render response
my $accept = $cgi->http('Accept'); my $accept = $cgi->http('Accept');
@ -119,24 +162,31 @@ EOF
} }
print_response(200, $body, $format); print_response(200, $body, $format);
} else {
print_response(404, $not_found);
}
} }
##### GET /users/{email}?token={token} ##### POST /verify/{email}
sub verify_email { print_response(501, $not_implemented); } sub verify_plan { print_response(501, $not_implemented); }
##### GET /users/{email}/token ##### POST /multi
sub get_token { print_response(501, $not_implemented); } sub multi_plan { print_response(501, $not_implemented); }
##### POST /users ###################
sub create_user { print_response(501, $not_implemented); } # Utility Functions
###################
##### PUT /plan/{email} sub util_get_plan {
sub update_plan { print_response(501, $not_implemented); } my $email = shift;
# return {plan => 'I have no plans & aspirations in life. </sarcasm>'};
##### DELETE /users/{email}/token return undef;
sub delete_token { print_response(501, $not_implemented); } }
} }
# start server in background # start server in background
my $pid = DotplanApi->new($server_port)->background(); my $pid = DotplanApi->new($server_port)->background();
open(my $pidout, '>', $pid_file) || die "Error writing pid: $!";
print $pidout "$pid";
close($pidout);
print "Use 'kill $pid' to stop server.\n"; print "Use 'kill $pid' to stop server.\n";