From ebc9808211bef8ff109939b86f51d307ab5d0457 Mon Sep 17 00:00:00 2001 From: SunnyQeen Date: Thu, 4 Jun 2026 18:30:59 +0200 Subject: [PATCH] support plugin --- .github/workflows/payload.yml | 8 +- Makefile.pc | 3 +- Makefile.ps5 | 2 +- README.md | 37 +++ assets/index.html | 8 + assets/main.css | 19 +- plugin/demo/Makefile | 32 +++ plugin/demo/demo.c | 184 +++++++++++++++ src/plugin.c | 427 ++++++++++++++++++++++++++++++++++ src/plugin.h | 40 ++++ src/plugin_api.h | 160 +++++++++++++ src/websrv.c | 13 ++ 12 files changed, 929 insertions(+), 4 deletions(-) create mode 100644 plugin/demo/Makefile create mode 100644 plugin/demo/demo.c create mode 100644 src/plugin.c create mode 100644 src/plugin.h create mode 100644 src/plugin_api.h diff --git a/.github/workflows/payload.yml b/.github/workflows/payload.yml index 2967132..2fbb43d 100644 --- a/.github/workflows/payload.yml +++ b/.github/workflows/payload.yml @@ -28,11 +28,17 @@ jobs: run: | make clean all PS5_PAYLOAD_SDK=/opt/ps5-payload-sdk + - name: Build demo plugin + run: | + cd plugin/demo && make clean all PS5_PAYLOAD_SDK=/opt/ps5-payload-sdk + - name: Upload uses: actions/upload-artifact@v4 with: name: Payload - path: websrv-ps5.elf + path: | + websrv-ps5.elf + plugin/demo/demo.so if-no-files-found: error release: diff --git a/Makefile.pc b/Makefile.pc index 681f58b..cc300dd 100644 --- a/Makefile.pc +++ b/Makefile.pc @@ -19,11 +19,12 @@ VERSION_TAG := $(shell git describe --abbrev=10 --dirty --always --tags) PYTHON ?= python3 BIN := websrv.pc -SRCS := src/main.c src/websrv.c src/asset.c src/fs.c src/mime.c +SRCS := src/main.c src/websrv.c src/asset.c src/fs.c src/mime.c src/plugin.c SRCS += src/pc/sys.c CFLAGS := -Wall -DVERSION_TAG=\"$(VERSION_TAG)\" LDADD += `pkg-config libmicrohttpd --libs` +LDADD += -ldl ASSETS := $(wildcard assets/*) GEN_SRCS := $(patsubst assets/%,gen/%, $(ASSETS:=.c)) diff --git a/Makefile.ps5 b/Makefile.ps5 index 8fc2bcb..427f4e7 100644 --- a/Makefile.ps5 +++ b/Makefile.ps5 @@ -29,7 +29,7 @@ PYTHON ?= python3 BIN := websrv-ps5.elf -SRCS := src/main.c src/websrv.c src/asset.c src/fs.c src/mime.c +SRCS := src/main.c src/websrv.c src/asset.c src/fs.c src/mime.c src/plugin.c SRCS += src/mdns.c src/smb.c SRCS += src/ps5/sys.c src/ps5/pt.c src/ps5/elfldr.c src/ps5/hbldr.c SRCS += src/ps5/notify.c src/ps5/http.c diff --git a/README.md b/README.md index d2c0128..41d6c5a 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,8 @@ Examples: - http://ps5:8080/smb?addr=192.168.1.1 - List shares on a remote SMB host (json) - http://ps5:8080/smb/share?addr=192.168.1.1 - List files and folders shared by a remote SMB host (json) - http://ps5:8080/smb/share/file?addr=192.168.1.1 - Download a remote SMB file via websrv +- http://ps5:8080/plugin - List loaded native plugins (html) +- http://ps5:8080/plugin/demo - Example native plugin (if installed) ## Installing Homebrew The web server will search for homebrew in /data/homebrew, /mnt/usb%d/homebrew, /mnt/ext%d/homebrew, @@ -48,6 +50,41 @@ For real-world homebrew, checkout: - https://github.com/ps5-payload-dev/websrv/releases - https://github.com/cy33hc/ps5-ezremote-client +## Native plugins +websrv can load native plugins from shared objects (`.so` files) placed under +`/data/homebrew/websrv/plugin` (and the same `homebrew/websrv/plugin` path on +USB/external volumes). Each plugin is a shared object named `.so` and +must export: + +- `_plugin_register_url()` — returns the URL prefix without a leading + slash, e.g. `"plugin/demo"` (serves `http://ps5:8080/plugin/demo` and + sub-paths). +- `_plugin_handle_request()` — handles HTTP requests routed to that + prefix. Receives a per-request `plugin_context_t` (`void*`, the host + `MHD_Connection`), parsed POST fields as a `plugin_post_data_t` list (NULL for + GET/HEAD), and a `plugin_response_fn` callback. Call `respond(ctx, resp)` to + send a response (the host copies the body before the callback returns); both + `respond` and `handle_request` return **0 on success**. Return non-zero from + `handle_request` if the request is not handled. Release per-request buffers + after `respond` returns. Plugins need only [src/plugin_api.h](src/plugin_api.h), + not libmicrohttpd. + +For `demo.so`, the symbols are `demo_plugin_register_url` and +`demo_plugin_handle_request`. + +See [src/plugin_api.h](src/plugin_api.h) and the example in +[plugin/demo](plugin/demo). Build the demo plugin with: + +```console +john@localhost:websrv/plugin/demo$ export PS5_PAYLOAD_SDK=/opt/ps5-payload-sdk +john@localhost:websrv/plugin/demo$ make +john@localhost:websrv/plugin/demo$ mkdir -p /data/homebrew/websrv/plugin/demo +john@localhost:websrv/plugin/demo$ cp demo.so /data/homebrew/websrv/plugin/demo/ +``` + +Restart or reload websrv; matching requests are dispatched to the plugin +instead of the built-in static assets. + ## Building Assuming you have the [packbrew][packbrew] SDK installed on a Debian-flavored operating system, the payload can be compiled using the following commands: diff --git a/assets/index.html b/assets/index.html index c4578bb..605e986 100644 --- a/assets/index.html +++ b/assets/index.html @@ -125,6 +125,14 @@ +
+

