OverviewFilesCommitsIssuesDocsReleasesPeers

opencog-cogserver / opencog / cogserver / server / WebServer.cc

391 lines · cc ref:
/*
 * opencog/cogserver/server/WebServer.cc
 *
 * Copyright (C) 2022 Linas Vepstas
 * SPDX-License-Identifier: AGPL-3.0-or-later
 */

#ifdef HAVE_OPENSSL

#include <cstring>
#include <string>
#include <openssl/sha.h>

#include <opencog/util/exceptions.h>
#include <opencog/util/Logger.h>
#include <opencog/util/misc.h>

#include <opencog/cogserver/server/CogServer.h>
#include <opencog/cogserver/server/PageServer.h>
#include <opencog/cogserver/server/WebServer.h>
#include <opencog/eval/GenericEval.h>

using namespace opencog;

WebServer::WebServer(const Handle&, CogServer& cs, SocketManager* mgr) :
	ConsoleSocket(mgr),
	_cserver(cs),
	_request(nullptr)
{
}

WebServer::~WebServer()
{
	// Log this as 'debug' not 'info', due to nuisance websocket
	// traffic coming from systemd which pings it every 5 seconds.
	// Or maybe its the gnome dbus or soething like that.
	logger().debug("Closed WebSocket Shell");
}

// ==================================================================

// Called before any data is sent/received.
void WebServer::OnConnection(void)
{
	if (0 == _url.compare("/favicon.ico"))
	{
		Send(favicon());
		throw SilentException();
	}
	if (0 == _url.compare("/stats"))
	{
		Send(html_stats());
		throw SilentException();
	}

#ifdef HAVE_MCP
	// Handle OAuth discovery endpoints for MCP
	if (0 == _url.compare("/.well-known/oauth-protected-resource"))
	{
		Send(oauth_protected_resource());
		throw SilentException();
	}
	if (0 == _url.compare("/.well-known/oauth-authorization-server"))
	{
		Send(oauth_authorization_server());
		throw SilentException();
	}
	// Handle the /register endpoint that Claude is looking for
	if (0 == _url.compare("/register"))
	{
		Send(oauth_register_not_required());
		throw SilentException();
	}
#endif

	// We expect the URL to have the form /json or /scm or
	// whatever, and, stripping away the leading slash, it
	// should be one of the supported commands.
	std::string cmdName = _url.substr(1);
	_request = _cserver.createRequest(cmdName);

	// Reject URL's we don't know about.
	// Try to serve static pages from /usr/local/share/cogserver
	if (nullptr == _request)
	{
		logger().info("[WebServer] Request not found, trying PageServer for %s", _url.c_str());
		std::string response = PageServer::serve(_url);
		Send(response);
		throw SilentException();
	}

	logger().info("Opened Http Socket %s Shell", cmdName.c_str());
}

// Called for each newline-terminated line received.
void WebServer::OnLine(const std::string& line)
{
	if (_request)
	{
		// Use the request mechanism to get a fully configured
		// shell. This is a hang-over from the telnet interfaces,
		// where input strings become Requests, which, when executed
		// are looked up in the module system, passed to the correct
		// module, and then configured to send replies on this socket.
		// It works, so don't mess with it.
		std::list<std::string> params;
		params.push_back("hush");
		_request->setParameters(params);
		_request->set_console(this);
		_request->execute();
		delete _request;
		_request = nullptr;

		// Disable line discipline
		_shell->discipline(false);
	}

	// WebSocket connections use normal threaded evaluation
	if (_got_websock_header)
	{
		_shell->eval(line);
		return;
	}

	// For non-WebSocket HTTP connections, don't use the shell's threaded evaluation
	// Instead, get the evaluator directly and use it synchronously
	GenericEval* eval = _shell->get_evaluator();

	// Start evaluation
	eval->begin_eval();

	// Evaluate the expression
	eval->eval_expr(line);

	// Collect all results
	std::string result;
	std::string chunk;
	do {
		chunk = eval->poll_result();
		result += chunk;
	} while (!chunk.empty());

	// Send with appropriate HTTP headers
	if (!result.empty())
	{
		// Determine content type based on shell type
		std::string content_type = "text/plain";
		if (strcmp(_shell->_name, "mcp") == 0 || strcmp(_shell->_name, "json") == 0)
			content_type = "application/json";

		SendWithHeader(result, content_type);
	}
}


// ==================================================================

std::string WebServer::html_stats(void)
{
	std::string response =
		"HTTP/1.1 200 OK\r\n"
		"Server: CogServer\r\n"
		"Content-Type: text/html\r\n"
		"\r\n"
		"<!DOCTYPE html>\n"
		"<html lang=\"en\">\n"
		"<head>\n"
		"  <meta charset=\"UTF-8\">\n"
		"  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n"
		"  <title>CogServer Status</title>\n"
		"  <style>\n"
		"    body {\n"
		"      font-family: monospace;\n"
		"      margin: 20px;\n"
		"      line-height: 1.6;\n"
		"      background: #fafafa;\n"
		"    }\n"
		"    h1 {\n"
		"      border-bottom: 2px solid #333;\n"
		"      padding-bottom: 10px;\n"
		"      color: #333;\n"
		"    }\n"
		"    h2 {\n"
		"      border-bottom: 1px solid #ccc;\n"
		"      padding-bottom: 5px;\n"
		"      margin-top: 30px;\n"
		"      color: #444;\n"
		"    }\n"
		"    pre {\n"
		"      background: #f0f0f0;\n"
		"      padding: 15px;\n"
		"      border: 1px solid #ccc;\n"
		"      border-radius: 4px;\n"
		"      overflow-x: auto;\n"
		"      font-size: 14px;\n"
		"      line-height: 1.4;\n"
		"    }\n"
		"    a {\n"
		"      color: #007bff;\n"
		"      text-decoration: none;\n"
		"    }\n"
		"    a:hover {\n"
		"      text-decoration: underline;\n"
		"    }\n"
		"  </style>\n"
		"</head>\n"
		"<body>\n"
		"  <h1>CogServer Status</h1>\n"
		"  <h2>Loaded Modules</h2>\n"
		"  <pre>\n";
	response += _cserver.listModules();
	response +=
		"</pre>\n"
		"  <h2>Connection Statistics</h2>\n"
		"  <pre>\n";
	response += _cserver.display_web_stats();
	response +=
		"</pre>\n"
		"  <h2>Connection Stats Legend</h2>\n"
		"  <pre>";
	response += CogServer::stats_legend();
	response +=
		"</pre>\n"
		"</body>\n"
		"</html>";

	return response;
}

