mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2025-12-06 15:59:52 +03:00
Refactor MQTT subscriptions #173
This commit is contained in:
@@ -35,19 +35,15 @@ WebAPIService::WebAPIService(AsyncWebServer * server, SecurityManager * security
|
||||
// GET /{device}/{name}
|
||||
// GET /device={device}?cmd={name}?data={value}[?id={hc}]
|
||||
void WebAPIService::webAPIService_get(AsyncWebServerRequest * request) {
|
||||
// initialize parameters. These will be extracted from the URL
|
||||
std::string device_s("");
|
||||
std::string cmd_s("");
|
||||
std::string value_s("");
|
||||
int id = -1;
|
||||
|
||||
parse(request, device_s, cmd_s, id, value_s);
|
||||
// has no body JSON so create dummy as empty object
|
||||
StaticJsonDocument<EMSESP_JSON_SIZE_SMALL> input_doc;
|
||||
JsonObject input = input_doc.to<JsonObject>();
|
||||
parse(request, input);
|
||||
}
|
||||
|
||||
// For POSTS with an optional JSON body
|
||||
// HTTP_POST | HTTP_PUT | HTTP_PATCH
|
||||
// POST /{device}[/{hc|id}][/{name}]
|
||||
// the body must have 'value'. Optional are device, name, hc and id
|
||||
void WebAPIService::webAPIService_post(AsyncWebServerRequest * request, JsonVariant & json) {
|
||||
// if no body then treat it as a secure GET
|
||||
if (not json.is<JsonObject>()) {
|
||||
@@ -56,282 +52,68 @@ void WebAPIService::webAPIService_post(AsyncWebServerRequest * request, JsonVari
|
||||
}
|
||||
|
||||
// extract values from the json. these will be used as default values
|
||||
auto && body = json.as<JsonObject>();
|
||||
|
||||
#if defined(EMSESP_STANDALONE)
|
||||
Serial.println("webAPIService_post: ");
|
||||
serializeJson(body, Serial);
|
||||
Serial.println();
|
||||
#endif
|
||||
|
||||
// make sure we have a value. There must always be a value
|
||||
if (!body.containsKey(F_(value))) {
|
||||
send_message_response(request, 400, "Problems parsing JSON, missing value"); // Bad Request
|
||||
return;
|
||||
}
|
||||
|
||||
std::string value_s = body["value"].as<std::string>(); // always convert value to string
|
||||
std::string device_s = body["device"].as<std::string>();
|
||||
|
||||
// get the command. It can be either 'name' or 'cmd'
|
||||
std::string cmd_s("");
|
||||
if (body.containsKey("name")) {
|
||||
cmd_s = body["name"].as<std::string>();
|
||||
} else if (body.containsKey("cmd")) {
|
||||
cmd_s = body["cmd"].as<std::string>();
|
||||
}
|
||||
|
||||
// for id, it can be part of the hc or id keys in the json body
|
||||
int id = -1;
|
||||
if (body.containsKey("id")) {
|
||||
id = body["id"];
|
||||
} else if (body.containsKey("hc")) {
|
||||
id = body["hc"];
|
||||
} else {
|
||||
id = -1;
|
||||
}
|
||||
|
||||
// now parse the URL. The URL is always leading and will overwrite anything provided in the json body
|
||||
parse(request, device_s, cmd_s, id, value_s); // pass it defaults
|
||||
auto && input = json.as<JsonObject>();
|
||||
parse(request, input);
|
||||
}
|
||||
|
||||
// parse the URL looking for query or path parameters
|
||||
// reporting back any errors
|
||||
void WebAPIService::parse(AsyncWebServerRequest * request, std::string & device_s, std::string & cmd_s, int id, std::string & value_s) {
|
||||
// parse URL for the path names
|
||||
SUrlParser p;
|
||||
|
||||
p.parse(request->url().c_str());
|
||||
|
||||
// remove the /api from the path
|
||||
if (p.paths().front() == "api") {
|
||||
p.paths().erase(p.paths().begin());
|
||||
} else {
|
||||
return; // bad URL
|
||||
}
|
||||
|
||||
uint8_t device_type;
|
||||
int8_t id_n = -1; // default hc
|
||||
|
||||
// check for query parameters first, the old style from v2
|
||||
// /device={device}?cmd={name}?data={value}[?id={hc}
|
||||
if (p.paths().size() == 0) {
|
||||
// get the device
|
||||
if (request->hasParam(F_(device))) {
|
||||
device_s = request->getParam(F_(device))->value().c_str();
|
||||
}
|
||||
|
||||
// get cmd
|
||||
if (request->hasParam(F_(cmd))) {
|
||||
cmd_s = request->getParam(F_(cmd))->value().c_str();
|
||||
}
|
||||
|
||||
// get data, which is optional. This is now replaced with the name 'value' in JSON body
|
||||
if (request->hasParam(F_(data))) {
|
||||
value_s = request->getParam(F_(data))->value().c_str();
|
||||
}
|
||||
if (request->hasParam(F_(value))) {
|
||||
value_s = request->getParam(F_(value))->value().c_str();
|
||||
}
|
||||
|
||||
// get id (or hc), which is optional
|
||||
if (request->hasParam(F_(id))) {
|
||||
id_n = Helpers::atoint(request->getParam(F_(id))->value().c_str());
|
||||
}
|
||||
if (request->hasParam("hc")) {
|
||||
id_n = Helpers::atoint(request->getParam("hc")->value().c_str());
|
||||
}
|
||||
} else {
|
||||
// parse paths and json data from the newer OpenAPI standard
|
||||
// [/{device}][/{hc}][/{name}]
|
||||
// all paths are optional. If not set then take the values from the json body (if available)
|
||||
|
||||
// see if we have a device in the path
|
||||
size_t num_paths = p.paths().size();
|
||||
if (num_paths) {
|
||||
// assume the next path is the 'device'. Note this could also have the value of system.
|
||||
device_s = p.paths().front();
|
||||
|
||||
if (num_paths == 2) {
|
||||
// next path is the name or cmd
|
||||
cmd_s = p.paths()[1];
|
||||
} else if (num_paths > 2) {
|
||||
// check in Command::find_command makes prefix to TAG
|
||||
cmd_s = p.paths()[1] + "/" + p.paths()[2];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// device checks
|
||||
if (device_s.empty()) {
|
||||
// see if we have a device embedded in the json body, then use that
|
||||
send_message_response(request, 422, "Missing device"); // Unprocessable Entity
|
||||
return;
|
||||
}
|
||||
|
||||
device_type = EMSdevice::device_name_2_device_type(device_s.c_str());
|
||||
if (device_type == EMSdevice::DeviceType::UNKNOWN) {
|
||||
send_message_response(request, 422, "Invalid call"); // Unprocessable Entity
|
||||
return;
|
||||
}
|
||||
|
||||
// check that we have permissions first. We require authenticating on 1 or more of these conditions:
|
||||
// 1. any HTTP POSTs or PUTs
|
||||
// 2. an HTTP GET which has a 'data' parameter which is not empty (to keep v2 compatibility)
|
||||
void WebAPIService::parse(AsyncWebServerRequest * request, JsonObject & input) {
|
||||
auto method = request->method();
|
||||
bool have_data = !value_s.empty();
|
||||
bool authenticated = false;
|
||||
|
||||
EMSESP::webSettingsService.read([&](WebSettings & settings) {
|
||||
Authentication authentication = _securityManager->authenticateRequest(request);
|
||||
authenticated = settings.notoken_api | AuthenticationPredicates::IS_ADMIN(authentication);
|
||||
});
|
||||
|
||||
if ((method != HTTP_GET) || ((method == HTTP_GET) && have_data)) {
|
||||
if (!authenticated) {
|
||||
send_message_response(request, 401, "Bad credentials"); // Unauthorized
|
||||
return;
|
||||
if (method == HTTP_GET) {
|
||||
// special case if there is no command, then default to 'info'
|
||||
if (!input.size()) {
|
||||
input["cmd"] = "info";
|
||||
}
|
||||
} else {
|
||||
// if its a POST then check authentication
|
||||
EMSESP::webSettingsService.read([&](WebSettings & settings) {
|
||||
Authentication authentication = _securityManager->authenticateRequest(request);
|
||||
authenticated = settings.notoken_api | AuthenticationPredicates::IS_ADMIN(authentication);
|
||||
});
|
||||
}
|
||||
|
||||
// output json buffer
|
||||
PrettyAsyncJsonResponse * response = new PrettyAsyncJsonResponse(false, EMSESP_JSON_SIZE_XXLARGE_DYN);
|
||||
JsonObject json = response->getRoot();
|
||||
JsonObject output = response->getRoot();
|
||||
|
||||
// now we have all the parameters go and execute the command
|
||||
// the function will also determine if authentication is needed to execute its command
|
||||
uint8_t cmd_reply = Command::call(device_type, cmd_s.c_str(), (have_data ? value_s.c_str() : nullptr), authenticated, id_n, json);
|
||||
// call command
|
||||
uint8_t command_ret = Command::process(request->url().c_str(), authenticated, input, output);
|
||||
|
||||
// check for errors
|
||||
if (cmd_reply == CommandRet::NOT_FOUND) {
|
||||
delete response;
|
||||
send_message_response(request, 400, "Command not found"); // Bad Request
|
||||
return;
|
||||
} else if (cmd_reply == CommandRet::NOT_ALLOWED) {
|
||||
delete response;
|
||||
send_message_response(request, 401, "Bad credentials"); // Unauthorized
|
||||
return;
|
||||
} else if (cmd_reply != CommandRet::OK) {
|
||||
delete response;
|
||||
send_message_response(request, 400, "Problems parsing elements"); // Bad Request
|
||||
return;
|
||||
}
|
||||
|
||||
if (!json.size()) {
|
||||
delete response;
|
||||
send_message_response(request, 200, "OK"); // OK
|
||||
return;
|
||||
// handle response codes
|
||||
// the output will be populated with a message key if an error occurred
|
||||
int ret_code;
|
||||
if (command_ret == CommandRet::NOT_ALLOWED) {
|
||||
ret_code = 401; // Unauthorized
|
||||
} else if (command_ret == CommandRet::NOT_FOUND) {
|
||||
ret_code = 400; // Bad request
|
||||
} else if (command_ret == CommandRet::OK) {
|
||||
ret_code = 200; //OK
|
||||
if (output.isNull()) {
|
||||
output["message"] = "OK"; // only add if there is no json output already
|
||||
}
|
||||
} else {
|
||||
ret_code = 400; // Bad request
|
||||
}
|
||||
|
||||
// send the json that came back from the command call
|
||||
response->setCode(ret_code);
|
||||
response->setLength();
|
||||
response->setContentType("application/json");
|
||||
request->send(response); // send json response
|
||||
|
||||
#if defined(EMSESP_STANDALONE)
|
||||
Serial.print(COLOR_YELLOW);
|
||||
if (json.size() != 0) {
|
||||
serializeJsonPretty(json, Serial);
|
||||
Serial.print("return code: ");
|
||||
Serial.println(ret_code);
|
||||
if (output.size() != 0) {
|
||||
serializeJsonPretty(output, Serial);
|
||||
}
|
||||
Serial.println();
|
||||
Serial.print(COLOR_RESET);
|
||||
#endif
|
||||
}
|
||||
|
||||
// send a HTTP error back, with optional JSON body data
|
||||
void WebAPIService::send_message_response(AsyncWebServerRequest * request, uint16_t error_code, const char * message) {
|
||||
if (message == nullptr) {
|
||||
AsyncWebServerResponse * response = request->beginResponse(error_code); // just send the code
|
||||
request->send(response);
|
||||
} else {
|
||||
// build a return message and send it
|
||||
PrettyAsyncJsonResponse * response = new PrettyAsyncJsonResponse(false, EMSESP_JSON_SIZE_SMALL);
|
||||
JsonObject json = response->getRoot();
|
||||
json["message"] = message;
|
||||
response->setCode(error_code);
|
||||
response->setLength();
|
||||
response->setContentType("application/json");
|
||||
request->send(response);
|
||||
}
|
||||
|
||||
EMSESP::logger().debug(F("API return code: %d, message: %s"), error_code, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract only the path component from the passed URI and normalized it.
|
||||
* Ex. //one/two////three/// becomes /one/two/three
|
||||
*/
|
||||
std::string SUrlParser::path() {
|
||||
std::string s = "/"; // set up the beginning slash
|
||||
for (std::string & f : m_folders) {
|
||||
s += f;
|
||||
s += "/";
|
||||
}
|
||||
s.pop_back(); // deleting last letter, that is slash '/'
|
||||
return std::string(s);
|
||||
}
|
||||
|
||||
SUrlParser::SUrlParser(const char * uri) {
|
||||
parse(uri);
|
||||
}
|
||||
|
||||
bool SUrlParser::parse(const char * uri) {
|
||||
m_folders.clear();
|
||||
m_keysvalues.clear();
|
||||
enum Type { begin, folder, param, value };
|
||||
std::string s;
|
||||
|
||||
const char * c = uri;
|
||||
enum Type t = Type::begin;
|
||||
std::string last_param;
|
||||
|
||||
if (c != NULL || *c != '\0') {
|
||||
do {
|
||||
if (*c == '/') {
|
||||
if (s.length() > 0) {
|
||||
m_folders.push_back(s);
|
||||
s.clear();
|
||||
}
|
||||
t = Type::folder;
|
||||
} else if (*c == '?' && (t == Type::folder || t == Type::begin)) {
|
||||
if (s.length() > 0) {
|
||||
m_folders.push_back(s);
|
||||
s.clear();
|
||||
}
|
||||
t = Type::param;
|
||||
} else if (*c == '=' && (t == Type::param || t == Type::begin)) {
|
||||
m_keysvalues[s] = "";
|
||||
last_param = s;
|
||||
s.clear();
|
||||
t = Type::value;
|
||||
} else if (*c == '&' && (t == Type::value || t == Type::param || t == Type::begin)) {
|
||||
if (t == Type::value) {
|
||||
m_keysvalues[last_param] = s;
|
||||
} else if ((t == Type::param || t == Type::begin) && (s.length() > 0)) {
|
||||
m_keysvalues[s] = "";
|
||||
last_param = s;
|
||||
}
|
||||
t = Type::param;
|
||||
s.clear();
|
||||
} else if (*c == '\0' && s.length() > 0) {
|
||||
if (t == Type::value) {
|
||||
m_keysvalues[last_param] = s;
|
||||
} else if (t == Type::folder || t == Type::begin) {
|
||||
m_folders.push_back(s);
|
||||
} else if (t == Type::param) {
|
||||
m_keysvalues[s] = "";
|
||||
last_param = s;
|
||||
}
|
||||
s.clear();
|
||||
} else if (*c == '\0' && s.length() == 0) {
|
||||
if (t == Type::param && last_param.length() > 0) {
|
||||
m_keysvalues[last_param] = "";
|
||||
}
|
||||
s.clear();
|
||||
} else {
|
||||
s += *c;
|
||||
}
|
||||
} while (*c++ != '\0');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace emsesp
|
||||
|
||||
Reference in New Issue
Block a user