* Copyright (c) 2024 dresden elektronik ingenieurtechnik gmbh.
* All rights reserved.
*
* The software in this package is published under the terms of the BSD
* style license a copy of which has been included with this distribution in
* the LICENSE.txt file.
*
*/
#include <QTcpSocket>
#include <deconz/dbg_trace.h>
#include "device_ddf_bundle.h"
#include "rest_api.h"
#include "rest_ddf.h"
#include "json.h"
#include "deconz/u_assert.h"
#include "deconz/file.h"
#include "deconz/u_sstream.h"
#include "deconz/u_memory.h"
#include "deconz/util.h"
#include "utils/scratchmem.h"
#define MAX_PATH_LENGTH 2048
Converts data into hex-ascii string.
The memory area \p ascii must have at least the
size \p 2 * length + 1 bytes.
A terminating zero will be appended at the end.
\returns a pointer to the last byte (zero).
*/
static unsigned char *BinToHexAscii(const void *hex, unsigned length, void *ascii)
{
unsigned i;
unsigned char *h;
unsigned char *a;
static char lookup[] = { '0', '1', '2','3','4','5','6','7','8','9','a','b','c','d','e','f' };
if (!hex || length == 0 || !ascii)
return 0;
h = (unsigned char*)hex;
a = (unsigned char*)ascii;
for (i = 0; i < length; i++)
{
*a++ = lookup[(h[i] & 0xf0) >> 4];
*a++ = lookup[h[i] & 0xf];
}
*a = '\0';
return a;
}
Test upload of .ddf file
curl -F 'data=@/home/mpi/some.ddb' 127.0.0.1:8090/api/12345/ddf
*/
static int WriteBundleDescriptorToResponse(U_BStream *bs, U_SStream *ss, unsigned nRecords)
{
cj_ctx cj;
cj_token tokens[2048];
cj_token *tok;
char ibuf[512];
unsigned chunkSize;
unsigned char sha256[U_SHA256_HASH_SIZE];
char sha256Str[(U_SHA256_HASH_SIZE * 2) + 1];
if (DDFB_FindChunk(bs, "RIFF", &chunkSize) == 0)
{
return 0;
}
if (DDFB_FindChunk(bs, "DDFB", &chunkSize) == 0)
{
return 0;
}
{
if (U_Sha256(&bs->data[bs->pos - 8], chunkSize + 8, sha256) == 0)
{
U_ASSERT(0 && "SHA-256 not working");
return 0;
}
BinToHexAscii(sha256, U_SHA256_HASH_SIZE, sha256Str);
}
U_BStream bsDDFB;
U_bstream_init(&bsDDFB, &bs->data[bs->pos], chunkSize);
if (DDFB_FindChunk(&bsDDFB, "DESC", &chunkSize) == 0)
{
return 0;
}
U_ASSERT(chunkSize > 32);
cj_parse_init(&cj, (char*)&bsDDFB.data[bsDDFB.pos], chunkSize, tokens, sizeof(tokens)/sizeof(tokens[0]));
cj_parse(&cj);
U_ASSERT(cj.status == CJ_OK);
if (cj.status != CJ_OK)
{
return 0;
}
U_ASSERT(cj.tokens_pos > 0);
U_ASSERT(cj.tokens[0].type == CJ_TOKEN_OBJECT_BEG);
U_ASSERT(cj.tokens[cj.tokens_pos - 1].type == CJ_TOKEN_OBJECT_END);
if ((ss->pos + chunkSize + 128 + 128 + 96) < ss->len)
{
if (nRecords > 0)
U_sstream_put_str(ss, ",");
U_sstream_put_str(ss, "\"");
U_sstream_put_str(ss, sha256Str);
U_sstream_put_str(ss, "\":");
for (cj_size i = 0; i < cj.tokens_pos - 1; i++)
{
tok = &cj.tokens[i];
U_ASSERT(tok->len != 0);
if (sizeof(ibuf) - 1 < tok->len)
{
U_ASSERT(0 && "unexpected large JSON token");
return 0;
}
if (tok->type == CJ_TOKEN_STRING)
{
U_ASSERT(tok->pos != 0);
U_memcpy(ibuf, &cj.buf[tok->pos - 1], tok->len + 2);
ibuf[tok->len + 2] = '\0';
}
else
{
U_memcpy(ibuf, &cj.buf[tok->pos], tok->len);
ibuf[tok->len] = '\0';
}
U_sstream_put_str(ss, ibuf);
U_ASSERT(ss->status == U_SSTREAM_OK);
}
if (U_Sha256(&bs->data[0], bs->size, sha256) == 0)
{
U_ASSERT(0 && "SHA-256 not working");
return 0;
}
BinToHexAscii(sha256, U_SHA256_HASH_SIZE, sha256Str);
U_sstream_put_str(ss, ", \"file_hash\": \"");
U_sstream_put_str(ss, sha256Str);
U_sstream_put_str(ss, "\"}");
return ss->status == U_SSTREAM_OK;
}
return 0;
}
int REST_DDF_GetDescriptors(const ApiRequest &req, ApiResponse &rsp)
{
unsigned reqCursor = 1;
unsigned curCursor = 1;
unsigned nextCursor = 0;
unsigned nRecords = 0;
unsigned maxRecords = 64;
auto url = req.hdr.url();
{
U_SStream ss;
U_sstream_init(&ss, (void*)url.data(), (unsigned)url.size());
if (U_sstream_find(&ss, "?next="))
{
U_sstream_find(&ss, "=");
ss.pos++;
long n = U_sstream_get_long(&ss);
if (ss.status == U_SSTREAM_OK && n > 0)
{
reqCursor = (unsigned)n;
}
else
{
rsp.httpStatus = HttpStatusBadRequest;
return REQ_READY_SEND;
}
}
}
unsigned maxResponseSize = 1 << 20;
char *bundleData = SCRATCH_ALLOC(char*, MAX_BUNDLE_SIZE);
char *path = SCRATCH_ALLOC(char*, MAX_PATH_LENGTH);
char *rspData = SCRATCH_ALLOC(char*, maxResponseSize);
if (!bundleData || !path || !rspData)
{
rsp.httpStatus = HttpStatusServiceUnavailable;
return REQ_READY_SEND;
}
FS_Dir dir;
FS_File fp;
U_SStream ss;
U_SStream ssRsp;
unsigned basePathLength;
deCONZ::StorageLocation locations[2] = { deCONZ::DdfBundleUserLocation, deCONZ::DdfBundleLocation };
U_sstream_init(&ssRsp, rspData, maxResponseSize);
U_sstream_put_str(&ssRsp, "{");
for (int locIt = 0; locIt < 2; locIt++)
{
{
QString loc = deCONZ::getStorageLocation(locations[locIt]);
U_sstream_init(&ss, path, MAX_PATH_LENGTH);
U_sstream_put_str(&ss, qPrintable(loc));
basePathLength = ss.pos;
}
if (FS_OpenDir(&dir, path))
{
for (;FS_ReadDir(&dir);)
{
if (dir.entry.type != FS_TYPE_FILE)
continue;
U_sstream_init(&ss, dir.entry.name, strlen(dir.entry.name));
if (U_sstream_find(&ss, ".ddf") == 0 && U_sstream_find(&ss, ".ddb") == 0)
continue;
if (curCursor < reqCursor)
{
curCursor++;
continue;
}
if (nRecords < maxRecords)
{
U_sstream_init(&ss, path, MAX_PATH_LENGTH);
ss.pos = basePathLength;
U_sstream_put_str(&ss, "/");
U_sstream_put_str(&ss, dir.entry.name);
if (FS_OpenFile(&fp, FS_MODE_R, path))
{
long n = FS_ReadFile(&fp, bundleData, MAX_BUNDLE_SIZE);
if (n > 32)
{
U_BStream bs;
U_bstream_init(&bs, bundleData, (unsigned)n);
if (WriteBundleDescriptorToResponse(&bs, &ssRsp, nRecords))
{
curCursor++;
nRecords++;
}
}
FS_CloseFile(&fp);
}
}
else
{
nextCursor = curCursor;
break;
}
DBG_Printf(DBG_INFO, "BUNDLE: %s\n", ss.str);
}
if (nextCursor != 0)
{
U_sstream_put_str(&ssRsp, ",\"next\":");
U_sstream_put_long(&ssRsp, (long)nextCursor);
}
FS_CloseDir(&dir);
}
}
U_sstream_put_str(&ssRsp, "}");
rsp.httpStatus = HttpStatusOk;
rsp.str = ssRsp.str;
return REQ_READY_SEND;
}
int REST_DDF_GetBundle(const ApiRequest &req, ApiResponse &rsp)
{
FS_File fp;
FS_Dir dir;
unsigned basePathLength;
char bundleHashStr[U_SHA256_HASH_SIZE * 2 + 1];
{
auto urlBundleHash = req.hdr.pathAt(4);
if (urlBundleHash.size() != (U_SHA256_HASH_SIZE * 2))
{
rsp.httpStatus = HttpStatusBadRequest;
return REQ_READY_SEND;
}
U_memcpy(bundleHashStr, urlBundleHash.data(), U_SHA256_HASH_SIZE * 2);
bundleHashStr[U_SHA256_HASH_SIZE * 2] = '\0';
if (!DDFB_SanitizeBundleHashString(bundleHashStr, U_SHA256_HASH_SIZE * 2))
{
rsp.httpStatus = HttpStatusBadRequest;
return REQ_READY_SEND;
}
}
unsigned maxFileNameLength = 72;
char *path = SCRATCH_ALLOC(char*, MAX_PATH_LENGTH);
char *fileName = SCRATCH_ALLOC(char*, maxFileNameLength);
rsp.bin = SCRATCH_ALLOC(char*, MAX_BUNDLE_SIZE);
if (!path || !fileName || !rsp.bin)
{
rsp.httpStatus = HttpStatusServiceUnavailable;
return REQ_READY_SEND;
}
{
U_SStream ssFileName;
U_sstream_init(&ssFileName, fileName, maxFileNameLength);
U_sstream_put_str(&ssFileName, bundleHashStr);
U_sstream_put_str(&ssFileName, ".ddb");
}
deCONZ::StorageLocation locations[2] = { deCONZ::DdfBundleUserLocation, deCONZ::DdfBundleLocation };
for (int locIt = 0; locIt < 2; locIt++)
{
U_SStream ss;
{
QString loc = deCONZ::getStorageLocation(locations[locIt]);
U_sstream_init(&ss, path, MAX_PATH_LENGTH);
U_sstream_put_str(&ss, qPrintable(loc));
basePathLength = ss.pos;
}
if (FS_OpenDir(&dir, path))
{
for (;FS_ReadDir(&dir);)
{
if (dir.entry.type != FS_TYPE_FILE)
continue;
U_sstream_init(&ss, dir.entry.name, strlen(dir.entry.name));
if (U_sstream_find(&ss, ".ddf") == 0 && U_sstream_find(&ss, ".ddb") == 0)
continue;
U_sstream_init(&ss, path, MAX_PATH_LENGTH);
ss.pos = basePathLength;
U_sstream_put_str(&ss, "/");
U_sstream_put_str(&ss, dir.entry.name);
if (FS_OpenFile(&fp, FS_MODE_R, path))
{
long fileSize = FS_GetFileSize(&fp);
if (fileSize > 0 && fileSize <= MAX_BUNDLE_SIZE)
{
if (FS_ReadFile(&fp, rsp.bin, fileSize) == fileSize)
{
U_BStream bs;
uint8_t sha256[U_SHA256_HASH_SIZE];
U_bstream_init(&bs, rsp.bin, fileSize);
if (IsValidDDFBundle(&bs, sha256))
{
char sha256Str[(U_SHA256_HASH_SIZE * 2) + 1];
BinToHexAscii(sha256, U_SHA256_HASH_SIZE, sha256Str);
if (U_memcmp(bundleHashStr, sha256Str, U_SHA256_HASH_SIZE * 2) == 0)
{
FS_CloseFile(&fp);
FS_CloseDir(&dir);
rsp.contentLength = (unsigned)fileSize;
rsp.fileName = fileName;
rsp.httpStatus = HttpStatusOk;
rsp.contentType = HttpContentOctetStream;
return REQ_READY_SEND;
}
}
}
}
FS_CloseFile(&fp);
}
}
FS_CloseDir(&dir);
}
}
{
rsp.httpStatus = HttpStatusNotFound;
rsp.list.append(errorToMap(ERR_RESOURCE_NOT_AVAILABLE,
QString("/ddf/bundles/%1").arg(bundleHashStr),
QString("resource, /ddf/bundles/%1, not available").arg(bundleHashStr)));
}
return REQ_READY_SEND;
}
int REST_DDF_PostBundles(const ApiRequest &req, ApiResponse &rsp)
{
ScratchMemWaypoint swp;
const QVariantMap errInvalidData = errorToMap(ERR_INVALID_DDF_BUNDLE, "/ddf/bundles", "body contains invalid DDF bundle(s) data");
const QVariantMap errServiceNotAvailable = errorToMap(ERR_INTERNAL_ERROR, "/ddf/bundles", "Internal error, can't process request");
if (req.hdr.contentLength() < 32 || req.hdr.contentLength() > 512000)
{
return REQ_NOT_HANDLED;
}
QLatin1String contentType = req.hdr.value(QLatin1String("Content-Type"));
U_SStream ss;
U_sstream_init(&ss, (void*)contentType.data(), contentType.size());
if (U_sstream_starts_with(&ss, "multipart/form-data") == 0)
{
rsp.httpStatus = HttpStatusBadRequest;
rsp.list.append(errInvalidData);
return REQ_READY_SEND;
}
if (U_sstream_find(&ss, "boundary=") == 0)
{
rsp.httpStatus = HttpStatusBadRequest;
rsp.list.append(errInvalidData);
return REQ_READY_SEND;
}
if (U_sstream_find(&ss, "=") == 0)
{
rsp.httpStatus = HttpStatusBadRequest;
rsp.list.append(errInvalidData);
return REQ_READY_SEND;
}
ss.pos++;
const unsigned boundaryLen = ss.len - ss.pos;
char *boundary = SCRATCH_ALLOC(char*, boundaryLen + 8);
if (!boundary)
{
rsp.httpStatus = HttpStatusServiceUnavailable;
rsp.list.append(errServiceNotAvailable);
return REQ_READY_SEND;
}
U_memcpy(boundary, &ss.str[ss.pos], boundaryLen);
boundary[boundaryLen] = '\0';
unsigned dataSize = req.hdr.contentLength() + 1;
if (MAX_BUNDLE_SIZE < dataSize)
{
rsp.httpStatus = HttpStatusBadRequest;
rsp.list.append(errInvalidData);
return REQ_READY_SEND;
}
char *data = SCRATCH_ALLOC(char*, dataSize);
if (!data)
{
rsp.httpStatus = HttpStatusServiceUnavailable;
rsp.list.append(errServiceNotAvailable);
return REQ_READY_SEND;
}
unsigned binStart = 0;
unsigned binEnd = 0;
int n = req.sock->read(data, dataSize - 1);
if (n > 0)
{
data[dataSize - 1] = '\0';
U_sstream_init(&ss, data, n);
if (U_sstream_find(&ss, boundary) == 0)
{
rsp.httpStatus = HttpStatusBadRequest;
rsp.list.append(errInvalidData);
return REQ_READY_SEND;
}
if (U_sstream_find(&ss, "\r\n\r\n") == 0)
{
return REQ_NOT_HANDLED;
}
ss.pos += 4;
binStart = ss.pos;
if (U_sstream_find(&ss, boundary) == 0)
{
rsp.httpStatus = HttpStatusBadRequest;
rsp.list.append(errInvalidData);
return REQ_READY_SEND;
}
binEnd = ss.pos;
}
if (binStart < binEnd && (binEnd - binStart) > 16)
{
if (data[binEnd - 1] == '-' && data[binEnd - 2] == '-')
{
binEnd -= 2;
}
if (data[binEnd - 1] == '\n' && data[binEnd - 2] == '\r')
{
binEnd -= 2;
}
data[binEnd] = '\0';
U_BStream bs;
U_bstream_init(&bs, &data[binStart], binEnd - binStart);
unsigned char *bundleHash = SCRATCH_ALLOC(unsigned char*, U_SHA256_HASH_SIZE);
char *bundleHashStr = SCRATCH_ALLOC(char*, (U_SHA256_HASH_SIZE * 2) + 1);
if (!bundleHash || !bundleHashStr)
{
rsp.httpStatus = HttpStatusServiceUnavailable;
rsp.list.append(errServiceNotAvailable);
return REQ_READY_SEND;
}
if (IsValidDDFBundle(&bs, bundleHash) == 0)
{
rsp.httpStatus = HttpStatusBadRequest;
rsp.list.append(errInvalidData);
return REQ_READY_SEND;
}
BinToHexAscii(bundleHash, U_SHA256_HASH_SIZE, bundleHashStr);
DBG_Printf(DBG_INFO, "received %d bytes (binary: %u), bundle-hash: %s\n", n, binEnd - binStart, bundleHashStr);
QString loc = deCONZ::getStorageLocation(deCONZ::DdfBundleUserLocation);
FS_File fp;
char *bundlePath = SCRATCH_ALLOC(char*, MAX_PATH_LENGTH);
if (!bundlePath)
{
rsp.httpStatus = HttpStatusServiceUnavailable;
rsp.list.append(errServiceNotAvailable);
return REQ_READY_SEND;
}
#if 1
{
{
U_SStream ssPath;
U_sstream_init(&ssPath, bundlePath, MAX_PATH_LENGTH);
U_sstream_put_str(&ssPath, qPrintable(loc));
U_sstream_put_str(&ssPath, "/");
U_sstream_put_str(&ssPath, bundleHashStr);
U_sstream_put_str(&ssPath, ".ddf");
}
if (FS_OpenFile(&fp, FS_MODE_R, bundlePath))
{
FS_CloseFile(&fp);
FS_DeleteFile(bundlePath);
}
}
#endif
{
U_SStream ssPath;
U_sstream_init(&ssPath, bundlePath, MAX_PATH_LENGTH);
U_sstream_put_str(&ssPath, qPrintable(loc));
U_sstream_put_str(&ssPath, "/");
U_sstream_put_str(&ssPath, bundleHashStr);
U_sstream_put_str(&ssPath, ".ddb");
}
if (FS_OpenFile(&fp, FS_MODE_R, bundlePath))
{
FS_CloseFile(&fp);
FS_DeleteFile(bundlePath);
}
if (FS_OpenFile(&fp, FS_MODE_RW, bundlePath))
{
long n = FS_WriteFile(&fp, bs.data, bs.size);
FS_CloseFile(&fp);
if (n != bs.size)
{
rsp.httpStatus = HttpStatusServiceUnavailable;
rsp.list.append(errServiceNotAvailable);
return REQ_READY_SEND;
}
DEV_DDF_BundleUpdated((uint8_t*)&data[binStart], binEnd - binStart);
DBG_Printf(DBG_INFO, "DDF bundle written: %s\n", bundlePath);
rsp.httpStatus = HttpStatusOk;
QVariantMap result;
QVariantMap item;
item["id"] = QString::fromLatin1(bundleHashStr);
result["success"] = item;
rsp.list.append(result);
return REQ_READY_SEND;
}
}
return REQ_NOT_HANDLED;
}
int REST_DDF_HandleApi(const ApiRequest &req, ApiResponse &rsp)
{
if (req.hdr.pathComponentsCount() == 4 && req.hdr.httpMethod() == HttpGet && req.hdr.pathAt(3) == "descriptors")
{
return REST_DDF_GetDescriptors(req, rsp);
}
if (req.hdr.pathComponentsCount() == 5 && req.hdr.httpMethod() == HttpGet && req.hdr.pathAt(3) == "bundles")
{
return REST_DDF_GetBundle(req, rsp);
}
if (req.hdr.pathComponentsCount() == 4 && req.hdr.httpMethod() == HttpPost && req.hdr.pathAt(3) == "bundles")
{
return REST_DDF_PostBundles(req, rsp);
}
return REQ_NOT_HANDLED;
}