|
|
@@ -6,6 +6,7 @@
|
|
|
#include "common/string_utils.hpp"
|
|
|
#include "proto/runner.grpc.pb.h"
|
|
|
#include <grpcpp/grpcpp.h>
|
|
|
+#include <openssl/crypto.h>
|
|
|
#include <cctype>
|
|
|
#include <optional>
|
|
|
#include <sstream>
|
|
|
@@ -41,6 +42,7 @@ WebhookController::WebhookController(storage::StorageClient& storage,
|
|
|
WebSocketServer& ws_server,
|
|
|
nodes::NodeStore& node_store,
|
|
|
WorkflowScheduler& scheduler,
|
|
|
+ auth::JwtUtils& jwt_utils,
|
|
|
DispatchConfig dispatch_config)
|
|
|
: storage_(storage)
|
|
|
, registry_(registry)
|
|
|
@@ -48,6 +50,7 @@ WebhookController::WebhookController(storage::StorageClient& storage,
|
|
|
, ws_server_(ws_server)
|
|
|
, node_store_(node_store)
|
|
|
, scheduler_(scheduler)
|
|
|
+ , jwt_utils_(jwt_utils)
|
|
|
, dispatch_queue_capacity_(dispatch_config.queue_capacity) {
|
|
|
dispatch_workers_.reserve(dispatch_config.threads);
|
|
|
for (std::size_t i = 0; i < dispatch_config.threads; ++i) {
|
|
|
@@ -218,6 +221,34 @@ void WebhookController::handleWebhook(const httplib::Request& req, httplib::Resp
|
|
|
sendError(res, "Path not found", 404);
|
|
|
return;
|
|
|
}
|
|
|
+
|
|
|
+ const std::string form_password = form_config.value("password", "");
|
|
|
+ if (!form_password.empty() && !formCookieValid(req, workflow_id)) {
|
|
|
+ const std::string action = "/webhook/" + workflow_id + path;
|
|
|
+ std::string attempt;
|
|
|
+ if (req.method == "POST" && req.has_param("__form_password")) {
|
|
|
+ attempt = req.get_param_value("__form_password");
|
|
|
+ }
|
|
|
+ if (!attempt.empty() && attempt.size() == form_password.size() &&
|
|
|
+ CRYPTO_memcmp(attempt.data(), form_password.data(), attempt.size()) == 0) {
|
|
|
+ const int64_t expires_at = TimeUtils::nowMs() + 3600 * 1000;
|
|
|
+ res.set_header("Set-Cookie",
|
|
|
+ "sb_form_" + workflow_id + "=" + formCookieToken(workflow_id, expires_at) +
|
|
|
+ "; Path=/webhook/" + workflow_id + "; HttpOnly; SameSite=Lax; Max-Age=3600");
|
|
|
+ res.status = 200;
|
|
|
+ res.set_content(form_renderer::renderForm(form_config, action, ""),
|
|
|
+ "text/html; charset=utf-8");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ res.status = 200;
|
|
|
+ // Deliberately generic: a message naming the form would confirm that
|
|
|
+ // one exists at this URL to somebody guessing.
|
|
|
+ res.set_content(form_renderer::renderPasswordPrompt(
|
|
|
+ action, attempt.empty() ? "" : "That did not work."),
|
|
|
+ "text/html; charset=utf-8");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
if (req.method == "GET") {
|
|
|
handleFormGet(req, res, form_node.value(), workflow_id, path);
|
|
|
return;
|
|
|
@@ -644,6 +675,43 @@ void WebhookController::handleFormGet(const httplib::Request& req, httplib::Resp
|
|
|
res.set_content(form_renderer::renderForm(config, action, ""), "text/html; charset=utf-8");
|
|
|
}
|
|
|
|
|
|
+std::string WebhookController::formCookieToken(const std::string& workflow_id, int64_t expires_at) {
|
|
|
+ // A signed token rather than the password: the password never reaches the
|
|
|
+ // browser, and a stolen cookie opens one form until it expires.
|
|
|
+ const std::string payload = workflow_id + ":" + std::to_string(expires_at);
|
|
|
+ return payload + ":" + jwt_utils_.signDetached(payload);
|
|
|
+}
|
|
|
+
|
|
|
+bool WebhookController::formCookieValid(const httplib::Request& req, const std::string& workflow_id) {
|
|
|
+ const std::string cookie_header = req.get_header_value("Cookie");
|
|
|
+ const std::string key = "sb_form_" + workflow_id + "=";
|
|
|
+ const auto at = cookie_header.find(key);
|
|
|
+ if (at == std::string::npos) return false;
|
|
|
+
|
|
|
+ std::string value = cookie_header.substr(at + key.size());
|
|
|
+ const auto end = value.find(';');
|
|
|
+ if (end != std::string::npos) value = value.substr(0, end);
|
|
|
+
|
|
|
+ const auto first = value.find(':');
|
|
|
+ const auto second = value.rfind(':');
|
|
|
+ if (first == std::string::npos || second == first) return false;
|
|
|
+
|
|
|
+ const std::string payload = value.substr(0, second);
|
|
|
+ const std::string signature = value.substr(second + 1);
|
|
|
+ if (!jwt_utils_.verifyDetached(payload, signature)) return false;
|
|
|
+
|
|
|
+ const std::string id = payload.substr(0, first);
|
|
|
+ if (id != workflow_id) return false;
|
|
|
+
|
|
|
+ int64_t expires_at = 0;
|
|
|
+ try {
|
|
|
+ expires_at = std::stoll(payload.substr(first + 1));
|
|
|
+ } catch (const std::exception&) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return TimeUtils::nowMs() < expires_at;
|
|
|
+}
|
|
|
+
|
|
|
bool WebhookController::buildFormTriggerData(const httplib::Request& req,
|
|
|
const nlohmann::json& config,
|
|
|
nlohmann::json& out, std::string& error) {
|