Reticulum-Go/bindings/java/examples/pageserver/Main.java

179 lines
7.7 KiB
Java

// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2024-2026 Quad4.io
//
// NomadNet-style pageserver over Java librns bindings.
// Usage: java Main -c config [-i identity] [-a announce_sec] [-p page_file]
import io.quad4.rns.Destination;
import io.quad4.rns.Event;
import io.quad4.rns.Identity;
import io.quad4.rns.Link;
import io.quad4.rns.Node;
import io.quad4.rns.Rns;
import io.quad4.rns.RnsException;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Arrays;
public final class Main {
private static final int DEFAULT_ANNOUNCE_SEC = 900;
private static final String DEFAULT_PAGE_PATH = "/page/index.mu";
private static final String DEFAULT_FILE_PATH = "/file/test.txt";
private static final String DEFAULT_PAGE_FILE = "pages/index.mu";
private static final String DEFAULT_FILE_FILE = "files/test.txt";
private static final String DEFAULT_IDENTITY_PATH = "identity";
private static final int REQ_DATA_CAP = 64 * 1024;
private static final String FALLBACK_PAGE =
"> Java pageserver\n\nlibrns via Reticulum-Go\n\nFallback page (file not found).\n\n";
private static final String FALLBACK_FILE = "Test file from Reticulum-Go node!\n";
public static void main(String[] args) throws Exception {
String configPath = null;
String identityPath = DEFAULT_IDENTITY_PATH;
int announceSec = DEFAULT_ANNOUNCE_SEC;
String pageFile = DEFAULT_PAGE_FILE;
String fileFile = DEFAULT_FILE_FILE;
String requestPath = DEFAULT_PAGE_PATH;
for (int i = 0; i < args.length; i++) {
if ("-c".equals(args[i]) && i + 1 < args.length) {
configPath = args[++i];
} else if ("-i".equals(args[i]) && i + 1 < args.length) {
identityPath = args[++i];
} else if ("-a".equals(args[i]) && i + 1 < args.length) {
announceSec = Integer.parseInt(args[++i]);
} else if ("-p".equals(args[i]) && i + 1 < args.length) {
pageFile = args[++i];
} else if ("-f".equals(args[i]) && i + 1 < args.length) {
fileFile = args[++i];
} else if ("-P".equals(args[i]) && i + 1 < args.length) {
requestPath = args[++i];
} else {
die("unexpected argument: " + args[i]);
}
}
if (configPath == null || announceSec < 0) {
die("usage: java Main -c config [options]");
}
if (!Rns.API_VERSION.equals(Rns.version())) {
die("librns version mismatch: got " + Rns.version());
}
byte[] pageBody = loadBytes(pageFile, FALLBACK_PAGE);
byte[] fileBody = loadBytes(fileFile, FALLBACK_FILE);
try (Node node = Node.create(configPath);
Identity identity = loadOrCreateIdentity(identityPath);
Destination dest =
Destination.create(node, null, "nomadnetwork", Arrays.asList("node"), true)) {
node.setIdentity(identity);
node.start();
dest.registerRequestHandler(requestPath);
dest.registerRequestHandler(DEFAULT_FILE_PATH);
String destHex = Rns.hashToHex(dest.hash());
System.out.println("DEST_HASH=" + destHex);
System.out.println("REQUEST_PATH=" + requestPath);
System.out.println("FILE_PATH=" + DEFAULT_FILE_PATH);
System.err.println("librns " + Rns.version() + " pageserver listening as nomadnetwork.node");
byte[] appData = "librns-java-pageserver".getBytes(StandardCharsets.UTF_8);
try {
dest.announce(appData);
System.err.println("announce sent");
} catch (RnsException e) {
printLastError("destination.announce failed");
}
long lastAnnounce = System.currentTimeMillis();
while (true) {
if (announceSec > 0 && System.currentTimeMillis() - lastAnnounce >= announceSec * 1000L) {
try {
dest.announce(appData);
System.err.println("announce refreshed");
} catch (RnsException ignored) {
// continue
}
lastAnnounce = System.currentTimeMillis();
}
try {
Event ev = Event.poll(node, 200, REQ_DATA_CAP);
if (ev.kind() == Event.LINK_ESTABLISHED) {
System.err.println("inbound link established");
} else if (ev.kind() == Event.LINK_CLOSED) {
System.err.println("link closed");
} else if (ev.kind() == Event.REQUEST_INCOMING) {
String path = ev.path();
System.err.println("request incoming path=" + path);
byte[] reqId = ev.requestId();
if (requestPath.equals(path)) {
try {
Link.requestRespond(node, reqId, pageBody);
System.err.println("served " + requestPath + " (" + pageBody.length + " bytes)");
} catch (RnsException e) {
printLastError("request_respond failed");
}
} else if (DEFAULT_FILE_PATH.equals(path)) {
try {
Link.requestRespondFile(node, reqId, "test.txt", fileBody);
System.err.println(
"served " + DEFAULT_FILE_PATH + " (" + fileBody.length + " bytes)");
} catch (RnsException e) {
printLastError("request_respond_file failed");
}
} else {
try {
Link.requestRespond(node, reqId, "page not found\n".getBytes(StandardCharsets.UTF_8));
} catch (RnsException e) {
printLastError("request_respond failed");
}
}
}
} catch (RnsException e) {
if (e.getCode() != RnsException.TIMEOUT) {
printLastError("Event.poll failed");
System.exit(1);
}
}
}
}
}
private static byte[] loadBytes(String path, String fallback) {
try {
return Files.readAllBytes(Paths.get(path));
} catch (IOException e) {
System.err.println("warning: could not read " + path + ", using built-in content");
return fallback.getBytes(StandardCharsets.UTF_8);
}
}
private static Identity loadOrCreateIdentity(String path) {
try {
Identity identity = Identity.load(path);
System.err.println("loaded identity from " + path);
return identity;
} catch (RnsException e) {
Identity identity = Identity.generate();
identity.save(path);
System.err.println("created and saved identity to " + path);
return identity;
}
}
private static void die(String msg) {
System.err.println(msg);
System.exit(1);
}
private static void printLastError(String what) {
String msg = Rns.lastError();
if (msg.isEmpty()) {
System.err.println(what);
} else {
System.err.println(what + ": " + msg);
}
}
}