From 2833b882d26f5ea0238b40d555b341c0882d2b93 Mon Sep 17 00:00:00 2001 From: Rudis Muiznieks Date: Thu, 16 Jul 2020 23:55:59 -0500 Subject: [PATCH] ctl script --- .gitignore | 2 + README.md | 58 ++++++++++++++++-------- ctl | 29 ++++++++++++ schema.sql | 11 +++++ server.pl | 128 +++++++++++++++++++++++++++++++++++++---------------- 5 files changed, 170 insertions(+), 58 deletions(-) create mode 100644 .gitignore create mode 100755 ctl create mode 100644 schema.sql diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7cc7808 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +dotplan.pid +*.db diff --git a/README.md b/README.md index 881fa9b..ac020ce 100644 --- a/README.md +++ b/README.md @@ -5,26 +5,46 @@ - User-provided content tied to an email address. - Text only, limited to 4kb. - 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. -- Self-hostable, discovery via SRV records. +- Open source. +- Self-hostable, discovery via domain SRV records. +- Single giant Perl script because PERL IS AWESOME! ## API -- `POST /users` to register an email address. - - Request data: `{"email":"whatever","password":"whatever"}` - - Will require validation. Email with token link will be sent. -- `GET /users/{email}?token={token}` to validate an email. -- Token-based authentication. - - `GET /users/{email}/token` with basic auth validation to get a token. - - `DELETE /users/{email}/token` to manually invalidate any token. -- `PUT /plan/{email}` to update a .plan - - Request data: `{"plan":"whatever","signature":"whatever"}` - - Signature is optional PGP digital signature for the plan. -- `GET /plan/{email}` to retrieve a .plan without verification - - Plain text by default, or based on `accept` header, or force: - - `?format=html` will html-escape special characters. - - `?format=json` response data: `{"plan":"whatever","signature":"whatever"}` -- `POST /verify/{email}` to retrieve and verify the signature of a .plan - - Request data: `{"pgpkey":"public key"}` - - Response data: `{"plan":"whatever","verified":(true|false)}` +### Authentication + +- `POST /users/{email}` - request new account + - request data: `{"password":"whatever"}` + - email with validation token will be sent +- `GET /users/{email}?token={token}` - validate new account +- `GET /users/{email}/token` - retrieve auth token + - http basic auth + - `?expires={date}` sets an explicit expiration, default is 300 seconds from creation + - response data: `{"token":"whatever"}` +- `DELETE /users/{email}/token` - invalidate current auth token + - http basic auth +- `GET /users/{email}/pwtoken` - get password change token + - email with password change token will be sent +- `PUT /users/{email}` - update password + - request data: `{"password":"whatever","pwtoken":"whatever"}` + - 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 diff --git a/ctl b/ctl new file mode 100755 index 0000000..a31f389 --- /dev/null +++ b/ctl @@ -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 diff --git a/schema.sql b/schema.sql new file mode 100644 index 0000000..b4347f4 --- /dev/null +++ b/schema.sql @@ -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 +); diff --git a/server.pl b/server.pl index a8f459d..a458014 100644 --- a/server.pl +++ b/server.pl @@ -10,6 +10,7 @@ use open qw(:std :utf8); ###################### my $server_port = 4227; +my $pid_file = './dotplan.pid'; ######################################### # dotplan.online Reference Implementation @@ -18,6 +19,9 @@ my $server_port = 4227; { package DotplanApi; 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 JSON qw(encode_json decode_json); use HTML::Entities qw(encode_entities); @@ -28,6 +32,14 @@ my $server_port = 4227; my $not_found = encode_json({error => 'Not found.'}); 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 @@ -39,24 +51,32 @@ my $server_port = 4227; my $method = $cgi->request_method(); if ($method eq 'GET') { - if ($path =~ /^\/plan\/(.*)$/) { - get_plan($1, $cgi); - } elsif ($path =~ /^\/users\/([^\/]*)$/) { - verify_email($1, $cgi); + if ($path =~ /^\/users\/([^\/]*)$/) { + validate_email($1, $cgi); } elsif ($path =~ /^\/users\/([^\/]*)\/token$/) { get_token($1, $cgi); + } elsif ($path =~ /^\/users\/([^\/]*)\/pwtoken$/) { + get_pwtoken($1, $cgi); + } elsif ($path =~ /^\/plan\/(.*)$/) { + get_plan($1, $cgi); } else { print_response(404, $not_found); } } elsif ($method eq 'POST') { - if ($path =~ /^\/users\/?$/) { - create_user($cgi); + if ($path =~ /^\/users\/([^\/]*)$/) { + create_user($1, $cgi); + } elsif ($path =~ /^\/verify\/([^\/]*)$/) { + verify_plan($1, $cgi); + } elsif ($path =~ /^\/multi$/) { + multi_plan($cgi); } else { print_response(404, $not_found); } } elsif ($method eq 'PUT') { - if ($path =~ /^\/plan\/(.*)$/) { - update_plan($cgi); + if ($path =~ /^\/users\/([^\/]*)$/) { + update_password($1, $cgi); + } elsif ($path =~ /^\/plan\/(.*)$/) { + update_plan($1, $cgi); } else { print_response(404, $not_found); } @@ -67,7 +87,7 @@ my $server_port = 4227; print_response(404, $not_found); } } 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 { my ($code, $body, $type) = @_; + my $header = $resp_header->{$code}; if (!defined $type) { $type = 'application/json'; } my $length = length($body); my $date = strftime("%a, %d %b %Y %H:%M:%S %z", localtime(time())); print < 'my plan & goals in life '}; - - # found plan, render response - - my $accept = $cgi->http('Accept'); - my $format = lc($cgi->param('format') || $cgi->http('Accept')); - my $body; - if ($format eq 'json' || $format eq 'application/json') { - $format = 'application/json'; - $body = encode_json($plan); - } elsif ($format eq 'html' || $format eq 'text/html') { - $format = 'text/html'; - $body = encode_entities($plan->{'plan'}); - } else { - $format = 'text/plain'; - $body = $plan->{'plan'}; - } - - print_response(200, $body, $format); - } + ##### POST /users/{email} + sub create_user { print_response(501, $not_implemented); } ##### GET /users/{email}?token={token} - sub verify_email { print_response(501, $not_implemented); } + sub validate_email { print_response(501, $not_implemented); } ##### GET /users/{email}/token sub get_token { print_response(501, $not_implemented); } - ##### POST /users - sub create_user { 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); } - ##### DELETE /users/{email}/token - sub delete_token { print_response(501, $not_implemented); } + ##### GET /plan/{email} + sub get_plan { + my ($email, $cgi) = @_; + + my $plan = util_get_plan($email); + + if (defined $plan) { + # found plan, render response + + my $accept = $cgi->http('Accept'); + my $format = lc($cgi->param('format') || $cgi->http('Accept')); + my $body; + if ($format eq 'json' || $format eq 'application/json') { + $format = 'application/json'; + $body = encode_json($plan); + } elsif ($format eq 'html' || $format eq 'text/html') { + $format = 'text/html'; + $body = encode_entities($plan->{'plan'}); + } else { + $format = 'text/plain'; + $body = $plan->{'plan'}; + } + + print_response(200, $body, $format); + } else { + print_response(404, $not_found); + } + } + + ##### POST /verify/{email} + sub verify_plan { print_response(501, $not_implemented); } + + ##### POST /multi + sub multi_plan { print_response(501, $not_implemented); } + + ################### + # Utility Functions + ################### + + sub util_get_plan { + my $email = shift; + # return {plan => 'I have no plans & aspirations in life. '}; + return undef; + } } # start server in 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";