+ Filesystem + · + Plugins +

+
+ diff --git a/assets/main.css b/assets/main.css index 76cf74e..1eecd92 100644 --- a/assets/main.css +++ b/assets/main.css @@ -84,8 +84,14 @@ body { } .header { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 100; text-align: center; - margin-top: 2rem; + margin-top: 0.75rem; + pointer-events: none; } .header * { @@ -93,6 +99,17 @@ body { padding: 0; } +.header a { + pointer-events: auto; + color: var(--secondary-text-color); + text-decoration: none; +} + +.header a:hover { + color: var(--text-color); + text-decoration: underline; +} + div.carousel { padding: 0 512px; height: 100vh; diff --git a/plugin/demo/Makefile b/plugin/demo/Makefile new file mode 100644 index 0000000..83e83c6 --- /dev/null +++ b/plugin/demo/Makefile @@ -0,0 +1,32 @@ +# Copyright (C) 2025 John Törnblom +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. + +ifdef PS5_PAYLOAD_SDK + include $(PS5_PAYLOAD_SDK)/toolchain/prospero.mk +else + CC ?= gcc +endif + +WEBSRV_SRC := ../../src +CFLAGS += -I$(WEBSRV_SRC) -fPIC -shared -Wall -Werror +PLUGIN := demo.so + +all: $(PLUGIN) + +install: $(PLUGIN) + mkdir -p ../../plugin/demo + @if ! cmp -s $(PLUGIN) ../../plugin/demo/$(PLUGIN) 2>/dev/null; then \ + cp -f $(PLUGIN) ../../plugin/demo/; \ + fi + +$(PLUGIN): demo.c + $(CC) $(CFLAGS) -o $@ $^ + +clean: + rm -f $(PLUGIN) + +.PHONY: all install clean diff --git a/plugin/demo/demo.c b/plugin/demo/demo.c new file mode 100644 index 0000000..be1d81b --- /dev/null +++ b/plugin/demo/demo.c @@ -0,0 +1,184 @@ +/* Copyright (C) 2025 John Törnblom + +This program is free software; you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by the +Free Software Foundation; either version 3, or (at your option) any +later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; see the file COPYING. If not, see +. */ + +#include +#include +#include +#include + +#include "plugin_api.h" + + +#define DEMO_CTX_SLOTS 64 + + +static const char PAGE[] = + "websrv plugin demo" + "" + "

plugin/demo

" + "
" + "" + "" + "
" + ""; + +static const char BODY_429[] = "Too Many Requests"; + + +typedef struct demo_request_state { + plugin_context_t ctx; + plugin_response_header_t hdr; + plugin_response_data_t resp; + char json_body[4096]; +} demo_request_state_t; + + +static demo_request_state_t g_demo_state[DEMO_CTX_SLOTS]; + + +static demo_request_state_t* +demo_state_for(plugin_context_t ctx) { + size_t i; + demo_request_state_t* free_slot = 0; + + for(i=0; ictx = ctx; + return free_slot; +} + + +static void +demo_state_release(plugin_context_t ctx) { + size_t i; + + for(i=0; ihdr.key = "Content-Type"; + state->hdr.val = mime; + state->hdr.next = 0; + + state->resp.status = status; + state->resp.headers = &state->hdr; + state->resp.body = body; + state->resp.body_len = body_len; + return &state->resp; +} + + +static plugin_response_data_t* +demo_too_many_requests(void) { + static plugin_response_header_t hdr; + static plugin_response_data_t resp; + + hdr.key = "Content-Type"; + hdr.val = "text/plain"; + hdr.next = 0; + resp.status = PLUGIN_HTTP_TOO_MANY_REQUESTS; + resp.headers = &hdr; + resp.body = (const uint8_t*)BODY_429; + resp.body_len = sizeof(BODY_429) - 1; + return &resp; +} + + +const char* +demo_plugin_register_url(void) { + return "plugin/demo"; +} + + +int +demo_plugin_handle_request(plugin_context_t ctx, const char* url, + const char* method, const plugin_post_data_t* post, + plugin_response_fn respond) { + demo_request_state_t* state; + size_t len; + int first; + int ret; + + (void)url; + + state = demo_state_for(ctx); + if(!state) { + return respond(ctx, demo_too_many_requests()); + } + + if(!strcmp(method, PLUGIN_METHOD_GET) || + !strcmp(method, PLUGIN_METHOD_HEAD)) { + ret = respond(ctx, demo_build_response(state, PLUGIN_HTTP_OK, "text/html", + (const uint8_t*)PAGE, + sizeof(PAGE) - 1)); + demo_state_release(ctx); + return ret; + } + + if(!strcmp(method, PLUGIN_METHOD_POST)) { + first = 1; + len = snprintf(state->json_body, sizeof(state->json_body), + "{\"ctx\":%" PRId64 ",\"fields\":{", + (int64_t)(intptr_t)ctx); + for(; post && len < sizeof(state->json_body) - 64; post=post->next) { + if(!first) { + len += snprintf(state->json_body + len, + sizeof(state->json_body) - len, ","); + } + first = 0; + len += snprintf(state->json_body + len, + sizeof(state->json_body) - len, + "\"%s\":\"", post->key); + if(post->val && post->len) { + len += snprintf(state->json_body + len, + sizeof(state->json_body) - len, "%.*s", + (int)post->len, (const char*)post->val); + } + len += snprintf(state->json_body + len, + sizeof(state->json_body) - len, "\""); + } + len += snprintf(state->json_body + len, + sizeof(state->json_body) - len, "}}"); + ret = respond(ctx, demo_build_response(state, PLUGIN_HTTP_OK, + "application/json", + (const uint8_t*)state->json_body, + len)); + demo_state_release(ctx); + return ret; + } + + demo_state_release(ctx); + return 1; +} diff --git a/src/plugin.c b/src/plugin.c new file mode 100644 index 0000000..0380806 --- /dev/null +++ b/src/plugin.c @@ -0,0 +1,427 @@ +/* Copyright (C) 2025 John Törnblom + +This program is free software; you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by the +Free Software Foundation; either version 3, or (at your option) any +later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; see the file COPYING. If not, see +. */ + +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include "plugin.h" +#include "plugin_api.h" +#include "websrv.h" + + +typedef const char* (*plugin_register_url_fn)(void); +typedef int (*plugin_handle_request_fn)(plugin_context_t ctx, + const char* url, + const char* method, + const plugin_post_data_t* post, + plugin_response_fn respond); + + +static enum MHD_Result +plugin_queue_response(struct MHD_Connection* conn, + const plugin_response_data_t* resp) { + struct MHD_Response* mresp; + plugin_response_header_t* hdr; + enum MHD_Result ret = MHD_NO; + + if(!resp || resp->status <= 0) { + return MHD_NO; + } + + if(!(mresp=MHD_create_response_from_buffer(resp->body_len, (void*)resp->body, + MHD_RESPMEM_MUST_COPY))) { + return MHD_NO; + } + + for(hdr=resp->headers; hdr; hdr=hdr->next) { + if(hdr->key && hdr->val) { + MHD_add_response_header(mresp, hdr->key, hdr->val); + } + } + + ret = websrv_queue_response(conn, (unsigned int)resp->status, mresp); + MHD_destroy_response(mresp); + return ret; +} + + +static int +plugin_respond(plugin_context_t ctx, plugin_response_data_t* resp) { + struct MHD_Connection* conn = (struct MHD_Connection*)ctx; + + if(!resp) { + return -1; + } + + return plugin_queue_response(conn, resp) == MHD_YES ? 0 : -1; +} + + +typedef struct plugin_entry { + void* handle; + char* path; + char* soname; + char* url_prefix; + plugin_handle_request_fn handle_request; + struct plugin_entry* next; +} plugin_entry_t; + + +static plugin_entry_t* g_plugins = 0; + + +static const char* const plugin_scan_roots[] = { + "plugin", + "/data/homebrew/websrv/plugin", + "/mnt/usb0/homebrew/websrv/plugin", + "/mnt/usb1/homebrew/websrv/plugin", + "/mnt/usb2/homebrew/websrv/plugin", + "/mnt/usb3/homebrew/websrv/plugin", + "/mnt/usb4/homebrew/websrv/plugin", + "/mnt/usb5/homebrew/websrv/plugin", + "/mnt/usb6/homebrew/websrv/plugin", + "/mnt/usb7/homebrew/websrv/plugin", + "/mnt/ext0/homebrew/websrv/plugin", + "/mnt/ext1/homebrew/websrv/plugin", +}; + + +static char* +plugin_normalize_url_prefix(const char* url) { + const char* start = url; + char* out; + size_t len; + + while(*start == '/') { + start++; + } + + if(!*start) { + return 0; + } + + len = strlen(start); + while(len > 0 && start[len - 1] == '/') { + len--; + } + + if(!len) { + return 0; + } + + out = malloc(len + 2); + if(!out) { + return 0; + } + + out[0] = '/'; + memcpy(out + 1, start, len); + out[len + 1] = '\0'; + return out; +} + + +static int +plugin_is_so_file(const char* name) { + size_t len = strlen(name); + + if(len < 4) { + return 0; + } + + return !strcmp(name + len - 3, ".so"); +} + + +static char* +plugin_soname_from_basename(const char* name) { + size_t len = strlen(name); + char* soname; + + if(len < 4 || strcmp(name + len - 3, ".so")) { + return 0; + } + + len -= 3; + soname = malloc(len + 1); + if(!soname) { + return 0; + } + + memcpy(soname, name, len); + soname[len] = '\0'; + return soname; +} + + +static int +plugin_url_matches(const char* prefix, const char* url) { + size_t len = strlen(prefix); + + if(strncmp(url, prefix, len)) { + return 0; + } + + return !url[len] || url[len] == '/'; +} + + +static void +plugin_try_load(const char* path, const char* basename) { + void* handle; + plugin_register_url_fn register_url; + plugin_handle_request_fn handle_request; + const char* raw_url; + char* url_prefix; + char* soname; + char sym_register[256]; + char sym_handle[256]; + plugin_entry_t* entry; + + soname = plugin_soname_from_basename(basename); + if(!soname) { + fprintf(stderr, "plugin: %s: invalid plugin filename (expected .so)\n", + path); + return; + } + + for(entry=g_plugins; entry; entry=entry->next) { + if(!strcmp(entry->soname, soname)) { + fprintf(stderr, "plugin: %s: %s.so already loaded from %s\n", + path, soname, entry->path); + free(soname); + return; + } + } + + snprintf(sym_register, sizeof(sym_register), "%s_plugin_register_url", soname); + snprintf(sym_handle, sizeof(sym_handle), "%s_plugin_handle_request", soname); + + handle = dlopen(path, RTLD_NOW | RTLD_LOCAL); + if(!handle) { + fprintf(stderr, "plugin: dlopen(%s): %s\n", path, dlerror()); + free(soname); + return; + } + + register_url = (plugin_register_url_fn)dlsym(handle, sym_register); + if(!register_url) { + fprintf(stderr, "plugin: %s: missing %s: %s\n", path, sym_register, dlerror()); + dlclose(handle); + free(soname); + return; + } + + handle_request = (plugin_handle_request_fn)dlsym(handle, sym_handle); + if(!handle_request) { + fprintf(stderr, "plugin: %s: missing %s: %s\n", path, sym_handle, dlerror()); + dlclose(handle); + free(soname); + return; + } + + raw_url = register_url(); + if(!raw_url || !*raw_url) { + fprintf(stderr, "plugin: %s: %s returned empty string\n", path, sym_register); + dlclose(handle); + free(soname); + return; + } + + url_prefix = plugin_normalize_url_prefix(raw_url); + if(!url_prefix) { + fprintf(stderr, "plugin: %s: invalid URL from %s: %s\n", + path, sym_register, raw_url); + dlclose(handle); + free(soname); + return; + } + + for(entry=g_plugins; entry; entry=entry->next) { + if(!strcmp(entry->url_prefix, url_prefix)) { + fprintf(stderr, "plugin: %s: URL %s already registered by %s\n", + path, url_prefix, entry->path); + free(url_prefix); + free(soname); + dlclose(handle); + return; + } + } + + entry = calloc(1, sizeof(plugin_entry_t)); + if(!entry) { + free(url_prefix); + free(soname); + dlclose(handle); + return; + } + + entry->handle = handle; + entry->path = strdup(path); + entry->soname = soname; + entry->url_prefix = url_prefix; + entry->handle_request = handle_request; + entry->next = g_plugins; + g_plugins = entry; + + printf("plugin: loaded %s at %s\n", path, url_prefix); +} + + +static void +plugin_scan_dir(const char* dir) { + DIR* dp; + struct dirent* ent; + char path[PATH_MAX]; + struct stat st; + + dp = opendir(dir); + if(!dp) { + return; + } + + while((ent=readdir(dp))) { + if(!strcmp(ent->d_name, ".") || !strcmp(ent->d_name, "..")) { + continue; + } + + snprintf(path, sizeof(path), "%s/%s", dir, ent->d_name); + + if(stat(path, &st) < 0) { + continue; + } + + if(S_ISDIR(st.st_mode)) { + plugin_scan_dir(path); + continue; + } + + if(S_ISREG(st.st_mode) && plugin_is_so_file(ent->d_name)) { + plugin_try_load(path, ent->d_name); + } + } + + closedir(dp); +} + + +void +plugin_unload_all(void) { + plugin_entry_t* entry; + plugin_entry_t* next; + + for(entry=g_plugins; entry; entry=next) { + next = entry->next; + dlclose(entry->handle); + free(entry->path); + free(entry->soname); + free(entry->url_prefix); + free(entry); + } + + g_plugins = 0; +} + + +void +plugin_load_all(void) { + size_t i; + + plugin_unload_all(); + + for(i=0; inext) { + len = strlen(entry->url_prefix); + if(plugin_url_matches(entry->url_prefix, url) && len >= best) { + match = entry; + best = len; + } + } + + if(!match) { + return MHD_NO; + } + + return match->handle_request((plugin_context_t)conn, url, method, post, + plugin_respond) == 0 ? MHD_YES : MHD_NO; +} + + +enum MHD_Result +plugin_list_request(struct MHD_Connection* conn) { + plugin_entry_t* entry; + char* body; + size_t cap = 4096; + size_t len = 0; + enum MHD_Result ret = MHD_NO; + struct MHD_Response* resp; + + body = malloc(cap); + if(!body) { + return MHD_NO; + } + + len += snprintf(body + len, cap - len, + "" + "Plugins" + "

