|
|
@@ -3,6 +3,7 @@
|
|
|
#include "logging/logger.hpp"
|
|
|
#include "common/time_utils.hpp"
|
|
|
#include "common/uuid.hpp"
|
|
|
+#include "common/string_utils.hpp"
|
|
|
#include "proto/runner.grpc.pb.h"
|
|
|
#include <grpcpp/grpcpp.h>
|
|
|
#include <cctype>
|
|
|
@@ -62,6 +63,7 @@ void WebhookController::handleWebhook(const httplib::Request& req, httplib::Resp
|
|
|
// A form is one node answering two verbs - GET renders it, POST submits it -
|
|
|
// which the method-to-node-type mapping below cannot express, so it is
|
|
|
// matched first.
|
|
|
+ nlohmann::json form_trigger_data;
|
|
|
auto form_node = findFormNode(workflow);
|
|
|
if (form_node.has_value()) {
|
|
|
const auto form_config = form_node->value("config", nlohmann::json::object());
|
|
|
@@ -74,6 +76,18 @@ void WebhookController::handleWebhook(const httplib::Request& req, httplib::Resp
|
|
|
handleFormGet(req, res, form_node.value(), workflow_id, path);
|
|
|
return;
|
|
|
}
|
|
|
+ if (req.method == "POST") {
|
|
|
+ nlohmann::json form_data;
|
|
|
+ std::string error;
|
|
|
+ if (!buildFormTriggerData(req, form_config, form_data, error)) {
|
|
|
+ res.status = 400;
|
|
|
+ const std::string action = "/webhook/" + workflow_id + path;
|
|
|
+ res.set_content(form_renderer::renderForm(form_config, action, error),
|
|
|
+ "text/html; charset=utf-8");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ form_trigger_data = form_data; // consumed below, in place of the JSON body
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
// Find matching HTTP trigger node for this method
|
|
|
@@ -150,7 +164,10 @@ void WebhookController::handleWebhook(const httplib::Request& req, httplib::Resp
|
|
|
for (const auto& [key, value] : req.headers) {
|
|
|
trigger_data["headers"][key] = value;
|
|
|
}
|
|
|
- if (!req.body.empty()) {
|
|
|
+ if (!form_trigger_data.is_null()) {
|
|
|
+ trigger_data["form"] = form_trigger_data["form"];
|
|
|
+ trigger_data["submittedAt"] = form_trigger_data["submittedAt"];
|
|
|
+ } else if (!req.body.empty()) {
|
|
|
try {
|
|
|
trigger_data["body"] = nlohmann::json::parse(req.body);
|
|
|
} catch (...) {
|
|
|
@@ -373,6 +390,94 @@ void WebhookController::handleFormGet(const httplib::Request& req, httplib::Resp
|
|
|
res.set_content(form_renderer::renderForm(config, action, ""), "text/html; charset=utf-8");
|
|
|
}
|
|
|
|
|
|
+bool WebhookController::buildFormTriggerData(const httplib::Request& req,
|
|
|
+ const nlohmann::json& config,
|
|
|
+ nlohmann::json& out, std::string& error) {
|
|
|
+ nlohmann::json form = nlohmann::json::object();
|
|
|
+ const auto fields = config.value("fields", nlohmann::json::array());
|
|
|
+
|
|
|
+ for (const auto& f : fields) {
|
|
|
+ const std::string name = f.value("name", "");
|
|
|
+ if (name.empty()) continue;
|
|
|
+ const std::string type = f.value("type", "text");
|
|
|
+ const std::string label = f.value("label", name);
|
|
|
+ const bool required = f.value("required", false);
|
|
|
+
|
|
|
+ if (type == "file") {
|
|
|
+ if (!req.has_file(name)) {
|
|
|
+ if (required) {
|
|
|
+ error = label + " is required.";
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ const auto file = req.get_file_value(name);
|
|
|
+
|
|
|
+ // A per-field ceiling under the server-wide one from Task 2. The
|
|
|
+ // server limit has already refused anything larger than itself, so
|
|
|
+ // this only tightens, never loosens.
|
|
|
+ const double max_mb = f.value("maxSizeMb", 0.0);
|
|
|
+ if (max_mb > 0 && static_cast<double>(file.content.size()) > max_mb * 1024 * 1024) {
|
|
|
+ error = label + " is larger than the " + std::to_string(static_cast<int>(max_mb)) +
|
|
|
+ " MB limit for this field.";
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ const std::string b64 = common::StringUtils::base64Encode(file.content);
|
|
|
+ form[name] = {
|
|
|
+ {"type", "binary"},
|
|
|
+ {"data", b64},
|
|
|
+ {"mimeType", file.content_type.empty() ? "application/octet-stream" : file.content_type},
|
|
|
+ {"filename", file.filename},
|
|
|
+ {"size", file.content.size()},
|
|
|
+ {"checksum", common::StringUtils::sha256Hex(b64)}
|
|
|
+ };
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // cpp-httplib only fills req.params for a urlencoded body; a
|
|
|
+ // multipart/form-data body (required once any field is a file) puts
|
|
|
+ // every non-file part into req.files instead, keyed by name with an
|
|
|
+ // empty filename. Both sources have to be checked or a text field
|
|
|
+ // submitted alongside an upload reads back empty.
|
|
|
+ std::string value;
|
|
|
+ if (req.has_param(name.c_str())) {
|
|
|
+ value = req.get_param_value(name.c_str());
|
|
|
+ } else if (req.has_file(name)) {
|
|
|
+ value = req.get_file_value(name).content;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (type == "checkbox") {
|
|
|
+ form[name] = !value.empty();
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if (value.empty()) {
|
|
|
+ if (required) {
|
|
|
+ error = label + " is required.";
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ form[name] = (type == "number") ? nlohmann::json(nullptr) : nlohmann::json("");
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if (type == "number") {
|
|
|
+ try {
|
|
|
+ form[name] = std::stod(value);
|
|
|
+ } catch (const std::exception&) {
|
|
|
+ error = label + " must be a number.";
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ form[name] = value;
|
|
|
+ }
|
|
|
+
|
|
|
+ out = nlohmann::json::object();
|
|
|
+ out["form"] = form;
|
|
|
+ out["submittedAt"] = TimeUtils::nowMs();
|
|
|
+ out["clientIp"] = req.remote_addr;
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
bool WebhookController::validateApiKey(
|
|
|
const httplib::Request& req,
|
|
|
const nlohmann::json& config) {
|