chibipub

experimental activitypub node in C
git clone git://jb55.com/chibipub
Log | Files | Refs | README | LICENSE

commit 40634c2de9d6c7b255581d0707c20e603859cb45
parent 12b9bedc8e77981dfaf9b0a659701d7e28b4ee26
Author: William Casarin <jb55@jb55.com>
Date:   Tue, 12 Oct 2021 08:36:17 -0700

post command

can be used to create outbox objects

we still need to add these to our object store

Diffstat:
M.envrc | 2++
M.gitignore | 6++++++
MMakefile | 5++++-
ATODO | 1+
Msrc/base64.c | 16++++++++--------
Msrc/base64.h | 8++++----
Msrc/chibipub.c | 509+------------------------------------------------------------------------------
Asrc/chibipub.h | 8++++++++
Asrc/debug.h | 4++++
Msrc/hash.h | 13+++++--------
Asrc/hex.c | 17+++++++++++++++++
Asrc/hex.h | 4++++
Asrc/main.c | 538+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/post.c | 184+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/post.h | 12++++++++++++
Msrc/sigcheck.c | 32++++++--------------------------
16 files changed, 808 insertions(+), 551 deletions(-)

diff --git a/.envrc b/.envrc @@ -1 +1,3 @@ use nix + +export TODO_FILE=TODO diff --git a/.gitignore b/.gitignore @@ -3,8 +3,14 @@ *.o *.a *.so +/.chibipub +/outbox.json /tags /test_out.ubjson src/test_json /corpus/math.json /activities.json +.buildcmd +TODO.bak +.direnv +/*.json diff --git a/Makefile b/Makefile @@ -12,6 +12,9 @@ OBJS = src/http.o \ src/io.o \ src/util.o \ src/sigcheck.o \ + src/hex.o \ + src/post.o \ + src/chibipub.o \ deps/sha256/sha256.o \ deps/blake3/blake3.a @@ -46,7 +49,7 @@ deps/blake3/blake3.a: $(BLAKE3_OBJS) corpus/math.json: curl --compressed -sL 'https://jb55.com/s/5aaaae6d64be61fd.json' > $@ -chibipub: src/chibipub.c $(OBJS) $(HEADERS) +chibipub: src/main.c $(OBJS) $(HEADERS) @echo "ld $@" @$(CC) $(CFLAGS) $< $(OBJS) $(LDFLAGS) -o $@ diff --git a/TODO b/TODO @@ -0,0 +1 @@ +implement post diff --git a/src/base64.c b/src/base64.c @@ -12,7 +12,7 @@ #include <assert.h> static const unsigned char base64_table[65] = - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; static const unsigned char base62_table[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; @@ -30,11 +30,11 @@ static const unsigned char base62_table[] = * not included in out_len. */ -unsigned char * base_encode(const unsigned char *src, size_t len, - unsigned char *out, size_t out_capacity, +char * base_encode(const unsigned char *src, size_t len, + char *out, size_t out_capacity, size_t *out_len, const unsigned char *base_table) { - unsigned char *pos; + char *pos; const unsigned char *end, *in; size_t olen; @@ -77,14 +77,14 @@ unsigned char * base_encode(const unsigned char *src, size_t len, return out; } -unsigned char * -base64_encode(const unsigned char *src, size_t len, unsigned char *out, +char * +base64_encode(const unsigned char *src, size_t len, char *out, size_t out_capacity, size_t *out_len) { return base_encode(src, len, out, out_capacity, out_len, base64_table); } -unsigned char * -base62_encode(const unsigned char *src, size_t len, unsigned char *out, +char * +base62_encode(const unsigned char *src, size_t len, char *out, size_t out_capacity, size_t *out_len) { return base_encode(src, len, out, out_capacity, out_len, base62_table); } diff --git a/src/base64.h b/src/base64.h @@ -11,11 +11,11 @@ #include <stddef.h> -unsigned char * base62_encode(const unsigned char *src, size_t len, - unsigned char *out, size_t out_capacity, +char * base62_encode(const unsigned char *src, size_t len, + char *out, size_t out_capacity, size_t *out_len); -unsigned char * base64_encode(const unsigned char *src, size_t len, - unsigned char *out, size_t out_capacity, +char * base64_encode(const unsigned char *src, size_t len, + char *out, size_t out_capacity, size_t *out_len); unsigned char * base64_decode(const unsigned char *src, size_t len, unsigned char *out, size_t out_capacity, diff --git a/src/chibipub.c b/src/chibipub.c @@ -1,514 +1,15 @@ -#include <stdio.h> -#include <unistd.h> -#include <stdlib.h> -#include <string.h> -#include <netdb.h> -#include <sys/types.h> -#include <sys/socket.h> -#include <netinet/in.h> -#include <arpa/inet.h> -#include <ctype.h> -#include <assert.h> -#include <base64.h> - -#include "http.h" -#include "ap_json.h" -#include "inbox.h" -#include "outbox.h" -#include "json.h" -#include "sigcheck.h" +#include "chibipub.h" #include "env.h" -#define BUF_SIZE 1048576 -#define ARENA_SIZE 134217728 /* 128 MB virtual mem arena */ -#define streq(a,b) (!strcmp(a,b)) -#define patheq(req, x) (streq(x, req->path) || streq(x "/", req->path)) - -struct webfinger { - const char *acct; - const char *alias; - const char *profile_page; - const char *self; -}; - -static void error(char *msg) -{ - perror(msg); - exit(1); -} - - -#define SCHEMA "https://" - -static void init_test_webfinger(struct webfinger *finger) -{ - static unsigned char buf[512]; - struct cursor c; - make_cursor(buf, buf+sizeof(buf), &c); - - const char *host = get_hostname(); - const char *id = get_id(); - assert(host); - - finger->acct = (char*)c.p; - - assert( - push_str(&c, "jb55@") && push_c_str(&c, id) - ); - - finger->alias = (char*)c.p; - - assert( - push_str(&c, SCHEMA) && push_str(&c, id) && - push_c_str(&c, "/") - ); - - finger->profile_page = (char*)c.p; - - assert( - push_str(&c, SCHEMA) && push_str(&c, host) && - push_c_str(&c, "/profile") - ); - - finger->self = (char*)c.p; - - assert( - push_str(&c, SCHEMA) && push_str(&c, id) && - push_c_str(&c, "/") - ); -} - -static int handle_webfinger(struct http_req *req) -{ - struct webfinger finger; - - init_test_webfinger(&finger); - - http_write_header(req, "200 OK"); - - fprintf(req->client.socket_file, - "{\"subject\": \"acct:%s\"," - " \"aliases\": [\"%s\"]," - " \"links\": [{" - " \"rel\": \"http://webfinger.net/rel/profile-page\"," - " \"type\": \"text/html\"," - " \"href\": \"%s\"" - " },{" - " \"rel\":\"self\"," - " \"type\": \"application/activity+json\"," - " \"href\": \"%s\"" - " }]}\r\n", - finger.acct, - finger.alias, - finger.profile_page, - finger.self); - - return 1; -} - - -static inline int starts_with(const char *str, const char *prefix) -{ - unsigned int prefix_size; - prefix_size = strlen(prefix); - - if (prefix_size > strlen(str)) { - return 0; - } - - return !memcmp(str, prefix, prefix_size); -} - -static void http_log(struct http_req *req) -{ - printf("%s - \"%s %s %s\"\n", - req->client.addr_str, - req->method, - req->path, - req->ver); -} - -static int handle_inbox_request(struct http_req *req, struct cursor *arena) -{ - const char *signature; - unsigned char *start; - FILE *out; - int len; - struct sig_header sig; - struct json_parser pull; - struct json_pusher push; - struct ap_json apjson; - - struct json_handlers compact; - struct json_handlers ap_handler; - - make_compact_handlers(&compact, &push); - make_ap_json_pusher(&ap_handler, &apjson); - init_ap_json(&apjson, &compact); - - memset(&sig, 0, sizeof(sig)); - - if (!get_header(req->headers, "signature", &signature)) { - note_error(&req->errs, "signature"); - return 0; - } - - printf("signature: %s\n", signature); - - if (!parse_signature_header(&req->errs, arena, signature, strlen(signature), &sig)) { - note_error(&req->errs, "parse signature header"); - return 0; - } - - apjson.sig = &sig; - apjson.req = req; - - start = arena->p; - init_json_pusher_with(&push, arena); - - init_json_parser(&pull, req->body, req->body_len, &ap_handler); - if (!parse_json(&pull)) { - note_error(&req->errs, "json parse failed"); - return 0; - } - - if (!(out = fopen("activities.json", "a"))) { - note_error(&req->errs, "could not open activities.json"); - return 0; - } - - len = push.cur.p - start; - - // 128 KB - if (len >= 131072) { - note_error(&req->errs, "ActivityPub message too big"); - return 0; - } - - fwrite(start, len, 1, out); - fwrite("\n", 1, 1, out); - fclose(out); - - http_write_header(req, "200 OK"); - return 1; -} - -static inline int is_get(struct http_req *req) -{ - return streq(req->method, "GET"); -} - -static inline int is_post(struct http_req *req) -{ - return streq(req->method, "POST"); -} - - -static inline int is_accept_activity(struct http_req *req) -{ - const char *accept; - - if (!get_header(req->headers, "accept", &accept)) { - note_error(&req->errs, "accept"); - return 0; - } - - if (!(strstr(accept, "application/activity+json") || - strstr(accept, "application/ld+json"))) { - note_error(&req->errs, "not accept: '%s'", accept); - return 0; - } - - return 1; -} - -struct profile -{ - const char *id; - const char *type; - const char *username; - const char *name; - const char *summary; - const char *url; - int manually_approves_followers; - const char *image_mime_type; - const char *image_url; - const char *key_id; - const char *pubkey_pem; -}; - -static void test_profile(struct profile *p) -{ - static unsigned char buf[128]; - struct cursor c; - make_cursor(buf, buf + sizeof(buf), &c); - - const char *host = get_hostname(); - const char *id = get_id(); - - assert(host); - - p->id = (char*)c.p; - assert( - push_str(&c, SCHEMA) && push_str(&c, id) && push_c_str(&c, "/") - ); - - p->url = (char*)c.p; - assert( - push_str(&c, SCHEMA) && push_str(&c, host) && push_c_str(&c, "/") - ); - - p->type = "Person"; - p->username = "jb55"; - p->name = "William Casarin"; - p->summary = "chibipub prototype"; - p->manually_approves_followers = 0; - p->image_mime_type = "image/jpeg"; - p->image_url = "https://jb55.com/s/blue-me.jpg"; - p->key_id = "main-key"; - p->pubkey_pem = get_pubkey(); -} - -static int handle_self(struct http_req *req) -{ - struct profile profile; - test_profile(&profile); - - http_write_header(req, "200 OK"); - - fprintf(req->client.socket_file, - "{" - "\"@context\": [" - "\"https://www.w3.org/ns/activitystreams\"" - "]," - "\"inbox\": \"%sinbox\"," - "\"id\": \"%s\"," - "\"type\": \"%s\"," - "\"preferredUsername\": \"%s\"," - "\"name\": \"%s\"," - "\"summary\": \"%s\"," - "\"url\": \"%s\"," - "\"manuallyApprovesFollowers\": %s," - "\"icon\": {" - "\"type\": \"Image\"," - "\"mediaType\": \"%s\"," - "\"url\": \"%s\"" - "}," - "\"publicKey\": {" - "\"id\": \"%s#%s\"," - "\"owner\": \"%s\"," - "\"publicKeyPem\": \"%s\"" - "}" - "}\n", - profile.url, - profile.id, - profile.type, - profile.username, - profile.name, - profile.summary, - profile.url, - profile.manually_approves_followers ? "true" : "false", - profile.image_mime_type, - profile.image_url, - profile.id, profile.key_id, - profile.id, - profile.pubkey_pem); - - return 1; -} - -static int handle_request(struct http_req *req, struct cursor *arena) -{ - http_log(req); - - if (starts_with(req->path, "/.well-known/webfinger?resource=acct:")) { - return handle_webfinger(req); - } else if (is_post(req) && patheq(req, "/inbox")) { - return handle_inbox_request(req, arena); - } else if (is_get(req) && patheq(req, "/outbox")) { - return handle_outbox_request(req); - } else if (is_get(req) && patheq(req, "/outbox?page=true")) { - return handle_outbox_page_request(req, "outbox.json"); - } else if (is_get(req) && - is_accept_activity(req) && - streq(req->path, "/")) { - return handle_self(req); - } - - return 0; -} +#include <stdio.h> -static int http_accept_client(struct http_req *req, int parent) +int init_chibipub(struct chibipub *pub) { - struct sockaddr_in *addr; - socklen_t client_len; - struct hostent *client_host; - - client_len = sizeof(addr); - - addr = &req->client.sockaddr; - - req->client.socket = - accept(parent, (struct sockaddr *) &req->client.sockaddr, - &client_len); - - if (req->client.socket < 0) { - note_error(&req->errs, "bad socket"); - return 0; - } - - req->client.socket_file = fdopen(req->client.socket, "wb"); - - client_host = gethostbyaddr((const char *)&addr->sin_addr.s_addr, - sizeof(addr->sin_addr.s_addr), AF_INET); - - if (!client_host) { - note_error(&req->errs, "gethostbyaddr"); - return 0; - } - - req->client.addr_str = inet_ntoa(addr->sin_addr); - - if (!req->client.addr_str) { - note_error(&req->errs, "inet_htoa"); + if (!(pub->hostname = get_hostname())) { + fprintf(stderr, "CHIBIPUB_HOST not set\n"); return 0; } return 1; } - -void run_http_server() -{ - static unsigned char buffer[BUF_SIZE]; - unsigned char *arena; - - ssize_t len, size; - - int parent; - struct sockaddr_in server_addr; - int optval; - - struct http_req req; - - struct parser parser; - - const int port = 5188; - arena = malloc(ARENA_SIZE); - assert(arena); - - if ((parent = socket(AF_INET, SOCK_STREAM, 0)) < 0) { - error("socket"); - } - - optval = 1; - setsockopt(parent, SOL_SOCKET, SO_REUSEADDR, (const void *)&optval, - sizeof(optval)); - - memset(&server_addr, 0, sizeof(server_addr)); - server_addr.sin_family = AF_INET; - server_addr.sin_addr.s_addr = htonl(INADDR_ANY); - server_addr.sin_port = htons(port); - - if (bind(parent, (struct sockaddr *)&server_addr, - sizeof(server_addr)) < 0) { - error("bind"); - } - - printf("listening for activities on 0.0.0.0:%d\n", port); - - if (listen(parent, 5) < 0) { - error("listen"); - } - - while (1) { - init_http_req(&req); - make_cursor(buffer, buffer + BUF_SIZE, &parser.cur); - make_cursor(arena, arena + ARENA_SIZE, &parser.arena); - - http_accept_client(&req, parent); - - for(size = 0;1;) { - len = read(req.client.socket, (void*)(buffer+size), BUF_SIZE-size); - size += len; - if (len == 0) { - break; - } - - if (len != (BUF_SIZE-size)) { - break; - } - } - - if (parse_http_request(&req, &parser, size)) { - handle_request(&req, &parser.arena); - } - - fclose(req.client.socket_file); - close(req.client.socket); - } - - free(arena); -} - -static int load_config() -{ - if (!get_hostname()) { - printf("CHIBIPUB_HOST env not set\n"); - return 0; - } - return 1; -} - -static int checksigs() -{ - struct sigcheck check; - check.activity_file = "activities.json"; - if (!sigcheck(&check)) { - printf("sigcheck failed\n"); - return 1; - } - printf("ok\n"); - return 0; -} - -int usage() -{ - printf( - "usage: chibipub [OPTION]... <command>\n" - "\n" - "commands\n" - "\n" - "help show this help\n" - "serve run the server\n" - "post post a new message\n" - "checksigs check inbox signatures\n" - ); - - return 0; -} - -static int serve() -{ - if (!load_config()) - return usage(); - run_http_server(); - return 0; -} - -int main(int argc, char *argv[]) -{ - if (argc < 2) { - return usage(); - } - - if (!strcmp(argv[1], "help") || !strcmp(argv[1], "--help")) { - return usage(); - } else if (!strcmp(argv[1], "serve")) { - return serve(); - } else if (!strcmp(argv[1], "checksigs")) { - return checksigs(); - } else { - return usage(); - } -} diff --git a/src/chibipub.h b/src/chibipub.h @@ -0,0 +1,8 @@ + +#pragma once + +struct chibipub { + const char *hostname; +}; + +int init_chibipub(struct chibipub *); diff --git a/src/debug.h b/src/debug.h @@ -0,0 +1,4 @@ + +#pragma once + +#define debug(fmt, ...) fprintf(stderr, fmt "\n", __VA_ARGS__) diff --git a/src/hash.h b/src/hash.h @@ -1,7 +1,4 @@ - - -#ifndef CHIBIPUB_HASH -#define CHIBIPUB_HASH +#pragma once #include "blake3/blake3.h" @@ -21,14 +18,14 @@ static inline uint32_t fnv1a(unsigned char *bytes, int len) return hash; } -static inline int blake3_hash(unsigned char *data, int data_len, - unsigned char *dest) +static inline int hashdata32(unsigned char *data, int data_len, + unsigned char *dest, int dest_len) { + if (dest_len < BLAKE3_OUT_LEN) + return 0; blake3_hasher hasher; blake3_hasher_init(&hasher); blake3_hasher_update(&hasher, data, data_len); blake3_hasher_finalize(&hasher, dest, BLAKE3_OUT_LEN); return BLAKE3_OUT_LEN; } - -#endif /* CHIBIPUB_HASH */ diff --git a/src/hex.c b/src/hex.c @@ -0,0 +1,17 @@ + +int hex_bytes(unsigned char *bytes, int n_bytes, char *buf, int buf_size) +{ + static const char *hex = "0123456789abcdef"; + int b; + + if (n_bytes * 2 < buf_size) + return 0; + + for (int i = 0; i < buf_size; i++) { + b = bytes[i/2]; + b = i % 2 ? b & 0x0F : (b & 0xF0) >> 4; + buf[i] = hex[b]; + } + + return 1; +} diff --git a/src/hex.h b/src/hex.h @@ -0,0 +1,4 @@ +#pragma once + +int hex_bytes(unsigned char *bytes, int n_bytes, char *buf, int buf_size); +int b62_bytes(unsigned char *bytes, int n_bytes, char *buf, int buf_size); diff --git a/src/main.c b/src/main.c @@ -0,0 +1,538 @@ + +#include <stdio.h> +#include <unistd.h> +#include <stdlib.h> +#include <string.h> +#include <netdb.h> +#include <sys/types.h> +#include <sys/socket.h> +#include <netinet/in.h> +#include <arpa/inet.h> +#include <ctype.h> +#include <assert.h> +#include <base64.h> + +#include "http.h" +#include "ap_json.h" +#include "inbox.h" +#include "outbox.h" +#include "json.h" +#include "sigcheck.h" +#include "env.h" +#include "post.h" + +#define BUF_SIZE 1048576 +#define ARENA_SIZE 134217728 /* 128 MB virtual mem arena */ +#define streq(a,b) (!strcmp(a,b)) +#define patheq(req, x) (streq(x, req->path) || streq(x "/", req->path)) + +struct webfinger { + const char *acct; + const char *alias; + const char *profile_page; + const char *self; +}; + +static void error(char *msg) +{ + perror(msg); + exit(1); +} + + +#define SCHEMA "https://" + +static void init_test_webfinger(struct webfinger *finger) +{ + static unsigned char buf[512]; + struct cursor c; + make_cursor(buf, buf+sizeof(buf), &c); + + const char *host = get_hostname(); + const char *id = get_id(); + assert(host); + + finger->acct = (char*)c.p; + + assert( + push_str(&c, "jb55@") && push_c_str(&c, id) + ); + + finger->alias = (char*)c.p; + + assert( + push_str(&c, SCHEMA) && push_str(&c, id) && + push_c_str(&c, "/") + ); + + finger->profile_page = (char*)c.p; + + assert( + push_str(&c, SCHEMA) && push_str(&c, host) && + push_c_str(&c, "/profile") + ); + + finger->self = (char*)c.p; + + assert( + push_str(&c, SCHEMA) && push_str(&c, id) && + push_c_str(&c, "/") + ); +} + +static int handle_webfinger(struct http_req *req) +{ + struct webfinger finger; + + init_test_webfinger(&finger); + + http_write_header(req, "200 OK"); + + fprintf(req->client.socket_file, + "{\"subject\": \"acct:%s\"," + " \"aliases\": [\"%s\"]," + " \"links\": [{" + " \"rel\": \"http://webfinger.net/rel/profile-page\"," + " \"type\": \"text/html\"," + " \"href\": \"%s\"" + " },{" + " \"rel\":\"self\"," + " \"type\": \"application/activity+json\"," + " \"href\": \"%s\"" + " }]}\r\n", + finger.acct, + finger.alias, + finger.profile_page, + finger.self); + + return 1; +} + + +static inline int starts_with(const char *str, const char *prefix) +{ + unsigned int prefix_size; + prefix_size = strlen(prefix); + + if (prefix_size > strlen(str)) { + return 0; + } + + return !memcmp(str, prefix, prefix_size); +} + +static void http_log(struct http_req *req) +{ + printf("%s - \"%s %s %s\"\n", + req->client.addr_str, + req->method, + req->path, + req->ver); +} + +static int handle_inbox_request(struct http_req *req, struct cursor *arena) +{ + const char *signature; + unsigned char *start; + FILE *out; + int len; + struct sig_header sig; + struct json_parser pull; + struct json_pusher push; + struct ap_json apjson; + + struct json_handlers compact; + struct json_handlers ap_handler; + + make_compact_handlers(&compact, &push); + make_ap_json_pusher(&ap_handler, &apjson); + init_ap_json(&apjson, &compact); + + memset(&sig, 0, sizeof(sig)); + + if (!get_header(req->headers, "signature", &signature)) { + note_error(&req->errs, "signature"); + return 0; + } + + printf("signature: %s\n", signature); + + if (!parse_signature_header(&req->errs, arena, signature, strlen(signature), &sig)) { + note_error(&req->errs, "parse signature header"); + return 0; + } + + apjson.sig = &sig; + apjson.req = req; + + start = arena->p; + init_json_pusher_with(&push, arena); + + init_json_parser(&pull, req->body, req->body_len, &ap_handler); + if (!parse_json(&pull)) { + note_error(&req->errs, "json parse failed"); + return 0; + } + + if (!(out = fopen("activities.json", "a"))) { + note_error(&req->errs, "could not open activities.json"); + return 0; + } + + len = push.cur.p - start; + + // 128 KB + if (len >= 131072) { + note_error(&req->errs, "ActivityPub message too big"); + return 0; + } + + fwrite(start, len, 1, out); + fwrite("\n", 1, 1, out); + fclose(out); + + http_write_header(req, "200 OK"); + return 1; +} + +static inline int is_get(struct http_req *req) +{ + return streq(req->method, "GET"); +} + +static inline int is_post(struct http_req *req) +{ + return streq(req->method, "POST"); +} + + +static inline int is_accept_activity(struct http_req *req) +{ + const char *accept; + + if (!get_header(req->headers, "accept", &accept)) { + note_error(&req->errs, "accept"); + return 0; + } + + if (!(strstr(accept, "application/activity+json") || + strstr(accept, "application/ld+json"))) { + note_error(&req->errs, "not accept: '%s'", accept); + return 0; + } + + return 1; +} + +struct profile +{ + const char *id; + const char *type; + const char *username; + const char *name; + const char *summary; + const char *url; + int manually_approves_followers; + const char *image_mime_type; + const char *image_url; + const char *key_id; + const char *pubkey_pem; +}; + +static void test_profile(struct profile *p) +{ + static unsigned char buf[128]; + struct cursor c; + make_cursor(buf, buf + sizeof(buf), &c); + + const char *host = get_hostname(); + const char *id = get_id(); + + assert(host); + + p->id = (char*)c.p; + assert( + push_str(&c, SCHEMA) && push_str(&c, id) && push_c_str(&c, "/") + ); + + p->url = (char*)c.p; + assert( + push_str(&c, SCHEMA) && push_str(&c, host) && push_c_str(&c, "/") + ); + + p->type = "Person"; + p->username = "jb55"; + p->name = "William Casarin"; + p->summary = "chibipub prototype"; + p->manually_approves_followers = 0; + p->image_mime_type = "image/jpeg"; + p->image_url = "https://jb55.com/s/blue-me.jpg"; + p->key_id = "main-key"; + p->pubkey_pem = get_pubkey(); +} + +static int handle_self(struct http_req *req) +{ + struct profile profile; + test_profile(&profile); + + http_write_header(req, "200 OK"); + + fprintf(req->client.socket_file, + "{" + "\"@context\": [" + "\"https://www.w3.org/ns/activitystreams\"" + "]," + "\"inbox\": \"%sinbox\"," + "\"id\": \"%s\"," + "\"type\": \"%s\"," + "\"preferredUsername\": \"%s\"," + "\"name\": \"%s\"," + "\"summary\": \"%s\"," + "\"url\": \"%s\"," + "\"manuallyApprovesFollowers\": %s," + "\"icon\": {" + "\"type\": \"Image\"," + "\"mediaType\": \"%s\"," + "\"url\": \"%s\"" + "}," + "\"publicKey\": {" + "\"id\": \"%s#%s\"," + "\"owner\": \"%s\"," + "\"publicKeyPem\": \"%s\"" + "}" + "}\n", + profile.url, + profile.id, + profile.type, + profile.username, + profile.name, + profile.summary, + profile.url, + profile.manually_approves_followers ? "true" : "false", + profile.image_mime_type, + profile.image_url, + profile.id, profile.key_id, + profile.id, + profile.pubkey_pem); + + return 1; +} + +static int handle_request(struct http_req *req, struct cursor *arena) +{ + http_log(req); + + if (starts_with(req->path, "/.well-known/webfinger?resource=acct:")) { + return handle_webfinger(req); + } else if (is_post(req) && patheq(req, "/inbox")) { + return handle_inbox_request(req, arena); + } else if (is_get(req) && patheq(req, "/outbox")) { + return handle_outbox_request(req); + } else if (is_get(req) && patheq(req, "/outbox?page=true")) { + return handle_outbox_page_request(req, "outbox.json"); + } else if (is_get(req) && + is_accept_activity(req) && + streq(req->path, "/")) { + return handle_self(req); + } + + return 0; +} + +static int http_accept_client(struct http_req *req, int parent) +{ + struct sockaddr_in *addr; + socklen_t client_len; + struct hostent *client_host; + + client_len = sizeof(addr); + + addr = &req->client.sockaddr; + + req->client.socket = + accept(parent, (struct sockaddr *) &req->client.sockaddr, + &client_len); + + if (req->client.socket < 0) { + note_error(&req->errs, "bad socket"); + return 0; + } + + req->client.socket_file = fdopen(req->client.socket, "wb"); + + client_host = gethostbyaddr((const char *)&addr->sin_addr.s_addr, + sizeof(addr->sin_addr.s_addr), AF_INET); + + if (!client_host) { + note_error(&req->errs, "gethostbyaddr"); + return 0; + } + + req->client.addr_str = inet_ntoa(addr->sin_addr); + + if (!req->client.addr_str) { + note_error(&req->errs, "inet_htoa"); + return 0; + } + + return 1; +} + +void run_http_server() +{ + static unsigned char buffer[BUF_SIZE]; + unsigned char *arena; + + ssize_t len, size; + + int parent; + struct sockaddr_in server_addr; + int optval; + + struct http_req req; + + struct parser parser; + + const int port = 5188; + arena = malloc(ARENA_SIZE); + assert(arena); + + if ((parent = socket(AF_INET, SOCK_STREAM, 0)) < 0) { + error("socket"); + } + + optval = 1; + setsockopt(parent, SOL_SOCKET, SO_REUSEADDR, (const void *)&optval, + sizeof(optval)); + + memset(&server_addr, 0, sizeof(server_addr)); + server_addr.sin_family = AF_INET; + server_addr.sin_addr.s_addr = htonl(INADDR_ANY); + server_addr.sin_port = htons(port); + + if (bind(parent, (struct sockaddr *)&server_addr, + sizeof(server_addr)) < 0) { + error("bind"); + } + + printf("listening for activities on 0.0.0.0:%d\n", port); + + if (listen(parent, 5) < 0) { + error("listen"); + } + + while (1) { + init_http_req(&req); + make_cursor(buffer, buffer + BUF_SIZE, &parser.cur); + make_cursor(arena, arena + ARENA_SIZE, &parser.arena); + + http_accept_client(&req, parent); + + for(size = 0;1;) { + len = read(req.client.socket, (void*)(buffer+size), BUF_SIZE-size); + size += len; + if (len == 0) { + break; + } + + if (len != (BUF_SIZE-size)) { + break; + } + } + + if (parse_http_request(&req, &parser, size)) { + handle_request(&req, &parser.arena); + } + + fclose(req.client.socket_file); + close(req.client.socket); + } + + free(arena); +} + +static int load_config() +{ + if (!get_hostname()) { + printf("CHIBIPUB_HOST env not set\n"); + return 0; + } + return 1; +} + +static int checksigs() +{ + struct sigcheck check; + check.activity_file = "activities.json"; + if (!sigcheck(&check)) { + printf("sigcheck failed\n"); + return 0; + } + printf("ok\n"); + return 1; +} + +int usage() +{ + printf( + "usage: chibipub [OPTION]... <command>\n" + "\n" + "commands\n" + "\n" + "help show this help\n" + "serve run the server\n" + "post post a new message\n" + "checksigs check inbox signatures\n" + ); + + return 0; +} + +static int serve() +{ + if (!load_config()) + return usage(); + run_http_server(); + return 0; +} + +static int mkpost(struct chibipub *pub, int argc, char **argv) +{ + struct post post; + + if (argc != 1) + return 1; + + post.content = argv[0]; + + return !create_post(pub, &post); +} + +int main(int argc, char *argv[]) +{ + struct chibipub pub; + + if (argc < 2) { + return usage(); + } + + // early cmds + if (!strcmp(argv[1], "help") || !strcmp(argv[1], "--help")) { + return usage(); + } + + if (!init_chibipub(&pub)) + return 1; + + if (!strcmp(argv[1], "serve")) { + return serve(); + } else if (!strcmp(argv[1], "post")) { + if (argc - 2 < 1) return usage(); + return mkpost(&pub, argc - 2, &argv[2]); + } else if (!strcmp(argv[1], "checksigs")) { + return checksigs(); + } else { + return usage(); + } +} diff --git a/src/post.c b/src/post.c @@ -0,0 +1,184 @@ + +#include "post.h" +#include "env.h" +#include "hex.h" +#include "hash.h" +#include "debug.h" +#include "base64.h" + +#include <stdio.h> +#include <time.h> +#include <string.h> +#include <assert.h> + +/* +{ + "id": "https://bitcoinhackers.org/users/jb55/statuses/107067111309333161/activity", + "type": "Create", + "actor": "https://bitcoinhackers.org/users/jb55", + "published": "2021-10-08T17:34:26Z", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [ + "https://bitcoinhackers.org/users/jb55/followers" + ], + "object": { + "id": "https://bitcoinhackers.org/users/jb55/statuses/107067111309333161", + "type": "Note", + "summary": null, + "inReplyTo": "https://bitcoinhackers.org/users/jb55/statuses/107067094137302681", + "published": "2021-10-08T17:34:26Z", + "url": "https://bitcoinhackers.org/@jb55/107067111309333161", + "attributedTo": "https://bitcoinhackers.org/users/jb55", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [ + "https://bitcoinhackers.org/users/jb55/followers" + ], + "sensitive": false, + "atomUri": "https://bitcoinhackers.org/users/jb55/statuses/107067111309333161", + "inReplyToAtomUri": "https://bitcoinhackers.org/users/jb55/statuses/107067094137302681", + "conversation": "tag:bitcoinhackers.org,2021-10-08:objectId=12204931:objectType=Conversation", + "content": "<p>now this is retro computer gaming: &apos;70s Colossal Cave Adventure on &apos;60s teletype</p><p><a href=\"https://www.youtube.com/watch?v=Fr-HRPr4LxQ\" rel=\"nofollow noopener noreferrer\" target=\"_blank\"><span class=\"invisible\">https://www.</span><span class=\"ellipsis\">youtube.com/watch?v=Fr-HRPr4Lx</span><span class=\"invisible\">Q</span></a></p>", + "contentMap": { + "en": "<p>now this is retro computer gaming: &apos;70s Colossal Cave Adventure on &apos;60s teletype</p><p><a href=\"https://www.youtube.com/watch?v=Fr-HRPr4LxQ\" rel=\"nofollow noopener noreferrer\" target=\"_blank\"><span class=\"invisible\">https://www.</span><span class=\"ellipsis\">youtube.com/watch?v=Fr-HRPr4Lx</span><span class=\"invisible\">Q</span></a></p>" + }, + "attachment": [], + "tag": [], + "replies": { + "id": "https://bitcoinhackers.org/users/jb55/statuses/107067111309333161/replies", + "type": "Collection", + "first": { + "type": "CollectionPage", + "next": "https://bitcoinhackers.org/users/jb55/statuses/107067111309333161/replies?only_other_accounts=true&page=true", + "partOf": "https://bitcoinhackers.org/users/jb55/statuses/107067111309333161/replies", + "items": [] + } + } + } +} +*/ + +//static inline int writekv_(FILE *outbox, const char *key, const ch + + +static int make_id(const char *activity, const char *dt, const char *content, + unsigned char buffer[32]) +{ + debug("make_id: hashing '%s%s'", dt, content); + + blake3_hasher hasher; + blake3_hasher_init(&hasher); + blake3_hasher_update(&hasher, activity, strlen(activity)); + blake3_hasher_update(&hasher, dt, strlen(dt)); + blake3_hasher_update(&hasher, content, strlen(content)); + blake3_hasher_finalize(&hasher, buffer, 32); + + return 1; +} + +#define writekv(key, fmt, ...) fprintf(outbox, "\"" key "\":" fmt, ## __VA_ARGS__) + +static void make_pubdate(char *buffer, int buflen) +{ + char scratch[128]; + struct timespec dt; + struct tm utc; + + clock_gettime(CLOCK_REALTIME, &dt); + gmtime_r(&dt.tv_sec, &utc); + strftime(scratch, sizeof(scratch)-1, "%F %T", &utc); + snprintf(buffer, buflen-1, "%s.%ldZ", scratch, dt.tv_nsec); +} + + +int create_post(struct chibipub *pub, struct post *post) +{ + char note_id[65]; + char create_id[65]; + char pubdate[128]; + unsigned char rawid[32]; + FILE *outbox; + int ok; + + outbox = fopen("outbox.json", "a"); + if (!outbox) return 0; + + //base64_encode(rawid, sizeof(rawid), id, sizeof(id), &enclen); + + // published date, includes nanos + make_pubdate(pubdate, sizeof(pubdate)); + + // note_id = H("note" + formatted_pubdate + content) + ok = make_id("note", pubdate, post->content, rawid); + assert(ok); + + hex_bytes(rawid, sizeof(rawid), note_id, 64); + note_id[64] = '\0'; + + // create_id = H("create" + formatted_pubdate + note_id) + ok = make_id("create", pubdate, note_id, rawid); + assert(ok); + + hex_bytes(rawid, sizeof(rawid), create_id, 64); + create_id[64] = '\0'; + + fprintf(outbox, "{"); + // we can just use the first 16 bytes of our id as the shortid + writekv("id", "\"https://%s/obj/cr_%.*s\",", pub->hostname, 32, create_id); + writekv("type", "\"Create\","); + + writekv("published", "\"%s\",", pubdate); + writekv("to", "[\"https://www.w3.org/ns/activitystreams#Public\"],"); + writekv("cc", "[\"https://%s/followers\"],", pub->hostname); + writekv("object", "{"); + + writekv("id", "\"https://%s/obj/note_%.*s\",", pub->hostname, 32, note_id); + writekv("type", "\"Note\","); + writekv("summary", "null,"); + + if (post->in_reply_to) + writekv("inReplyTo", "\"%s\",", post->in_reply_to); + + writekv("published", "\"%s\",", pubdate); + writekv("url", "\"https://%s/obj/note_%.*s\",", pub->hostname, 32, note_id); + + // TODO: multiuser + writekv("attributedTo", "\"https://%s\",", pub->hostname); + writekv("to", "[\"https://www.w3.org/ns/activitystreams#Public\"],"); + writekv("cc", "[\"https://%s/followers\"],", pub->hostname); + writekv("sensitive", "false,"); + writekv("atomUri", "\"https://%s/obj/note_%.*s\",", pub->hostname, 32, note_id); + writekv("inReplyToAtomUri", "\"https://%s/obj/note_%.*s\",", pub->hostname, 32, note_id); + writekv("content", "\"%s\"", post->content); + + // TODO: attachments + //writekv("attachment", "[],"); + // TODO: tags + //writekv("tag", "[],"); + // TODO: replies + //writekv("replies", "{"); + // + fprintf(outbox, "}"); + + /* +"object": { + "replies": { + "id": "https://bitcoinhackers.org/users/jb55/statuses/107067111309333161/replies", + "type": "Collection", + "first": { + "type": "CollectionPage", + "next": "https://bitcoinhackers.org/users/jb55/statuses/107067111309333161/replies?only_other_accounts=true&page=true", + "partOf": "https://bitcoinhackers.org/users/jb55/statuses/107067111309333161/replies", + "items": [] + } + } + } + */ + + fprintf(outbox, "}\n"); + + return 1; +} diff --git a/src/post.h b/src/post.h @@ -0,0 +1,12 @@ + +#pragma once + +#include "chibipub.h" + +struct post +{ + const char *content; + const char *in_reply_to; +}; + +int create_post(struct chibipub *, struct post *); diff --git a/src/sigcheck.c b/src/sigcheck.c @@ -12,8 +12,10 @@ #include <openssl/pem.h> #include "json.h" +#include "debug.h" #include "ubjson.h" #include "hash.h" +#include "hex.h" #include "base64.h" #include "io.h" #include "sigcheck.h" @@ -26,8 +28,6 @@ #include <ctype.h> #include <curl/curl.h> -#define debug_info(...) - struct keyid_pubkey { unsigned char *data; int len; @@ -48,24 +48,6 @@ struct key_writer { int flags; }; -static int hex_bytes(unsigned char *bytes, int n_bytes, char *buf, - int buf_size) -{ - static const char *hex = "0123456789abcdef"; - int b; - - if (n_bytes * 2 < buf_size) - return 0; - - for (int i = 0; i < buf_size; i++) { - b = bytes[i/2]; - b = i % 2 ? b & 0x0F : (b & 0xF0) >> 4; - buf[i] = hex[b]; - } - - return 1; -} - static int is_delete_activity(struct ubjson *ubjson) { struct json val; @@ -93,7 +75,7 @@ static int get_cached_pubkey(const char *keyid, int keyid_len, return 0; } - blake3_hash((unsigned char *)keyid, keyid_len, hash); + hashdata32((unsigned char *)keyid, keyid_len, hash, sizeof(hash)); if (!hex_bytes(hash, 32, hash_str, 64)) { assert(0); } @@ -101,12 +83,10 @@ static int get_cached_pubkey(const char *keyid, int keyid_len, sprintf(path, ".chibipub/objects/%c%c/%.*s", hash_str[0], hash_str[1], 64, hash_str); - debug_info(stderr, "key cache exists? '%s' ", path); if (access(path, F_OK)) { - debug_info(stderr, "no.\n"); + debug("key cache '%s' doesn't exist", path); return 0; } - debug_info(stderr, "yes!\n"); *pubkey = arena->p; ok = read_file(path, arena->p, arena->end - arena->p, pubkey_size); @@ -397,7 +377,7 @@ static int get_keyid_hash(const char *keyid, struct cursor *arena, return 0; } - blake3_hash((unsigned char*)keyid, strlen(keyid), hash); + hashdata32((unsigned char*)keyid, strlen(keyid), hash, 32); if (!hex_bytes(hash, 32, *hash_str, 64)) { return 0; } @@ -664,7 +644,7 @@ int sigcheck(struct sigcheck *check) start = out_cur.p; while (parse_json(&jsonp)) { count++; - debug_info("[%d] parse success\n", count); + debug("[%d] parse success", count); res = verify_signature(out_cur, out_cur);