add microhttpd server

This commit is contained in:
2024-11-14 18:15:27 +03:00
parent ee34e8a72e
commit cdde340efe
12 changed files with 599 additions and 221 deletions

View File

@@ -0,0 +1,308 @@
#include "microhttpd_server_p.h"
#include "pidir.h"
#include "pifile.h"
#include "piliterals_string.h"
#include <microhttpd.h>
struct MicrohttpdServerConnection {
bool ready();
int send_reply(const MicrohttpdServer::Reply & r);
int send_error();
bool done = false;
MicrohttpdServer::Method method = MicrohttpdServer::Method::Unknown;
PIString path;
PIByteArray body;
PIMap<PIString, PIString> headers, args, post;
MHD_Connection * connection = nullptr;
MicrohttpdServer * server = nullptr;
MHD_PostProcessor * postprocessor = nullptr;
};
bool MicrohttpdServerConnection::ready() {
if (!server) return false;
if (done) return true;
done = true;
MicrohttpdServer::Reply rep;
if (method == MicrohttpdServer::Method::Get) {
PIByteArray file = server->getFile(path);
if (!file.isEmpty()) {
rep.addHeader(MHD_HTTP_HEADER_CACHE_CONTROL, "public");
rep.addHeader(MHD_HTTP_HEADER_CONTENT_TYPE, "application/octet-stream");
rep.setBody(file);
send_reply(rep);
return true;
}
if (path == "/favicon.ico"_a) {
piCout << "send favicon" << server->favicon.size() << "bytes";
rep.setBody(server->favicon);
send_reply(rep);
return true;
}
}
piCout << "ready" << (int)method << path;
MicrohttpdServer::Request req;
req.method = method;
req.path = path;
req.body = body;
req.headers = headers;
req.args = args;
// rep.headers = headers;
if (server->callback) rep = server->callback(req);
rep.addFixedHeaders();
send_reply(rep);
return true;
}
int MicrohttpdServerConnection::send_reply(const MicrohttpdServer::Reply & r) {
MHD_Response * response = MHD_create_response_from_buffer(r.body.size(), (void *)r.body.data(), MHD_RESPMEM_MUST_COPY);
if (!response) {
piCout << "null response" << r.body.size() << (void *)r.body.data();
return MHD_NO;
}
auto it = r.headers.makeIterator();
while (it.next())
MHD_add_response_header(response, it.key().dataAscii(), it.value().dataUTF8());
piCout << "status" << r.code;
int ret = MHD_queue_response(connection, r.code, response);
MHD_destroy_response(response);
return ret;
}
int MicrohttpdServerConnection::send_error() {
return MHD_queue_response(connection, MHD_HTTP_BAD_REQUEST, MHD_create_response_from_buffer(0, nullptr, MHD_RESPMEM_MUST_COPY));
}
int iterate_post(void * conn_cls,
MHD_ValueKind kind,
const char * key,
const char * filename,
const char * content_type,
const char * transfer_encoding,
const char * data,
uint64_t off,
size_t size) {
MicrohttpdServerConnection * conn = (MicrohttpdServerConnection *)conn_cls;
if (!conn) return MHD_NO;
conn->post[PIString::fromUTF8(key)] = PIString::fromUTF8(data);
return MHD_YES;
}
void request_completed(void * cls, MHD_Connection * connection, void ** con_cls, MHD_RequestTerminationCode toe) {
MicrohttpdServerConnection *& conn((MicrohttpdServerConnection *&)(*con_cls));
// qDebug() << "request_completed" << conn << conn->headers << conn->post << '"' << conn->body << '"';
if (!conn) return;
if (conn->method == MicrohttpdServer::Method::Post && conn->postprocessor) MHD_destroy_post_processor(conn->postprocessor);
conn->ready();
piDeleteSafety(conn);
}
int header_iterate(void * cls, MHD_ValueKind kind, const char * key, const char * value) {
MicrohttpdServerConnection * conn = (MicrohttpdServerConnection *)cls;
if (!conn) return MHD_NO;
conn->headers[PIString::fromUTF8(key)] = PIString::fromUTF8(value);
return MHD_YES;
}
int args_iterate(void * cls, MHD_ValueKind kind, const char * key, const char * value) {
MicrohttpdServerConnection * conn = (MicrohttpdServerConnection *)cls;
if (!conn) return MHD_NO;
conn->args[PIString::fromUTF8(key)] = PIString::fromUTF8(value);
return MHD_YES;
}
int answer_to_connection(void * cls,
MHD_Connection * connection,
const char * url,
const char * method,
const char * version,
const char * upload_data,
size_t * upload_data_size,
void ** con_cls) {
MicrohttpdServer * server = (MicrohttpdServer *)cls;
MicrohttpdServer::Method m = MicrohttpdServer::Method::Unknown;
if (0 == strcmp(method, "GET"))
m = MicrohttpdServer::Method::Get;
else if (0 == strcmp(method, "POST"))
m = MicrohttpdServer::Method::Post;
else if (0 == strcmp(method, "HEAD"))
m = MicrohttpdServer::Method::Head;
else if (0 == strcmp(method, "PUT"))
m = MicrohttpdServer::Method::Put;
else if (0 == strcmp(method, "DELETE"))
m = MicrohttpdServer::Method::Delete;
else if (0 == strcmp(method, "CONNECT"))
m = MicrohttpdServer::Method::Connect;
else if (0 == strcmp(method, "OPTIONS"))
m = MicrohttpdServer::Method::Options;
else if (0 == strcmp(method, "TRACE"))
m = MicrohttpdServer::Method::Trace;
else if (0 == strcmp(method, "PATCH"))
m = MicrohttpdServer::Method::Patch;
if (m == MicrohttpdServer::Method::Unknown) {
piCout << "[MicrohttpdServer] Unknown method!";
return MHD_NO;
}
// piCout << "answer" << url << method << server;
MicrohttpdServerConnection *& conn((MicrohttpdServerConnection *&)(*con_cls));
if (!conn) {
conn = new MicrohttpdServerConnection();
conn->connection = connection;
conn->server = server;
conn->path = PIString::fromUTF8(url);
conn->method = m;
MHD_get_connection_values(connection, MHD_HEADER_KIND, (MHD_KeyValueIterator)header_iterate, *con_cls);
MHD_get_connection_values(connection, MHD_GET_ARGUMENT_KIND, (MHD_KeyValueIterator)args_iterate, *con_cls);
if (m == MicrohttpdServer::Method::Post) {
// qDebug() << "new POST" << *upload_data_size;
conn->postprocessor = MHD_create_post_processor(connection, 65536, (MHD_PostDataIterator)iterate_post, (void *)conn);
}
return MHD_YES;
}
switch (m) {
case MicrohttpdServer::Method::Get:
if (!conn->ready()) return conn->send_error();
return MHD_YES;
case MicrohttpdServer::Method::Post:
// qDebug() << "add POST" << *upload_data_size << PIString::fromUtf8(upload_data, *upload_data_size);
if (*upload_data_size) {
conn->body.append(upload_data, *upload_data_size);
if (conn->postprocessor) MHD_post_process(conn->postprocessor, upload_data, *upload_data_size);
*upload_data_size = 0;
return MHD_YES;
} else {
// qDebug() << "answer ok";
if (!conn->ready()) return conn->send_error();
return MHD_YES;
}
break;
default: break;
}
return conn->send_error();
}
PRIVATE_DEFINITION_START(MicrohttpdServer)
MHD_Daemon * daemon;
PRIVATE_DEFINITION_END(MicrohttpdServer)
MicrohttpdServer::MicrohttpdServer() {
PRIVATE->daemon = nullptr;
}
MicrohttpdServer::~MicrohttpdServer() {
stop();
}
void MicrohttpdServer::setWWWRoot(const PIString & path) {
www_root = path;
if (www_root.isEmpty()) return;
www_root = PIDir(www_root).absolutePath();
}
void MicrohttpdServer::setFavicon(const PIByteArray & im) {
favicon = im;
}
bool MicrohttpdServer::listen(PINetworkAddress addr) {
stop();
uint flags = 0;
#if MHD_VERSION <= 0x00095100
flags = MHD_USE_POLL_INTERNALLY;
#else
flags = MHD_USE_AUTO | MHD_USE_INTERNAL_POLLING_THREAD;
#endif
sockaddr_in sa_addr;
memset(&sa_addr, 0, sizeof(sa_addr));
sa_addr.sin_port = htons(addr.port());
sa_addr.sin_addr.s_addr = addr.ip();
sa_addr.sin_family = AF_INET;
PRIVATE->daemon = MHD_start_daemon(flags,
addr.port(),
nullptr,
nullptr,
(MHD_AccessHandlerCallback)answer_to_connection,
this,
MHD_OPTION_NOTIFY_COMPLETED,
request_completed,
nullptr,
MHD_OPTION_SOCK_ADDR,
&sa_addr,
MHD_OPTION_END);
return isListen();
}
bool MicrohttpdServer::isListen() const {
return PRIVATE->daemon;
}
PIByteArray MicrohttpdServer::getFile(PIString name) {
if (www_root.isEmpty()) return PIByteArray();
if (name == "/") name += "index.html";
PIString path = PIDir(www_root).relative(name);
// qDebug() << "getFile" << path;
if (path.isEmpty() || !path.startsWith(www_root)) return PIByteArray();
return PIFile::readAll(PIDir(www_root).absolute(name));
}
void MicrohttpdServer::stop() {
if (PRIVATE->daemon) {
MHD_stop_daemon(PRIVATE->daemon);
PRIVATE->daemon = nullptr;
}
}
void MicrohttpdServer::Reply::addHeader(const PIString & header, const PIString & value) {
headers[header] = value;
}
void MicrohttpdServer::Reply::removeHeader(const PIString & header) {
headers.remove(header);
}
void MicrohttpdServer::Reply::setBody(const PIByteArray & b) {
body = b;
}
void MicrohttpdServer::Reply::setCode(int c) {
code = c;
}
void MicrohttpdServer::Reply::addFixedHeaders() {
if (!headers.contains(MHD_HTTP_HEADER_CONTENT_TYPE)) {
if (body.startsWith(PIByteArray::fromAscii("<!DOCTYPE html>")))
addHeader(MHD_HTTP_HEADER_CONTENT_TYPE, "text/html; charset=utf-8");
else
addHeader(MHD_HTTP_HEADER_CONTENT_TYPE, "application/json; charset=utf-8");
}
addHeader(MHD_HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, "*");
}

