External Plugins
External plugins are third-party plugins built with the Kraken and RuneLite APIs that run natively on Kraken's client. This makes the Kraken client a platform: developers use the open source API to write plugins and can share them with Kraken users, while Kraken keeps the client, the Kraken API and RuneLite compatibility up to date for them.
This document covers both how to use external plugins and how to create and share them.
⚠️ Security Warning: External plugins are NOT Kraken plugins. They run inside your client with the same access as RuneLite itself, including your game session. Kraken checks that downloads are intact, shows you what a repository contains before anything runs, and can block known-bad repositories, but it cannot make untrusted code safe. Add external repositories AT YOUR OWN RISK.
Using External Plugins
External plugins are loaded from "repositories". A repository is a manifest file on a developer's server that lists plugin JAR files.
Adding a repository
To add a repository, open the Kraken plugin in the sidebar, click the External Plugins tab, and click Add Repository. Enter the Repository URL and, if the developer gave you one, the Authentication Token (Optional). The repository's name comes from its manifest, not from you.

Private repositories need a token that the developer generates for you. It is only ever sent to the server that hosts the manifest.
Before anything is saved or run, Kraken downloads and checks the repository's plugins and shows a review screen. It lists:
- the repository, its author and website, and the server the manifest comes from;
- whether the manifest is signed, and with which key (see Signed repositories);
- every plugin, with its version, the server it is downloaded from, its size, and the start of its SHA-256 hash;
- what each plugin's code was found to do. Lines marked
!are high risk:
| Flag | Meaning |
|---|---|
! Runs other programs | Can start programs on your computer |
! Loads native code | Can run code outside Java |
! Loads code at runtime | Can download or generate code that the review screen cannot see |
! Accesses Kraken client internals | Refers to the Kraken loader's own code |
! Names login or credential files | Mentions files such as RuneLite's credentials.properties |
| Uses deep reflection | Can read private parts of RuneLite or other plugins |
| Connects to the internet | Makes network requests, with the web addresses found in its code |
| Reads or writes files | Uses files on your computer |
These flags help you decide, but they are not a sandbox: a plugin can hide what it does. Only approve repositories you trust. Click Approve and load to add the repository and start its plugins, or Cancel to add nothing.
Once a repository is added, its plugins appear in your Kraken plugin list:

Each external plugin is shown with the first letter of its repository in front of its name:

