Tutorials

Tutorials

Inside the GPAL REST API Server (OttoMagic's Backend)

GPALRestAPI is the local HTTP server that GPAL.OttoMagicClient talks to. It runs alongside a browser extension, listens on a local port, and translates REST calls into native-messaging commands the extension executes against the live browser. Understanding this server helps explain what's actually happening when your GPAL workflow calls .Execute().

What This Program Is

GPALRestAPI.exe is a small console/Forms hybrid app that does two jobs at once: it hosts an HttpListener on a local port (default 3000, matching GPAL.OttoMagicRestApiBaseUrl), and it speaks Chrome's native messaging protocol over stdin/stdout to a companion browser extension. When GPAL.OttoMagicClient sends an endpoint like LeftClick or GoTo, this server receives the HTTP request, forwards it to the extension as a native message, and the extension performs the action in the real browser tab.

[STAThread]

static void Main(string[] args)

{

// only one instance runs at a time - kill any others first

Process current = Process.GetCurrentProcess();

var otherInstances = Process.GetProcessesByName(current.ProcessName)

.Where(p => p.Id != current.Id);

foreach (var process in otherInstances)

process.Kill();

if (args.Length == 1 && int.TryParse(args[0], out int port) && port > 1024 && port < 65536)

_listenPort = args[0];

StartRestServer();

ListenForNativeMessages();

}

Starting the HTTP Listener

StartRestServer binds an HttpListener to GPAL.OttoMagicRestApiBaseUrl (with the port substituted) plus every active local IPv4 address, so the API is reachable both from localhost and from the LAN. Each incoming request runs through HandleAPIRequest on a background thread.

private static void StartRestServer()

{

listener = new HttpListener();

listener.Prefixes.Add(GPAL.OttoMagicRestApiBaseUrl.Replace("3000", _listenPort));

foreach (var ip in GetActiveIPv4Addresses())

listener.Prefixes.Add($"http://{ip}:{_listenPort}/");

listener.Start();

new Thread(() =>

{

while (listener.IsListening)

{

HttpListenerContext context = listener.GetContext();

HandleAPIRequest(context);

}

}).Start();

}

WARNING

If binding to a LAN IP fails with a permission error, the server copies netsh urlacl and firewall rule commands to the clipboard and shows a notification. Paste them into an elevated command prompt and restart the server.

Routing Requests: Built-In Commands vs Endpoint Forwarding

HandleAPIRequest first checks for a small set of built-in routes - start, stop, status, help, controller, and a couple of OAuth helpers (access-token / get-access-token). Anything else falls through to the default case: GET requests are forwarded to the extension as-is, and POST requests forward the body too. This default case is what handles every ApiEndpoint that GPAL.OttoMagicClient calls.

switch (message)

{

case "status":

SendRestResponse(response,

null == listener ? HttpStatusCode.NotFound : HttpStatusCode.OK,

null == listener ? "Server is not running" : "Server is running");

break;

case "help":

SendRestResponse(response, HttpStatusCode.OK,

$"Commands: {string.Join(",", Enum.GetNames(typeof(GenerallyPositive.Enums.ApiEndpoint)))}");

break;

default:

if ("GET" == context.Request.HttpMethod)

SendNativeMessageToExtension(message);

else if ("POST" == context.Request.HttpMethod)

{

string jsonBody = new StreamReader(context.Request.InputStream).ReadToEnd();

SendNativeMessageToExtension(message, jsonBody);

}

else

SendRestResponse(response, HttpStatusCode.MethodNotAllowed, "Method not supported");

break;

}

TIP

When GPAL.OttoMagicClient calls .GoTo("https://example.com").Execute(), it issues an HTTP request whose path matches the endpoint (for example /goto). The path becomes 'message' here, with any leading slash stripped, and is forwarded straight to the extension.

Native Messaging: Talking to the Browser Extension

Native messages use a 4-byte length prefix followed by a UTF-8 JSON payload, read from and written to stdin/stdout. ReadNativeMessage blocks waiting for the extension's responses, and SendNativeMessageToExtension packages an outgoing command (with optional JSON body) the same way. A heartbeat timer keeps the connection alive and shuts the server down if the extension goes quiet for too long.

private static void SendNativeMessageToExtension(string message, string jsonBody = null)

{

string payload = string.IsNullOrEmpty(jsonBody)

? $@"{{""message"":""{message}""}}"

: $@"{{""message"":""{message}"",""data"":{jsonBody}}}";

byte[] data = Encoding.UTF8.GetBytes(payload);

byte[] len = BitConverter.GetBytes(data.Length);

_stdout.Write(len, 0, 4);

_stdout.Write(data, 0, data.Length);

_stdout.Flush();

}

WARNING

If no native message activity occurs for 30 seconds (no heartbeat from the extension), the server calls Environment.Exit(0) and terminates itself. This is by design - it prevents an orphaned server process from lingering after the browser extension is closed.

The Controller Page

Hitting /controller serves a small HTML/JS workflow builder page. It lists every ApiEndpoint, lets you fill in parameters for each step, and runs the resulting sequence by calling the same endpoints over fetch() - useful for manually testing or demoing the API without writing any C# at all.

case "controller":

ServeControllerPage(response);

break;

// inside the served page:

// const endpoints = ["Back","CaptureVisibleTab","CheckNetworkIdle", ... ];

// async function executeStep(selectedEndpoint, params) {

// const apiEndpoint = endpointMap[selectedEndpoint];

// const url = `${window.location.origin}/${apiEndpoint}`;

// const options = (params && Object.keys(params).length)

// ? { method: "POST", headers: {"Content-Type":"application/json"}, body: JSON.stringify(params) }

// : { method: "GET" };

// const response = await fetch(url, options);

// return await response.json();

// }