diff --git a/CHANGELOG.md b/CHANGELOG.md index 1724b70b..f609cd78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,11 @@ Merged since v2.5.0, not yet released. the wheel. ([#154]) - Free-threaded (`cp314t`) wheels are no longer published, because the core is not yet thread-safe; CPython 3.14 is declared supported. ([#155]) +- `listenHTTP[]` binds to `localhost` by default and to the host given in + `listenHTTP[host:port]`; use `listenHTTP[0.0.0.0:8000]` for the network, + which prints a warning. + `systemExec` is refused over HTTP, and the file server stays inside the run + directory and `FOREFIREHOME/tools/htdocs`. ## [v2.5.0] — 2026-08-11 diff --git a/src/Command.cpp b/src/Command.cpp index d2509161..2e02f540 100644 --- a/src/Command.cpp +++ b/src/Command.cpp @@ -29,6 +29,7 @@ namespace libforefire double Command::endTime = 0; bool Command::firstCommand = true; + thread_local bool Command::remoteCommand = false; size_t Command::refTabs = 0; FFPoint *Command::lastReadLoc = 0; @@ -1184,6 +1185,13 @@ namespace libforefire } int Command::systemExec(const string &arg, size_t &numTabs) { + // No shell commands from the network, even through include[] or @t=. + if (remoteCommand) + { + cout << "systemExec is not available over HTTP" << endl; + return error; + } + SimulationParameters *simParam = SimulationParameters::GetInstance(); string finalStr; @@ -3373,7 +3381,9 @@ namespace libforefire std::cout.rdbuf(captured.rdbuf()); std::string fireCommand = cmd; // Execute the command. (Assuming ExecuteCommand is defined elsewhere.) + remoteCommand = true; ExecuteCommand(fireCommand); + remoteCommand = false; std::cout.rdbuf(oldbuf); return captured.str(); @@ -3407,14 +3417,10 @@ namespace libforefire } } - char hostname[256]; - if (gethostname(hostname, sizeof(hostname)) != 0) - { - cout << "Cannot get hostname info " << endl; - return error; - } - - std::string address = arg.empty() ? std::string(hostname) + ":" + std::to_string(port) : arg; + // Local only by default; listenHTTP[0.0.0.0:8000] to open it to the network. + std::string address = arg.empty() ? defaultHostname + ":" + std::to_string(port) : arg; + if (address.compare(0, 5, "host=") == 0) + address = address.substr(5); // Allocate a new HTTP server if none exists. if (currentSession.server == 0) @@ -3434,7 +3440,7 @@ namespace libforefire std::ofstream infoFile("httpConnectInfo.txt"); if (infoFile) { - infoFile << hostname << ":" << port << std::endl; + infoFile << address << std::endl; infoFile.close(); } else diff --git a/src/Command.h b/src/Command.h index bd0f8b36..7387fc55 100644 --- a/src/Command.h +++ b/src/Command.h @@ -232,6 +232,9 @@ class Command { public: + // True while running a command received over HTTP (or scheduled by one) + static thread_local bool remoteCommand; + // Reference time static double refTime; diff --git a/src/EventCommand.cpp b/src/EventCommand.cpp index ca686902..099a4d87 100644 --- a/src/EventCommand.cpp +++ b/src/EventCommand.cpp @@ -19,6 +19,7 @@ namespace libforefire{ EventCommand::EventCommand(string scommand, double schedueledTime) : ForeFireAtom(schedueledTime) { schedueledCommand = scommand; + remote = Command::remoteCommand; }; @@ -29,7 +30,10 @@ EventCommand::~EventCommand() {}; void EventCommand::update(){ + bool caller = Command::remoteCommand; + Command::remoteCommand = remote; Command::ExecuteCommand(schedueledCommand); + Command::remoteCommand = caller; } void EventCommand::timeAdvance(){ setUpdateTime(numeric_limits::infinity()); diff --git a/src/EventCommand.h b/src/EventCommand.h index e2d947b2..d3fb6ae1 100644 --- a/src/EventCommand.h +++ b/src/EventCommand.h @@ -25,10 +25,11 @@ namespace libforefire{ class EventCommand: public ForeFireAtom { string schedueledCommand; + bool remote; // scheduled by a command received over HTTP public: /*! \brief Default constructor */ - EventCommand() : ForeFireAtom(0.) {}; + EventCommand() : ForeFireAtom(0.), remote(false) {}; /*! \brief standard constructor */ EventCommand( string, double ) ; /*! \brief Default destructor */ diff --git a/src/HttpCommandServer.hpp b/src/HttpCommandServer.hpp index 5708462a..134d0821 100644 --- a/src/HttpCommandServer.hpp +++ b/src/HttpCommandServer.hpp @@ -23,6 +23,7 @@ #include #include #include +#include #include namespace http_command { @@ -47,6 +48,20 @@ namespace http_command { return false; } int port = std::stoi(address.substr(colon + 1)); + std::string host = address.substr(0, colon); + + // Bind to the requested host only (0.0.0.0 for all interfaces). + addrinfo hints; + std::memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_STREAM; + addrinfo *resolved = nullptr; + if (getaddrinfo(host.c_str(), nullptr, &hints, &resolved) != 0 || resolved == nullptr) { + std::cerr << "Cannot resolve host " << host << std::endl; + return false; + } + in_addr bindAddr = reinterpret_cast(resolved->ai_addr)->sin_addr; + freeaddrinfo(resolved); server_fd = socket(AF_INET, SOCK_STREAM, 0); if (server_fd < 0) { @@ -59,7 +74,7 @@ namespace http_command { sockaddr_in addr; std::memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; - addr.sin_addr.s_addr = INADDR_ANY; + addr.sin_addr = bindAddr; addr.sin_port = htons(port); if (::bind(server_fd, reinterpret_cast(&addr), sizeof(addr)) < 0) { perror("bind"); @@ -73,10 +88,18 @@ namespace http_command { server_thread = std::thread(&HttpCommandServer::acceptLoop, this); std::cout << "\033[35m" // start color - << "ForeFire HTTP command server listening at http://localhost:" << port + << "ForeFire HTTP command server listening at http://" << host << ":" << port << "\033[0m" << std::endl; + if ((ntohl(bindAddr.s_addr) >> 24) != 127) { + std::cout + << "\033[31m" // start red + << "WARNING: development server, reachable from the network. Anyone who can reach this port can run ForeFire commands. Use listenHTTP[localhost:" << port << "] unless you need remote access." + << "\033[0m" + << std::endl; + } + std::cout << "\033[33m" // start yellow << "To stop the server, press Ctrl-C or type quit in the ForeFire shell." @@ -269,6 +292,10 @@ namespace http_command { if (!path.empty() && path[0] == '/') path = path.substr(1); + // Only files under the run directory or FOREFIREHOME/tools/htdocs. + if ((!path.empty() && path[0] == '/') || ("/" + path + "/").find("/../") != std::string::npos) + return buildSimpleResponse("403 Forbidden", "Forbidden path: " + uri); + // For an empty path, try current directory's index.html; // if not available, check the FOREFIREHOME environment variable. if (path.empty()) {