Newer
Older
smart-home-server / devices / sh_core_esp8266 / src / REST_API.cpp
#include "sh_core_esp8266.h"

// ---------------------- weak device hooks (можно переопределить в прошивке) ----------------------
__attribute__((weak)) void appendStatusJsonFields(String &json) { (void)json; }
__attribute__((weak)) void appendAboutJsonFields(String &json)  { (void)json; }

__attribute__((weak)) bool deviceHandleAction(const String &action,
                                              const String &paramsJson,
                                              String &errorCode,
                                              String &errorMessage)
{
  (void)action;
  (void)paramsJson;
  errorCode = "IllegalActionOrParams";
  errorMessage = "Action not implemented";
  return false;
}

__attribute__((weak)) void deviceHandleReset() {}

// ---------------------- helpers ----------------------
static String deviceModeToString(DeviceMode m) {
  switch (m) {
    case DEVICE_MODE_SETUP:    return F("setup");
    case DEVICE_MODE_NORMAL:   return F("normal");
    case DEVICE_MODE_ERROR:    return F("error");
    case DEVICE_MODE_UPDATING: return F("updating");
  }
  return F("error");
}

static void sendJson(int code, const String &body) {
  server.sendHeader(F("Connection"), F("close"));
  server.sendHeader(F("Content-Length"), String(body.length()));
  server.send(code, F("application/json; charset=utf-8"), body);
  server.client().stop();
}

static String extractJsonStringValue(const String &body, const String &key) {
  String pattern = "\"" + key + "\"";
  int keyIndex = body.indexOf(pattern);
  if (keyIndex < 0) return "";

  int colonIndex = body.indexOf(':', keyIndex);
  if (colonIndex < 0) return "";

  int firstQuote = body.indexOf('"', colonIndex + 1);
  if (firstQuote < 0) return "";

  int secondQuote = body.indexOf('"', firstQuote + 1);
  if (secondQuote < 0) return "";

  return body.substring(firstQuote + 1, secondQuote);
}

static String extractJsonObject(const String &body, const String &key) {
  String pattern = "\"" + key + "\"";
  int keyIndex = body.indexOf(pattern);
  if (keyIndex < 0) return "";

  int colonIndex = body.indexOf(':', keyIndex);
  if (colonIndex < 0) return "";

  int braceIndex = body.indexOf('{', colonIndex);
  if (braceIndex < 0) return "";

  int depth = 0;
  for (int i = braceIndex; i < (int)body.length(); i++) {
    char c = body[i];
    if (c == '{') depth++;
    else if (c == '}') {
      depth--;
      if (depth == 0) {
        return body.substring(braceIndex, i + 1);
      }
    }
  }
  return "";
}

// ---------------------- auth ----------------------
static bool hasValidToken() {
  if (authToken.length() == 0) return false;

  String header = server.header(F("Authorization"));
  if (!header.startsWith(F("Bearer "))) return false;

  String token = header.substring(7);
  token.trim();
  if (token != authToken) return false;

  if (serverBaseUrl.length() > 0) {
    IPAddress remote = server.client().remoteIP();
    String ipStr = remote.toString();

    // if (ipStr != serverBaseUrl) {
    //   Serial.print(F("Auth IP mismatch: got "));
    //   Serial.print(ipStr);
    //   Serial.print(F(", expected "));
    //   Serial.println(serverBaseUrl);
    //   return false;
    // }
  }

  return true;
}

static bool requireAuth() {
  if (!hasValidToken()) {
    String json = F("{\"status\":\"error\",\"error\":\"Unauthorized\",\"message\":\"Missing or invalid token\"}");
    sendJson(401, json);
    return false;
  }
  return true;
}

static void sendNotAvailable() {
  String json = F("{\"status\":\"error\",\"error\":\"NotAvailable\",\"message\":\"Setup mode is not active\"}");
  sendJson(403, json);
}

uint8_t wifiSignalPercent() {
  if (WiFi.status() != WL_CONNECTED) return 0;

  int rssi = WiFi.RSSI(); // обычно от -100 до -40 dBm

  if (rssi <= -100) return 0;
  if (rssi >= -50)  return 100;

  // линейная аппроксимация
  return (uint8_t)(2 * (rssi + 100));
}


// ---------------------- handlers ----------------------
static void handleAbout() {
  IPAddress ip = WiFi.localIP();

  if(deviceMode == DEVICE_MODE_SETUP) {
    serverBaseUrl = "0.0.0.0";
  }

  String json = "{";
  json += "\"device_name\":\""      + deviceName + "\",";
  json += "\"device_type\":\""      + String(DEVICE_TYPE) + "\",";
  json += "\"firmware_version\":\"" + String(FW_VERSION) + "\",";
  json += "\"core_version\":\""     + String(CORE_VERSION) + "\",";
  json += "\"device_id\":\""        + getUniqueID() + "\",";
  json += "\"server\":\""           + serverBaseUrl + "\",";
  json += "\"status\":\""           + deviceModeToString(deviceMode) + "\",";
  json += "\"ip_address\":\""       + ip.toString() + "\",";
  json += "\"mac_address\":\""      + getMAC() + "\",";
  json += "\"wifi_signal\":"        + String(wifiSignalPercent()) + ",";
  json += "\"uptime\":"             + String(millis() / 1000);

  appendAboutJsonFields(json);

  json += "}";
  sendJson(200, json);
}

static void handleStatus() {
  if (deviceMode == DEVICE_MODE_NORMAL) {
    if (!requireAuth()) return;
  }

  String json = "{";
  json += "\"status\":\"ok\"";
  appendStatusJsonFields(json);
  json += "}";

  sendJson(200, json);
}

