tesses-webserver-cpp/tesseswebserver.hpp

1142 lines
29 KiB
C++
Raw Permalink Normal View History

2024-09-02 03:19:22 +00:00
#pragma once
//#define GEKKO
#include "tesseswebserverfeatures.hpp"
#include <variant>
#include <cstdio>
#include <iostream>
#include <exception>
#include <vector>
#include <atomic>
#include <string>
#include <cstring>
#include <filesystem>
#include <initializer_list>
#include <map>
#include <stack>
#include <functional>
#include <cmath>
#include <bit>
#include <cctype>
#if defined(USE_JANSSON)
#include <jansson.h>
#endif
#if defined(USE_CURL)
#include <curl/curl.h>
#include <curl/easy.h>
#endif
#if defined(USE_SQLITE3)
#include <sqlite3.h>
#endif
#if defined(USE_MBEDTLS)
#include <mbedtls/ssl.h>
#endif
#if defined(DEBUG)
#define DEBUGOUT(...) printf(__VA_ARGS__)
#else
#define DEBUGOUT(...)
#endif
#if defined(GEKKO)
#include <ogc/lwp.h>
#include <ogc/mutex.h>
#if defined(HW_RVL) && defined(USE_WIISOCKETS)
#include <sys/socket.h>
#include <wiisocket.h>
#else
#include <network.h>
#define socket net_socket
#define bind net_bind
#define listen net_listen
#define recv net_recv
#define send net_send
#define close net_close
#endif
#else
#include <threads.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#endif
namespace Tesses::WebServer {
#if defined(GEKKO)
typedef void* THREAD_RETURN_TYPE;
#define THREAD_DEFAULT_RETURN_TYPE (NULL)
typedef THREAD_RETURN_TYPE (*callback)(void* arg);
#else
typedef int THREAD_RETURN_TYPE;
#define THREAD_DEFAULT_RETURN_TYPE 0
typedef THREAD_RETURN_TYPE (*callback)(void* arg);
#endif
class Statics {
public:
#if defined(GEKKO)
std::vector<Thread*> joined_threads;
mutex_t mtx;
#else
mtx_t mtx;
#endif
std::atomic<bool> isRunning;
};
extern Tesses::WebServer::Statics tesses_webserver_statics;
class Thread {
#if defined(GEKKO)
lwp_t thrd;
std::atomic<bool> hasExited;
callback cb;
void* arg;
#else
thrd_t thrd;
#endif
public:
static void Init()
{
#if defined(GEKKO)
LWP_MutexInit(&tesses_webserver_statics.mtx,true);
#else
mtx_init(&tesses_webserver_statics.mtx,mtx_recursive);
#endif
}
Thread(callback cb,void* arg,uint32_t stackSize)
{
#if defined(GEKKO)
this->hasExited=false;
this->cb = cb;
this->arg = arg;
LWP_CreateThread(&this->thrd,HANDLER,arg,NULL,stackSize,80);
#else
thrd_create(&thrd,cb,arg);
#endif
}
#if defined(GEKKO)
static THREAD_RETURN_TYPE HANDLER(void* arg)
{
Thread* thrd = (Thread*)arg;
THREAD_RETURN_TYPE t = thrd->cb(arg);
thrd->hasExited=true;
return t;
}
#endif
int Join(THREAD_RETURN_TYPE* retType)
{
#if defined(GEKKO)
return LWP_JoinThread(this->thrd,retType);
#else
return thrd_join(this->thrd,retType);
#endif
}
void Detach()
{
#if defined(GEKKO)
Lock();
tesses_webserver_statics.joined_thrads.push_back(this);
Unlock();
#else
thrd_detach(this->thrd);
delete this;
#endif
}
static void Lock()
{
#if defined(GEKKO)
LWP_MutexLock(tesses_webserver_statics.mtx);
#else
mtx_lock(&tesses_webserver_statics.mtx);
#endif
}
static void Unlock()
{
#if defined(GEKKO)
LWP_MutexUnlock(tesses_webserver_statics.mtx);
#else
mtx_unlock(&tesses_webserver_statics.mtx);
#endif
}
static void CleanThreads()
{
#if defined(GEKKO)
Lock();
for(auto i = tesses_webserver_statics.joined_threads.begin();i<tesses_webserver_statics.joined_threads.end();i++)
{
auto item = *i;
if(item->hasExited)
{
THREAD_RETURN_TYPE arg;
item->Join(&arg);
delete item;
tesses_webserver_statics.joined_threads.erase(i);
i--;
}
}
Unlock();
#endif
}
};
class Stream {
public:
virtual ssize_t Read(uint8_t* buffer,size_t len)=0;
virtual ssize_t Write(uint8_t* buffer,size_t len)=0;
virtual void Seek(uint64_t offset,int whence)
{
}
virtual int64_t Position()
{
return 0;
}
virtual bool CanSeek()
{
return false;
}
void WriteBlock(uint8_t* buffer,size_t len)
{
size_t read;
do{
read = 1024;
if(len < 1024)
read = len;
if(read > 0)
read=this->Write(buffer,read);
buffer += read;
len -= read;
} while(read > 0);
}
virtual int64_t Length()
{
return -1;
}
virtual ~Stream()
{
}
int ReadByte()
{
uint8_t byte;
if(Read(&byte,1) == 0) return -1;
return byte;
}
std::string ReadLine()
{
std::string str = {};
int c = ReadByte();
while(c != '\n' && c != -1)
{
if(c != '\r')
{
str.push_back((char)c);
}
c = ReadByte();
}
DEBUGOUT("ReadLine: %s\n",str.c_str());
return str;
}
};
class FileStream : public Stream
{
private:
FILE* f;
bool canSeek;
bool owns;
public:
FileStream(std::string filename, std::string mode)
{
this->f = fopen(filename.c_str(),mode.c_str());
this->canSeek=true;
this->owns=true;
}
FileStream(FILE* f,bool canSeek,bool owns)
{
this->f = f;
this->canSeek = canSeek;
this->owns = owns;
}
ssize_t Read(uint8_t* buffer,size_t len)
{
return (ssize_t)fread(buffer,1,len,this->f);
}
ssize_t Write(uint8_t* buffer, size_t len)
{
return (ssize_t)fwrite(buffer,1,len,this->f);
}
int64_t Position()
{
#if(_FILE_OFFSET_BITS == 64)
#if defined(WIN32)
return (int64_t)_ftelli64(this->f);
#else
return (int64_t)ftello(this->f);
#endif
#else
return (int64_t)ftell(this->f);
#endif
}
void Seek(uint64_t offset,int whence)
{
#if(_FILE_OFFSET_BITS == 64)
#if defined(WIN32)
_ftelli64(this->f,(__int64)offset,whence);
#else
fseeko(this->f,(off_t)offset,whence);
#endif
#else
fseek(this->f,(long)offset,whence);
#endif
}
};
class SocketStream : public Stream {
int sock;
public:
SocketStream(int sock)
{
this->sock = sock;
}
ssize_t Read(uint8_t* buffer,size_t len)
{
ssize_t r= recv(this->sock,buffer,len,0);
return r;
}
ssize_t Write(uint8_t* buffer,size_t len)
{
return send(this->sock,buffer,len,0);
}
~SocketStream()
{
close(sock);
}
};
typedef enum StatusCode {
Continue=100,
SwitchingProtocols=101,
Processing=102,
EarlyHints=103,
OK=200,
Created=201,
Accepted=202,
NonAuthoritativeInformation=203,
NoContent=204,
ResetContent=205,
PartialContent=206,
MultiStatus=207,
AlreadyReported=208,
IMUsed=226,
MultipleChoices=300,
MovedPermanently=301,
Found=302,
SeeOther=303,
NotModified=304,
UseProxy=305,
TemporaryRedirect=307,
PermanentRedirect=308,
BadRequest=400,
Unauthorized=401,
PaymentRequired=402,
Forbidden=403,
NotFound=404,
MethodNotAllowed=405,
NotAcceptable=406,
ProxyAuthenticationRequired=407,
RequestTimeout=408,
Conflict=409,
Gone=410,
LengthRequired=411,
PreconditionFailed=412,
PayloadTooLarge=413,
URITooLong=414,
UnsupportedMediaType=415,
RangeNotSatisfiable=416,
ExpectationFailed=417,
ImATeapot=418,
MisdirectedRequest=421,
UnprocessableContent=422,
Locked=423,
FailedDependency=424,
TooEarly=425,
UpgradeRequired=426,
PreconditionRequired=428,
TooManyRequests=429,
RequestHeaderFieldsTooLarge=431,
UnavailableForLegalReasons=451,
InternalServerError=500,
NotImplemented=501,
BadGateway=502,
ServiceUnavailable=503,
GatewayTimeout=504,
HTTPVersionNotSupported=505,
VariantAlsoNegotiates=506,
InsufficientStorage=507,
LoopDetected=508,
NotExtended=510,
NetworkAuthenticationRequired=511
} StatusCode;
class HttpUtils {
public:
static char nibble_to_hex_char(uint8_t number)
{
if(number >= 0 && number <= 9)
{
return '0'+number;
}
if(number >= 0xA && number <= 0xF)
{
return (char)(55+number);
}
return '0';
}
static uint8_t hex_char_to_nibble(char c)
{
if(c >= '0' && c <= '9')
return c - '0';
if(c >= 'A' && c <= 'F')
return (c-'A')+0xA;
if(c >= 'a' && c <= 'f')
return (c-'a')+0xA;
return 0;
}
static std::vector<std::pair<std::string,std::string>> QueryParamsDecode(std::string query)
{
std::vector<std::pair<std::string,std::string>> strs;
for(auto item : SplitString(query,"&"))
{
std::vector<std::string> ss=SplitString(item,"=",2);
if(ss.size() >= 1)
{
std::string value = {};
if(ss.size() == 2)
{
value = UrlDecode(ss[1]);
}
strs.push_back(std::pair<std::string,std::string>(ss[0],value));
}
}
return strs;
}
static std::string QueryParamsEncode(std::vector<std::pair<std::string,std::string>> query)
{
std::string s={};
bool first = true;
for(auto item : query)
{
if(!first)
{
s.push_back('&');
}
s.insert(s.size(),item.first);
s.push_back('=');
s.insert(s.size(),UrlEncode(item.second));
first=false;
}
return s;
}
static std::string UrlDecode(std::string v)
{
std::string s = {};
for(size_t i = 0;i<v.size();i++)
{
if(v[i] == '+')
s.push_back(' ');
else if(v[i] == '%')
{
i++;
uint8_t n = hex_char_to_nibble(v[i])<<4;
i++;
n |= hex_char_to_nibble(v[i]);
s.push_back((char)n);
}
else
s.push_back(v[i]);
}
return s;
}
static std::string UrlPathEncode(std::string v)
{
std::string s = {};
for(auto item : v)
{
if(item >= 'A' && item <= 'Z')
s.push_back(item);
else if(item >= 'a' && item <= 'z')
s.push_back(item);
else if(item >= '0' && item <= '9')
s.push_back(item);
else if(item == '-' || item == '_' || item == '.' || item == '~')
s.push_back(item);
else
{
s.push_back('%');
s.push_back(nibble_to_hex_char((item >> 4) & 0xF));
s.push_back(nibble_to_hex_char((item) & 0xF));
}
}
return s;
}
static std::string UrlPathDecode(std::string v)
{
std::string s = {};
for(size_t i = 0;i<v.size();i++)
{
if(v[i] == '%')
{
i++;
uint8_t n = hex_char_to_nibble(v[i])<<4;
i++;
n |= hex_char_to_nibble(v[i]);
s.push_back((char)n);
}
else
s.push_back(v[i]);
}
return s;
}
static std::string UrlEncode(std::string v)
{
std::string s = {};
for(auto item : v)
{
if(item == ' ')
s.push_back('+');
else if(item >= 'A' && item <= 'Z')
s.push_back(item);
else if(item >= 'a' && item <= 'z')
s.push_back(item);
else if(item >= '0' && item <= '9')
s.push_back(item);
else if(item == '-' || item == '_' || item == '.' || item == '~')
s.push_back(item);
else
{
s.push_back('%');
s.push_back(nibble_to_hex_char((item >> 4) & 0xF));
s.push_back(nibble_to_hex_char((item) & 0xF));
}
}
return s;
}
static std::vector<std::string> SplitString(std::string text, std::string delimiter,std::size_t maxCnt = std::string::npos)
{
std::vector<std::string> strs;
std::size_t i = 1;
while(text.length() > 0)
{
if(i == maxCnt)
{
strs.push_back(text);
break;
}
std::size_t index= text.find_first_of(delimiter);
if(index == std::string::npos)
{
strs.push_back(text);
break;
}
else
{
std::string left = text.substr(0,index);
text = text.substr(index+delimiter.size());
strs.push_back(left);
}
i++;
}
return strs;
}
static std::string HtmlEncode(std::string html)
{
std::string myHtml = {};
for(auto item : html)
{
if(item == '\"')
{
myHtml.append("&quot;");
}
else if(item == '\'')
{
myHtml.append("&apos;");
}
else if(item == '&')
{
myHtml.append("&amp;");
}
else if(item == '<')
{
myHtml.append("&lt;");
}
else if(item == '>')
{
myHtml.append("&gt;");
}
else
{
myHtml.push_back(item);
}
}
return myHtml;
}
static std::string StatusCodeString(StatusCode code)
{
switch(code)
{
case StatusCode::Continue:
return "Continue";
case StatusCode::SwitchingProtocols:
return "Switching Protocols";
case StatusCode::Processing:
return "Processing";
case StatusCode::EarlyHints:
return "Early Hints";
case StatusCode::OK:
return "OK";
case StatusCode::Created:
return "Created";
case StatusCode::Accepted:
return "Accepted";
case StatusCode::NonAuthoritativeInformation:
return "Non-Authoritative Information";
case StatusCode::NoContent:
return "No Content";
case StatusCode::ResetContent:
return "Reset Content";
case StatusCode::PartialContent:
return "PartialContent";
case StatusCode::MultiStatus:
return "Multi-Status";
case StatusCode::AlreadyReported:
return "Already Reported";
case StatusCode::IMUsed:
return "IM Used";
case StatusCode::MultipleChoices:
return "Multiple Choices";
case StatusCode::MovedPermanently:
return "Moved Permanently";
case StatusCode::Found:
return "Found";
case StatusCode::SeeOther:
return "See Other";
case StatusCode::NotModified:
return "Not Modified";
case StatusCode::UseProxy:
return "Use Proxy";
case StatusCode::TemporaryRedirect:
return "Temporary Redirect";
case StatusCode::PermanentRedirect:
return "Permanent Redirect";
case StatusCode::BadRequest:
return "Bad Request";
case StatusCode::Unauthorized:
return "Unauthorized";
case StatusCode::PaymentRequired:
return "Payment Required";
case StatusCode::Forbidden:
return "Forbidden";
case StatusCode::NotFound:
return "Not Found";
case StatusCode::MethodNotAllowed:
return "Method Not Allowed";
case StatusCode::NotAcceptable:
return "Not Acceptable";
case StatusCode::ProxyAuthenticationRequired:
return "Proxy Authentication Required";
case StatusCode::RequestTimeout:
return "Request Timeout";
case StatusCode::Conflict:
return "Conflict";
case StatusCode::Gone:
return "Gone";
case StatusCode::LengthRequired:
return "Length Required";
case StatusCode::PreconditionFailed:
return "Precondition Failed";
case StatusCode::PayloadTooLarge:
return "Payload Too Large";
case StatusCode::URITooLong:
return "URI Too Long";
case StatusCode::UnsupportedMediaType:
return "Unsupported Media Type";
case StatusCode::RangeNotSatisfiable:
return "Range Not Satisfiable";
case StatusCode::ExpectationFailed:
return "Expectation Failed";
case StatusCode::ImATeapot:
return "I'm a teapot";
case StatusCode::MisdirectedRequest:
return "Misdirected Request";
case StatusCode::UnprocessableContent:
return "Unprocessable Content";
case StatusCode::Locked:
return "Locked";
case StatusCode::FailedDependency:
return "Failed Dependency";
case StatusCode::TooEarly:
return "Too Early";
case StatusCode::UpgradeRequired:
return "Upgrade Required";
case StatusCode::PreconditionRequired:
return "Precondition Required";
case StatusCode::TooManyRequests:
return "Too Many Requests";
case StatusCode::RequestHeaderFieldsTooLarge:
return "Request Header Fields Too Large";
case StatusCode::UnavailableForLegalReasons:
return "Unavailable For Legal Reasons";
case StatusCode::InternalServerError:
return "Internal Server Error";
case StatusCode::NotImplemented:
return "Not Implemented";
case StatusCode::ServiceUnavailable:
return "Service Unavailable";
case StatusCode::GatewayTimeout:
return "Gateway Timeout";
case StatusCode::HTTPVersionNotSupported:
return "HTTP Version Not Supported";
case StatusCode::VariantAlsoNegotiates:
return "Variant Also Negotiates";
case StatusCode::InsufficientStorage:
return "Insufficient Storage";
case StatusCode::LoopDetected:
return "Loop Detected";
case StatusCode::NotExtended:
return "Not Extended";
case StatusCode::NetworkAuthenticationRequired:
return "Network Authentication Required";
default:
return "";
}
}
};
class QueryParams {
private:
std::vector<std::pair<std::string,std::string>> items;
public:
void Add(std::string key, std::string value)
{
Add(std::pair<std::string,std::string>(key,value));
}
void Add(std::pair<std::string,std::string> pair)
{
items.push_back(pair);
}
void Add(std::vector<std::pair<std::string,std::string>>& items)
{
for(auto item : items)
this->items.push_back(item);
}
std::vector<std::pair<std::string,std::string>>& GetAll()
{
return this->items;
}
void Get(std::vector<std::string>& v, std::string key)
{
for(auto item : this->items)
if(item.first == key)
v.push_back(item.second);
}
void Set(std::string key,std::string value)
{
Clear(key);
Add(key,value);
}
void Set(std::pair<std::string,std::string> item)
{
Set(item.first,item.second);
}
void Set(std::string key, std::vector<std::string>& value)
{
Clear(key);
for(auto item : value)
Add(key,item);
}
bool TryGetFirst(std::string key,std::string& val)
{
for(auto item : this->items)
if(item.first == key)
{
val = item.second;
return true;
}
return false;
}
void Clear(std::string key)
{
for(auto item = items.begin();item<items.end();item++)
{
if((*item).first == key)
{
items.erase(item);
item--;
}
}
}
void Clear()
{
items.clear();
}
};
class ServerContext {
Stream* strm;
bool ok;
public:
ServerContext(Stream* strm)
{
DEBUGOUT("ServerContext Constructed\n");
this->strm = strm;
std::string firstLine = strm->ReadLine();
DEBUGOUT("READ Request line\n");
std::vector<std::string> firstLineA=HttpUtils::SplitString(firstLine," ",4);
DEBUGOUT("Split Request line\n");
if(firstLineA.size() == 4 || firstLineA.size() < 3)
{
DEBUGOUT("Request line does not have 3 entries\n");
this->ok=false;
}
else
{
this->ok=false;
method = firstLineA[0];
std::vector<std::string> p = HttpUtils::SplitString(firstLineA[1],"?",2);
if(p.size() >= 1)
{
this->ok = true;
this->originalPath = p[0];
DEBUGOUT("Path %s\n",this->originalPath.c_str());
this->path = originalPath;
if(p.size() == 2)
{
auto qp=HttpUtils::QueryParamsDecode(p[1]);
this->queryParams.Add(qp);
}
}
version = firstLineA[2];
responseVersion=version;
std::string line;
while((line=strm->ReadLine()).size() > 0)
{
std::vector<std::string> header=HttpUtils::SplitString(line,": ",2);
if(header.size() == 2)
{
this->requestHeaders.Add(std::pair<std::string,std::string>(header[0],header[1]));
}
}
}
}
bool CanHandleRequest()
{
return this->ok;
}
std::string method;
std::string originalPath;
std::string path;
std::string version;
std::string responseVersion;
bool ConnectionOpen()
{
std::string keep_alive;
if(requestHeaders.TryGetFirst("Connection",keep_alive))
{
return keep_alive.compare("keep-alive") == 0;
}
return false;
}
QueryParams queryParams;
QueryParams requestHeaders;
QueryParams responseHeaders;
StatusCode statusCode=StatusCode::OK;
void SendHeaders()
{
std::string headers = responseVersion + " " + std::to_string((int)statusCode) + " " + HttpUtils::StatusCodeString(statusCode) + "\r\n";
for(auto header : responseHeaders.GetAll())
{
headers += header.first + ": " + header.second + "\r\n";
}
headers += "\r\n";
this->strm->WriteBlock((uint8_t*)headers.c_str(),headers.size());
}
void SendErrorPage(bool showPath)
{
std::string errorHtml = showPath ? ("<html><head><title>File " + HttpUtils::HtmlEncode(this->originalPath) + " " + HttpUtils::StatusCodeString(this->statusCode) + "</title></head><body><h1>" + std::to_string((int)this->statusCode) + " " + HttpUtils::StatusCodeString(this->statusCode) + "</h1><h4>" + HttpUtils::HtmlEncode(this->originalPath) + "</h4></body></html>") : "";
SetContentType("text/html")->SendText(errorHtml);
}
ServerContext* SetContentType(std::string contentType)
{
return this->SetHeader("Content-Type",contentType);
}
ServerContext* SetHeader(std::string key,std::string value)
{
this->responseHeaders.Set(key,value);
return this;
}
void SendBytes(uint8_t* buffer,size_t len)
{
this->SetHeader("Content-Length",std::to_string(len))->SendHeaders();
this->strm->WriteBlock(buffer,len);
}
void SendText(std::string text)
{
SendBytes((uint8_t*)text.c_str(),text.size());
}
void SendNotFound()
{
statusCode = StatusCode::NotFound;
SendErrorPage(true);
}
void SendBadRequest()
{
statusCode = StatusCode::BadRequest;
SendErrorPage(false);
}
~ServerContext()
{
if(!ConnectionOpen())
delete strm;
}
};
class IServer {
public:
virtual bool Handle(ServerContext* ctx)=0;
};
#if defined(USE_SCRIPT_ENGINE)
#define INCLUDED_FROM_TESSESWEBSERVER_H
#include "tessesscriptengine.hpp"
#undef INCLUDED_FROM_TESSESWEBSERVER_H
#endif
class HttpServerListener {
static THREAD_RETURN_TYPE HandleRequestOnThread(void* arg)
{
DEBUGOUT("Started thread\n");
void** args = (void**)arg;
IServer* svr= static_cast<IServer*>(args[0]);
Stream* strm = static_cast<Stream*>(args[1]);
delete args;
HandleRequest(svr,strm);
return THREAD_DEFAULT_RETURN_TYPE;
}
public:
static void Init()
{
tesses_webserver_statics.isRunning=true;
Thread::Init();
}
static void HandleRequest(IServer* server,Stream* strm)
{
start:
ServerContext* ctx = new ServerContext(strm);
if(!ctx->CanHandleRequest())
{
ctx->SendBadRequest();
delete ctx;
return;
}
if(!server->Handle(ctx))
ctx->SendNotFound();
if(ctx->ConnectionOpen())
{
delete ctx;
goto start;
}
delete ctx;
}
static void ListenWithBoundSocket(IServer* server,int boundAndListeningSocket)
{
printf("\033[31mAlmost ready to listen\033[0m\n");
while(tesses_webserver_statics.isRunning)
{
struct sockaddr addr;
socklen_t addrlen;
int a = accept(boundAndListeningSocket, &addr, &addrlen);
void** myArg = new void*[2];
myArg[0] = static_cast<void*>(server);
myArg[1] = static_cast<void*>(new SocketStream(a));
Thread* thread = new Thread(HandleRequestOnThread,myArg,512000);
thread->Detach();
/*ServerContext* ctx = ReadHeaders(a);
if(!server->Handle(ctx))
ctx->SendNotFound();
delete ctx;
close(a);
*/
}
}
static void Listen(IServer* server, unsigned int domain,const struct sockaddr* addr, socklen_t addrlen)
{
int s=socket(domain,SOCK_STREAM,0);
if(s == -1)
{
printf("Socket error\n");
throw std::exception();
}
if(bind(s,addr,addrlen) == -1)
{
printf("Bind error\n");
throw std::exception();
}
if(listen(s,10) == -1)
{
printf("Listen error\n");
throw std::exception();
}
ListenWithBoundSocket(server,s);
}
static void Listen(IServer* server, int port)
{
struct sockaddr_in addr;
memset(&addr,0,sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons((uint16_t)port);
addr.sin_addr.s_addr = 0;
Listen(server,AF_INET,(struct sockaddr*)&addr,(socklen_t)sizeof(addr));
}
static void DoJobs()
{
#if defined(GEKKO)
Thread::CleanThreads();
#endif
}
static void Stop()
{
}
};
#define TESSESWEBSERVER_STATIC_DECLARATION Tesses::WebServer::Statics Tesses::WebServer::tesses_webserver_statics;
}