OverviewFilesCommitsIssuesDocsReleasesPeers

opencog-cogserver / opencog / cogserver / server / PageServer.cc

169 lines · cc ref:
/*
 * opencog/cogserver/server/PageServer.cc
 *
 * Copyright (C) 2025 BrainyBlaze Dynamics Inc.
 * SPDX-License-Identifier: AGPL-3.0-or-later
 *
 * Implementation of static page server
 */

#include <fstream>
#include <sstream>
#include <sys/stat.h>

#include <opencog/util/Logger.h>
#include <opencog/cogserver/server/PageServer.h>

using namespace opencog;

const std::string PageServer::base_path = PROJECT_INSTALL_PREFIX "/share/cogserver";

std::string PageServer::getMimeType(const std::string& filename)
{
    size_t dot_pos = filename.rfind('.');
    if (dot_pos == std::string::npos) {
        return "application/octet-stream";
    }

    std::string ext = filename.substr(dot_pos);

    if (ext == ".html" || ext == ".htm") {
        return "text/html";
    } else if (ext == ".css") {
        return "text/css";
    } else if (ext == ".js") {
        return "application/javascript";
    } else if (ext == ".ico") {
        return "image/x-icon";
    }

    return "application/octet-stream";
}

std::string PageServer::readFile(const std::string& filepath)
{
    std::ifstream file(filepath, std::ios::binary);
    if (!file) {
        return "";
    }

    std::ostringstream contents;
    contents << file.rdbuf();
    return contents.str();
}

bool PageServer::isSafePath(const std::string& path)
{
    // Check for directory traversal attempts
    if (path.find("..") != std::string::npos) {
        return false;
    }

    // Check for absolute paths
    if (!path.empty() && path[0] == '/') {
        // Allow paths starting with / but we'll strip it
        return true;
    }

    return true;
}

std::string PageServer::serve(const std::string& url)
{
    // Strip query string if present
    std::string path = url;
    size_t query_pos = path.find('?');
    if (query_pos != std::string::npos) {
        path = path.substr(0, query_pos);
    }

    // Security check
    if (!isSafePath(path)) {
        logger().warn("[PageServer] Unsafe path requested: %s", path.c_str());
        return notFound(url);
    }

    // Build the full file path
    std::string filepath = base_path;

    // Handle root request
    if (path == "/" || path.empty()) {
        filepath += "/index.html";
    } else if (path[0] == '/') {
        filepath += path;
    } else {
        filepath += "/" + path;
    }

    // Check if file exists
    struct stat file_stat;
    if (stat(filepath.c_str(), &file_stat) != 0) {
        // File doesn't exist
        logger().debug("[PageServer] File not found: %s", filepath.c_str());
        return notFound(url);
    }

    // Make sure it's a regular file, not a directory
    if (!S_ISREG(file_stat.st_mode)) {
        // If it's a directory, try index.html
        if (S_ISDIR(file_stat.st_mode)) {
            filepath += "/index.html";
            if (stat(filepath.c_str(), &file_stat) != 0 || !S_ISREG(file_stat.st_mode)) {
                logger().debug("[PageServer] Directory without index.html: %s", url.c_str());
                return notFound(url);
            }
        } else {
            logger().debug("[PageServer] Not a regular file: %s", filepath.c_str());
            return notFound(url);
        }
    }

    // Read the file
    std::string content = readFile(filepath);
    if (content.empty()) {
        logger().warn("[PageServer] Could not read file: %s", filepath.c_str());
        return notFound(url);
    }

    // Build HTTP response
    std::string mime_type = getMimeType(filepath);

    std::ostringstream response;
    response << "HTTP/1.1 200 OK\r\n";
    response << "Server: CogServer\r\n";
    response << "Content-Type: " << mime_type << "\r\n";
    response << "Content-Length: " << content.length() << "\r\n";
    response << "\r\n";
    response << content;

    logger().debug("[PageServer] Served %s (%s, %zu bytes)",
                  url.c_str(), mime_type.c_str(), content.length());

    return response.str();
}

std::string PageServer::notFound(const std::string& url)
{
    std::string body =
        "<!DOCTYPE html>\n"
        "<html lang=\"en\">\n"
        "<head><meta charset=\"UTF-8\"></head>\n"
        "<body><h1>404 Not Found</h1>\n"
        "The Cogserver doesn't know about " + url + "\n"
        "<p>The <a href=\"/stats\">stats page is here</a>.\n"
        "<p>The <a href=\"/websockets/json-test.html\">JSON test page is here</a>.\n"
        "<p>The <a href=\"/websockets/demo.html\">websockets demo is here</a>.\n"
        "<p>The <a href=\"/visualizer/\">visualizer is here</a>.\n"
        "</body></html>\n";

    std::ostringstream response;
    response << "HTTP/1.1 404 Not Found\r\n";
    response << "Server: CogServer\r\n";
    response << "Content-Type: text/html\r\n";
    response << "Content-Length: " << body.length() << "\r\n";
    response << "\r\n";
    response << body;

    return response.str();
}