View File

@@ -0,0 +1,50 @@
#include "pihttpserver.h"
#include "piliterals_string.h"
PIHTTPServer::PIHTTPServer() {
setRequestCallback([this](const MicrohttpdServer::Request & r) -> MicrohttpdServer::Reply {
MicrohttpdServer::Reply rep;
rep.setCode(404);
auto in_path = r.path.split("/");
in_path.removeAll("");
auto it = functions.makeReverseIterator();
while (it.next()) {
if (it.value().function) {
if (it.value().match(in_path)) {
rep.setCode(200);
rep = it.value().function(r);
break;
}
}
}
auto hit = reply_headers.makeIterator();
while (hit.next())
rep.addHeader(hit.key(), hit.value());
return rep;
});
}
PIHTTPServer::~PIHTTPServer() {
stop();
}
void PIHTTPServer::registerPath(const PIString & path, RequestFunction functor) {
auto & ep(functions[path]);
ep.path = path.split("/");
ep.function = functor;
ep.path.removeAll("");
}
bool PIHTTPServer::Endpoint::match(const PIStringList & in_path) const {
if (in_path.size() != path.size()) return false;
for (int i = 0; i < path.size_s(); ++i) {
if (path[i] == "*"_a) continue;
if (path[i] != in_path[i]) return false;
}
return true;
}

