Automation Engines

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

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 (3000 at startup, incrementing until it finds a free one, which is what browser.RestApiBaseUrl then points at), and it speaks Chrome's native messaging protocol over stdin/stdout to a companion browser extension. When browser.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 the port it settled on 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($"http://localhost:{_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 browser.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;

}

NOTE

When browser.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. That same blocking read is what tells the server when to stop: the loop runs until the pipe closes, and closing the pipe is what the browser does on its way out.

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

The server lives exactly as long as the browser does. Closing the browser or reloading the extension breaks stdin, and the server exits with it rather than lingering as an orphan. The port goes too: a workflow still holding that base url has nothing to talk to, and the next server takes whichever port is free and broadcasts the new one.

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();

// }

💬 Ask GPAL