Exemple #1
0
void BlogFree(BlogRef *const blogptr) {
	BlogRef blog = *blogptr;
	if(!blog) return;

	blog->repo = NULL;

	FREE(&blog->dir);
	FREE(&blog->cacheDir);

	TemplateFree(&blog->header);
	TemplateFree(&blog->footer);
	TemplateFree(&blog->entry_start);
	TemplateFree(&blog->entry_end);
	TemplateFree(&blog->preview);
	TemplateFree(&blog->empty);
	TemplateFree(&blog->compose);
	TemplateFree(&blog->upload);
	TemplateFree(&blog->login);
	TemplateFree(&blog->notfound);
	TemplateFree(&blog->noresults);

	async_mutex_destroy(blog->pending_mutex);
	async_cond_destroy(blog->pending_cond);

	assert_zeroed(blog, 1);
	FREE(blogptr); blog = NULL;
}
Exemple #2
0
void SLNSubmissionFree(SLNSubmissionRef *const subptr) {
	SLNSubmissionRef sub = *subptr;
	if(!sub) return;

	sub->session = NULL;
	FREE(&sub->knownURI);
	FREE(&sub->type);

	if(sub->tmppath) async_fs_unlink(sub->tmppath);
	FREE(&sub->tmppath);
	if(sub->tmpfile >= 0) async_fs_close(sub->tmpfile);
	sub->tmpfile = 0;
	sub->size = 0;

	SLNHasherFree(&sub->hasher);
	sub->metaFileID = 0;

	if(sub->URIs) for(size_t i = 0; sub->URIs[i]; ++i) FREE(&sub->URIs[i]);
	FREE(&sub->URIs);
	FREE(&sub->primaryURI);
	FREE(&sub->internalHash);

	assert_zeroed(sub, 1);
	FREE(subptr); sub = NULL;
}
Exemple #3
0
void SLNFilterPositionCleanup(SLNFilterPosition *const pos) {
	assert(pos);
	pos->dir = 0;
	FREE(&pos->URI);
	pos->sortID = 0;
	pos->fileID = 0;
	assert_zeroed(pos, 1);
}
Exemple #4
0
void SLNFileInfoCleanup(SLNFileInfo *const info) {
	if(!info) return;
	FREE(&info->hash);
	FREE(&info->path);
	FREE(&info->type);
	info->size = 0;
	assert_zeroed(info, 1);
}
Exemple #5
0
void HTTPServerFree(HTTPServerRef *const serverptr) {
	HTTPServerRef server = *serverptr;
	if(!server) return;
	HTTPServerClose(server);
	server->listener = NULL;
	server->context = NULL;
	assert_zeroed(server, 1);
	FREE(serverptr); server = NULL;
}
void MultipartFormFree(MultipartFormRef *const formptr) {
	MultipartFormRef form = *formptr;
	if(!form) return;
	form->conn = NULL;
	multipart_parser_free(form->parser); form->parser = NULL;
	form->type = MultipartNothing;
	*form->out = uv_buf_init(NULL, 0);
	form->flags = 0;
	assert_zeroed(form, 1);
	FREE(formptr); form = NULL;
}
Exemple #7
0
void HTTPHeadersFree(HTTPHeadersRef *const hptr) {
	HTTPHeadersRef h = *hptr;
	if(!h) return;
	FREE(&h->fields);
	for(uint16_t i = 0; i < h->count; i++) FREE(&h->values[i]);
	h->count = 0;
	h->offset = 0;
	h->total = 0;
	assert_zeroed(h, 1);
	FREE(hptr); h = NULL;
}
void SLNJSONFilterParserFree(SLNJSONFilterParserRef *const parserptr) {
	SLNJSONFilterParserRef parser = *parserptr;
	if(!parser) return;
	parser->session = NULL;
	if(parser->JSONParser) {
		yajl_free(parser->JSONParser); parser->JSONParser = NULL;
	}
	for(size_t i = 0; i < DEPTH_MAX; ++i) {
		SLNFilterFree(&parser->stack[i]);
	}
	assert_zeroed(parser, 1);
	FREE(parserptr); parser = NULL;
}
Exemple #9
0
void SocketFree(SocketRef *const socketptr) {
	SocketRef socket = *socketptr;
	if(!socket) return;
	if(socket->secure) tls_close(socket->secure);
	tls_free(socket->secure); socket->secure = NULL;
	async_close((uv_handle_t *)socket->stream);
	FREE(&socket->rdmem);
	socket->rd->base = NULL; socket->rd->len = 0;
	FREE(&socket->wr->base); socket->wr->len = 0;
	socket->err = 0;
	assert_zeroed(socket, 1);
	FREE(socketptr); socket = NULL;
}
Exemple #10
0
void kvs_helper_txn_abort(KVS_helper_txn *const helper) {
	if(!helper) return;
	if(helper->child) {
		kvs_txn_abort(helper->child); helper->child = NULL;
	}
	kvs_cursor_close(helper->cursor); helper->cursor = NULL;
	if(helper->parent) {
		kvs_txn_set_config(helper->parent, KVS_TXN_CHILD, NULL);
	}
	helper->env = NULL;
	helper->parent = NULL;
	helper->flags = 0;
	assert_zeroed(helper, 1);
}
Exemple #11
0
void SLNSessionRelease(SLNSessionRef *const sessionptr) {
	SLNSessionRef session = *sessionptr;
	if(!session) return;
	assert(session->refcount);
	if(--session->refcount) return;
	session->cache = NULL;
	session->sessionID = 0;
	FREE(&session->sessionKeyRaw);
	FREE(&session->sessionKeyEnc);
	session->userID = 0;
	session->mode = 0;
	FREE(&session->username);
	assert_zeroed(session, 1);
	FREE(sessionptr); session = NULL;
}
Exemple #12
0
ssize_t SLNFilterWriteURIBatch(SLNFilterRef const filter, SLNSessionRef const session, SLNFilterPosition *const pos, bool const meta, uint64_t const max, SLNFilterWriteCB const writecb, void *ctx) {
	str_t *URIs[BATCH_SIZE];
	ssize_t const count = SLNFilterCopyURIs(filter, session, pos, pos->dir, meta, URIs, MIN(max, BATCH_SIZE));
	if(count <= 0) return count;
	uv_buf_t parts[BATCH_SIZE*2];
	for(size_t i = 0; i < count; i++) {
		parts[i*2+0] = uv_buf_init((char *)URIs[i], strlen(URIs[i]));
		parts[i*2+1] = uv_buf_init((char *)STR_LEN("\r\n"));
	}
	int rc = writecb(ctx, parts, count*2);
	for(size_t i = 0; i < count; i++) FREE(&URIs[i]);
	assert_zeroed(URIs, count);
	if(rc < 0) return rc;
	return count;
}
void EFSSyncFree(EFSSyncRef *const syncptr) {
	assert(syncptr);
	EFSSyncRef sync = *syncptr;
	if(!sync) return;

	sync->stop = true;
	// TODO: Join db_thread

	sync->session = NULL;
	FREE(&sync->syncID);
	async_mutex_destroy(sync->mutex);
	async_cond_destroy(sync->cond);
	sync->cur = NULL;

	// TODO: Clear queues

	assert_zeroed(sync, 1);
	FREE(syncptr); sync = NULL;
}
Exemple #14
0
void SLNPullFree(SLNPullRef *const pullptr) {
	SLNPullRef pull = *pullptr;
	if(!pull) return;

	SLNPullStop(pull);

	pull->pullID = 0;
	SLNSessionRelease(&pull->session);
	FREE(&pull->host);
	FREE(&pull->cookie);
//	FREE(&pull->query);

	async_mutex_destroy(pull->connlock);
	async_mutex_destroy(pull->mutex);
	async_cond_destroy(pull->cond);
	pull->stop = false;

	assert_zeroed(pull, 1);
	FREE(pullptr); pull = NULL;
}
int SLNSubmissionParseMetaFile(SLNSubmissionRef const sub, uint64_t const fileID, DB_txn *const txn, uint64_t *const out) {
	assert(out);
	if(!sub) return DB_EINVAL;
	if(!fileID) return DB_EINVAL;
	if(!txn) return DB_EINVAL;

	strarg_t const type = SLNSubmissionGetType(sub);
	if(!type) return 0;
	if(0 != strcasecmp(SLN_META_TYPE, type) &&
	   0 != strcasecmp("text/x-sln-meta+json; charset=utf-8", type) &&
	   0 != strcasecmp("text/efs-meta+json; charset=utf-8", type)) return 0;
	// TODO: Get rid of these obsolete types.

	uv_file const fd = SLNSubmissionGetFile(sub);
	if(fd < 0) return DB_EINVAL;

	int rc = 0;
	uv_buf_t buf[1] = {};
	DB_txn *subtxn = NULL;
	parser_t ctx[1] = {};
	yajl_handle parser = NULL;

	buf->base = malloc(BUF_LEN);
	if(!buf->base) { rc = DB_ENOMEM; goto cleanup; }

	buf->len = URI_MAX;
	int64_t pos = 0;
	ssize_t len = async_fs_read(fd, buf, 1, pos);
	if(len < 0) {
		fprintf(stderr, "Submission meta-file read error (%s)\n", sln_strerror(len));
		rc = DB_EIO;
		goto cleanup;
	}

	size_t i;
	for(i = 0; i < len; i++) {
		char const c = buf->base[i];
		if('\r' == c || '\n' == c) break;
	}
	if(i >= len) {
		fprintf(stderr, "Submission meta-file parse error (invalid target URI)\n");
		rc = DB_EIO;
		goto cleanup;
	}
	assert(i < len);
	str_t targetURI[URI_MAX];
	memcpy(targetURI, buf->base, i);
	targetURI[i] = '\0';
	pos += i;

	// TODO: Support sub-transactions in LevelDB backend.
	// TODO: db_txn_begin should support NULL env.
//	rc = db_txn_begin(NULL, txn, DB_RDWR, &subtxn);
//	if(rc < 0) goto cleanup;
	subtxn = txn;

	uint64_t const metaFileID = add_metafile(subtxn, fileID, targetURI);
	if(!metaFileID) goto cleanup;
	// Duplicate meta-file, not an error.
	// TODO: Unless the previous version wasn't actually a meta-file.

	ctx->txn = subtxn;
	ctx->metaFileID = metaFileID;
	ctx->targetURI = targetURI;
	ctx->depth = -1;
	parser = yajl_alloc(&callbacks, NULL, ctx);
	if(!parser) rc = DB_ENOMEM;
	if(rc < 0) goto cleanup;
	yajl_config(parser, yajl_allow_partial_values, (int)true);

	yajl_status status = yajl_status_ok;
	for(;;) {
		buf->len = MIN(BUF_LEN, PARSE_MAX-pos);
		if(!buf->len) break;
		len = async_fs_read(fd, buf, 1, pos);
		if(len < 0) {
			fprintf(stderr, "Submission meta-file read error (%s)\n", sln_strerror(len));
			rc = DB_EIO;
			goto cleanup;
		}
		if(0 == len) break;
		status = yajl_parse(parser, (byte_t const *)buf->base, len);
		if(yajl_status_ok != status) break;
		pos += len;
	}
	status = yajl_complete_parse(parser);
	if(yajl_status_ok != status) {
		unsigned char *msg = yajl_get_error(parser, true, (byte_t const *)buf->base, len);
		fprintf(stderr, "%s", msg);
		yajl_free_error(parser, msg); msg = NULL;
		rc = DB_EIO;
		goto cleanup;
	}

	assert(-1 == ctx->depth);

//	rc = db_txn_commit(subtxn); subtxn = NULL;
	if(rc < 0) goto cleanup;

	*out = metaFileID;

cleanup:
//	db_txn_abort(subtxn); subtxn = NULL;
	FREE(&buf->base);
	if(parser) yajl_free(parser); parser = NULL;
	assert_zeroed(ctx->fields, DEPTH_MAX);
	return rc;
}
Exemple #16
0
int HTTPConnectionCreateOutgoing(strarg_t const domain, unsigned const flags, HTTPConnectionRef *const out) {
	str_t host[1023+1];
	str_t service[15+1];
	host[0] = '\0';
	service[0] = '\0';
	int matched = sscanf(domain, "%1023[^:]:%15[0-9]", host, service);
	if(matched < 1) return UV_EINVAL;
	if('\0' == host[0]) return UV_EINVAL;

	static struct addrinfo const hints = {
		.ai_flags = AI_V4MAPPED | AI_ADDRCONFIG | AI_NUMERICSERV,
		.ai_family = AF_UNSPEC,
		.ai_socktype = SOCK_STREAM,
		.ai_protocol = 0, // ???
	};
	struct addrinfo *info = NULL;
	HTTPConnectionRef conn = NULL;
	int rc;

	rc = async_getaddrinfo(host, service[0] ? service : "80", &hints, &info);
	if(rc < 0) goto cleanup;

	conn = calloc(1, sizeof(struct HTTPConnection));
	if(!conn) rc = UV_ENOMEM;
	if(rc < 0) goto cleanup;

	rc = UV_EADDRNOTAVAIL;
	for(struct addrinfo *each = info; each; each = each->ai_next) {
		rc = uv_tcp_init(async_loop, conn->stream);
		if(rc < 0) break;

		rc = async_tcp_connect(conn->stream, each->ai_addr);
		if(rc >= 0) break;

		async_close((uv_handle_t *)conn->stream);
	}
	if(rc < 0) goto cleanup;

	http_parser_init(conn->parser, HTTP_RESPONSE);
	conn->parser->data = conn;
	*out = conn; conn = NULL;

cleanup:
	uv_freeaddrinfo(info); info = NULL;
	HTTPConnectionFree(&conn);
	return rc;
}
void HTTPConnectionFree(HTTPConnectionRef *const connptr) {
	HTTPConnectionRef conn = *connptr;
	if(!conn) return;

	async_close((uv_handle_t *)conn->stream);

	// http_parser does not need to be freed, closed or destroyed.
	memset(conn->parser, 0, sizeof(*conn->parser));

	FREE(&conn->buf);
	*conn->raw = uv_buf_init(NULL, 0);

	conn->type = HTTPNothing;
	*conn->out = uv_buf_init(NULL, 0);

	conn->flags = 0;

	assert_zeroed(conn, 1);
	FREE(connptr); conn = NULL;
}
Exemple #17
0
static int GET_query(BlogRef const blog, SLNSessionRef const session, HTTPConnectionRef const conn, HTTPMethod const method, strarg_t const URI, HTTPHeadersRef const headers) {
	if(HTTP_GET != method && HTTP_HEAD != method) return -1;
	strarg_t qs = NULL;
	if(0 != uripathcmp("/", URI, &qs)) return -1;

	if(HTTP_HEAD == method) return 501; // TODO

	// TODO: This is the most complicated function in the whole program.
	// It's unbearable.

	str_t *query = NULL;
	str_t *query_HTMLSafe = NULL;
	str_t *parsed_HTMLSafe = NULL;
	SLNFilterRef filter = NULL;
	int rc;

	static strarg_t const fields[] = {
		"q",
	};
	str_t *values[numberof(fields)] = {};
	QSValuesParse(qs, values, fields, numberof(fields));
	query = values[0]; values[0] = NULL;
	query_HTMLSafe = htmlenc(values[0]);
	rc = SLNUserFilterParse(session, query, &filter);
	QSValuesCleanup(values, numberof(values));
	if(DB_EACCES == rc) {
		FREE(&query);
		FREE(&query_HTMLSafe);
		return 403;
	}
//	SLNFilterPrintSexp(filter, stderr, 0); // DEBUG
	if(DB_EINVAL == rc) rc = SLNFilterCreate(session, SLNVisibleFilterType, &filter);
	if(rc < 0) {
		FREE(&query);
		FREE(&query_HTMLSafe);
		return 500;
	}

	str_t tmp[URI_MAX]; tmp[0] = '\0'; // fmemopen shim ignores mode.
	FILE *parsed = fmemopen(tmp, sizeof(tmp), "w");
	if(!parsed) {
		FREE(&query);
		FREE(&query_HTMLSafe);
		return 500;
	}
	SLNFilterPrintUser(filter, parsed, 0);
	fclose(parsed);
	tmp[sizeof(tmp)-1] = '\0'; // fmemopen(3) says this isn't guaranteed.
	parsed_HTMLSafe = htmlenc(tmp);

	SLNFilterPosition pos[1] = {{ .dir = -1 }};
	str_t *URIs[RESULTS_MAX];
	uint64_t max = numberof(URIs);
	int outdir = -1;
	SLNFilterParseOptions(qs, pos, &max, &outdir, NULL);
	if(max < 1) max = 1;
	if(max > numberof(URIs)) max = numberof(URIs);
	bool const has_start = !!pos->URI;

	uint64_t const t1 = uv_hrtime();

	ssize_t const count = SLNFilterCopyURIs(filter, session, pos, outdir, false, URIs, (size_t)max);
	SLNFilterPositionCleanup(pos);
	if(count < 0) {
		FREE(&query);
		FREE(&query_HTMLSafe);
		FREE(&parsed_HTMLSafe);
		SLNFilterFree(&filter);
		if(DB_NOTFOUND == count) {
			// Possibly a filter age-function bug.
			alogf("Invalid start parameter? %s\n", URI);
			return 500;
		}
		alogf("Filter error: %s\n", sln_strerror(count));
		return 500;
	}
	SLNFilterFree(&filter);

	uint64_t const t2 = uv_hrtime();

	str_t *reponame_HTMLSafe = htmlenc(SLNRepoGetName(blog->repo));

	snprintf(tmp, sizeof(tmp), "Queried in %.6f seconds", (t2-t1) / 1e9);
	str_t *querytime_HTMLSafe = htmlenc(tmp);

	str_t *account_HTMLSafe;
	if(0 == SLNSessionGetUserID(session)) {
		account_HTMLSafe = htmlenc("Log In");
	} else {
		strarg_t const user = SLNSessionGetUsername(session);
		snprintf(tmp, sizeof(tmp), "Account: %s", user);
		account_HTMLSafe = htmlenc(tmp);
	}


	// TODO: Write a real function for building query strings
	// Don't use ?: GNUism
	// Preserve other query parameters like `dir`
	str_t *query_encoded = !query ? NULL : QSEscape(query, strlen(query), true);
	FREE(&query);

	str_t *firstpage_HTMLSafe = NULL;
	str_t *prevpage_HTMLSafe = NULL;
	str_t *nextpage_HTMLSafe = NULL;
	str_t *lastpage_HTMLSafe = NULL;
	snprintf(tmp, sizeof(tmp), "?q=%s&start=-", query_encoded ?: "");
	firstpage_HTMLSafe = htmlenc(tmp);
	str_t *p = !count ? NULL : URIs[outdir > 0 ? 0 : count-1];
	str_t *n = !count ? NULL : URIs[outdir > 0 ? count-1 : 0];
	if(p) p = QSEscape(p, strlen(p), 1);
	if(n) n = QSEscape(n, strlen(n), 1);
	snprintf(tmp, sizeof(tmp), "?q=%s&start=%s", query_encoded ?: "", p ?: "");
	prevpage_HTMLSafe = htmlenc(tmp);
	snprintf(tmp, sizeof(tmp), "?q=%s&start=-%s", query_encoded ?: "", n ?: "");
	nextpage_HTMLSafe = htmlenc(tmp);
	snprintf(tmp, sizeof(tmp), "?q=%s", query_encoded ?: "");
	lastpage_HTMLSafe = htmlenc(tmp);

	FREE(&query_encoded);
	FREE(&p);
	FREE(&n);

	str_t *qs_HTMLSafe = htmlenc(qs);

	TemplateStaticArg const args[] = {
		{"reponame", reponame_HTMLSafe},
		{"querytime", querytime_HTMLSafe},
		{"account", account_HTMLSafe},
		{"query", query_HTMLSafe},
		{"parsed", parsed_HTMLSafe},
		{"firstpage", firstpage_HTMLSafe},
		{"prevpage", prevpage_HTMLSafe},
		{"nextpage", nextpage_HTMLSafe},
		{"lastpage", lastpage_HTMLSafe},
		{"qs", qs_HTMLSafe},
		{NULL, NULL},
	};

	if(count > 0) {
		HTTPConnectionWriteResponse(conn, 200, "OK");
	} else {
		HTTPConnectionWriteResponse(conn, 404, "Not Found");
	}
	HTTPConnectionWriteHeader(conn, "Content-Type", "text/html; charset=utf-8");
	HTTPConnectionWriteHeader(conn, "Transfer-Encoding", "chunked");
	if(0 == SLNSessionGetUserID(session)) {
		HTTPConnectionWriteHeader(conn, "Cache-Control", "no-cache, public");
	} else {
		HTTPConnectionWriteHeader(conn, "Cache-Control", "no-cache, private");
	}
	HTTPConnectionBeginBody(conn);
	TemplateWriteHTTPChunk(blog->header, &TemplateStaticCBs, args, conn);

	if(0 == count) {
		TemplateWriteHTTPChunk(blog->noresults, &TemplateStaticCBs, args, conn);
	}
	for(size_t i = 0; i < count; i++) {
		str_t algo[SLN_ALGO_SIZE]; // SLN_INTERNAL_ALGO
		str_t hash[SLN_HASH_SIZE];
		SLNParseURI(URIs[i], algo, hash);
		str_t *previewPath = BlogCopyPreviewPath(blog, hash);
		rc = send_preview(blog, conn, session, URIs[i], previewPath);
		FREE(&previewPath);
		if(rc < 0) break;
	}

	// TODO: HACK
	// Hide the pagination buttons when there are less than one full page of results.
	if(count >= max || has_start) {
		TemplateWriteHTTPChunk(blog->footer, &TemplateStaticCBs, args, conn);
	}

	FREE(&reponame_HTMLSafe);
	FREE(&querytime_HTMLSafe);
	FREE(&account_HTMLSafe);
	FREE(&query_HTMLSafe);
	FREE(&parsed_HTMLSafe);
	FREE(&firstpage_HTMLSafe);
	FREE(&prevpage_HTMLSafe);
	FREE(&nextpage_HTMLSafe);
	FREE(&lastpage_HTMLSafe);
	FREE(&qs_HTMLSafe);

	HTTPConnectionWriteChunkEnd(conn);
	HTTPConnectionEnd(conn);

	for(size_t i = 0; i < count; i++) FREE(&URIs[i]);
	assert_zeroed(URIs, count);
	return 0;
}
Exemple #18
0
static void writer(SLNPullRef const pull) {
	SLNSubmissionRef queue[QUEUE_SIZE];
	size_t count = 0;
	size_t skipped = 0;
	double time = uv_now(async_loop) / 1000.0;
	for(;;) {
		if(pull->stop) goto stop;

		async_mutex_lock(pull->mutex);
		while(0 == count || (count < QUEUE_SIZE && pull->count > 0)) {
			size_t const pos = pull->cur;
			while(!pull->filled[pos]) {
				async_cond_wait(pull->cond, pull->mutex);
				if(pull->stop) {
					async_mutex_unlock(pull->mutex);
					goto stop;
				}
				if(!count) time = uv_now(async_loop) / 1000.0;
			}
			assert(pull->filled[pos]);
			// Skip any bubbles in the queue.
			if(pull->queue[pos]) queue[count++] = pull->queue[pos];
			else skipped++;
			pull->queue[pos] = NULL;
			pull->filled[pos] = false;
			pull->cur = (pull->cur + 1) % QUEUE_SIZE;
			pull->count--;
			async_cond_broadcast(pull->cond);
		}
		async_mutex_unlock(pull->mutex);
		assert(count <= QUEUE_SIZE);

		for(;;) {
			int rc = SLNSubmissionStoreBatch(queue, count);
			if(rc >= 0) break;
			alogf("Submission error: %s (%d)\n", sln_strerror(rc), rc);
			async_sleep(1000 * 5);
		}
		for(size_t i = 0; i < count; ++i) {
			SLNSubmissionFree(&queue[i]);
		}

		double const now = uv_now(async_loop) / 1000.0;
		alogf("Pulled %f files per second\n", count / (now - time));
		time = now;
		count = 0;
		skipped = 0;

	}

stop:
	for(size_t i = 0; i < count; ++i) {
		SLNSubmissionFree(&queue[i]);
	}
	assert_zeroed(queue, count);

	async_mutex_lock(pull->mutex);
	assertf(pull->stop, "Writer ended early");
	assert(pull->tasks > 0);
	pull->tasks--;
	async_cond_broadcast(pull->cond);
	async_mutex_unlock(pull->mutex);
}
Exemple #19
0
static int GET_query(BlogRef const blog, SLNSessionRef const session, HTTPConnectionRef const conn, HTTPMethod const method, strarg_t const URI, HTTPHeadersRef const headers) {
	if(HTTP_GET != method) return -1;
	strarg_t qs = NULL;
	if(!URIPath(URI, "/", &qs)) return -1;

	// TODO: This is the most complicated function in the whole program.
	// It's unbearable.

	str_t *query = NULL;
	str_t *query_HTMLSafe = NULL;
	SLNFilterRef filter = NULL;
	int rc;

	static strarg_t const fields[] = {
		"q",
	};
	str_t *values[numberof(fields)] = {};
	QSValuesParse(qs, values, fields, numberof(fields));
	query = values[0] ? strdup(values[0]) : NULL;
	query_HTMLSafe = htmlenc(values[0]);
	rc = SLNUserFilterParse(session, values[0], &filter);
	QSValuesCleanup(values, numberof(values));
	if(DB_EACCES == rc) {
		FREE(&query);
		FREE(&query_HTMLSafe);
		return 403;
	}
	if(DB_EINVAL == rc) rc = SLNFilterCreate(session, SLNVisibleFilterType, &filter);
	if(rc < 0) {
		FREE(&query);
		FREE(&query_HTMLSafe);
		return 500;
	}

	str_t tmp[URI_MAX];

	SLNFilterToUserFilterString(filter, tmp, sizeof(tmp), 0);
	str_t *parsed_HTMLSafe = htmlenc(tmp);

	str_t *primaryURI = NULL;
	SLNFilterRef core = SLNFilterUnwrap(filter);
	SLNFilterType const filtertype = SLNFilterGetType(core);
	if(SLNURIFilterType == filtertype) {
		primaryURI = strdup(SLNFilterGetStringArg(core, 0));
		assert(primaryURI); // TODO
		SLNFilterRef alt;
		rc = SLNFilterCreate(session, SLNLinksToFilterType, &alt);
		assert(rc >= 0); // TODO
		SLNFilterAddStringArg(alt, primaryURI, -1);
		SLNFilterFree(&filter);
		filter = alt; alt = NULL;
	}
	core = NULL;

//	SLNFilterPrint(filter, 0); // DEBUG

	SLNFilterPosition pos[1] = {{ .dir = -1 }};
	uint64_t max = RESULTS_MAX;
	int outdir = -1;
	SLNFilterParseOptions(qs, pos, &max, &outdir, NULL);
	if(max < 1) max = 1;
	if(max > RESULTS_MAX) max = RESULTS_MAX;

	uint64_t const t1 = uv_hrtime();

	str_t *URIs[RESULTS_MAX];
	ssize_t const count = SLNFilterCopyURIs(filter, session, pos, outdir, false, URIs, (size_t)max);
	SLNFilterPositionCleanup(pos);
	if(count < 0) {
		fprintf(stderr, "Filter error: %s\n", sln_strerror(count));
		FREE(&query);
		FREE(&query_HTMLSafe);
		SLNFilterFree(&filter);
		return 500;
	}
	SLNFilterFree(&filter);

	uint64_t const t2 = uv_hrtime();

	str_t *reponame_HTMLSafe = htmlenc(SLNRepoGetName(blog->repo));

	snprintf(tmp, sizeof(tmp), "Queried in %.3f seconds", (t2-t1) / 1e9);
	str_t *querytime_HTMLSafe = htmlenc(tmp);

	str_t *account_HTMLSafe;
	if(0 == SLNSessionGetUserID(session)) {
		account_HTMLSafe = htmlenc("Log In");
	} else {
		strarg_t const user = SLNSessionGetUsername(session);
		snprintf(tmp, sizeof(tmp), "Account: %s", user);
		account_HTMLSafe = htmlenc(tmp);
	}


	// TODO: Write a real function for building query strings
	// Don't use ?: GNUism
	// Preserve other query parameters like `dir`
	str_t *query_encoded = !query ? NULL : QSEscape(query, strlen(query), true);
	FREE(&query);

	str_t *firstpage_HTMLSafe = NULL;
	str_t *prevpage_HTMLSafe = NULL;
	str_t *nextpage_HTMLSafe = NULL;
	str_t *lastpage_HTMLSafe = NULL;
	snprintf(tmp, sizeof(tmp), "?q=%s&start=-", query_encoded ?: "");
	firstpage_HTMLSafe = htmlenc(tmp);
	str_t *p = !count ? NULL : URIs[outdir > 0 ? 0 : count-1];
	str_t *n = !count ? NULL : URIs[outdir > 0 ? count-1 : 0];
	if(p) p = QSEscape(p, strlen(p), 1);
	if(n) n = QSEscape(n, strlen(n), 1);
	snprintf(tmp, sizeof(tmp), "?q=%s&start=%s", query_encoded ?: "", p ?: "");
	prevpage_HTMLSafe = htmlenc(tmp);
	snprintf(tmp, sizeof(tmp), "?q=%s&start=-%s", query_encoded ?: "", n ?: "");
	nextpage_HTMLSafe = htmlenc(tmp);
	snprintf(tmp, sizeof(tmp), "?q=%s", query_encoded ?: "");
	lastpage_HTMLSafe = htmlenc(tmp);

	FREE(&query_encoded);
	FREE(&p);
	FREE(&n);

	TemplateStaticArg const args[] = {
		{"reponame", reponame_HTMLSafe},
		{"querytime", querytime_HTMLSafe},
		{"account", account_HTMLSafe},
		{"query", query_HTMLSafe},
		{"parsed", parsed_HTMLSafe},
		{"firstpage", firstpage_HTMLSafe},
		{"prevpage", prevpage_HTMLSafe},
		{"nextpage", nextpage_HTMLSafe},
		{"lastpage", lastpage_HTMLSafe},
		{NULL, NULL},
	};

	HTTPConnectionWriteResponse(conn, 200, "OK");
	HTTPConnectionWriteHeader(conn, "Content-Type", "text/html; charset=utf-8");
	HTTPConnectionWriteHeader(conn, "Transfer-Encoding", "chunked");
	if(0 == SLNSessionGetUserID(session)) {
		HTTPConnectionWriteHeader(conn, "Cache-Control", "no-cache, public");
	} else {
		HTTPConnectionWriteHeader(conn, "Cache-Control", "no-cache, private");
	}
	HTTPConnectionBeginBody(conn);
	TemplateWriteHTTPChunk(blog->header, &TemplateStaticCBs, args, conn);

	if(primaryURI) {
		SLNFileInfo info[1];
		rc = SLNSessionGetFileInfo(session, primaryURI, info);
		if(rc >= 0) {
			str_t *preferredURI = SLNFormatURI(SLN_INTERNAL_ALGO, info->hash);
			str_t *previewPath = BlogCopyPreviewPath(blog, info->hash);
			send_preview(blog, conn, session, preferredURI, previewPath);
			FREE(&preferredURI);
			FREE(&previewPath);
			SLNFileInfoCleanup(info);
		} else if(DB_NOTFOUND == rc) {
			TemplateWriteHTTPChunk(blog->notfound, &TemplateStaticCBs, args, conn);
		}
		if(count) {
			TemplateWriteHTTPChunk(blog->backlinks, &TemplateStaticCBs, args, conn);
		}
	}

	bool const broadest_possible_filter =
		SLNVisibleFilterType == filtertype ||
		SLNAllFilterType == filtertype;
	if(0 == count && !primaryURI && !broadest_possible_filter) {
		TemplateWriteHTTPChunk(blog->noresults, &TemplateStaticCBs, args, conn);
	}
	for(size_t i = 0; i < count; i++) {
		str_t algo[SLN_ALGO_SIZE]; // SLN_INTERNAL_ALGO
		str_t hash[SLN_HASH_SIZE];
		SLNParseURI(URIs[i], algo, hash);
		str_t *previewPath = BlogCopyPreviewPath(blog, hash);
		rc = send_preview(blog, conn, session, URIs[i], previewPath);
		FREE(&previewPath);
		if(rc < 0) break;
	}

	FREE(&primaryURI);

	TemplateWriteHTTPChunk(blog->footer, &TemplateStaticCBs, args, conn);
	FREE(&reponame_HTMLSafe);
	FREE(&querytime_HTMLSafe);
	FREE(&account_HTMLSafe);
	FREE(&query_HTMLSafe);
	FREE(&parsed_HTMLSafe);
	FREE(&firstpage_HTMLSafe);
	FREE(&prevpage_HTMLSafe);
	FREE(&nextpage_HTMLSafe);
	FREE(&lastpage_HTMLSafe);

	HTTPConnectionWriteChunkEnd(conn);
	HTTPConnectionEnd(conn);

	for(size_t i = 0; i < count; i++) FREE(&URIs[i]);
	assert_zeroed(URIs, count);
	return 0;
}