View File

@@ -0,0 +1,75 @@
#ifndef MICROHTTPD_SERVER_P_H
#define MICROHTTPD_SERVER_P_H
#include "pibase.h"
#include "piobject.h"
#include "pip_http_server_export.h"
struct MicrohttpdServerConnection;
class PIP_HTTP_SERVER_EXPORT MicrohttpdServer: public PIObject {
PIOBJECT(MicrohttpdServer)
friend struct MicrohttpdServerConnection;
public:
MicrohttpdServer();
virtual ~MicrohttpdServer();
enum class Method {
Unknown,
Get,
Head,
Post,
Put,
Delete,
Connect,
Options,
Trace,
Patch
};
struct PIP_HTTP_SERVER_EXPORT Request {
MicrohttpdServer::Method method;
PIString path;
PIByteArray body;
PIMap<PIString, PIString> headers;
PIMap<PIString, PIString> args;
};
class PIP_HTTP_SERVER_EXPORT Reply {
friend struct MicrohttpdServerConnection;
public:
void addHeader(const PIString & header, const PIString & value);
void removeHeader(const PIString & header);
void setBody(const PIByteArray & b);
void setCode(int c);
private:
void addFixedHeaders();
int code = 200;
PIByteArray body;
PIMap<PIString, PIString> headers;
};
void setWWWRoot(const PIString & path);
void setFavicon(const PIByteArray & im);
bool listen(PINetworkAddress addr);
bool listenAll(ushort port) { return listen({0, port}); }
bool isListen() const;
void stop();
void setRequestCallback(std::function<Reply(Request)> c) { callback = c; }
private:
PIByteArray getFile(PIString name);
PRIVATE_DECLARATION(PIP_HTTP_SERVER_EXPORT)
PIString www_root;
PIByteArray favicon;
std::function<Reply(Request)> callback;
};
#endif

View File

