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:
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: '70s Colossal Cave Adventure on '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: '70s Colossal Cave Adventure on '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);