Plugins

    "); + + for(entry=g_plugins; entry; entry=entry->next) { + const char* label = entry->url_prefix + 1; + int n = snprintf(body + len, cap - len, + "
  • %s
  • ", + entry->url_prefix, label); + + if(n < 0 || (size_t)n >= cap - len) { + free(body); + return MHD_NO; + } + len += (size_t)n; + } + + len += snprintf(body + len, cap - len, "
"); + + if((resp=MHD_create_response_from_buffer(len, body, MHD_RESPMEM_MUST_FREE))) { + MHD_add_response_header(resp, MHD_HTTP_HEADER_CONTENT_TYPE, "text/html"); + ret = websrv_queue_response(conn, MHD_HTTP_OK, resp); + MHD_destroy_response(resp); + } else { + free(body); + } + + return ret; +} diff --git a/src/plugin.h b/src/plugin.h new file mode 100644 index 0000000..dfba660 --- /dev/null +++ b/src/plugin.h @@ -0,0 +1,40 @@ +/* Copyright (C) 2025 John Törnblom + +This program is free software; you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by the +Free Software Foundation; either version 3, or (at your option) any +later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; see the file COPYING. If not, see +. */ + +#pragma once + +#include + +#include "plugin_api.h" + + +void plugin_load_all(void); + +void plugin_unload_all(void); + +/** + * Dispatch @p url to a loaded plugin when it matches a registered prefix. + * @return MHD_YES if a plugin handled the request, MHD_NO otherwise. + */ +enum MHD_Result plugin_request(struct MHD_Connection* conn, + const char* url, + const char* method, + const plugin_post_data_t* post); + +/** + * Respond to GET /plugin with an HTML list of loaded plugins. + */ +enum MHD_Result plugin_list_request(struct MHD_Connection* conn); diff --git a/src/plugin_api.h b/src/plugin_api.h new file mode 100644 index 0000000..cf4b0fc --- /dev/null +++ b/src/plugin_api.h @@ -0,0 +1,160 @@ +/* Copyright (C) 2025 John Törnblom + +This program is free software; you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by the +Free Software Foundation; either version 3, or (at your option) any +later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; see the file COPYING. If not, see +. */ + +#pragma once + +#include +#include + +/** + * @file plugin_api.h + * + * Native plugin API for websrv. Plugins are shared objects named `.so`. + * The loader resolves these exported symbols (example for `demo.so`): + * + * demo_plugin_register_url + * demo_plugin_handle_request + * + * Plugins need only this header; libmicrohttpd is linked in the host. + * + * Return convention: **0 = OK**, non-zero = error or not handled. + */ + +/** + * Per-request context. In the host this is `struct MHD_Connection *`, cast to + * `void *`. Each concurrent request has its own value; use it to key per-request + * state. Valid for the duration of `_plugin_handle_request`. + */ +typedef void* plugin_context_t; + +/* 2xx success */ +#define PLUGIN_HTTP_OK 200 +#define PLUGIN_HTTP_CREATED 201 +#define PLUGIN_HTTP_ACCEPTED 202 +#define PLUGIN_HTTP_NO_CONTENT 204 + +/* 3xx redirection */ +#define PLUGIN_HTTP_MOVED_PERMANENTLY 301 +#define PLUGIN_HTTP_FOUND 302 +#define PLUGIN_HTTP_SEE_OTHER 303 +#define PLUGIN_HTTP_NOT_MODIFIED 304 + +/* 4xx client error */ +#define PLUGIN_HTTP_BAD_REQUEST 400 +#define PLUGIN_HTTP_UNAUTHORIZED 401 +#define PLUGIN_HTTP_FORBIDDEN 403 +#define PLUGIN_HTTP_NOT_FOUND 404 +#define PLUGIN_HTTP_METHOD_NOT_ALLOWED 405 +#define PLUGIN_HTTP_NOT_ACCEPTABLE 406 +#define PLUGIN_HTTP_CONFLICT 409 +#define PLUGIN_HTTP_GONE 410 +#define PLUGIN_HTTP_PAYLOAD_TOO_LARGE 413 +#define PLUGIN_HTTP_UNSUPPORTED_MEDIA_TYPE 415 +#define PLUGIN_HTTP_TOO_MANY_REQUESTS 429 + +/* 5xx server error */ +#define PLUGIN_HTTP_INTERNAL_SERVER_ERROR 500 +#define PLUGIN_HTTP_NOT_IMPLEMENTED 501 +#define PLUGIN_HTTP_BAD_GATEWAY 502 +#define PLUGIN_HTTP_SERVICE_UNAVAILABLE 503 +#define PLUGIN_HTTP_GATEWAY_TIMEOUT 504 + +/** HTTP method strings passed to `_plugin_handle_request`. */ +#define PLUGIN_METHOD_GET "GET" +#define PLUGIN_METHOD_POST "POST" +#define PLUGIN_METHOD_HEAD "HEAD" + +/** + * One field from a parsed POST body (`application/x-www-form-urlencoded` or + * `multipart/form-data`). Fields are a singly linked list via @p next. + */ +typedef struct plugin_post_data { + const char* key; /**< field name */ + const uint8_t* val; /**< field value (not necessarily NUL-terminated) */ + size_t len; /**< length of @p val in bytes */ + struct plugin_post_data* next; /**< next field, or NULL */ +} plugin_post_data_t; + +/** + * One HTTP response header. Headers are a singly linked list via @p next. + */ +typedef struct plugin_response_header { + const char* key; /**< header name, e.g. `"Content-Type"` */ + const char* val; /**< header value, e.g. `"text/html"` */ + struct plugin_response_header* next; /**< next header, or NULL */ +} plugin_response_header_t; + +/** + * HTTP response descriptor passed to @p respond. Use PLUGIN_HTTP_* for @p status. + * @p body and header strings must remain valid until @p respond returns (the host + * copies the body before the callback returns). + */ +typedef struct plugin_response_data { + int status; /**< HTTP status code (e.g. PLUGIN_HTTP_OK) */ + plugin_response_header_t* headers; /**< response headers, or NULL */ + const uint8_t* body; /**< response body, or NULL for empty body */ + size_t body_len; /**< length of @p body in bytes */ +} plugin_response_data_t; + +/** + * Host-provided callback to queue an HTTP response. + * + * @param ctx Request context (`plugin_context_t` for this connection). + * @param resp Response to send; may be stack- or slot-allocated in the plugin. + * @return 0 on success, non-zero on failure. + */ +typedef int (*plugin_response_fn)(plugin_context_t ctx, + plugin_response_data_t* resp); + +/** + * `_plugin_register_url` — return the URL prefix owned by this plugin. + * + * @return Prefix without a leading slash, e.g. `"plugin/demo"` serves + * `/plugin/demo` and sub-paths. Must remain valid for the lifetime of + * the loaded plugin (typically a string literal). + * + * Example: + * @code + * const char *demo_plugin_register_url(void) { + * return "plugin/demo"; + * } + * @endcode + */ + +/** + * `_plugin_handle_request` — handle a request routed to this plugin. + * + * @param ctx Per-request context; use to key temporary state. + * @param url Request path with leading slash, e.g. `"/plugin/demo/page"`. + * @param method HTTP method (`PLUGIN_METHOD_GET`, `PLUGIN_METHOD_POST`, …). + * @param post Parsed POST form fields, or NULL for GET/HEAD and POST with + * no body fields. + * @param respond Host callback; call with the response when handled. + * @return 0 if the request was handled and @p respond succeeded; non-zero if + * the request is not handled or @p respond failed. Release per-request + * buffers after @p respond returns. + * + * Example: + * @code + * int demo_plugin_handle_request(plugin_context_t ctx, const char *url, + * const char *method, + * const plugin_post_data_t *post, + * plugin_response_fn respond) { + * plugin_response_data_t resp = { .status = PLUGIN_HTTP_OK, ... }; + * return respond(ctx, &resp); + * } + * @endcode + */ diff --git a/src/websrv.c b/src/websrv.c index 1fcd368..fa96c81 100644 --- a/src/websrv.c +++ b/src/websrv.c @@ -28,6 +28,7 @@ along with this program; see the file COPYING. If not, see #include "asset.h" #include "fs.h" #include "mdns.h" +#include "plugin.h" #include "smb.h" #include "sys.h" #include "version.h" @@ -332,9 +333,15 @@ websrv_on_request(void *cls, struct MHD_Connection *conn, if(!strcmp("/version", url)) { return version_request(conn); } + if(!strcmp("/plugin", url) || !strcmp("/plugin/", url)) { + return plugin_list_request(conn); + } if(!strcmp("/", url) || !url[0]) { return asset_request(conn, "/index.html"); } + if(plugin_request(conn, url, method, 0) == MHD_YES) { + return MHD_YES; + } return asset_request(conn, url); } @@ -347,6 +354,10 @@ websrv_on_request(void *cls, struct MHD_Connection *conn, if(!strcmp("/elfldr", url)) { return elfldr_request(conn, req->data); } + if(plugin_request(conn, url, method, + (const plugin_post_data_t*)req->data) == MHD_YES) { + return MHD_YES; + } } return MHD_NO; @@ -387,6 +398,8 @@ websrv_listen(unsigned short port) { signal(SIGPIPE, SIG_IGN); + plugin_load_all(); + if((srvfd=socket(AF_INET, SOCK_STREAM, 0)) < 0) { perror("socket"); return -1;