@@ -0,0 +1,33 @@
#ifndef PIHTTPSERVER_H
#define PIHTTPSERVER_H
#include "microhttpd_server_p.h"
#include "pip_http_server_export.h"
class PIP_HTTP_SERVER_EXPORT PIHTTPServer: public MicrohttpdServer {
PIOBJECT_SUBCLASS(PIHTTPServer, MicrohttpdServer)
public:
PIHTTPServer();
virtual ~PIHTTPServer();
using RequestFunction = std::function<MicrohttpdServer::Reply(const MicrohttpdServer::Request &)>;
void registerPath(const PIString & path, RequestFunction functor);
void addReplyHeader(const PIString & name, const PIString & value) { reply_headers[name] = value; }
void removeReplyHeader(const PIString & name) { reply_headers.remove(name); }
void clearReplyHeaders() { reply_headers.clear(); }
private:
struct Endpoint {
bool match(const PIStringList & in_path) const;
PIStringList path;
RequestFunction function;
};
PIMap<PIString, PIString> reply_headers;
PIMap<PIString, Endpoint> functions;
};
#endif

View File

@@ -553,6 +553,17 @@ uint PIString::hash() const {
}
PIByteArray PIString::toAscii() const {
if (isEmpty()) return PIByteArray();
PIByteArray ret;
ret.resize(size());
for (int i = 0; i < size_s(); ++i) {
ret[i] = uchar(at(i).ch);
}
return ret;
}
PIByteArray PIString::toSystem() const {
if (isEmpty()) return PIByteArray();
buildData(__syslocname__);

View File

@@ -1107,6 +1107,10 @@ public:
//! \~russian Тоже самое, что \a toUTF8().
PIByteArray toByteArray() const { return toUTF8(); }
//! \~english Returns \a PIByteArray contains \a dataAscii() of this string without terminating null-char.
//! \~russian Возвращает \a PIByteArray содержащий \a dataAscii() строки без завершающего нулевого байта.
PIByteArray toAscii() const;
//! \~english Returns \a PIByteArray contains \a data() of this string without terminating null-char.
//! \~russian Возвращает \a PIByteArray содержащий \a data() строки без завершающего нулевого байта.
PIByteArray toSystem() const;

View File

@@ -32,16 +32,9 @@
template<typename T>
class PIP_EXPORT PIProtectedVariable {
public:
//! \~english Sets value to copy of \"v\"
//! \~russian Устанавливает значение как копию \"v\"
void set(const T & v) {
PIMutexLocker _ml(mutex);
var = v;
}
//! \~english Sets value by moving \"v\"
//! \~russian Устанавливает значение перемещением \"v\"
void set(T && v) {
//! \~english Sets value to \"v\"
//! \~russian Устанавливает значение как \"v\"
void set(T v) {
PIMutexLocker _ml(mutex);
var = std::move(v);
}
@@ -64,10 +57,10 @@ public:
//! \~russian Разблокирует мьютекс
void unlock() { mutex.unlock(); }
//! \~english Sets value to copy of \"v\"
//! \~russian Устанавливает значение как копию \"v\"
PIProtectedVariable<T> & operator=(const T & v) {
set(v);
//! \~english Sets value to \"v\"
//! \~russian Устанавливает значение как \"v\"
PIProtectedVariable<T> & operator=(T v) {
set(std::move(v));
return *this;
}

View File

@@ -129,6 +129,20 @@ struct base64HelpStruct {
// clang-format on
bool PIByteArray::startsWith(const PIByteArray & o) const {
if (o.isEmpty()) return false;
if (size() < o.size()) return false;
return piCompareBinary(data(), o.data(), o.size());
}
bool PIByteArray::endsWith(const PIByteArray & o) const {
if (o.isEmpty()) return false;
if (size() < o.size()) return false;
return piCompareBinary(data(size() - o.size()), o.data(), o.size());
}
PIByteArray & PIByteArray::convertToBase64() {
return *this = toBase64();
}
@@ -396,6 +410,17 @@ PIByteArray PIByteArray::fromHex(PIString str) {
}
PIByteArray PIByteArray::fromAscii(const char * str) {
PIByteArray ret;
int ind = 0;
while (str[ind] != 0) {
ret.append(str[ind]);
++ind;
}
return ret;
}
PICout operator<<(PICout s, const PIByteArray & ba) {
s.space();
s.saveAndSetControls(0);

View File

@@ -340,6 +340,10 @@ public:
//! \~\sa \a every(), \a any(), \a contains(), \a indexWhere()
inline int entries(std::function<bool(uchar e)> test, ssize_t start = 0) const { return d.entries(test, start); }
bool startsWith(const PIByteArray & o) const;
bool endsWith(const PIByteArray & o) const;
//! \~english Returns the first index at which a given element `e`
//! can be found in the array, or `-1` if it is not present.
//! \~russian Возвращает первый индекс, по которому данный элемент `e`
@@ -1165,6 +1169,8 @@ public:
static PIByteArray fromHex(PIString str);
static PIByteArray fromAscii(const char * str);
//! \~english Return converted from Base 64 data
//! \~russian Возвращает массив из Base 64 представления
static PIByteArray fromBase64(const PIByteArray & base64);