Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .github/workflows/payload.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
3 changes: 2 additions & 1 deletion Makefile.pc
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
2 changes: 1 addition & 1 deletion Makefile.ps5
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 `<soname>.so` and
must export:

- `<soname>_plugin_register_url()` — returns the URL prefix without a leading
slash, e.g. `"plugin/demo"` (serves `http://ps5:8080/plugin/demo` and
sub-paths).
- `<soname>_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:
Expand Down
8 changes: 8 additions & 0 deletions assets/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,14 @@
</symbol>
</svg>

<header class="header">
<p>
<a href="/fs/">Filesystem</a>
&middot;
<a href="/plugin">Plugins</a>
</p>
</header>

<!-- router relies on the div with id="content" -->
<!-- <div class="container" id="content">
</div> -->
Expand Down
19 changes: 18 additions & 1 deletion assets/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -84,15 +84,32 @@ 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 * {
margin: 0;
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;
Expand Down
32 changes: 32 additions & 0 deletions plugin/demo/Makefile
Original file line number Diff line number Diff line change
@@ -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
184 changes: 184 additions & 0 deletions plugin/demo/demo.c
Original file line number Diff line number Diff line change
@@ -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
<http://www.gnu.org/licenses/>. */

#include <inttypes.h>
#include <stdio.h>
#include <stdint.h>
#include <string.h>

#include "plugin_api.h"


#define DEMO_CTX_SLOTS 64


static const char PAGE[] =
"<html><head><title>websrv plugin demo</title></head>"
"<body>"
"<h1>plugin/demo</h1>"
"<form method=\"POST\">"
"<label>msg <input name=\"msg\" value=\"hello\"></label>"
"<button type=\"submit\">POST</button>"
"</form>"
"</body></html>";

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; i<DEMO_CTX_SLOTS; i++) {
if(g_demo_state[i].ctx == ctx) {
return &g_demo_state[i];
}
if(!free_slot && !g_demo_state[i].ctx) {
free_slot = &g_demo_state[i];
}
}

if(!free_slot) {
return 0;
}

free_slot->ctx = ctx;
return free_slot;
}


static void
demo_state_release(plugin_context_t ctx) {
size_t i;

for(i=0; i<DEMO_CTX_SLOTS; i++) {
if(g_demo_state[i].ctx == ctx) {
memset(&g_demo_state[i], 0, sizeof(g_demo_state[i]));
return;
}
}
}


static plugin_response_data_t*
demo_build_response(demo_request_state_t* state, int status,
const char* mime, const uint8_t* body, size_t body_len) {
state->hdr.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;
}
Loading
Loading