// ==================================================================

/// Given an input in base64, return the raw binary, stuffed into
/// a string.
// Found code blob on stackexchange from user Manuel Martinez.
static std::string base64_decode(const std::string &in)
{
	std::string out;

	std::vector<int> T(256,-1);
	for (int i=0; i<64; i++) T["ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"[i]] = i;

	int val=0, valb=-8;
	for (unsigned char c : in)
	{
		if (T[c] == -1) break;
		val = (val << 6) + T[c];
		valb += 6;
		if (valb >= 0)
		{
			out.push_back(char((val>>valb) & 0xFF));
			valb -= 8;
		}
	}
	return out;
}

// ==================================================================

/// Return an HTTP response holding the opencog favicon.ico image.
std::string WebServer::favicon(void)
{
	// I do not want to open and read a file; so we're going to
	// just insert this into the source code. Which means it cannot
	// be a binary blob. So we insert base64 into the code.
	// Now, it would be great if Firefox/Mozilla accepted
	// Content-Transfer-Encoding: base64 but it doesn't, so we
	// convert the thing into binary, and send that.
	std::string bicon =
#include "favicon.ico.base64"
	;
	std::string icon = base64_decode(bicon);

	std::string response =
		"HTTP/1.1 200 OK\r\n"
		"Server: CogServer\r\n"
		"Content-Length: ";

	char buf[20];
	snprintf(buf, 20, "%lu", icon.size());
	response += buf;
	response += "\r\n"
		"Content-Type: image/vnd.microsoft.icon\r\n"
		"\r\n";

	response += icon;

	return response;
}

#ifdef HAVE_MCP
// ==================================================================
// OAuth discovery endpoints for MCP

/// Return OAuth protected resource metadata indicating no authentication required
std::string WebServer::oauth_protected_resource(void)
{
	// This indicates that no authentication is required
	// by not specifying any authorization_servers
	std::string json_body = "{}";

	std::string response =
		"HTTP/1.1 200 OK\r\n"
		"Server: CogServer\r\n"
		"Content-Type: application/json\r\n"
		"Content-Length: ";

	char buf[20];
	snprintf(buf, 20, "%lu", json_body.size());
	response += buf;
	response += "\r\n\r\n";
	response += json_body;

	return response;
}

/// Return OAuth authorization server metadata indicating no authentication
std::string WebServer::oauth_authorization_server(void)
{
	// Build the issuer URL from the Host header if available
	std::string issuer_url = "http://";
	if (!_host_header.empty()) {
		issuer_url += _host_header;
	} else {
		// Fallback to localhost with actual port if no Host header was provided
		short port = _cserver.getWebServerPort();
		issuer_url += "localhost:" + std::to_string(port);
	}

	// Return minimal OAuth metadata indicating no authentication required
	std::string json_body = "{\"issuer\":\"" + issuer_url + "\"}";

	std::string response =
		"HTTP/1.1 200 OK\r\n"
		"Server: CogServer\r\n"
		"Content-Type: application/json\r\n"
		"Content-Length: ";

	char buf[20];
	snprintf(buf, 20, "%lu", json_body.size());
	response += buf;
	response += "\r\n\r\n";
	response += json_body;

	return response;
}

/// Return a response indicating registration is not required
std::string WebServer::oauth_register_not_required(void)
{
	// Return a response indicating that registration is not needed
	std::string json_body = "{\"error\":\"registration_not_supported\",\"error_description\":\"This MCP server does not require OAuth registration\"}";

	std::string response =
		"HTTP/1.1 400 Bad Request\r\n"
		"Server: CogServer\r\n"
		"Content-Type: application/json\r\n"
		"Content-Length: ";

	char buf[20];
	snprintf(buf, 20, "%lu", json_body.size());
	response += buf;
	response += "\r\n\r\n";
	response += json_body;

	return response;
}
#endif // HAVE_MCP

// Send with HTTP headers for shell output
void WebServer::SendWithHeader(const std::string& msg, const std::string& content_type)
{
	// Build HTTP response with Content-Length
	std::string response = "HTTP/1.1 200 OK\r\n";
	response += "Server: CogServer\r\n";
	response += "Content-Type: ";
	response += content_type;
	response += "\r\n";
	response += "Content-Length: ";
	char buf[20];
	snprintf(buf, 20, "%lu", msg.size());
	response += buf;
	response += "\r\n\r\n";
	response += msg;

	// Send the complete HTTP response
	Send(response);
}

#endif // HAVE_OPENSSL
// ==================================================================