From 1b7a57d7f88cab0a1968e8c886eac3629dc74617 Mon Sep 17 00:00:00 2001 From: Pixel Date: Thu, 17 Nov 2011 18:41:23 -0800 Subject: HTTP server's first real test, alongside multiple taskmanager threads. I'm not really sure I fully like the way I'm designing this, but I guess it could be solved with an HTTP/HTML helper class around the Action class. However, the HTTP server awfully need reference counting, so it doesn't go away before all of the workers disappear, which means a bit of a redesign of the Listener template. --- Makefile | 1 + includes/BRegex.h | 6 +++ includes/HttpServer.h | 33 ++++++++++++ includes/TaskMan.h | 1 + src/BRegex.cc | 2 + src/HttpServer.cc | 142 ++++++++++++++++++++++++++++++++++++++------------ src/TaskMan.cc | 23 ++++++-- tests/test-Http.cc | 62 ++++++++++++++++++++++ 8 files changed, 233 insertions(+), 37 deletions(-) create mode 100644 tests/test-Http.cc diff --git a/Makefile b/Makefile index e836703..f4ee968 100644 --- a/Makefile +++ b/Makefile @@ -169,6 +169,7 @@ test-Tasks.cc \ test-Threads.cc \ test-Handles.cc \ test-Sockets.cc \ +test-Http.cc \ test-Lua.cc \ test-Regex.cc \ diff --git a/includes/BRegex.h b/includes/BRegex.h index 128597e..d7957bd 100644 --- a/includes/BRegex.h +++ b/includes/BRegex.h @@ -19,4 +19,10 @@ class Regex { regex_t m_regex; }; +class Regexes { + public: + static Regex any; + static Regex empty; +}; + }; diff --git a/includes/HttpServer.h b/includes/HttpServer.h index 7ae0dc1..2226d23 100644 --- a/includes/HttpServer.h +++ b/includes/HttpServer.h @@ -1,7 +1,14 @@ #pragma once +#include +#include + +#include #include +#include #include +#include +#include namespace Balau { @@ -9,17 +16,43 @@ class HttpWorker; class HttpServer { public: + + typedef std::map StringMap; + typedef std::map > FileList; + + class Action { + public: + Action(Regex & regex, Regex & host = Regexes::any) : m_regex(regex), m_host(host), m_refCount(0) { } + ~Action() { Assert(m_refCount == 0); } + typedef std::pair ActionMatches; + ActionMatches matches(const char * uri, const char * host); + void unref() { if (Atomic::Prefetch::Decrement(&m_refCount) == 0) delete this; } + void ref() { Atomic::Increment(&m_refCount); } + void registerMe(HttpServer * server) { server->registerAction(this); } + virtual bool Do(HttpServer * server, ActionMatches & m, IO out, StringMap & vars, StringMap & headers, FileList & files) = 0; + private: + Regex m_regex, m_host; + volatile int m_refCount; + }; + HttpServer() : m_started(false), m_listenerPtr(NULL), m_port(80) { } ~HttpServer() { if (!m_started) stop(); } void start(); void stop(); void setPort(int port) { Assert(!m_started); m_port = port; } void setLocal(const char * local) { Assert(!m_started); m_local = local; } + void registerAction(Action * action); + void flushAllActions(); + typedef std::pair ActionFound; + ActionFound findAction(const char * uri, const char * host); private: bool m_started; void * m_listenerPtr; int m_port; String m_local; + typedef std::list ActionList; + ActionList m_actions; + Lock m_actionsLock; friend class HttpWorker; }; diff --git a/includes/TaskMan.h b/includes/TaskMan.h index c63695a..026ea88 100644 --- a/includes/TaskMan.h +++ b/includes/TaskMan.h @@ -33,6 +33,7 @@ class TaskMan { void signalTask(Task * t); static void stop(); void stopMe() { m_stopped = true; } + static Thread * createThreadedTaskMan(); private: static void registerTask(Task * t, Task * stick); void * getStack(); diff --git a/src/BRegex.cc b/src/BRegex.cc index a7b37b0..d3a36f7 100644 --- a/src/BRegex.cc +++ b/src/BRegex.cc @@ -47,3 +47,5 @@ Balau::String Balau::Regex::getError(int err) { return r; } +Balau::Regex Balau::Regexes::any(".*"); +Balau::Regex Balau::Regexes::empty("^$"); diff --git a/src/HttpServer.cc b/src/HttpServer.cc index b027824..c57c63d 100644 --- a/src/HttpServer.cc +++ b/src/HttpServer.cc @@ -1,5 +1,3 @@ -#include - #include "Http.h" #include "HttpServer.h" #include "Socket.h" @@ -10,8 +8,6 @@ static const ev_tstamp s_httpTimeout = 15; namespace Balau { -typedef std::map StringMap; - class HttpWorker : public Task { public: HttpWorker(IO io, void * server); @@ -21,21 +17,23 @@ class HttpWorker : public Task { virtual const char * getName(); bool handleClient(); - void send400(Events::BaseEvent * evt); + void send400(); + void send404(); String httpUnescape(const char * in); - void readVariables(StringMap & variables, char * str); + void readVariables(HttpServer::StringMap & variables, char * str); IO m_socket; IO m_strm; String m_name; + HttpServer * m_server; }; }; Balau::HttpWorker::HttpWorker(IO io, void * _server) : m_socket(new WriteOnly(io)), m_strm(new BStream(io)) { - HttpServer * server = (HttpServer *) _server; + m_server = (HttpServer *) _server; m_name.set("HttpWorker(%s)", m_socket->getName()); - // copy stuff from server, such as port number, root document, base URL, etc... + // get stuff from server, such as port number, root document, base URL, default 400/404 actions, etc... } Balau::HttpWorker::~HttpWorker() { @@ -72,7 +70,7 @@ Balau::String Balau::HttpWorker::httpUnescape(const char * in) { return r; } -void Balau::HttpWorker::readVariables(StringMap & variables, char * str) { +void Balau::HttpWorker::readVariables(HttpServer::StringMap & variables, char * str) { char * ampPos; do { ampPos = strchr(str, '&'); @@ -92,7 +90,7 @@ void Balau::HttpWorker::readVariables(StringMap & variables, char * str) { } while (ampPos); } -void Balau::HttpWorker::send400(Events::BaseEvent * evt) { +void Balau::HttpWorker::send400() { static const char str[] = "HTTP/1.0 400 Bad Request\r\n" "Content-Type: text/html; charset=UTF-8\r\n" @@ -111,8 +109,29 @@ void Balau::HttpWorker::send400(Events::BaseEvent * evt) { " \n" "\n"; - setOkayToEAgain(false); - m_socket->forceWrite(str, sizeof(str) - 1, evt); + m_socket->forceWrite(str, sizeof(str) - 1); + Balau::Printer::elog(Balau::E_HTTPSERVER, "%s had an invalid request", m_name.to_charp()); +} + +void Balau::HttpWorker::send404() { + static const char str[] = +"HTTP/1.1 404 Not Found\r\n" +"Content-Type: text/html; charset=UTF-8\r\n" +"Server: " DAEMON_NAME "\r\n" +"\r\n" +"\n" +"\n" +" \n" +" 404 Not Found\n" +" \n" +"\n" +" \n" +" The HTTP request you've sent didn't match any action on this server.\n" +" \n" +"\n"; + + m_socket->forceWrite(str, sizeof(str) - 1); Balau::Printer::elog(Balau::E_HTTPSERVER, "%s had an invalid request", m_name.to_charp()); } @@ -127,8 +146,9 @@ bool Balau::HttpWorker::handleClient() { String host; String uri; String httpVersion; - StringMap httpHeaders; - StringMap variables; + HttpServer::StringMap httpHeaders; + HttpServer::StringMap variables; + HttpServer::FileList files; bool persistent = false; // read client's request @@ -168,14 +188,14 @@ bool Balau::HttpWorker::handleClient() { break; } if (urlBegin == 0) { - send400(&evtTimeout); + send400(); return false; } int urlEnd = line.strrchr(' ') - 1; if (urlEnd < urlBegin) { - send400(&evtTimeout); + send400(); return false; } @@ -184,7 +204,7 @@ bool Balau::HttpWorker::handleClient() { int httpBegin = urlEnd + 2; if ((httpBegin + 5) >= line.strlen()) { - send400(&evtTimeout); + send400(); return false; } @@ -195,19 +215,19 @@ bool Balau::HttpWorker::handleClient() { (line[httpBegin + 4] == '/')) { httpVersion = line.extract(httpBegin + 5); } else { - send400(&evtTimeout); + send400(); return false; } if ((httpVersion != "1.0") && (httpVersion != "1.1")) { - send400(&evtTimeout); + send400(); return false; } } else { // parse HTTP header. int colon = line.strchr(':'); if (colon <= 0) { - send400(&evtTimeout); + send400(); return false; } @@ -221,16 +241,16 @@ bool Balau::HttpWorker::handleClient() { } while(true); if (!gotFirst) { - send400(&evtTimeout); + send400(); return false; } if (httpVersion == "1.1") { - StringMap::iterator i = httpHeaders.find("Connection"); + HttpServer::StringMap::iterator i = httpHeaders.find("Connection"); if (i != httpHeaders.end()) { if (i->second != "close") { - send400(&evtTimeout); + send400(); return false; } } else { @@ -240,7 +260,7 @@ bool Balau::HttpWorker::handleClient() { if (method == Http::POST) { int length = 0; - StringMap::iterator i; + HttpServer::StringMap::iterator i; bool multipart = false; String boundary; @@ -255,10 +275,10 @@ bool Balau::HttpWorker::handleClient() { static const String multipartStr = "multipart/form-data"; if (i->second.extract(multipartStr.strlen()) == multipartStr) { if (i->second[multipartStr.strlen() + 1] != ';') { - send400(&evtTimeout); + send400(); return false; } - StringMap t; + HttpServer::StringMap t; char * b = i->second.extract(sizeof(multipartStr) + 1).trim().strdup(); readVariables(t, b); free(b); @@ -266,7 +286,7 @@ bool Balau::HttpWorker::handleClient() { i = t.find("boundary"); if (i == t.end()) { - send400(&evtTimeout); + send400(); return false; } @@ -319,11 +339,11 @@ bool Balau::HttpWorker::handleClient() { } } - StringMap::iterator hostIter = httpHeaders.find("host"); + HttpServer::StringMap::iterator hostIter = httpHeaders.find("host"); if (hostIter != httpHeaders.end()) { if (host != "") { - send400(&evtTimeout); + send400(); return false; } @@ -332,10 +352,16 @@ bool Balau::HttpWorker::handleClient() { // process query; everything should be here now - + HttpServer::ActionFound f = m_server->findAction(uri.to_charp(), host.to_charp()); + if (f.first) { + if (!f.first->Do(m_server, f.second, m_socket, variables, httpHeaders, files)) { + persistent = false; + } + } else { + send404(); + } // query process finished; wrapping up and exiting. - return persistent; } @@ -354,7 +380,7 @@ typedef Balau::Listener HttpListener; void Balau::HttpServer::start() { Assert(!m_started); - m_listenerPtr = createTask(new HttpListener(m_port, m_local.to_charp())); + m_listenerPtr = createTask(new HttpListener(m_port, m_local.to_charp(), this)); m_started = true; } @@ -363,3 +389,55 @@ void Balau::HttpServer::stop() { reinterpret_cast(m_listenerPtr)->stop(); m_started = false; } + +void Balau::HttpServer::registerAction(Action * action) { + m_actionsLock.enter(); + action->ref(); + m_actions.push_front(action); + m_actionsLock.leave(); +} + +void Balau::HttpServer::flushAllActions() { + m_actionsLock.enter(); + Action * a; + while (!m_actions.empty()) { + a = m_actions.front(); + m_actions.pop_front(); + a->unref(); + } + m_actionsLock.leave(); +} + +Balau::HttpServer::Action::ActionMatches Balau::HttpServer::Action::matches(const char * uri, const char * host) { + ActionMatches r; + + r.second = m_host.match(host); + if (r.second.empty()) + return r; + + r.first = m_regex.match(uri); + return r; +} + +Balau::HttpServer::ActionFound Balau::HttpServer::findAction(const char * uri, const char * host) { + m_actionsLock.enter(); + + ActionList::iterator i; + ActionFound r; + + for (i = m_actions.begin(); i != m_actions.end(); i++) { + r.first = *i; + r.second = r.first->matches(uri, host); + if (!r.second.first.empty()) + break; + } + + if (r.second.first.empty()) + r.first = NULL; + else + r.first->ref(); + + m_actionsLock.leave(); + + return r; +} diff --git a/src/TaskMan.cc b/src/TaskMan.cc index 0980a3b..73bce5c 100644 --- a/src/TaskMan.cc +++ b/src/TaskMan.cc @@ -206,7 +206,7 @@ void Balau::TaskMan::mainLoop() { Task * t; bool noWait = false; - Printer::elog(E_TASK, "TaskMan::mainLoop() with m_tasks.size = %i", m_tasks.size()); + Printer::elog(E_TASK, "TaskMan::mainLoop() at %p with m_tasks.size = %i", this, m_tasks.size()); // checking "STARTING" tasks, and running them once; also try to build the status of the noWait boolean. for (iH = m_tasks.begin(); iH != m_tasks.end(); iH++) { @@ -226,9 +226,9 @@ void Balau::TaskMan::mainLoop() { // libev's event "loop". We always runs it once though. m_allowedToSignal = true; - Printer::elog(E_TASK, "Going to libev main loop"); + Printer::elog(E_TASK, "TaskMan at %p Going to libev main loop", this); ev_run(m_loop, noWait || m_stopped ? EVRUN_NOWAIT : EVRUN_ONCE); - Printer::elog(E_TASK, "Getting out of libev main loop"); + Printer::elog(E_TASK, "TaskMan at %p Getting out of libev main loop", this); // let's check what task got stopped, and signal them for (iH = m_tasks.begin(); iH != m_tasks.end(); iH++) { @@ -247,7 +247,7 @@ void Balau::TaskMan::mainLoop() { // let's check who got signaled, and call them for (iH = m_signaledTasks.begin(); iH != m_signaledTasks.end(); iH++) { t = *iH; - Printer::elog(E_TASK, "Switching to task %p (%s - %s) that got signaled somehow.", t, t->getName(), ClassName(t).c_str()); + Printer::elog(E_TASK, "TaskMan at %p Switching to task %p (%s - %s) that got signaled somehow.", this, t, t->getName(), ClassName(t).c_str()); Assert(t->getStatus() == Task::IDLE); t->switchTo(); } @@ -279,7 +279,7 @@ void Balau::TaskMan::mainLoop() { } while (didDelete); } while (!m_stopped); - Printer::elog(E_TASK, "TaskManager stopping."); + Printer::elog(E_TASK, "TaskManager at %p stopping.", this); } void Balau::TaskMan::registerTask(Balau::Task * t, Balau::Task * stick) { @@ -305,3 +305,16 @@ void Balau::TaskMan::signalTask(Task * t) { void Balau::TaskMan::stop() { s_scheduler.stopAll(); } + +class ThreadedTaskMan : public Balau::Thread { + virtual void * proc() { + m_taskMan = new Balau::TaskMan(); + m_taskMan->mainLoop(); + } + Balau::TaskMan * m_taskMan; +}; + +Balau::Thread * Balau::TaskMan::createThreadedTaskMan() { + Thread * r = new ThreadedTaskMan(); + r->threadStart(); +} diff --git a/tests/test-Http.cc b/tests/test-Http.cc new file mode 100644 index 0000000..496d7e6 --- /dev/null +++ b/tests/test-Http.cc @@ -0,0 +1,62 @@ +#include +#include + +BALAU_STARTUP; + +#define DAEMON_NAME "Balau/1.0" + +using namespace Balau; + +class TestAction : public HttpServer::Action { + public: + TestAction() : Action(Regexes::any) { } + virtual bool Do(HttpServer * server, HttpServer::Action::ActionMatches & m, IO out, HttpServer::StringMap & vars, HttpServer::StringMap & headers, HttpServer::FileList & files); +}; + +bool TestAction::Do(HttpServer * server, HttpServer::Action::ActionMatches & m, IO out, HttpServer::StringMap & vars, HttpServer::StringMap & headers, HttpServer::FileList & files) { + static const char str[] = +"HTTP/1.1 200 Found\r\n" +"Content-Type: text/html; charset=UTF-8\r\n" +"Content-Length: 266\r\n" +"Server: " DAEMON_NAME "\r\n" +"\r\n" +"\n" +"\n" +" \n" +" Test\n" +" \n" +"\n" +" \n" +" This is a test document.\n" +" \n" +"\n"; + + out->forceWrite(str, sizeof(str) - 1); + return true; +} + +void MainTask::Do() { + Printer::log(M_STATUS, "Test::Http running."); + + Thread * tms[4]; + + for (int i = 0; i < 4; i++) + tms[i] = TaskMan::createThreadedTaskMan(); + + HttpServer * s = new HttpServer(); + TestAction * a = new TestAction(); + a->registerMe(s); + s->setPort(8080); + s->setLocal("localhost"); + s->start(); + + yield(); + + s->stop(); + + for (int i = 0; i < 4; i++) + tms[i]->join(); + + Printer::log(M_STATUS, "Test::Http passed."); +} -- cgit v1.2.3