ctl script
This commit is contained in:
parent
5386a3fd0d
commit
2833b882d2
|
@ -0,0 +1,2 @@
|
||||||
|
dotplan.pid
|
||||||
|
*.db
|
58
README.md
58
README.md
|
@ -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
|
||||||
|
|
|
@ -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
|
|
@ -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
|
||||||
|
);
|
128
server.pl
128
server.pl
|
@ -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,47 +117,76 @@ EOF
|
||||||
# API Implementation
|
# API Implementation
|
||||||
####################
|
####################
|
||||||
|
|
||||||
##### GET /plan/{email}
|
##### POST /users/{email}
|
||||||
sub get_plan {
|
sub create_user { print_response(501, $not_implemented); }
|
||||||
my ($email, $cgi) = @_;
|
|
||||||
|
|
||||||
my $plan = {plan => 'my plan & goals in life </sarcasm>'};
|
|
||||||
|
|
||||||
# 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
##### GET /users/{email}?token={token}
|
##### 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
|
##### GET /users/{email}/token
|
||||||
sub get_token { print_response(501, $not_implemented); }
|
sub get_token { print_response(501, $not_implemented); }
|
||||||
|
|
||||||
##### POST /users
|
##### DELETE /users/{email}/token
|
||||||
sub create_user { print_response(501, $not_implemented); }
|
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}
|
##### PUT /plan/{email}
|
||||||
sub update_plan { print_response(501, $not_implemented); }
|
sub update_plan { print_response(501, $not_implemented); }
|
||||||
|
|
||||||
##### DELETE /users/{email}/token
|
##### GET /plan/{email}
|
||||||
sub delete_token { print_response(501, $not_implemented); }
|
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. </sarcasm>'};
|
||||||
|
return undef;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# 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";
|
||||||
|
|
Reference in New Issue