static void handleSetDeviceName() {
  if (deviceMode != DEVICE_MODE_NORMAL) {
    String json = F("{\"status\":\"error\",\"error\":\"IllegalActionOrParams\",\"message\":\"Not in normal mode\"}");
    sendJson(400, json);
    return;
  }

  if (!requireAuth()) return;

  String body = server.arg("plain");
  body.trim();

  String newName = extractJsonStringValue(body, F("device_name"));
  newName.trim();

  if (newName.length() == 0) {
    String json = F("{\"status\":\"error\",\"error\":\"IllegalActionOrParams\",\"message\":\"device_name is required\"}");
    sendJson(400, json);
    return;
  }

  String sanitized;
  for (uint16_t i = 0; i < newName.length() && sanitized.length() < DEVICE_NAME_MAX_LEN - 1; i++) {
    char c = newName[i];
    if (c >= 32 && c != '\"') sanitized += c;
  }

  if (sanitized.length() == 0) {
    String json = F("{\"status\":\"error\",\"error\":\"IllegalActionOrParams\",\"message\":\"Invalid device_name\"}");
    sendJson(400, json);
    return;
  }

  deviceName = sanitized;
  saveDeviceConfig();

  String json = F("{\"status\":\"ok\",\"message\":\"Device name updated\"}");
  sendJson(200, json);
}

static void handleAction() {
  if (deviceMode != DEVICE_MODE_NORMAL) {
    String json = F("{\"status\":\"error\",\"error\":\"NotAvailable\",\"message\":\"Action not available in this mode\"}");
    sendJson(403, json);
    return;
  }

  if (!requireAuth()) return;

  String body = server.arg("plain");
  body.trim();

  String action = extractJsonStringValue(body, F("action"));
  if (action.length() == 0) {
    String json = F("{\"status\":\"error\",\"error\":\"IllegalActionOrParams\",\"message\":\"Action is required\"}");
    sendJson(400, json);
    return;
  }

  String paramsJson = extractJsonObject(body, F("params"));
  if (paramsJson.length() == 0) paramsJson = body;

  String errorCode;
  String errorMessage;

  bool ok = deviceHandleAction(action, paramsJson, errorCode, errorMessage);
  if (!ok) {
    if (errorCode.length() == 0)     errorCode = "IllegalActionOrParams";
    if (errorMessage.length() == 0)  errorMessage = "Device action failed";

    String json = "{\"status\":\"error\",\"error\":\"" + errorCode +
                  "\",\"message\":\"" + errorMessage + "\"}";
    sendJson(400, json);
    return;
  }

  String json = F("{\"status\":\"ok\",\"message\":\"Action executed\"}");
  sendJson(200, json);
}

static void handleSetToken() {
  String body = server.arg("plain");
  body.trim();

  String newToken = extractJsonStringValue(body, F("token"));
  if (newToken.length() == 0) {
    String json = F("{\"status\":\"error\",\"error\":\"IllegalActionOrParams\",\"message\":\"Token is required\"}");
    sendJson(400, json);
    return;
  }

  if (deviceMode == DEVICE_MODE_SETUP) {
    IPAddress remote = server.client().remoteIP();
    serverBaseUrl    = remote.toString();

    authToken  = newToken;
    deviceMode = DEVICE_MODE_NORMAL;
    saveDeviceConfig();

    String json = F("{\"status\":\"ok\",\"message\":\"Token set. Device mode: normal\"}");
    sendJson(200, json);
    return;
  }

  if (deviceMode == DEVICE_MODE_NORMAL) {
    if (!requireAuth()) return;

    authToken = newToken;
    saveDeviceConfig();

    String json = F("{\"status\":\"ok\",\"message\":\"Token updated\"}");
    sendJson(200, json);
    return;
  }

  String json = F("{\"status\":\"error\",\"error\":\"NotAvailable\",\"message\":\"Cannot set token in current mode\"}");
  sendJson(403, json);
}

static void handleRebootApi() {
  if (deviceMode != DEVICE_MODE_NORMAL) {
    String json = F("{\"status\":\"error\",\"error\":\"NotAvailable\",\"message\":\"Reboot available only in normal mode\"}");
    sendJson(403, json);
    return;
  }

  if (!requireAuth()) return;

  String json = F("{\"status\":\"ok\",\"message\":\"Device will reboot now\"}");
  sendJson(200, json);

  Serial.println(F("Reboot requested via REST API"));
  delay(500);
  ESP.restart();
}

static void handleResetApi() {
  if (deviceMode != DEVICE_MODE_NORMAL) {
    String json = F("{\"status\":\"error\",\"error\":\"NotAvailable\",\"message\":\"Reset available only in normal mode\"}");
    sendJson(403, json);
    return;
  }

  if (!requireAuth()) return;

  savedSSID = "";
  savedPASS = "";
  saveWiFiConfig(savedSSID, savedPASS);

  authToken = "";
  serverBaseUrl = "";
  deviceMode = DEVICE_MODE_SETUP;
  saveDeviceConfig();

  String json = F("{\"status\":\"ok\",\"message\":\"Device reset to factory settings. Entering setup mode.\"}");
  sendJson(200, json);

  delay(800);
  ESP.restart();
}

// ---------------------- routes register ----------------------
void registerRestApiRoutes() {
  server.on(F("/about"),     HTTP_GET,  handleAbout);
  server.on(F("/status"),    HTTP_GET,  handleStatus);
  server.on(F("/action"),    HTTP_POST, handleAction);
  server.on(F("/set_token"), HTTP_POST, handleSetToken);
  server.on(F("/set_device_name"), HTTP_POST, handleSetDeviceName);

  server.on(F("/reboot"),    HTTP_POST, handleRebootApi);
  server.on(F("/reset"),     HTTP_POST, handleResetApi);
}