Updates
When a developer changes their repository, Kraken compares it with what you approved:
- Unchanged plugins load as usual, and refreshing does not restart them.
- Signed repositories update automatically when the update is signed with the key you approved. If an update adds a new high-risk flag, you are asked to review it first.
- Unsigned repositories need your review for every change.
- A signed repository whose key changes, or that stops being signed, always needs your review, and the review screen warns you. This can mean the developer changed keys, or that someone else is publishing to the repository.
While an update waits for review, the repository card shows Review and a short reason. The version you approved keeps running. After a restart, Kraken downloads the version you approved, if the developer still offers it.
Managing repositories
Each repository card shows its status and has these controls:
- The switch turns the repository off, which unloads its plugins and stops them loading, without removing it. Turning it back on loads it again.
- Refresh (or Retry, after a failure) fetches the repository again. Review opens a pending update.
- Remove unloads the repository's plugins and forgets the repository.
| Status | Meaning |
|---|---|
| N plugins loaded | Running |
| Waiting for your review | Nothing from the repository runs until you review it |
| Loads when you sign in to Kraken | External plugins only run while you are signed in to Kraken |
| Disabled | Turned off with the switch |
| Blocked by Kraken | Kraken has blocked this repository for everyone. The reason is shown on the card |
| Failed to load | The problems listed under the status explain why |
Good to know
- Sign-in required: external plugins only load while you are signed in to Kraken, and signing out unloads them.
- Where repositories are stored: in
~/.runelite/kraken/external/repositories.txt, encrypted with a key created for your computer (store.keyin the same folder).- On Windows the key is protected by your Windows account. Elsewhere only your user account can read the key file.
- This keeps your tokens safe from other users and from copies of the file. It does not protect against malicious software running as you.
- Deleting both files removes every repository.
- Repositories from 1.15.0 or earlier need one review after updating to Kraken-Client 1.16.0, because older versions did not record what you approved.
- Integrity checks: every plugin JAR must match the SHA-256 in its manifest. This proves the download wasn't corrupted or swapped on the way. It doesn't prove who published it; a signature does.
- Isolation: each repository loads in its own isolated classloader, so plugins from two repositories cannot conflict with each other. A plugin whose name or class matches a plugin you already have is skipped, and the card says why.
- The kill switch: Kraken can block a repository, a server, a plugin build or a signing key for every user. Kraken checks its blocklist when you sign in and every 15 minutes, and unloads anything it blocks.
- Safe mode: if the client closed or froze while external plugins were starting, the next launch skips them and shows a Load now button. Remove or refresh the repository that caused it first.
- Plugin tags: the tag in front of an external plugin's name can be hidden with the Plugin Tags Display option in the
Kraken Pluginsconfiguration. - Skip External Load: to stop external repositories loading at startup without removing them, tick Skip External Load in the
Kraken Pluginsconfiguration. To stop just one, use its switch.
For a working public example, add the Kraken example plugins repository with this URL and no token:
https://github.com/cbartram/kraken-example-plugin/releases/latest/download/manifest.json
External Plugin Development
The rest of this document covers how to create, sell, and host external plugin repositories.
What repository authors need to know
These rules apply from Kraken-Client 1.15.0 (1.16.0 for the review, signing and blocklist rules):
- Use HTTPS for manifests and artifacts. Plain
http://only works forlocalhost,127.0.0.1and::1, for local testing. - The token goes to the manifest's server only. It is sent to artifact URLs only when they have the same scheme, host and port as the manifest.
- Private GitHub release assets behind a manifest on another domain therefore don't receive the token.
- Host the manifest and the JARs together, or make the JAR URLs work without it (for example, pre-signed URLs).
- Relative artifact URLs resolve like links in a browser. A manifest URL without a trailing slash resolves relative to its parent folder.
- Size limits: a JAR is rejected if it is over 64 MB, unpacks to more than 256 MB, or doesn't match its declared
size. A manifest can be up to 1 MB. - Use unique
@PluginDescriptornames. A plugin whose name or class matches an installed plugin is skipped. If a Kraken plugin with the same name is installed later, yours is unloaded. - Don't shade RuneLite or the Kraken API into plugin JARs.
- The client already provides them and ignores the bundled copies.
- Everything you bundle is scanned and flagged too, so needless libraries make your review screen look riskier.
- The Kraken loader's own classes are off limits. External plugins can't see
net.runelite.client.plugins.kraken.*, so a plugin that links to it, including@PluginDependency(KrakenLoaderPlugin.class), fails to load. - Every update to an unsigned repository needs each user's review. Sign your manifest so updates reach your users automatically.
- Keep old versions downloadable. Use versioned artifact URLs, such as GitHub release downloads by tag, rather than overwriting a file in place. Users who haven't reviewed an update yet keep running the version they approved, and Kraken downloads it again at every launch.
- The manifest
idis ignored. Kraken identifies repositories by their URL. A per-build id, such as the example workflow'suuidgen, does no harm, but a stable id is cleaner.
Repository Manifest
Plugin repositories need a manifest file that contains repository metadata and says where the Kraken client can download each plugin. Manifests are JSON in this format:
{
"repository": {
"id": "a-unique-id-for-your-repository",
"name": "Your Plugins",
"version": "2.1.3",
"author": "[email protected]",
"description": "Collection of premium RuneLite automation plugins for skilling activities",
"website": "https://your-website.com/a-list-of-plugins",
"lastUpdated": "2026-01-12T00:00:00Z"
},
"plugins": [
{
"name": "Auto Fisher",
"version": "1.0.0",
"artifact": {
"url": "https://your-website.com/plugins/auto-fisher-1.0.0.jar",
"sha256": "479efaf92702366206884efdcf787155586971905a59ab1f",
"size": 184320
}
},
...
]
}url: absolute, or relative to the URL the manifest was fetched from (for exampleplugins/auto-fisher-1.0.0.jar).sha256: required, and must match the JAR bytes exactly.size: optional, but enforced when present.version: shown on the review screen and the repository card. The plugin list and the plugin's configuration panel show theImplementation-Versionfrom the JAR's own manifest instead, so keep the two the same (see Plugin Versions).- Text is shown as plain text. Kraken never renders manifest text such as names and descriptions as HTML.
When the Kraken client loads a repository, it loads ALL PLUGINS listed in its manifest. If you sell plugins, give each buyer a manifest that lists only the plugins they bought, for example by generating it per user on your server (see Authentication).
Signed repositories
Signing your manifest proves that updates come from you. Without a signature, anyone who gains access to your host or storage account could publish code to your users. With one, a user's client pins your key the first time they approve your repository. After that:
- updates signed with your key reach users without a review, unless they add a high-risk flag;
- a manifest that isn't signed with your key is held for review, with a warning.
Signatures use Ed25519, which openssl (1.1.1 or newer), Java, Node, Python and Go all support. On macOS, use Homebrew's openssl rather than the built-in LibreSSL.
1. Create a key (once)
openssl genpkey -algorithm ed25519 -out kraken-signing-key.pem
# Your fingerprint, the same one users see on the review screen. Publish it on your website or Discord.
openssl pkey -in kraken-signing-key.pem -pubout -outform DER | openssl dgst -sha256 | awk '{print toupper(substr($2,1,16))}'Keep kraken-signing-key.pem secret, for example as a CI secret, and back it up. If you lose it or change it, every user has to review your repository again.
2. Sign each manifest
A signed manifest is an envelope around your normal manifest:
{
"signedManifest": "<base64 of your manifest.json bytes>",
"signature": "<base64 signature over those bytes>",
"publicKey": "<base64 public key: the body of the public key PEM>"
}Serve the envelope at your manifest URL instead of the plain manifest. In a GitHub Actions workflow, such as the example repository's, add this step after manifest.json is generated, with the key stored as a KRAKEN_SIGNING_KEY secret:
- name: Sign manifest
env:
KRAKEN_SIGNING_KEY: ${{ secrets.KRAKEN_SIGNING_KEY }}
run: |
printf '%s\n' "$KRAKEN_SIGNING_KEY" > signing-key.pem
openssl pkeyutl -sign -inkey signing-key.pem -rawin -in manifest.json -out manifest.sig
PUBLIC_KEY=$(openssl pkey -in signing-key.pem -pubout -outform DER | base64 -w0)
jq -n --arg m "$(base64 -w0 manifest.json)" --arg s "$(base64 -w0 manifest.sig)" --arg k "$PUBLIC_KEY" \
'{signedManifest: $m, signature: $s, publicKey: $k}' > signed-manifest.json
mv signed-manifest.json manifest.json
rm signing-key.pem manifest.sig- Per-user manifests: a server that generates manifests per user signs each response the same way. Sign the exact bytes you put in
signedManifest. - Key formats:
publicKeymay be the body of the public key PEM, or the raw 32-byte key in base64. - Invalid signatures: an envelope whose signature doesn't verify is rejected outright.
Manifest Updates
After a user adds your repository, the client fetches your manifest at startup and whenever the user clicks Refresh. Users can refresh at will, so rate limit your server. The manifest itself is never written to disk. The details needed to fetch it, the URL and token, are stored encrypted on the user's machine.
To add, update, or remove plugins for a user, change their manifest on your server. They get the change the next time their client starts or refreshes:
- Unchanged plugins are not downloaded again or restarted.
- Signed with your pinned key: changes apply automatically, unless they add a high-risk flag.
- Otherwise: the user is asked to review the update first.
- Partial failures: if an updated plugin can't be downloaded, the running version stays loaded and the card shows why.
Manifest Error Handling
If your server returns a non-200 response, or doesn't respond in time, nothing new loads:
- Timeouts: 5 seconds to connect, 5 seconds between reads, and 10 minutes per download.
- Plugins already running keep running, and the card shows the error.
- Users can retry once your server is back.
Authentication
The Kraken client supports token-based authentication in the Authorization HTTP header. The token is sent with the Bearer prefix and a space, as is standard. For example:
{
"Authorization": "Bearer <your token>"
}The client makes an HTTP GET request to the manifest URL with your token in the Authorization header and a kraken-client user agent. It expects a UTF-8 JSON response containing the manifest or a signed envelope. For example, for the following screenshot, the Kraken client makes this request to retrieve the manifest:

