chibipub

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

main.c (10628B)


      1 
      2 #include <stdio.h>
      3 #include <unistd.h>
      4 #include <stdlib.h>
      5 #include <string.h>
      6 #include <netdb.h>
      7 #include <sys/types.h>
      8 #include <sys/socket.h>
      9 #include <netinet/in.h>
     10 #include <arpa/inet.h>
     11 #include <ctype.h>
     12 #include <assert.h>
     13 #include <base64.h>
     14 
     15 #include "http.h"
     16 #include "ap_json.h"
     17 #include "inbox.h"
     18 #include "outbox.h"
     19 #include "json.h"
     20 #include "sigcheck.h"
     21 #include "env.h"
     22 #include "post.h"
     23 
     24 #define BUF_SIZE 1048576
     25 #define ARENA_SIZE 134217728 /* 128 MB virtual mem arena */
     26 #define streq(a,b) (!strcmp(a,b))
     27 #define patheq(req, x) (streq(x, req->path) || streq(x "/", req->path))
     28 
     29 struct webfinger {
     30 	const char *acct;
     31 	const char *alias;
     32 	const char *profile_page;
     33 	const char *self;
     34 };
     35 
     36 static void error(char *msg)
     37 {
     38 	perror(msg);
     39 	exit(1);
     40 }
     41 
     42 
     43 #define SCHEMA "https://"
     44 
     45 static void init_test_webfinger(struct webfinger *finger)
     46 {
     47 	static unsigned char buf[512];
     48 	struct cursor c;
     49 	make_cursor(buf, buf+sizeof(buf), &c);
     50 
     51 	const char *host = get_hostname();
     52 	const char *id = get_id();
     53 	assert(host);
     54 
     55 	finger->acct = (char*)c.p;
     56 
     57 	assert(
     58 		push_str(&c, "jb55@") && push_c_str(&c, id)
     59 	);
     60 
     61 	finger->alias = (char*)c.p;
     62 
     63 	assert(
     64 		push_str(&c, SCHEMA) && push_str(&c, id) &&
     65 		push_c_str(&c, "/")
     66 	);
     67 
     68 	finger->profile_page = (char*)c.p;
     69 
     70 	assert(
     71 		push_str(&c, SCHEMA) && push_str(&c, host) &&
     72 		push_c_str(&c, "/profile")
     73 	);
     74 
     75 	finger->self = (char*)c.p;
     76 
     77 	assert(
     78 		push_str(&c, SCHEMA) && push_str(&c, id) &&
     79 		push_c_str(&c, "/")
     80 	);
     81 }
     82 
     83 static int handle_webfinger(struct http_req *req)
     84 {
     85 	struct webfinger finger;
     86 
     87 	init_test_webfinger(&finger);
     88 
     89 	http_write_header(req, "200 OK");
     90 
     91 	fprintf(req->client.socket_file,
     92 	  "{\"subject\": \"acct:%s\","
     93 	  " \"aliases\": [\"%s\"],"
     94 	  " \"links\": [{"
     95 	  "   \"rel\": \"http://webfinger.net/rel/profile-page\","
     96 	  "   \"type\": \"text/html\","
     97 	  "   \"href\": \"%s\""
     98 	  " },{"
     99 	  "   \"rel\":\"self\","
    100 	  "   \"type\": \"application/activity+json\","
    101 	  "   \"href\": \"%s\""
    102 	  " }]}\r\n",
    103 		finger.acct,
    104 		finger.alias,
    105 		finger.profile_page,
    106 		finger.self);
    107 
    108 	return 1;
    109 }
    110 
    111 
    112 static inline int starts_with(const char *str, const char *prefix)
    113 {
    114 	unsigned int prefix_size;
    115 	prefix_size = strlen(prefix);
    116 
    117 	if (prefix_size > strlen(str)) {
    118 		return 0;
    119 	}
    120 
    121 	return !memcmp(str, prefix, prefix_size);
    122 }
    123 
    124 static void http_log(struct http_req *req)
    125 {
    126 	printf("%s - \"%s %s %s\"\n",
    127 		req->client.addr_str,
    128 		req->method,
    129 		req->path,
    130 		req->ver);
    131 }
    132 
    133 static int handle_inbox_request(struct http_req *req, struct cursor *arena)
    134 {
    135 	const char *signature;
    136 	unsigned char *start;
    137 	FILE *out;
    138 	int len;
    139 	struct sig_header sig;
    140 	struct json_parser pull;
    141 	struct json_pusher push;
    142 	struct ap_json apjson;
    143 
    144 	struct json_handlers compact;
    145 	struct json_handlers ap_handler;
    146 
    147 	make_compact_handlers(&compact, &push);
    148 	make_ap_json_pusher(&ap_handler, &apjson);
    149 	init_ap_json(&apjson, &compact);
    150 
    151 	memset(&sig, 0, sizeof(sig));
    152 
    153 	if (!get_header(req->headers, "signature", &signature)) {
    154 		note_error(&req->errs, "signature");
    155 		return 0;
    156 	}
    157 
    158 	printf("signature: %s\n", signature);
    159 
    160 	if (!parse_signature_header(&req->errs, arena, signature, strlen(signature), &sig)) {
    161 		note_error(&req->errs, "parse signature header");
    162 		return 0;
    163 	}
    164 
    165 	apjson.sig = &sig;
    166 	apjson.req = req;
    167 
    168 	start = arena->p;
    169 	init_json_pusher_with(&push, arena);
    170 
    171 	init_json_parser(&pull, req->body, req->body_len, &ap_handler);
    172 	if (!parse_json(&pull)) {
    173 		note_error(&req->errs, "json parse failed");
    174 		return 0;
    175 	}
    176 
    177 	if (!(out = fopen("activities.json", "a"))) {
    178 		note_error(&req->errs, "could not open activities.json");
    179 		return 0;
    180 	}
    181 
    182 	len = push.cur.p - start;
    183 
    184 	// 128 KB
    185 	if (len >= 131072) {
    186 		note_error(&req->errs, "ActivityPub message too big");
    187 		return 0;
    188 	}
    189 
    190 	fwrite(start, len, 1, out);
    191 	fwrite("\n", 1, 1, out);
    192 	fclose(out);
    193 
    194 	http_write_header(req, "200 OK");
    195 	return 1;
    196 }
    197 
    198 static inline int is_get(struct http_req *req)
    199 {
    200 	return streq(req->method, "GET");
    201 }
    202 
    203 static inline int is_post(struct http_req *req)
    204 {
    205 	return streq(req->method, "POST");
    206 }
    207 
    208 
    209 static inline int is_accept_activity(struct http_req *req)
    210 {
    211 	const char *accept;
    212 
    213 	if (!get_header(req->headers, "accept", &accept)) {
    214 		note_error(&req->errs, "accept");
    215 		return 0;
    216 	}
    217 
    218 	if (!(strstr(accept, "application/activity+json") ||
    219 	      strstr(accept, "application/ld+json"))) {
    220 		note_error(&req->errs, "not accept: '%s'", accept);
    221 		return 0;
    222 	}
    223 
    224 	return 1;
    225 }
    226 
    227 struct profile
    228 {
    229 	const char *id;
    230 	const char *type;
    231 	const char *username;
    232 	const char *name;
    233 	const char *summary;
    234 	const char *url;
    235 	int manually_approves_followers;
    236 	const char *image_mime_type;
    237 	const char *image_url;
    238 	const char *key_id;
    239 	const char *pubkey_pem;
    240 };
    241 
    242 static void test_profile(struct profile *p)
    243 {
    244 	static unsigned char buf[128];
    245 	struct cursor c;
    246 	make_cursor(buf, buf + sizeof(buf), &c);
    247 
    248 	const char *host = get_hostname();
    249 	const char *id = get_id();
    250 
    251 	assert(host);
    252 
    253 	p->id = (char*)c.p;
    254 	assert(
    255 		push_str(&c, SCHEMA) && push_str(&c, id) && push_c_str(&c, "/")
    256 	);
    257 
    258 	p->url = (char*)c.p;
    259 	assert(
    260 		push_str(&c, SCHEMA) && push_str(&c, host) && push_c_str(&c, "/")
    261 	);
    262 
    263 	p->type = "Person";
    264 	p->username = "jb55";
    265 	p->name = "William Casarin";
    266 	p->summary = "chibipub prototype";
    267 	p->manually_approves_followers = 0;
    268 	p->image_mime_type = "image/jpeg";
    269 	p->image_url = "https://jb55.com/s/blue-me.jpg";
    270 	p->key_id = "main-key";
    271 	p->pubkey_pem = get_pubkey();
    272 }
    273 
    274 static int handle_self(struct http_req *req)
    275 {
    276 	struct profile profile;
    277 	test_profile(&profile);
    278 
    279 	http_write_header(req, "200 OK");
    280 
    281 	fprintf(req->client.socket_file,
    282 	"{"
    283 	  "\"@context\": ["
    284 	    "\"https://www.w3.org/ns/activitystreams\""
    285 	  "],"
    286 	  "\"inbox\": \"%sinbox\","
    287 	  "\"id\": \"%s\","
    288 	  "\"type\": \"%s\","
    289 	  "\"preferredUsername\": \"%s\","
    290 	  "\"name\": \"%s\","
    291 	  "\"summary\": \"%s\","
    292 	  "\"url\": \"%s\","
    293 	  "\"manuallyApprovesFollowers\": %s,"
    294 	  "\"icon\": {"
    295 	    "\"type\": \"Image\","
    296 	    "\"mediaType\": \"%s\","
    297 	    "\"url\": \"%s\""
    298 	  "},"
    299 	  "\"publicKey\": {"
    300 	    "\"id\": \"%s#%s\","
    301 	    "\"owner\": \"%s\","
    302 	    "\"publicKeyPem\": \"%s\""
    303 	  "}"
    304 	"}\n",
    305 	profile.url,
    306 	profile.id,
    307 	profile.type,
    308 	profile.username,
    309 	profile.name,
    310 	profile.summary,
    311 	profile.url,
    312 	profile.manually_approves_followers ? "true" : "false",
    313 	profile.image_mime_type,
    314 	profile.image_url,
    315 	profile.id, profile.key_id,
    316 	profile.id,
    317 	profile.pubkey_pem);
    318 
    319 	return 1;
    320 }
    321 
    322 static int handle_request(struct http_req *req, struct cursor *arena)
    323 {
    324 	http_log(req);
    325 
    326 	if (starts_with(req->path, "/.well-known/webfinger?resource=acct:") ||
    327 	    starts_with(req->path, "/?resource=acct:")) {
    328 		return handle_webfinger(req);
    329 	} else if (is_post(req) && patheq(req, "/inbox")) {
    330 		return handle_inbox_request(req, arena);
    331 	} else if (is_get(req) && patheq(req, "/outbox")) {
    332 		return handle_outbox_request(req);
    333 	} else if (is_get(req) && patheq(req, "/outbox?page=true")) {
    334 		return handle_outbox_page_request(req, "outbox.json");
    335 	} else if (is_get(req) &&
    336 	           is_accept_activity(req) &&
    337 	           streq(req->path, "/")) {
    338 		return handle_self(req);
    339 	}
    340 
    341 	return 0;
    342 }
    343 
    344 static int http_accept_client(struct http_req *req, int parent)
    345 {
    346 	struct sockaddr_in *addr;
    347 	socklen_t client_len;
    348 	struct hostent *client_host;
    349 
    350 	client_len = sizeof(addr);
    351 
    352 	addr = &req->client.sockaddr;
    353 
    354 	req->client.socket =
    355 		accept(parent, (struct sockaddr *) &req->client.sockaddr,
    356 				&client_len);
    357 
    358 	if (req->client.socket < 0) {
    359 		note_error(&req->errs, "bad socket");
    360 		return 0;
    361 	}
    362 
    363 	req->client.socket_file = fdopen(req->client.socket, "wb");
    364 
    365 	client_host = gethostbyaddr((const char *)&addr->sin_addr.s_addr,
    366 			sizeof(addr->sin_addr.s_addr), AF_INET);
    367 
    368 	if (!client_host) {
    369 		note_error(&req->errs, "gethostbyaddr");
    370 		return 0;
    371 	}
    372 
    373 	req->client.addr_str = inet_ntoa(addr->sin_addr);
    374 
    375 	if (!req->client.addr_str) {
    376 		note_error(&req->errs, "inet_htoa");
    377 		return 0;
    378 	}
    379 
    380 	return 1;
    381 }
    382 
    383 void run_http_server()
    384 {
    385 	static unsigned char buffer[BUF_SIZE];
    386 	unsigned char *arena;
    387 
    388 	ssize_t len, size;
    389 
    390 	int parent;
    391 	struct sockaddr_in server_addr;
    392 	int optval;
    393 
    394 	struct http_req req;
    395 
    396 	struct parser parser;
    397 
    398 	const int port = 5188;
    399 	arena = malloc(ARENA_SIZE);
    400 	assert(arena);
    401 
    402 	if ((parent = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
    403 		error("socket");
    404 	}
    405 
    406 	optval = 1;
    407 	setsockopt(parent, SOL_SOCKET, SO_REUSEADDR, (const void *)&optval,
    408 			sizeof(optval));
    409 
    410 	memset(&server_addr, 0, sizeof(server_addr));
    411 	server_addr.sin_family = AF_INET;
    412 	server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    413 	server_addr.sin_port = htons(port);
    414 
    415 	if (bind(parent, (struct sockaddr *)&server_addr,
    416 				sizeof(server_addr)) < 0) {
    417 		error("bind");
    418 	}
    419 
    420 	printf("listening for activities on 0.0.0.0:%d\n", port);
    421 
    422 	if (listen(parent, 5) < 0) {
    423 		error("listen");
    424 	}
    425 
    426 	while (1) {
    427 		init_http_req(&req);
    428 		make_cursor(buffer, buffer + BUF_SIZE, &parser.cur);
    429 		make_cursor(arena, arena + ARENA_SIZE, &parser.arena);
    430 
    431 		http_accept_client(&req, parent);
    432 
    433 		for(size = 0;1;) {
    434 			len = read(req.client.socket, (void*)(buffer+size), BUF_SIZE-size);
    435 			size += len;
    436 			if (len == 0) {
    437 				break;
    438 			}
    439 
    440 			if (len != (BUF_SIZE-size)) {
    441 				break;
    442 			}
    443 		}
    444 
    445 		if (parse_http_request(&req, &parser, size)) {
    446 			handle_request(&req, &parser.arena);
    447 		}
    448 
    449 		fclose(req.client.socket_file);
    450 		close(req.client.socket);
    451 	}
    452 
    453 	free(arena);
    454 }
    455 
    456 static int load_config()
    457 {
    458 	if (!get_hostname()) {
    459 		printf("CHIBIPUB_HOST env not set\n");
    460 		return 0;
    461 	}
    462 	return 1;
    463 }
    464 
    465 static int checksigs()
    466 {
    467 	struct sigcheck check;
    468 	check.activity_file = "activities.json";
    469 	if (!sigcheck(&check)) {
    470 		printf("sigcheck failed\n");
    471 		return 0;
    472 	}
    473 	printf("ok\n");
    474 	return 1;
    475 }
    476 
    477 int usage()
    478 {
    479 	printf(
    480 	"usage: chibipub [OPTION]... <command>\n"
    481 	"\n"
    482 	"commands\n"
    483 	"\n"
    484 	"help          show this help\n"
    485 	"serve         run the server\n"
    486 	"post          post a new message\n"
    487 	"checksigs     check inbox signatures\n"
    488 	);
    489 
    490 	return 0;
    491 }
    492 
    493 static int serve()
    494 {
    495 	if (!load_config())
    496 		return usage();
    497 	run_http_server();
    498 	return 0;
    499 }
    500 
    501 static int mkpost(struct chibipub *pub, int argc, char **argv)
    502 {
    503 	struct post post;
    504 
    505 	if (argc != 1)
    506 		return 1;
    507 
    508 	post.content = argv[0];
    509 
    510 	return !create_post(pub, &post);
    511 }
    512 
    513 int main(int argc, char *argv[])
    514 {
    515 	struct chibipub pub;
    516 
    517 	if (argc < 2) {
    518 		return usage();
    519 	}
    520 
    521 	// early cmds
    522 	if (!strcmp(argv[1], "help") || !strcmp(argv[1], "--help")) {
    523 		return usage();
    524 	} 
    525 
    526 	if (!init_chibipub(&pub))
    527 		return 1;
    528 
    529 	if (!strcmp(argv[1], "serve")) {
    530 		return serve();
    531 	} else if (!strcmp(argv[1], "post")) {
    532 		if (argc - 2 < 1) return usage();
    533 		return mkpost(&pub, argc - 2, &argv[2]);
    534 	} else if (!strcmp(argv[1], "checksigs")) {
    535 		return checksigs();
    536 	} else {
    537 		return usage();
    538 	}
    539 }