commit 3c072d9e64faaf80517ede4e0e26f68d3ccd39d1
Author: Santtu Lakkala <santtu.lakkala@unikie.com>
Date:   Fri, 22 Nov 2024 13:48:54 +0200
Initial import
Diffstat:
| A | Makefile | | | 48 | ++++++++++++++++++++++++++++++++++++++++++++++++ | 
| A | buf.c | | | 11 | +++++++++++ | 
| A | buf.h | | | 13 | +++++++++++++ | 
| A | cpu.c | | | 43 | +++++++++++++++++++++++++++++++++++++++++++ | 
| A | cpu.h | | | 25 | +++++++++++++++++++++++++ | 
| A | http.c | | | 116 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | 
| A | http.h | | | 15 | +++++++++++++++ | 
| A | json.c | | | 291 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | 
| A | json.h | | | 35 | +++++++++++++++++++++++++++++++++++ | 
| A | mem.c | | | 44 | ++++++++++++++++++++++++++++++++++++++++++++ | 
| A | mem.h | | | 24 | ++++++++++++++++++++++++ | 
| A | minitls.h | | | 121 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | 
| A | net.c | | | 128 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | 
| A | net.h | | | 47 | +++++++++++++++++++++++++++++++++++++++++++++++ | 
| A | plong.c | | | 64 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | 
| A | util.h | | | 11 | +++++++++++ | 
16 files changed, 1036 insertions(+), 0 deletions(-)
diff --git a/Makefile b/Makefile
@@ -0,0 +1,48 @@
+.POSIX:
+SOURCES = plong.c buf.c json.c http.c cpu.c mem.c net.c
+HEADERS = buf.h json.h http.h cpu.h mem.h net.h minitls.h
+OBJS = $(SOURCES:.c=.o)
+PROGRAM = plong
+LDFLAGS = -lssl -lcrypto
+CFLAGS = -W -Wall -std=c99 -D_POSIX_C_SOURCE=200809L -fsanitize=address,undefined -g
+
+all: $(PROGRAM)
+
+$(PROGRAM): $(OBJS)
+	$(CC) $(CFLAGS) -o $@ $(OBJS) $(LDFLAGS)
+
+.c.o:
+	$(CC) $(CFLAGS) -c $< -o $@
+
+clean:
+	rm -f $(OBJS)
+
+depend:
+	for i in ${HEADERS} ${SOURCES}; do echo "$$i"; done | sort | uniq | while read i; do \
+		sed -ne 's!^# *include *"\([^"]*\)".*!'"$$(echo "$$i" | sed -e 's/\.c$$/\.o/')"': '"$$(dirname "$$i" | sed -ne 's!^[^.].*!&/!; T; p; ')"'\1!; T; p' <"$$i"; \
+	done | sort
+
+.PHONY: depend all
+
+buf.o: buf.h
+cpu.h: json.h
+cpu.o: cpu.h
+cpu.o: util.h
+http.h: buf.h
+http.h: minitls.h
+http.o: buf.h
+http.o: http.h
+http.o: minitls.h
+json.h: buf.h
+json.o: json.h
+mem.h: json.h
+mem.o: mem.h
+net.h: json.h
+net.o: net.h
+net.o: util.h
+plong.o: cpu.h
+plong.o: http.h
+plong.o: json.h
+plong.o: mem.h
+plong.o: minitls.h
+plong.o: net.h
diff --git a/buf.c b/buf.c
@@ -0,0 +1,11 @@
+#include <errno.h>
+#include "buf.h"
+
+int buf_char(struct buf *b, char c)
+{
+	if (b->data >= b->end)
+		return ENOMEM;
+	*b->data++ = c;
+	return 0;
+}
+
diff --git a/buf.h b/buf.h
@@ -0,0 +1,13 @@
+#ifndef PLONG_BUF_H
+#define PLONG_BUF_H
+
+#define BUF_FROM_ARR(a) (struct buf){ .data = a, .end = 1[&a] }
+
+struct buf {
+	char *data;
+	char *end;
+};
+
+int buf_char(struct buf *b, char c);
+
+#endif
diff --git a/cpu.c b/cpu.c
@@ -0,0 +1,43 @@
+#include <stdio.h>
+#include <errno.h>
+#include "cpu.h"
+#include "util.h"
+
+static int _cpu_read(struct cpu *c)
+{
+	FILE *f = fopen("/proc/stat", "r");
+	if (!f)
+		return errno;
+	if (fscanf(f, "cpu" UINT_SCAN(CPU_FIELDS)
+#define X(a) , &c->a
+		   CPU_FIELDS(X)
+#undef X
+	   ) < COUNT(CPU_FIELDS)) {
+		fclose(f);
+		return ENODATA;
+	}
+	fclose(f);
+
+	return 0;
+}
+
+int cpu_init(struct cpu *c)
+{
+	return _cpu_read(c);
+}
+
+int cpu_poll(struct cpu *c, struct json *j)
+{
+	int r;
+	struct cpu c0 = *c;
+	if ((r = _cpu_read(c)))
+		return r;
+#define X(a) \
+	if ((r = json_obj_key(j, #a))) \
+		return r; \
+	if ((r = json_int(j, c->a - c0.a))) \
+		return r;
+	CPU_FIELDS(X)
+#undef X
+	return 0;
+}
diff --git a/cpu.h b/cpu.h
@@ -0,0 +1,25 @@
+#ifndef PLONG_CPU_H
+#define PLONG_CPU_H
+
+#include <stdint.h>
+#include "json.h"
+
+#define CPU_FIELDS(x) \
+	x(user) \
+	x(nice) \
+	x(system) \
+	x(idle) \
+	x(iowait) \
+	x(irq) \
+	x(softirq)
+
+struct cpu {
+#define X(f) uintmax_t f;
+CPU_FIELDS(X)
+#undef X
+};
+
+int cpu_init(struct cpu *c);
+int cpu_poll(struct cpu *c, struct json *j);
+
+#endif
diff --git a/http.c b/http.c
@@ -0,0 +1,116 @@
+#include <unistd.h>
+#include <stdio.h>
+#include <netdb.h>
+#include <errno.h>
+#include <sys/poll.h>
+
+#include "http.h"
+#include "minitls.h"
+#include "buf.h"
+
+int http_post(const char *server,
+	      const char *port,
+	      const char *path,
+	      const struct buf *b,
+	      struct buf *reply,
+	      int *af,
+	      struct tls *ctx)
+{
+	char buffer[1024];
+	struct addrinfo *res;
+	struct addrinfo *i;
+	struct addrinfo hints = { .ai_family = *af, .ai_socktype = SOCK_STREAM };
+	ssize_t r;
+	ssize_t s;
+	int sock;
+
+	if ((r = getaddrinfo(server, port, &hints, &res)))
+		return r;
+
+	r = ENOENT;
+	for (i = res; i; i = i->ai_next) {
+		sock = socket(i->ai_family, i->ai_socktype, i->ai_protocol);
+
+		*af = i->ai_family;
+		if (!(r = connect(sock, i->ai_addr, i->ai_addrlen)))
+			break;
+		close(sock);
+	}
+
+	freeaddrinfo(res);
+	if (!i)
+		return r;
+
+	if ((r = snprintf(buffer, sizeof(buffer),
+			  "POST %s HTTP/1.0\r\nHost: %s\r\nContent-Type: application/json\r\nContent-Length: %zu\r\n\r\n",
+			  path, server, b->end - b->data)) >= (int)sizeof(buffer)) {
+		close(sock);
+		return ENOMEM;
+	}
+
+	if (ctx) {
+		if (tls_connect_socket(ctx, sock, server)) {
+			close(sock);
+			return -1;
+		}
+
+		if ((s = tls_write(ctx, buffer, r)) < r) {
+			tls_close(ctx);
+			tls_reset(ctx);
+			tls_configure(ctx, NULL);
+			close(sock);
+			return -1;
+		}
+	} else if ((s = write(sock, buffer, r)) < r) {
+		close(sock);
+		return -1;
+	}
+
+	if (ctx) {
+		if ((s = tls_write(ctx, b->data, b->end - b->data)) < b->end - b->data) {
+			tls_close(ctx);
+			tls_reset(ctx);
+			tls_configure(ctx, NULL);
+			close(sock);
+			return -1;
+		}
+	} else if ((s = write(sock, b->data, b->end - b->data)) < b->end - b->data) {
+		close(sock);
+		return -1;
+	}
+
+	if (reply) {
+		do {
+			if (ctx)
+				r = tls_read(ctx, reply->data, reply->end - reply->data);
+			else
+				r = read(sock, reply->data, reply->end - reply->data);
+			if (r < 0) {
+				if (ctx) {
+					if (r == TLS_WANT_POLLIN) {
+						poll(&(struct pollfd){ .fd = sock, .events = POLLIN }, 1, 0);
+						continue;
+					}
+
+					tls_close(ctx);
+					tls_reset(ctx);
+					tls_configure(ctx, NULL);
+				}
+				close(sock);
+				return -1;
+			}
+			reply->data += r;
+		} while (r && reply->data < reply->end);
+	}
+
+	if (ctx) {
+		tls_close(ctx);
+		tls_reset(ctx);
+		tls_configure(ctx, NULL);
+	}
+
+	close(sock);
+
+	return s;
+}
+
diff --git a/http.h b/http.h
@@ -0,0 +1,15 @@
+#ifndef PLONG_HTTP_H
+#define PLONG_HTTP_H
+
+#include "minitls.h"
+#include "buf.h"
+
+int http_post(const char *server,
+	      const char *port,
+	      const char *path,
+	      const struct buf *b,
+	      struct buf *reply,
+	      int *af,
+	      struct tls *ctx);
+
+#endif
diff --git a/json.c b/json.c
@@ -0,0 +1,291 @@
+#include <errno.h>
+#include <stdio.h>
+#include <string.h>
+#include "json.h"
+
+enum mode {
+	MODE_NONE = 0,
+	MODE_OBJECT_FIRST,
+	MODE_OBJECT,
+	MODE_OBJECT_VALUE,
+	MODE_ARRAY_FIRST,
+	MODE_ARRAY,
+};
+
+static enum mode _json_mode(struct json *j)
+{
+	if (j->depth == 0)
+		return MODE_NONE;
+	return j->modes[j->depth - 1];
+}
+
+static int _json_kw(struct json *j, const char *value, size_t vl)
+{
+	int r;
+
+	switch (_json_mode(j)) {
+	case MODE_ARRAY:
+		if ((r = buf_char(&j->b, ',')))
+			return r;
+		break;
+
+	case MODE_ARRAY_FIRST:
+		j->modes[j->depth - 1] = MODE_ARRAY;
+		break;
+
+	case MODE_OBJECT_VALUE:
+		j->modes[j->depth - 1] = MODE_OBJECT;
+		break;
+
+	default:
+		return EINVAL;
+	}
+
+	if (j->b.data + vl >= j->b.end)
+		return ENOMEM;
+	memcpy(j->b.data, value, vl);
+	j->b.data += vl;
+
+	return 0;
+}
+
+int _json_string(struct json *j, const char *value)
+{
+	int r;
+
+	switch (_json_mode(j)) {
+	case MODE_ARRAY:
+	case MODE_OBJECT:
+		if ((r = buf_char(&j->b, ',')))
+			return r;
+		break;
+
+	case MODE_ARRAY_FIRST:
+		j->modes[j->depth - 1] = MODE_ARRAY;
+		break;
+
+	case MODE_OBJECT_FIRST:
+	case MODE_OBJECT_VALUE:
+		break;
+
+	default:
+		return EINVAL;
+	}
+
+	if ((r = buf_char(&j->b, '"')))
+		return r;
+	for (; *value; value++) {
+		char c;
+		switch (*value) {
+		case '\r':
+			c = 'r';
+			break;
+		case '\n':
+			c = 'n';
+			break;
+		case '\b':
+			c = 'b';
+			break;
+		case '\f':
+			c = 'f';
+			break;
+
+		case '\\':
+		case '"':
+			c = *value;
+			break;
+
+		default:
+			if ((r = buf_char(&j->b, *value)))
+				return ENOMEM;
+			continue;
+		}
+		if ((r = buf_char(&j->b, '\\')) ||
+		    (r = buf_char(&j->b, c)))
+			return ENOMEM;
+	}
+
+	return buf_char(&j->b, '"');
+}
+
+int json_init(struct json *j, struct buf *b)
+{
+	j->b = *b;
+	j->buffer = b->data;
+	j->depth = 0;
+
+	return 0;
+}
+
+int json_obj_open(struct json *j)
+{
+	int r;
+
+	switch (_json_mode(j)) {
+	case MODE_OBJECT_FIRST:
+	case MODE_OBJECT:
+		return EINVAL;
+
+	case MODE_ARRAY:
+		if ((r = buf_char(&j->b, ',')))
+			return r;
+		break;
+
+	default:
+		break;
+	}
+
+	if (&j->modes[j->depth] == 1[&j->modes])
+		return ENOMEM;
+	j->modes[j->depth++] = MODE_OBJECT_FIRST;
+	return buf_char(&j->b, '{');
+}
+
+int json_obj_close(struct json *j)
+{
+	switch (_json_mode(j)) {
+	case MODE_OBJECT_FIRST:
+	case MODE_OBJECT:
+		break;
+
+	default:
+		return EINVAL;
+	}
+
+	j->depth--;
+	if (_json_mode(j) == MODE_OBJECT_VALUE)
+		j->modes[j->depth - 1] = MODE_OBJECT;
+	return buf_char(&j->b, '}');
+}
+
+int json_string(struct json *j, const char *value)
+{
+	int r;
+
+	switch (_json_mode(j)) {
+	case MODE_OBJECT_FIRST:
+	case MODE_OBJECT:
+		return EINVAL;
+
+	default:
+		break;
+	}
+
+	if ((r = _json_string(j, value)))
+		return r;
+	if (_json_mode(j) == MODE_OBJECT_VALUE)
+		j->modes[j->depth - 1] = MODE_OBJECT;
+	return 0;
+}
+
+int json_obj_key(struct json *j, const char *key)
+{
+	int r;
+
+	switch (_json_mode(j)) {
+	case MODE_OBJECT:
+	case MODE_OBJECT_FIRST:
+		break;
+
+	default:
+		return EINVAL;
+	}
+
+	if ((r = _json_string(j, key)))
+		return r;
+	j->modes[j->depth - 1] = MODE_OBJECT_VALUE;
+	return buf_char(&j->b, ':');
+}
+
+int json_arr_open(struct json *j) {
+	int r;
+
+	switch (_json_mode(j)) {
+	case MODE_OBJECT_FIRST:
+	case MODE_OBJECT:
+		return EINVAL;
+
+	case MODE_ARRAY:
+		if ((r = buf_char(&j->b, ',')))
+			return r;
+		break;
+
+	default:
+		break;
+	}
+
+	if (&j->modes[j->depth] == 1[&j->modes])
+		return ENOMEM;
+	j->modes[j->depth++] = MODE_ARRAY_FIRST;
+	return buf_char(&j->b, '[');
+}
+
+int json_arr_close(struct json *j)
+{
+	switch (_json_mode(j)) {
+	case MODE_ARRAY_FIRST:
+	case MODE_ARRAY:
+		break;
+
+	default:
+		return EINVAL;
+	}
+
+	j->depth--;
+	if (_json_mode(j) == MODE_OBJECT_VALUE)
+		j->modes[j->depth - 1] = MODE_OBJECT;
+	return buf_char(&j->b, ']');
+}
+
+int json_bool(struct json *j, int v)
+{
+	if (v)
+		return _json_kw(j, "true", sizeof("true") - 1);
+	return _json_kw(j, "false", sizeof("false") - 1);
+}
+
+int json_null(struct json *j)
+{
+	return _json_kw(j, "null", sizeof("null") - 1);
+}
+
+int json_int(struct json *j, intmax_t val)
+{
+	int r;
+
+	switch (_json_mode(j)) {
+	case MODE_ARRAY:
+		if ((r = buf_char(&j->b, ',')))
+			return r;
+		break;
+
+	case MODE_ARRAY_FIRST:
+		j->modes[j->depth - 1] = MODE_ARRAY;
+		break;
+
+	case MODE_OBJECT_VALUE:
+		j->modes[j->depth - 1] = MODE_OBJECT;
+		break;
+
+	default:
+		return EINVAL;
+	}
+
+	r = snprintf(j->b.data, j->b.end - j->b.data, "%jd", val);
+	if (j->b.data + r >= j->b.end - 1)
+		return ENOMEM;
+	j->b.data += r;
+
+	return 0;
+}
+
+int json_fini(struct json *j, struct buf *b)
+{
+	if (j->depth != 0)
+		return EINVAL;
+	b->data = j->buffer;
+	b->end = j->b.data;
+
+	return 0;
+}
+
diff --git a/json.h b/json.h
@@ -0,0 +1,35 @@
+#ifndef PLONG_JSON_H
+#define PLONG_JSON_H
+
+#include <stdint.h>
+#include <stdlib.h>
+
+#include "buf.h"
+
+#define JSON_MAX_DEPTH 16
+
+struct json {
+	char *buffer;
+	struct buf b;
+	int modes[JSON_MAX_DEPTH];
+	size_t depth;
+};
+
+int json_init(struct json *j, struct buf *b);
+int json_fini(struct json *j, struct buf *b);
+
+int json_obj_open(struct json *j);
+int json_obj_key(struct json *j, const char *key);
+int json_obj_close(struct json *j);
+
+int json_arr_open(struct json *j);
+int json_arr_close(struct json *j);
+
+int json_bool(struct json *j, int v);
+int json_null(struct json *j);
+
+int json_int(struct json *j, intmax_t val);
+
+int json_string(struct json *j, const char *value);
+
+#endif
diff --git a/mem.c b/mem.c
@@ -0,0 +1,44 @@
+#include <stdio.h>
+#include <errno.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdint.h>
+#include "mem.h"
+
+int mem_init(struct mem *m)
+{
+	(void)m;
+	return 0;
+}
+
+int mem_poll(struct mem *m, struct json *j)
+{
+	int r = 0;
+	char line[256];
+	uintmax_t val;
+	FILE *f = fopen("/proc/meminfo", "r");
+
+	(void)m;
+
+	if (!f)
+		return errno;
+	while (fgets(line, sizeof(line), f)) {
+#define X(a, b) \
+		if (!memcmp(line, #a ":", sizeof(#a ":") - 1)) { \
+			if (sscanf(line + sizeof(#a ":") - 1, "%ju", &val) != 1) { \
+				r = ENODATA; \
+				goto out; \
+			} \
+			if ((r = json_obj_key(j, #b))) \
+				goto out; \
+			if ((r = json_int(j, val))) \
+				goto out; \
+			continue; \
+		}
+		MEM_FIELDS(X)
+#undef X
+	}
+out:
+	fclose(f);
+	return 0;
+}
diff --git a/mem.h b/mem.h
@@ -0,0 +1,24 @@
+#ifndef PLONG_MEM_H
+#define PLONG_MEM_H
+
+#include <stdint.h>
+#include "json.h"
+
+#define MEM_FIELDS(x) \
+	x(MemTotal, total) \
+	x(MemFree, free) \
+	x(MemAvailable, available) \
+	x(Buffers, buffers) \
+	x(Cached, cached) \
+	x(SwapCached, swapcached) \
+	x(SwapTotal, swaptotal) \
+	x(SwapFree, swapfree)
+
+struct mem {
+	int dummy;
+};
+
+int mem_init(struct mem *c);
+int mem_poll(struct mem *c, struct json *j);
+
+#endif
diff --git a/minitls.h b/minitls.h
@@ -0,0 +1,121 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2023 Santtu Lakkala
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+ * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+#ifndef MINITLS_H
+#define MINITLS_H
+#include <openssl/ssl.h>
+#include <stdbool.h>
+
+#define TLS_WANT_POLLIN -2
+#define TLS_WANT_POLLOUT -3
+
+struct tls {
+	SSL_CTX *ctx;
+	SSL *ssl;
+	int err;
+};
+
+static inline struct tls *tls_client(void)
+{
+	static bool ready = false;
+	const SSL_METHOD *method;
+	struct tls *rv = malloc(sizeof(*rv));
+
+	if (!rv)
+		return NULL;
+
+	if (!ready) {
+		SSL_library_init();
+		OpenSSL_add_all_algorithms();
+		ready = true;
+	}
+
+	method = TLS_client_method();
+	rv->ctx = SSL_CTX_new(method);
+	rv->ssl = NULL;
+	SSL_CTX_set_default_verify_paths(rv->ctx);
+
+	return rv;
+}
+
+static inline ssize_t tls_read(struct tls *ctx, void *buf, size_t buflen)
+{
+	ssize_t r = SSL_read(ctx->ssl, buf, buflen);
+	if (r >= 0)
+		return r;
+	ctx->err = r;
+	r = SSL_get_error(ctx->ssl, r);
+	if (r == SSL_ERROR_WANT_READ)
+		return TLS_WANT_POLLIN;
+	if (r == SSL_ERROR_WANT_WRITE)
+		return TLS_WANT_POLLOUT;
+	return -1;
+}
+
+static inline ssize_t tls_write(struct tls *ctx, const void *buf, size_t buflen)
+{
+	ssize_t r = SSL_write(ctx->ssl, buf, buflen);
+	if (r >= 0)
+		return r;
+	ctx->err = r;
+	r = SSL_get_error(ctx->ssl, r);
+	if (r == SSL_ERROR_WANT_READ)
+		return TLS_WANT_POLLIN;
+	if (r == SSL_ERROR_WANT_WRITE)
+		return TLS_WANT_POLLOUT;
+	return -1;
+}
+
+static inline void tls_reset(struct tls *ctx)
+{
+	SSL_free(ctx->ssl);
+	ctx->ssl = NULL;
+}
+
+static inline int tls_connect_socket(struct tls *ctx, int fd, const char *servername)
+{
+	if (!(ctx->ssl = SSL_new(ctx->ctx)))
+		return -1;
+
+	SSL_set_fd(ctx->ssl, fd);
+	SSL_set_tlsext_host_name(ctx->ssl, servername);
+	if ((ctx->err = SSL_connect(ctx->ssl)) != 1)
+		return -1;
+	return 0;
+}
+
+static inline int tls_close(struct tls *ctx)
+{
+	SSL_shutdown(ctx->ssl);
+	return 0;
+}
+
+static inline void tls_free(struct tls *ctx)
+{
+	SSL_CTX_free(ctx->ctx);
+	free(ctx);
+}
+
+#define tls_configure(x, y)
+
+#endif
diff --git a/net.c b/net.c
@@ -0,0 +1,128 @@
+#include <stdio.h>
+#include <errno.h>
+#include <string.h>
+#include <net/if.h>
+#include "util.h"
+#include "net.h"
+
+#define _STRINGIFY(x) #x
+#define STRINGIFY(x) _STRINGIFY(x)
+
+static int _net_read(struct net *n, FILE *f, char *linebuf, size_t bsz)
+{
+	size_t i;
+	char namebuf[IF_NAMESIZE + 1];
+
+	for (i = 0; fgets(linebuf, bsz, f); i++) {
+		if (sscanf(linebuf, " %" STRINGIFY(IF_NAMESIZE) "[^:]:"
+			   UINT_SCAN(NET_FIELDS), namebuf
+#define X(d, na) , &n->ifaces[i].d ## na
+			   NET_FIELDS(X)
+#undef X
+		    ) < COUNT(NET_FIELDS))
+			return ENODATA;
+		if (!(n->ifaces[i].ifidx = if_nametoindex(namebuf)))
+			i--;
+	}
+	n->n = i;
+
+	clock_gettime(CLOCK_REALTIME, &n->stamp);
+
+	return 0;
+}
+
+int net_init(struct net *n)
+{
+	char linebuf[512];
+	char *i;
+	FILE *f = fopen("/proc/net/dev", "r");
+	int r = ENODATA;
+
+	if (!f)
+		return errno;
+	if (!fgets(linebuf, sizeof(linebuf), f) ||
+	    !fgets(linebuf, sizeof(linebuf), f))
+		goto out;
+	if (!*(i = linebuf + strcspn(linebuf, "|")))
+		goto out;
+	i++;
+#define X(d, n) \
+	if (memcmp(i, #n, sizeof(#n) - 1)) \
+		goto out; \
+	i += sizeof(#n) - 1; \
+	if (*i != ' ' && *i != '|' && *i != '\n') \
+		goto out; \
+	i += strspn(i, " ");
+
+	NET_FIELDS_RX(X)
+	
+	if (*i++ != '|')
+		goto out;
+
+	NET_FIELDS_TX(X)
+#undef X
+
+	if (*i != '\n')
+		goto out;
+	r = _net_read(n, f, linebuf, sizeof(linebuf));
+out:
+	fclose(f);
+	return r;
+}
+
+int net_poll(struct net *n, struct json *j)
+{
+	int r = ENODATA;
+	FILE *f = fopen("/proc/net/dev", "r");
+	char linebuf[512];
+	struct net n0 = *n;
+	size_t oi;
+	size_t ni;
+
+	if (!f)
+		return errno;
+	if (!fgets(linebuf, sizeof(linebuf), f) ||
+	    !fgets(linebuf, sizeof(linebuf), f))
+		goto out;
+
+	if ((r = _net_read(n, f, linebuf, sizeof(linebuf))))
+		goto out;
+
+	for (oi = 0, ni = 0; ni < n->n; ni++) {
+		char namebuf[IF_NAMESIZE + 1];
+		while (oi < n0.n && n0.ifaces[oi].ifidx < n->ifaces[ni].ifidx)
+			oi++;
+		if (!if_indextoname(n->ifaces[ni].ifidx, namebuf))
+			continue;
+		if ((r = json_obj_key(j, "milliseconds")))
+			goto out;
+		if ((r = json_int(j, ((intmax_t)n->stamp.tv_sec - n0.stamp.tv_sec) * 1000 + ((intmax_t)n->stamp.tv_nsec - n0.stamp.tv_nsec) / 1000000)))
+			goto out;
+		if ((r = json_obj_key(j, namebuf)))
+			goto out;
+		if ((r = json_obj_open(j)))
+			goto out;
+		if (oi < n0.n && n->ifaces[ni].ifidx == n0.ifaces[oi].ifidx) {
+#define X(d, na) \
+			if ((r = json_obj_key(j, #d #na))) \
+				goto out; \
+			if ((r = json_int(j, n->ifaces[ni].d ## na - n0.ifaces[oi].d ## na))) \
+				goto out;
+			NET_FIELDS(X)
+#undef X
+		} else {
+#define X(d, na) \
+			if ((r = json_obj_key(j, #d #na))) \
+				goto out; \
+			if ((r = json_int(j, 0))) \
+				goto out;
+			NET_FIELDS(X)
+#undef X
+		}
+		if ((r = json_obj_close(j)))
+			goto out;
+	}
+out:
+	fclose(f);
+	return r;
+}
diff --git a/net.h b/net.h
@@ -0,0 +1,47 @@
+#ifndef PLONG_NET_H
+#define PLONG_NET_H
+
+#include <stdint.h>
+#include <net/if.h>
+#include <time.h>
+#include "json.h"
+
+#define IF_MAX 32
+
+#define NET_FIELDS_RX(x) \
+	x(rx, bytes) \
+	x(rx, packets) \
+	x(rx, errs) \
+	x(rx, drop) \
+	x(rx, fifo) \
+	x(rx, frame) \
+	x(rx, compressed) \
+	x(rx, multicast)
+#define NET_FIELDS_TX(x) \
+	x(tx, bytes) \
+	x(tx, packets) \
+	x(tx, errs) \
+	x(tx, drop) \
+	x(tx, fifo) \
+	x(tx, colls) \
+	x(tx, carrier) \
+	x(tx, compressed)
+#define NET_FIELDS(x) \
+	NET_FIELDS_RX(x) \
+	NET_FIELDS_TX(x)
+
+struct net {
+	struct timespec stamp;
+	struct {
+		unsigned int ifidx;
+#define X(d, n) uintmax_t d ## n;
+NET_FIELDS(X)
+#undef X
+	} ifaces[IF_MAX];
+	size_t n;
+};
+
+int net_init(struct net *n);
+int net_poll(struct net *n, struct json *j);
+
+#endif
diff --git a/plong.c b/plong.c
@@ -0,0 +1,64 @@
+#include <stdio.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <unistd.h>
+
+#include "minitls.h"
+#include "json.h"
+#include "http.h"
+#include "cpu.h"
+#include "mem.h"
+#include "net.h"
+
+int main(int argc, char **argv)
+{
+	char buffer[4096];
+	struct buf b = { buffer, 1[&buffer] };
+	struct buf bjson = b;
+	struct buf breply = b;
+	struct json j;
+	struct tls *ctx;
+
+	struct cpu c;
+	struct mem m;
+	struct net n;
+
+#define E(v) do { int r = v; if (r) { fprintf(stderr, "%s failed: %s\n", #v, strerror(r)); }} while (0)
+	E(net_init(&n));
+	E(cpu_init(&c));
+	E(mem_init(&m));
+
+	sleep(5);
+
+	E(json_init(&j, &b));
+	E(json_obj_open(&j));
+
+	E(json_obj_key(&j, "cpu"));
+	E(json_obj_open(&j));
+	E(cpu_poll(&c, &j));
+	E(json_obj_close(&j));
+
+	E(json_obj_key(&j, "mem"));
+	E(json_obj_open(&j));
+	E(mem_poll(&m, &j));
+	E(json_obj_close(&j));
+
+	E(json_obj_key(&j, "net"));
+	E(json_obj_open(&j));
+	E(net_poll(&n, &j));
+	E(json_obj_close(&j));
+
+	E(json_obj_close(&j));
+
+	E(json_fini(&j, &bjson));
+
+	ctx = tls_client();
+	http_post("inz.fi", "443", "/plop.php", &bjson, &breply, &(int){ AF_UNSPEC }, ctx);
+
+	fwrite(b.data, breply.data - b.data, 1, stdout);
+	puts("");
+
+	tls_free(ctx);
+
+	return 0;
+}
diff --git a/util.h b/util.h
@@ -0,0 +1,11 @@
+#ifndef UTIL_H
+#define UTIL_H
+
+#define UINTFMT(...) "%ju"
+#define UINT_SCAN(X) X(UINTFMT)
+#define INTFMT(...) "%jd"
+#define INT_SCAN(X) X(INTFMT)
+#define COUNTER(...) + 1
+#define COUNT(X) (0 X(COUNTER))
+
+#endif