curl "https://your-server.com/get-manifest" \
-H "Authorization: Bearer somerandomtoken123" \
-H "User-Agent: kraken-client"Requests for the plugin JARs are also GET requests with the same User-Agent. They include the Authorization header only when the JAR is on the same scheme, host and port as the manifest.
You will usually need to know which Kraken user is asking, so you can return the manifest with the plugins that user bought. We recommend either:
- A user id in the URL. For example: "Thanks for purchasing our plugin! To run it on the Kraken client, add a repository with this URL:
https://my-server.com/get-manifest?userId=abc123". The client removes query strings from URLs before it logs them. - A user id inside a JWT sent as the token. For example: "Thanks for purchasing our plugin! To run it on the Kraken client, paste this token in the authentication field when adding a repository:
eyJhkpXVCJ9.eyJzdWIiOinMiGM6H9FNFUROf3wh7SmqJp-QV30".
Note: If your manifest and plugin JAR files are public, tell your users to leave the authentication field blank when adding a repository.
Plugin Hosting & Server Setup
Kraken external plugins are always loaded over HTTPS. Kraken does NOT host your plugin JAR files, and it never writes them to disk. Your plugins stay in memory for the length of the user's RuneLite session, and you are responsible for keeping your JAR files available over the internet, with or without authentication.
The Kraken client makes no assumptions about your backend, programming language, or how you serve your JARs. You can use:
- S3, with pre-signed URLs for private files
- GitHub Releases (public or private). Use a GitHub PAT for private repositories, and remember that the token is only sent to the manifest's host
- SeaweedFS
- Your own statically hosted files
- Any other way of serving files over HTTPS
GET
There are no header requirements. The client doesn't check Content-Type, so GitHub Releases and GitHub Pages work as they are.
Being blocked by Kraken
Kraken keeps a blocklist of repositories, servers, plugin builds (by SHA-256) and signing keys that must not be loaded. It uses this for malware and compromised repositories. A blocked repository is unloaded for every user, and the reason is shown on its card. If you think your repository was blocked by mistake, contact Kraken on Discord.
Examples
The following examples show how you can set up a Kraken client-compatible server for serving your plugins and manifests. They listen on plain HTTP, which the client only accepts from localhost. Put them behind HTTPS before sharing them with users.
Java
@Slf4j
public class ExampleHttpServer {
private static String authToken = null;
static void main(String[] args) throws IOException {
int port = args.length > 0 ? Integer.parseInt(args[0]) : 8080;
authToken = args.length > 1 ? args[1] : null;
HttpServer server = HttpServer.create(new InetSocketAddress(port), 0);
server.createContext("/get-manifest", new ManifestHandler());
server.createContext("/plugins/", new PluginHandler());
server.createContext("/", new RootHandler());
server.setExecutor(null);
server.start();
log.info("Example HTTP server started on port {}", port);
log.info("Auth token: {}", authToken != null ? authToken : "NONE (public access)");
log.info("Endpoints:");
log.info(" GET http://localhost:{}/get-manifest", port);
log.info(" GET http://localhost:{}/plugins/test-plugin.jar", port);
log.info("Press Ctrl+C to stop");
}
static class ManifestHandler implements HttpHandler {
private String manifest;
public ManifestHandler() {
try (InputStream json = getClass().getResourceAsStream("/kraken-manifest-http.json")) {
if(json == null) {
log.error("Failed to find or load kraken-manifest.json file - using empty default");
this.manifest = "{}";
return;
}
this.manifest = new String(json.readAllBytes(), StandardCharsets.UTF_8);
} catch (IOException e) {
log.error("Failed to read manifest file", e);
this.manifest = "{}";
}
}
@Override
public void handle(HttpExchange exchange) throws IOException {
if (!"GET".equals(exchange.getRequestMethod())) {
sendResponse(exchange, 405, "Method not allowed");
return;
}
if (!checkAuth(exchange)) {
sendResponse(exchange, 401, "Unauthorized");
return;
}
log.info("Serving manifest");
exchange.getResponseHeaders().set("Content-Type", "application/json");
String responseBody = this.manifest != null ? this.manifest : "{}";
sendResponse(exchange, 200, responseBody);
}
}
static class PluginHandler implements HttpHandler {
private byte[] jar;
public PluginHandler() {
try(InputStream json = getClass().getResourceAsStream("/test-plugin.jar")) {
if(json == null) {
log.error("Failed to find or load fishing plugin jar file");
this.jar = new byte[]{};
return;
}
this.jar = json.readAllBytes();
} catch (IOException e) {
log.error("Failed to read fishing plugin jar file from test resources ");
this.jar = new byte[]{};
}
}
@Override
public void handle(HttpExchange exchange) throws IOException {
if (!"GET".equals(exchange.getRequestMethod())) {
sendResponse(exchange, 405, "Method not allowed");
return;
}
if (!checkAuth(exchange)) {
sendResponse(exchange, 401, "Unauthorized");
return;
}
String path = exchange.getRequestURI().getPath();
String filename = path.substring(path.lastIndexOf('/') + 1);
// This always loads /test-plugin.jar, but you get the idea
log.info("Serving plugin: {}", filename);
exchange.getResponseHeaders().set("Content-Type", "application/octet-stream");
sendResponse(exchange, 200, this.jar);
}
}
static class RootHandler implements HttpHandler {
@Override
public void handle(HttpExchange exchange) throws IOException {
String response = "Example HTTP Server\n\nAvailable endpoints:\n" +
" GET /get-manifest.json\n" +
" GET /plugins/{filename}.jar\n";
sendResponse(exchange, 200, response);
}
}
// TODO You should definitely use an Auth provider like Cognito, Firebase, OAuth etc... for this. This
// is just an example
private static boolean checkAuth(HttpExchange exchange) {
if (authToken == null) {
return true; // No auth required
}
String auth = exchange.getRequestHeaders().getFirst("Authorization");
if (auth == null) {
log.warn("Missing Authorization header");
return false;
}
if (auth.startsWith("Bearer ")) {
String token = auth.substring(7);
if (authToken.equals(token)) {
return true;
}
log.warn("Invalid token provided");
}
return false;
}
private static void sendResponse(HttpExchange exchange, int status, String body) throws IOException {
// UTF-8 Encoded string response for the manifest
byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
exchange.sendResponseHeaders(status, bytes.length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(bytes);
}
}
private static void sendResponse(HttpExchange exchange, int status, byte[] body) throws IOException {
// Byte array response with the octet-stream header set for the plugin JAR file
exchange.sendResponseHeaders(status, body.length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(body);
}
}
}Python
import logging
import sys
from http.server import HTTPServer, BaseHTTPRequestHandler
from pathlib import Path
import json
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
log = logging.getLogger(__name__)
class ExampleHttpServer:
auth_token = None
@staticmethod
def main():
port = int(sys.argv[1]) if len(sys.argv) > 1 else 8080
ExampleHttpServer.auth_token = sys.argv[2] if len(sys.argv) > 2 else None
log.info(f"Example HTTP server started on port {port}")
log.info(f"Auth token: {ExampleHttpServer.auth_token if ExampleHttpServer.auth_token else 'NONE (public access)'}")
log.info("Endpoints:")
log.info(f" GET http://localhost:{port}/get-manifest")
log.info(f" GET http://localhost:{port}/plugins/test-plugin.jar")
log.info("Press Ctrl+C to stop")
server = HTTPServer(('', port), RequestHandler)
try:
server.serve_forever()
except KeyboardInterrupt:
log.info("Server stopped")
server.shutdown()
class RequestHandler(BaseHTTPRequestHandler):
manifest_data = None
plugin_jar = None
@classmethod
def load_resources(cls):
"""Load manifest and plugin JAR on first request"""
if cls.manifest_data is None:
try:
manifest_path = Path(__file__).parent / 'kraken-manifest.json'
if manifest_path.exists():
with open(manifest_path, 'r', encoding='utf-8') as f:
cls.manifest_data = f.read()
else:
log.error("Failed to find or load kraken-manifest.json file - using empty default")
cls.manifest_data = "{}"
except Exception as e:
log.error(f"Failed to read manifest file: {e}")
cls.manifest_data = "{}"
if cls.plugin_jar is None:
try:
jar_path = Path(__file__).parent / 'test-plugin.jar'
if jar_path.exists():
with open(jar_path, 'rb') as f:
cls.plugin_jar = f.read()
else:
log.error("Failed to find or load plugin jar file")
cls.plugin_jar = b''
except Exception as e:
log.error(f"Failed to read plugin jar file from test resources: {e}")
cls.plugin_jar = b''
def check_auth(self):
"""Check authorization header if auth token is set"""
if ExampleHttpServer.auth_token is None:
return True # No auth required
auth = self.headers.get('Authorization')
if auth is None:
log.warning("Missing Authorization header")
return False
if auth.startswith("Bearer "):
token = auth[7:]
if ExampleHttpServer.auth_token == token:
return True
log.warning("Invalid token provided")
return False
def send_text_response(self, status, body):
"""Send UTF-8 encoded string response"""
bytes_body = body.encode('utf-8')
self.send_response(status)
self.send_header('Content-Length', len(bytes_body))
self.end_headers()
self.wfile.write(bytes_body)
def send_binary_response(self, status, body):
"""Send binary response"""
self.send_response(status)
self.send_header('Content-Length', len(body))
self.end_headers()
self.wfile.write(body)
def do_GET(self):
"""Handle GET requests"""
self.load_resources()
if self.path == '/get-manifest':
self.handle_manifest()
elif self.path.startswith('/plugins/'):
self.handle_plugin()
elif self.path == '/':
self.handle_root()
else:
self.send_text_response(404, "Not found")
def handle_manifest(self):
"""Handle manifest endpoint"""
if not self.check_auth():
self.send_text_response(401, "Unauthorized")
return
log.info("Serving manifest")
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.send_header('Content-Length', len(self.manifest_data.encode('utf-8')))
self.end_headers()
self.wfile.write(self.manifest_data.encode('utf-8'))
def handle_plugin(self):
"""Handle plugin endpoint"""
if not self.check_auth():
self.send_text_response(401, "Unauthorized")
return
filename = self.path.split('/')[-1]
# This always loads test-plugin.jar, but you get the idea
log.info(f"Serving plugin: {filename}")
self.send_response(200)
self.send_header('Content-Type', 'application/octet-stream')
self.send_header('Content-Length', len(self.plugin_jar))
self.end_headers()
self.wfile.write(self.plugin_jar)
def handle_root(self):
"""Handle root endpoint"""
response = ("Example HTTP Server\n\nAvailable endpoints:\n"
" GET /get-manifest.json\n"
" GET /plugins/{filename}.jar\n")
self.send_text_response(200, response)
def log_message(self, format, *args):
"""Override to suppress default logging"""
pass
if __name__ == "__main__":
ExampleHttpServer.main()Golang
package main
import (
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"time"
)
var (
authToken string
manifestData []byte
pluginJar []byte
)
func main() {
port := "8080"
if len(os.Args) > 1 {
port = os.Args[1]
}
if len(os.Args) > 2 {
authToken = os.Args[2]
}
log.SetFlags(log.Ldate | log.Ltime)
log.Printf("Example HTTP server started on port %s", port)
if authToken != "" {
log.Printf("Auth token: %s", authToken)
} else {
log.Println("Auth token: NONE (public access)")
}
log.Println("Endpoints:")
log.Printf(" GET http://localhost:%s/get-manifest", port)
log.Printf(" GET http://localhost:%s/plugins/test-plugin.jar", port)
log.Println("Press Ctrl+C to stop")
// Load resources
loadResources()
// Set up routes
http.HandleFunc("/get-manifest", handleManifest)
http.HandleFunc("/plugins/", handlePlugin)
http.HandleFunc("/", handleRoot)
// Start server
addr := ":" + port
if err := http.ListenAndServe(addr, nil); err != nil {
log.Fatalf("Server error: %v", err)
}
}
func loadResources() {
// Load manifest
execPath, err := os.Executable()
if err != nil {
log.Printf("Failed to get executable path: %v", err)
execPath = "."
}
execDir := filepath.Dir(execPath)
manifestPath := filepath.Join(execDir, "kraken-manifest-http.json")
data, err := os.ReadFile(manifestPath)
if err != nil {
log.Printf("Failed to find or load kraken-manifest.json file - using empty default: %v", err)
manifestData = []byte("{}")
} else {
manifestData = data
}
// Load plugin JAR
jarPath := filepath.Join(execDir, "test-plugin.jar")
data, err = os.ReadFile(jarPath)
if err != nil {
log.Printf("Failed to find or load fishing plugin jar file: %v", err)
pluginJar = []byte{}
} else {
pluginJar = data
}
}
func checkAuth(r *http.Request) bool {
if authToken == "" {
return true // No auth required
}
auth := r.Header.Get("Authorization")
if auth == "" {
log.Println("Missing Authorization header")
return false
}
if strings.HasPrefix(auth, "Bearer ") {
token := auth[7:]
if token == authToken {
return true
}
log.Println("Invalid token provided")
}
return false
}
func handleManifest(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
if !checkAuth(r) {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
log.Println("Serving manifest")
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(manifestData)))
w.WriteHeader(http.StatusOK)
w.Write(manifestData)
}
func handlePlugin(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
if !checkAuth(r) {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
filename := filepath.Base(r.URL.Path)
// This always loads test-plugin.jar, but you get the idea
log.Printf("Serving plugin: %s", filename)
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(pluginJar)))
w.WriteHeader(http.StatusOK)
w.Write(pluginJar)
}
func handleRoot(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.Error(w, "Not found", http.StatusNotFound)
return
}
response := "Example HTTP Server\n\nAvailable endpoints:\n" +
" GET /get-manifest.json\n" +
" GET /plugins/{filename}.jar\n"
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusOK)
io.WriteString(w, response)
}Node.js (Javascript)
const http = require('http');
const fs = require('fs');
const path = require('path');
let authToken = null;
class ManifestHandler {
constructor() {
try {
const manifestPath = path.join(__dirname, 'kraken-manifest-http.json');
this.manifest = fs.readFileSync(manifestPath, 'utf8');
} catch (error) {
console.error('Failed to find or load kraken-manifest.json file - using empty default', error);
this.manifest = '{}';
}
}
handle(req, res) {
if (req.method !== 'GET') {
sendResponse(res, 405, 'Method not allowed');
return;
}
if (!checkAuth(req)) {
sendResponse(res, 401, 'Unauthorized');
return;
}
console.log('Serving manifest');
res.setHeader('Content-Type', 'application/json');
sendResponse(res, 200, this.manifest || '{}');
}
}
class PluginHandler {
constructor() {
try {
const jarPath = path.join(__dirname, 'test-plugin.jar');
this.jar = fs.readFileSync(jarPath);
} catch (error) {
console.error('Failed to find or load fishing plugin jar file', error);
this.jar = Buffer.alloc(0);
}
}
handle(req, res) {
if (req.method !== 'GET') {
sendResponse(res, 405, 'Method not allowed');
return;
}
if (!checkAuth(req)) {
sendResponse(res, 401, 'Unauthorized');
return;
}
const urlPath = req.url;
const filename = urlPath.substring(urlPath.lastIndexOf('/') + 1);
console.log(`Serving plugin: ${filename}`);
res.setHeader('Content-Type', 'application/octet-stream');
sendResponseBytes(res, 200, this.jar);
}
}
class RootHandler {
handle(req, res) {
const response = `Kraken Test HTTP Server
Available endpoints:
GET /get-manifest
GET /plugins/{filename}.jar
`;
sendResponse(res, 200, response);
}
}
function checkAuth(req) {
if (authToken === null) {
return true; // No auth required
}
const auth = req.headers['authorization'];
if (!auth) {
console.warn('Missing Authorization header');
return false;
}
if (auth.startsWith('Bearer ')) {
const token = auth.substring(7);
if (authToken === token) {
return true;
}
console.warn('Invalid token provided');
}
return false;
}
function sendResponse(res, status, body) {
const bytes = Buffer.from(body, 'utf8');
res.writeHead(status, { 'Content-Length': bytes.length });
res.end(bytes);
}
function sendResponseBytes(res, status, body) {
res.writeHead(status, { 'Content-Length': body.length });
res.end(body);
}
function main() {
const args = process.argv.slice(2);
const port = args.length > 0 ? parseInt(args[0]) : 8080;
authToken = args.length > 1 ? args[1] : null;
const manifestHandler = new ManifestHandler();
const pluginHandler = new PluginHandler();
const rootHandler = new RootHandler();
const server = http.createServer((req, res) => {
const url = req.url;
if (url === '/get-manifest') {
manifestHandler.handle(req, res);
} else if (url.startsWith('/plugins/')) {
pluginHandler.handle(req, res);
} else {
rootHandler.handle(req, res);
}
});
server.listen(port, () => {
console.log(`Test HTTP server started on port ${port}`);
console.log(`Auth token: ${authToken !== null ? authToken : 'NONE (public access)'}`);
console.log('Endpoints:');
console.log(` GET http://localhost:${port}/get-manifest`);
console.log(` GET http://localhost:${port}/plugins/test-plugin.jar`);
console.log('Press Ctrl+C to stop');
});
}
main();