C++ Klassen-Template für HTTP-Kommunikation
C++ HTTP communication class template
Kafreitagsprojekt, um das HTTP Protokoll ein Bisschen mehr zu
verinnerlichen (manche Sachen muss man einfach mal selbst implementiert
haben). Die statischen Funktionen und Teile des Parsers hatte ich schon
im voraus, jedoch galt es diese in eine "nette Struktur" zu packen, mit
der HTTP Anfragen vergleichsweise komfortabel direkt in C++ bearbeitet
werden können - ohne Schnittstellen für LUA oder JS. Zudem sollte die
Klasse selbst weitgehend unabhängig von plattformspezifischen Mechanismen
sein (Datei-IO, Dateipfadkonventionen, usw), auch Eingabe- und Ausgabestream
sollte wählbar sein (d.h. egal ob Dateien, stdin/stdout=GCI oder Socket).
Erste Tests mit Apache Benchmark und 128 gleichzeitigen Anfragen ergaben
(GET / HTTP1.1
) in einer virtuellen Maschine (VirtualBox mit Debian
und einem CPU) ca. 8000 Accepts pro Sekunde und durchschnittlich 200us
Antwortzeit. Das ist natürlich nicht repräsentativ, da die Anfragen
direkt im RAM behandelt wurden - und die CPU-Auslastung der VM 100% war.
Es geht hier nicht darum, Apache oder Ngnx wirderzuerfinden, Zielanwendung
ist vielmehr eine Konfigurationsschnittstelle für netzwerkangebundene
("CNC") Steuerungen, bei der z.B. Regler über eine HTML5-Schnittstelle
eingestellt werden können. Dazu müssen Echtzeitdaten recht schnell aus
RO-Registern oder Fifos über Websockets an den Browser gesendet werden -
das wird besser mit einem kompiliertem Programm bewerkstelligt.
Zur Klasse
sw::http
ist die Standardspezialisierung des Templates
sw::templates::basic_http<...>
, welchem als Argument eine Dateispeicherungsklasse
übergeben wird.
Temporary File Storage
Diese Speicherklasse ist abgeleitet von sw::templates::http_tmpstorage_provider<>
,
und dient ausschließlich dem Zweck, in eine temporäre Datei zu schreiben,
daraus zu lesen und sie beim Schließen zu löschen. Sie wird nur dann benötigt,
wenn PUT
oder POST
-Anfragen vom Client zu groß sind, um sie direkt im
RAM zu behandeln - z.B. wenn Dateien hochgeladen werden. Ansonsten sind alle
Eingaben inklusive der Dateninhalte in den Instanzobjekten gespeichert.
Die zu überladenen Methoden des Storage Providers sind vom Muster so gewählt,
dass sie den Konventionen von üblichen "low-level" C I/O Funktionen entsprechen,
z.B. kann read
direkt an ::read
oder ::fread
geklebt werden, d.h. es
wird mit Binärpuffern und deren Größen gearbeitet. Die Rückgabewerte sind
demnach die Anzahl geschriebener/gelesener Bytes oder negativ bei Fehlern.
Ansonsten gibt es open
, close
, eof
, size
, seek
usw - die bekannte
Pallette. Wo die temporäre Datei liegt (bzw. ob es überhaupt eine Datei ist),
wie sie heißt, und ob sie "gelocked" wird (was definitiv zu empfehlen ist),
ist der http-Klasse egal - wenn es Fehler gibt wird ein "500 Internal server error"
an den Client gesendet. Die default-Spezialisierung sw::http
verwendet direkt
die Basis sw::templates::http_tmpstorage_provider<>
, und diese gibt bei allen
Operationen -1 == Fehler
zurück, d.h. sie erlaubt Client-Eingaben, die im RAM
behandelt werden können. Noch eine Anmerkung: Die http-Klasse fängt keine
Exceptions aus dem Storage-Provider ab. Exceptions sollten nur bei wirklich üblen
Fehlern geworfen werden, nicht wenn z.B. kein Platz auf der Platte ist - dazu
kann open
negative Ergebnisse zurückgeben. Als Argument wird beim Aufruf von
open
direkt mit angegeben, wieviele Bytes benötigt werden (Content-Length
Angabe im Requestheader, und mehr wird auch nicht geschrieben).
Auf das Storage-Objekt einer http
-Instanz kann von außen mit der Methode
inline storage_t & storage() throw()
, also z.B.
if(my_http_instance.storage().eof()) { ... }
zugegriffen werden.
I/O Streams
Um die Ein- und Ausgabekanäle der basic_http<>
Klasse zu bestimmen müssen
nur zwei Methoden überladen werden:
virtual long read(void* data, unsigned long sz)
undvirtual long write(const void* data, unsigned long sz)
.
Diese Methoden entsprechen den Mustern der SysCalls ::read
und ::write
ohne File-Descriptor, und per Default wird von stdin
gelesen und auf stdout
geschrieben ("CGI Programm"). Für direkte Verwendung mit Sockets müssen diese
Methoden überladen werden, und einfach an ::send
und ::recv
geklebt werden.
Diese Implementierung macht es deutlich einfacher und übersichtlicher als eigene
c++ iostreams
zu schreiben. Anmerkung: Wie üblich bedeutet der Rückgabewert
0
bei read
End Of Stream. Der Fehlercode EGAGAIN bzw. EWOULDBLOCK
wird
berücksichtigt. Der Klasse ist es egal, ob die Eingabe "blocking" oder
"nonblocking" ist. Seitens des Parsers gibt es keine Zeitabhängigkeit.
Statische Funktionen
Der eigentliche Grund, warum die Standard-Spezialisierung sw::http
existiert,
sind die statischen Funktionen, für die nie ein Storage-Provider benötigt wird.
D.h. man kann diese Funktionen verwenden ohne einen eigenen typdef ... my_http;
anlegen zu müssen. Es sind meist HTML bzw. HTTP-Funktionen:
string urldecode(const string &in, bool form=false) // "hello%20" --> "hello " etc
string urlencode(const string &in) // "hello " --> "hello%20" etc
string htmlspecialchars(const string &s, bool with_quotes=true) // "<" --> "<" etc
string mime_content_type(const string &file_extension) // png --> image/png
string mime_content_type_add(const char *ext, const char *mime)
const char * reponse_status_text(unsigned code) // 404 -> "404 Not found"
int parse_url_encoded(const string & inp, str_map_t & out, ..) // string --> std::map<>
string timestamp2string(time_t t) // Local time --> RFC(GMT)
time_t string2timestamp(const string &stime) // RFC(GMT) --> local time
Hier kommen vielleicht noch ein paar hinzu.
Instanz und Methoden
Letztendlich will man sich weniger um das Protokoll kümmern und viel mehr mit den
"Payload" Daten arbeiten. Es bleibt einem natürlich nicht alles erspart, dazu ist
das HTTP-Protokoll zu vielfältig, aber die Basics dürften schon recht gut abgedeckt
sein. Auch hier wurden bei allen Methoden auf die direkte Verwendung von Exceptions
verzichtet - d.h. wenn eine Exception aus der Klasse kommt ist es ein "fataler Fehler"
(z.B. kein Speicher bekommen oder ein STL Container beschwert sich) - oder ein Bug.
Letztendlich führt es dazu, dass vielleicht noch ein 500
weggeschickt werden kann.
Exceptions können gerne durch Ableiten der Klasse hinzugefügt werden.
Da wäre zunächst einmal die Konfiguration static basic_http<...>::config
. Dabei
handelt es sich um eine struct
Instanz, in der Einstellungen für alle Klassenobjekte
angegeben werden, z.B. Log-Level, welche log
-Funktion verwendet werden soll, maximale
Größe der Client-Anfragen, usw. Dieses Konfigurationsobjekt ist nicht thread-safe, also
nicht durch Mutex/Critical Sections abgesperrt. Es sollte vor dem Starten des Server-Threads
geschrieben und dann nur noch gelesen werden. Wenn man sich die Einstellungen ansieht
wird klar, dass es tatsächlich konstante "write-once" Werte sind. Details siehe im Quelltext
struct basic_http::config_t
. Beispiel:
http::config.log_level = http::log_warn; // Log critical, error and warning
http::config.max_content_length = 2ul *(1ul<<20); // 2MB requests allowed, not more
Dann hat jede Instanz natürlich ein "Request-Objekt" und ein "Response-Objekt", auf die
mit instance.request()
und instance.response()
zugegriffen wird. Die Details sind im
Quelltext dokumentiert (struct request_t
und struct response_t
). Sie enthalten in
erster Linie HTTP-Headerinformationen, die beim Parsen des Requestheaders bzw. Zusammenstellen
des Response-Headers verwendet werden. Die Daten sind in einem für C++ verwendbarem
Format, d.h. Zeiten sind keine RFC Strings, sondern ein Zeitstempel, Cache-Control
-Daten
sind auf eine enum
abgebildet, usw. Vor allem wenn IDEs mit Auto-Complete (wie Netbeans
oder Eclipse) verwendet werden ist die Handhabung schnell und einfach. Unbekannte HTTP-Headerdaten
werden in std::map<string, string>
's gespeichert und können von dort abgerufen werden,
also beiepielsweise:
string auth=inst.request().headers["authorization"];
oderinst.response().headers["WWW-Authenticate"] = "Basic realm=\"My realm\""
.
Um die Verwendung zu beschleunigen gibt es noch ein paar typische Zugriffsmethoden, die auch
von anderen Programmiersprachen bekannt sind. Dabei ist str_t std::string
und
str_map_t std::map<std::string, std::string>
:
GET
-Variablen
const str_map_t & get()
gibt alle GET variablen als std::map zurück.const str_t & get(const str_t & var)
gibt eine GET Variable "by-name" zurück, oder "" falls nicht in der Map. (Achtung: Namen sind immer kleingeschrieben, auch wenn in der Anfrage Großbuchstaben dabei sind).double get_dbl(const str_t & var)
, das ganze alsdouble
oderNAN
wenn nicht gefunden oder nicht indouble
konvertierbar. (Achtung: Namen sind immer kleingeschrieben)
POST
-Variablen
const str_map_t & post() const throw()
, alle POST-Variablen AUßER DATEIEN.const str_t & post(const str_t & var)
, Stringwert einer bestimmten POST Variablen (Namen kleingeschrieben).double post_dbl(const str_t & var)
,double
-Wert einer POST Variablen oderNAN
, falls nicht gefunden oder keine Zahl. (Namen kleingeschrieben)const str_map_t & cookie()
: Alle COOKIE Werte von der Anfrage (Request-Header) als map.
COOKIE
-Variablen
const str_t & cookie(const str_t & var)
: Request-COOKIE-Wert "by-name", auch hier: Namen kleingeschrieben.double cookie_dbl(const str_t & var)
: Request-COOKIE-Wert "by-name" alsdouble
oderNAN
. (Namen kleingeschrieben)basic_http & cookie(const str_t & name, const str_t & value, time_t expires=0, const str_t & domain="", const str_t & path="", bool secure=false, bool httponly=false)
: Setzt ein Response-Header-Cookie (mit den bekannten Attributen, die noch hinzu kommen können).expires
ist ein lokaler Zeitstempel, GMT-Konvertierung kommt dann beim Abschicken des Headers.basic_http & cookie(const str_t & name, double d, time_t expires=0, const str_t & domain="", const str_t & path="", bool secure=false, bool httponly=false)
: Derselbe Spaß, aber stattstring
wird eindouble
-Wert gesetzt.
FILES
-Variablen
const upload_files_t & files()
: Map aller hochgeladenen Dateien.upload_files_t
ist dabei einestd::map<string, upload_file_t>
, undupload_file_t
Objekte, in denen Informationen über die Dateien stehen (Name, Größe, und wo sie imstorage()
zu finden ist).
HEADER
-Variablen
basic_http & header(const str_t& key, const str_t& value)
: Setzt einen Reponse-Header oder löscht ihn wennvalue==""
.const str_t & header(const str_t& key)
: Gibt einen Reponse-Header zurück, oder""
wenn nicht gefunden.str_t header()
: Erstellt den kompletten Text-Header für die Antwort, greift dabei auf Daten vonconfig
,response()
undrequest()
zurück. Normalerweise muss diese Methode nie aufgerufen werden. Diesend
Methode tut das implizit.
DUMP
str_t dump()
: Alle Objektinformationen als JSON-ähnlicher text. Kann hilfreich sein.
SEND
, RECV
und STATE
Hier wird es nochmals interessant. Wenn eine eingehende Verbindung akzeptiert und
ein neues http
-Objekt erstellt wurde, muss erstmal der Requestheader empfangen
werden bevor weitere Schritte folgen (Achtung: Read-Timeouts müssen dabei extern
implementiert werden). Wenn beim Parsen des Headers kein Fehler oder "Bad Request"
festgestellt wurde, wird auch der "Message Body" empfangen, andernfalls direkt
ein Fehler zum Rücksenden signalisiert. Beginnen wir mit recv
- zwei Möglichkeiten
gibt es:
int recv(const char* rq, sz_t sz)
: Empfängtsz
Bytes aus dem Pufferrq
.int recv()
: Empfängt mit Hilfe der Methodevirtual long read(...)
- so viel wie geht, also kein Fehler, kein0
(EOF) und keinEAGAIN
kommt.
Der Rückgabewert von recv
ist entweder
1
: "ich brauche mehr Daten, hatte EAGAIN bzw. allesz
Bytes gelesen.",0
: "ich brauche keine Daten mehr, egal ob noch welche da wären" (z.B. wennContent-Length
erreicht ist oder ein GET/HEAD/DELETE usw. geschickt wurde).<0
: "Fehler!", Dabei sind Werte > -100 fatal (bedeutet es kann nicht einmal eine Fehlermeldung an den Client geschickt werden. Das passiert z.B. wenn der Socket einfach geschlossen wird). Werte < -100 sind invertierte HTTP Status Codes. Wenn also-400
zurückgegeben wird, dann stimmt etwas mit dem Requestheader nicht (400="Bad Request"). Die Fehler werden zusätzlich mit Meldungen geloggt. Gleichzeitig wird der Response-Header-Status auf den Fehlercode gesetzt. Beim Zusammenbau des Response-Headers wird nochmals der Code geprüft und in500
umgewandelt falls der Fehlercode kein richtiger HTTP Statuscode ist.
Zum Senden gibt es:
int send()
: Die zuvor festgelegten Daten senden bzw. weitersenden ("flush").int send(int status)
: Response-Header Statuscode setzen und senden. Das geht nur wenn der Header noch nicht gesendet wurde, ansonsten wird der Status einfach ignoriert.int send(const str_t & s, int status=200, bool implicit_content_length=true)
: Einen String senden, mit Status. Nun kann man sich entscheiden, obContent-Length
gleich mitgesetzt werden soll oder nicht. Wenn ja ist der Transfer nach dem Senden dieses Strings abgeschlossen. Wenn nein wird im Header keine Angabe über die Größe der Daten gemacht. Der Client erwartet dann so lange Daten bis der Server den Socket schließt. Wenn der Antwortheader schon gesendet ist habenstatus
undimplicit_content_length
keinen Effekt mehr.int send_binary(const void *data, sz_t sz, int status=200, bool implicit_content_length=true)
Dasselbe für Binärdaten.int send_file(const str_t & filename, int status=200)
: Sendet eine Datei, 404 falls nicht gefunden, 403 falls nicht lesbar, 500 bei anderen Fehlern. Prüft "If-Modified-Since" und "Etag", sendet 304 falls nicht geändert. MIME Content-Type anhand der Dateiendung, "application/octet-stream" für unbekannte Endungen. Sendet 204 falls die Datei leer ist.
Wenn recv
ein Fehler aufgetreten ist, kann direkt send()
ohne Argumente aufgerufen
werden. Da der Statuscode schon gesetzt ist, aber kein Inhalt existiert, wird automatisch
eine kleine Meldung (z.B. <h1>Not found</h1>
) hinzugefügt. Allerdings (und das mit
Absicht) ist der Status code erstmal 0
, und wenn eine Antwort ohne Setzen eines
Status Codes abgesendet wird, so wird automatisch ein 500
ausgelöst - d.h. der
Default-Status ist nicht 200
.
Eigentlich wär's das schon. Weitere Features sind chunked
-Encoding, was vor allem
dann sinvoll ist (bzw. ehr nur dann), wenn kontinuierliche Daten ohne Client-Polling
gesendet werden sollen. Weitere Details in den Beispielen.
Originally a good Friday project composing a single file template for HTTP server features. Basic functionality was already implemented quite a bit ago for several applications, features like multipart support added recently.
Purpose: To emphasise what is it not: Apache, gnix, or mongoose/civet.
It is a template to provide functionality to parse requests, compose
responses in a socket
or stdin/stdout
compatible way. So the
class can be used as CGI, as well as direct server socket processor.
How the communication channel is implemented is not part of this class -
instead you simply glue the I/O to your system by overloading the
class methods read
and write
, which are compatible to ::read()
and ::write()
(except that errno
does not apply). You also have
full control of the process flow, as you call the I/O methods of the
http
class actively - so it is flexibly up to you weather threads,
callback based methodology or sync i/o applies.
Simply take a glance at the examples to see the usage.
Dateien
Files
http.hh Microtest: microtest.cc Examples: http.cc http1.cc http_multipart.cc httpd.cc
Beispiele
Examples
#include <http.hh>
#include "test.hh"
using sw::http;
// <editor-fold desc="static functions" defaultstate="collapsed">
static bool strieq(std::string s1, std::string s2)
{
std::transform(s1.begin(), s1.end(), s1.begin(), ::tolower);
std::transform(s2.begin(), s2.end(), s2.begin(), ::tolower);
return s1 == s2;
}
static void test_static_fn()
{
{
test_expect( http::htmlspecialchars("", true) == "" );
test_expect( http::htmlspecialchars("<a>", true) == "<a>" );
test_expect( http::htmlspecialchars("<a name=\"name\">", true) == "<a name="name">" );
test_expect( http::htmlspecialchars("<a name=\"name\">", false) == "<a name=\"name\">" );
test_expect( http::htmlspecialchars("<a name=\"&name\">", false) == "<a name=\"&name\">" );
test_expect( http::htmlspecialchars("<>&\"'", false) == "<>&\"'" );
test_expect( http::htmlspecialchars("<>&\"'", true) == "<>&"'" );
}
{
test_expect( strieq(http::mime_content_type(""), "text/plain") );
test_expect( strieq(http::mime_content_type("notregistered"), "text/plain") );
test_expect( strieq(http::mime_content_type("aac"), "audio/aac") );
test_expect( strieq(http::mime_content_type("asf"), "video/x-ms-asf") );
test_expect( strieq(http::mime_content_type("avi"), "video/x-msvideo") );
test_expect( strieq(http::mime_content_type("bmp"), "image/bmp") );
test_expect( strieq(http::mime_content_type("css"), "text/css") );
test_expect( strieq(http::mime_content_type("csv"), "text/csv") );
test_expect( strieq(http::mime_content_type("doc"), "application/msword") );
test_expect( strieq(http::mime_content_type("eps"), "application/postscript") );
test_expect( strieq(http::mime_content_type("exe"), "application/octet-stream") );
test_expect( strieq(http::mime_content_type("gif"), "image/gif") );
test_expect( strieq(http::mime_content_type("gz"), "application/x-gunzip") );
test_expect( strieq(http::mime_content_type("htm"), "text/html") );
test_expect( strieq(http::mime_content_type("html"), "text/html") );
test_expect( strieq(http::mime_content_type("ico"), "image/x-icon") );
test_expect( strieq(http::mime_content_type("jpeg"), "image/jpeg") );
test_expect( strieq(http::mime_content_type("jpg"), "image/jpeg") );
test_expect( strieq(http::mime_content_type("js"), "application/javascript") );
test_expect( strieq(http::mime_content_type("json"), "application/json") );
test_expect( strieq(http::mime_content_type("m4v"), "video/x-m4v") );
test_expect( strieq(http::mime_content_type("mid"), "audio/x-midi") );
test_expect( strieq(http::mime_content_type("mov"), "video/quicktime") );
test_expect( strieq(http::mime_content_type("mp3"), "audio/mpeg") );
test_expect( strieq(http::mime_content_type("mp4"), "video/mp4") );
test_expect( strieq(http::mime_content_type("mpeg"), "video/mpeg") );
test_expect( strieq(http::mime_content_type("mpg"), "video/mpeg") );
test_expect( strieq(http::mime_content_type("oga"), "audio/ogg") );
test_expect( strieq(http::mime_content_type("ogg"), "audio/ogg") );
test_expect( strieq(http::mime_content_type("ogv"), "video/ogg") );
test_expect( strieq(http::mime_content_type("pdf"), "application/pdf") );
test_expect( strieq(http::mime_content_type("png"), "image/png") );
test_expect( strieq(http::mime_content_type("ps"), "application/postscript") );
test_expect( strieq(http::mime_content_type("rar"), "application/x-arj-compressed") );
test_expect( strieq(http::mime_content_type("rtf"), "application/rtf") );
test_expect( strieq(http::mime_content_type("sgm"), "text/sgml") );
test_expect( strieq(http::mime_content_type("shtm"), "text/html") );
test_expect( strieq(http::mime_content_type("shtml"), "text/html") );
test_expect( strieq(http::mime_content_type("svg"), "image/svg+xml") );
test_expect( strieq(http::mime_content_type("swf"), "application/x-shockwave-flash") );
test_expect( strieq(http::mime_content_type("tar"), "application/x-tar") );
test_expect( strieq(http::mime_content_type("tgz"), "application/x-tar-gz") );
test_expect( strieq(http::mime_content_type("tif"), "image/tiff") );
test_expect( strieq(http::mime_content_type("tiff"), "image/tiff") );
test_expect( strieq(http::mime_content_type("txt"), "text/plain") );
test_expect( strieq(http::mime_content_type("wav"), "audio/x-wav") );
test_expect( strieq(http::mime_content_type("webm"), "video/webm") );
test_expect( strieq(http::mime_content_type("wrl"), "model/vrml") );
test_expect( strieq(http::mime_content_type("xhtml"), "application/xhtml+xml") );
test_expect( strieq(http::mime_content_type("xls"), "application/x-msexcel") );
test_expect( strieq(http::mime_content_type("xml"), "text/xml") );
test_expect( strieq(http::mime_content_type("xsl"), "application/xml") );
test_expect( strieq(http::mime_content_type("xslt"), "application/xml") );
test_expect( strieq(http::mime_content_type("zip"), "application/x-zip-compressed") );
}
{
test_expect( http::urlencode("") == "" );
test_expect( http::urlencode("a") == "a" );
test_expect( http::urlencode(" ") == "%20" );
test_expect( http::urldecode("") == "" );
test_expect( http::urldecode("a") == "a" );
test_expect( http::urldecode("%20") == " " );
for(int i=0; i<100; i++) {
std::string s = sw::utest::random<string>(100);
if(!test_expect_cond( http::urldecode(http::urlencode(s)) == s )) {
test_comment("test string = " << s);
}
}
}
{
test_expect( strieq(http::reponse_status_text(0 ), "500 internal server error") );
test_expect( strieq(http::reponse_status_text(-1 ), "500 internal server error") );
test_expect( strieq(http::reponse_status_text(999), "500 internal server error") );
test_expect( strieq(http::reponse_status_text(100), "100 Continue") );
test_expect( strieq(http::reponse_status_text(101), "101 Switching Protocols") );
test_expect( strieq(http::reponse_status_text(102), "102 Processing") );
test_expect( strieq(http::reponse_status_text(200), "200 OK") );
test_expect( strieq(http::reponse_status_text(201), "201 Created") );
test_expect( strieq(http::reponse_status_text(202), "202 Accepted") );
test_expect( strieq(http::reponse_status_text(203), "203 Non-Authoritative Information") );
test_expect( strieq(http::reponse_status_text(204), "204 No Content") );
test_expect( strieq(http::reponse_status_text(205), "205 Reset Content") );
test_expect( strieq(http::reponse_status_text(206), "206 Partial Content") );
test_expect( strieq(http::reponse_status_text(207), "207 Multi-Status") );
test_expect( strieq(http::reponse_status_text(208), "208 Already Reported") );
test_expect( strieq(http::reponse_status_text(226), "226 IM Used") );
test_expect( strieq(http::reponse_status_text(300), "300 Multiple Choices") );
test_expect( strieq(http::reponse_status_text(301), "301 Moved Permanently") );
test_expect( strieq(http::reponse_status_text(302), "302 Found") );
test_expect( strieq(http::reponse_status_text(303), "303 See Other") );
test_expect( strieq(http::reponse_status_text(304), "304 Not Modified") );
test_expect( strieq(http::reponse_status_text(305), "305 Use Proxy") );
test_expect( strieq(http::reponse_status_text(306), "306 Switch Proxy") );
test_expect( strieq(http::reponse_status_text(307), "307 Temporary Redirect") );
test_expect( strieq(http::reponse_status_text(308), "308 Permanent Redirect") );
test_expect( strieq(http::reponse_status_text(400), "400 Bad Request") );
test_expect( strieq(http::reponse_status_text(401), "401 Unauthorized") );
test_expect( strieq(http::reponse_status_text(402), "402 Payment Required") );
test_expect( strieq(http::reponse_status_text(403), "403 Forbidden") );
test_expect( strieq(http::reponse_status_text(404), "404 Not Found") );
test_expect( strieq(http::reponse_status_text(405), "405 Method Not Allowed") );
test_expect( strieq(http::reponse_status_text(406), "406 Not Acceptable") );
test_expect( strieq(http::reponse_status_text(407), "407 Proxy Authentication Required") );
test_expect( strieq(http::reponse_status_text(408), "408 Request Timeout") );
test_expect( strieq(http::reponse_status_text(409), "409 Conflict") );
test_expect( strieq(http::reponse_status_text(410), "410 Gone") );
test_expect( strieq(http::reponse_status_text(411), "411 Length Required") );
test_expect( strieq(http::reponse_status_text(412), "412 Precondition Failed") );
test_expect( strieq(http::reponse_status_text(413), "413 Request Entity Too Large") );
test_expect( strieq(http::reponse_status_text(414), "414 Request-URI Too Long") );
test_expect( strieq(http::reponse_status_text(415), "415 Unsupported Media Type") );
test_expect( strieq(http::reponse_status_text(416), "416 Requested Range Not Satisfiable") );
test_expect( strieq(http::reponse_status_text(417), "417 Expectation Failed") );
test_expect( strieq(http::reponse_status_text(419), "419 Authentication Timeout") );
test_expect( strieq(http::reponse_status_text(420), "420 Enhance Your Calm") );
test_expect( strieq(http::reponse_status_text(422), "422 Unprocessable Entity") );
test_expect( strieq(http::reponse_status_text(423), "423 Locked") );
test_expect( strieq(http::reponse_status_text(424), "424 Failed Dependency") );
test_expect( strieq(http::reponse_status_text(425), "425 Unordered Collection") );
test_expect( strieq(http::reponse_status_text(426), "426 Upgrade Required") );
test_expect( strieq(http::reponse_status_text(428), "428 Precondition Required") );
test_expect( strieq(http::reponse_status_text(429), "429 Too Many Requests") );
test_expect( strieq(http::reponse_status_text(431), "431 Request Header Fields Too Large") );
test_expect( strieq(http::reponse_status_text(440), "440 Login Timeout") );
test_expect( strieq(http::reponse_status_text(444), "444 No Response") );
test_expect( strieq(http::reponse_status_text(449), "449 Retry With") );
test_expect( strieq(http::reponse_status_text(450), "450 Blocked by Windows Parental Controls") );
test_expect( strieq(http::reponse_status_text(451), "451 Unavailable For Legal Reasons") );
test_expect( !strieq(http::reponse_status_text(451), "451 Redirect") ); // Microsoft only, collides with standard draft 451
test_expect( strieq(http::reponse_status_text(494), "494 Request Header Too Large") );
test_expect( strieq(http::reponse_status_text(495), "495 Cert Error") );
test_expect( strieq(http::reponse_status_text(496), "496 No Cert") );
test_expect( strieq(http::reponse_status_text(497), "497 HTTP to HTTPS") );
test_expect( strieq(http::reponse_status_text(499), "499 Client Closed Request") );
test_expect( strieq(http::reponse_status_text(500), "500 Internal Server Error") );
test_expect( strieq(http::reponse_status_text(501), "501 Not Implemented") );
test_expect( strieq(http::reponse_status_text(502), "502 Bad Gateway") );
test_expect( strieq(http::reponse_status_text(503), "503 Service Unavailable") );
test_expect( strieq(http::reponse_status_text(504), "504 Gateway Timeout") );
test_expect( strieq(http::reponse_status_text(505), "505 HTTP Version Not Supported") );
test_expect( strieq(http::reponse_status_text(506), "506 Variant Also Negotiates") );
test_expect( strieq(http::reponse_status_text(507), "507 Insufficient Storage") );
test_expect( strieq(http::reponse_status_text(508), "508 Loop Detected") );
test_expect( strieq(http::reponse_status_text(509), "509 Bandwidth Limit Exceeded") );
test_expect( strieq(http::reponse_status_text(510), "510 Not Extended") );
test_expect( strieq(http::reponse_status_text(511), "511 Network Authentication Required") );
test_expect( strieq(http::reponse_status_text(520), "520 Origin Error") );
test_expect( strieq(http::reponse_status_text(521), "521 Web server is down") );
test_expect( strieq(http::reponse_status_text(522), "522 Connection timed out") );
test_expect( strieq(http::reponse_status_text(523), "523 Proxy Declined Request") );
test_expect( strieq(http::reponse_status_text(524), "524 A timeout occurred") );
test_expect( strieq(http::reponse_status_text(598), "598 Network read timeout error") );
test_expect( strieq(http::reponse_status_text(599), "599 Network connect timeout error") );
}
}
// </editor-fold>
// <editor-fold desc="test_timestamp2string" defaultstate="collapsed">
static void test_timestamp2string()
{
test_expect( http::timestamp2string(0) == "Thu, 01 Jan 1970 00:00:00 GMT");
test_expect( http::string2timestamp("Thu, 01 Jan 1970 00:00:00 GMT") == 0);
test_expect( http::timestamp2string(1337374161) == "Fri, 18 May 2012 20:49:21 GMT" );
for(int i=0; i<10; ++i) {
time_t t_random = sw::utest::random<time_t>(0, std::numeric_limits<int32_t>::max());
test_note("Random time = " << long(t_random));
std::string s = http::timestamp2string(t_random);
test_note("Stringyfied time = '" << s << "'");
time_t t_parsed = http::string2timestamp(s);
test_note("Back-parsed time = '" << t_parsed << "'");
if(!test_expect_cond( t_parsed == t_random )) {
test_comment("t_random= " << (long long) t_random << ", str=" << s << ", t_parsed=" << t_parsed);
}
}
}
// </editor-fold>
// <editor-fold desc="class test_http" defaultstate="collapsed">
class test_http : public http
{
public:
typedef void (*rq_callback_t)(test_http&); // for std=98 no lambda
typedef void (*chk_callback_t)(std::string& s); // for std=98 no lambda
std::string* rq_input;
std::string* rq_output;
public:
static void log_comment(const string & s, int level)
{ (void) level; test_comment(s); }
static void test_request(const string & s, bool is_invalid = false, rq_callback_t fn=0, chk_callback_t chk=0)
{
test_http ht;
std::string inp = s;
std::string out;
ht.rq_input = &inp;
ht.rq_output = &out;
int n_read = 1;
while(n_read > 0) n_read = ht.recv();
if(n_read < 0) {
test_expect( is_invalid );
test_expect( ht.error() != 0 );
test_comment( "ok, expected error, which is: " << ht.error() << " == " << ht.error_msg() );
if(!test_expect_cond( n_read != -1 )) {
test_fail("Fatal error was not expected.");
return;
}
if(fn) fn(ht);
while(ht.send() > 0) ;
} else {
test_expect( !is_invalid );
if(fn) fn(ht); else ht.response().status = 200;
while(ht.send() > 0) ;
test_expect ( !ht.error() ); // that would mean send error or invalid response generated.
}
test_expect( !out.empty() );
test_comment("http response is:" << endl << out );
if(chk) chk(out);
}
protected:
virtual long read(void* data, unsigned long sz)
{
if(!sz) return 0;
if(rq_input->length() <= sz) {
sz = rq_input->length();
for(unsigned long i=0; i<sz; ++i) ((char*)data)[i] = (*rq_input)[i]; // without memcpy
rq_input->clear();
return sz;
} else {
for(unsigned long i=0; i<sz; ++i) ((char*)data)[i] = (*rq_input)[i];
*rq_input = rq_input->substr(sz);
return sz;
}
}
virtual long write(const void* data, unsigned long sz)
{
if(!sz) return 0;
rq_output->reserve(rq_output->length()+sz);
for(unsigned long i=0; i<sz; ++i) *rq_output += (char)(((const char*)data)[i]);
return sz;
}
};
// </editor-fold>
// <editor-fold desc="test_simple_request" defaultstate="collapsed">
static void get_01_rq(test_http & ht)
{
test_expect( ht.response().status == 0 );
ht.send("test ok");
}
static void get_01_chk(string& s)
{
std::transform(s.begin(), s.end(), s.begin(), ::tolower);
test_expect( s.find("\r\n\r\ntest ok") != s.npos );
test_expect( s.find("200 ok") != s.npos );
test_expect( s.find("http/1.0") != s.npos );
std::string::size_type p = s.find("content-length: ");
if(test_expect_cond( p != string::npos )) {
while(!::isdigit(s[p])) p++;
test_expect(s[p] == '7');
}
}
static void get_badrq_chk(string& s)
{
std::transform(s.begin(), s.end(), s.begin(), ::tolower);
test_expect( s.find("400 bad request") != string::npos);
}
static void post_rq(test_http & ht)
{
test_expect( ht.get("b") == " spaces " );
test_expect( ht.get_dbl("a") == 1 );
test_expect( ht.cookie().size() == 3 );
test_expect( ht.cookie_dbl("a") == 1 );
test_expect( ht.cookie("b") == "a string" );
test_expect( ht.cookie("c") == "a string" );
test_expect( ht.request().accept == "" );
test_expect( ht.request().content_length == 10 );
test_expect( ht.request().content_type.find("text/plain") != string::npos );
test_expect( ht.request().content_type_id == ht.cont_tp_text_plain );
test_expect( ht.ram_handled() );
test_expect( ht.request().data.empty() );
test_expect( ht.post().size() == 1 );
test_expect( ht.post("text") == "0123456789" );
test_expect( ht.request().files.empty() );
ht.cookie("counter", "123321", time(0)+3600);
ht.send("post test body");
}
static void post_chk(string& s)
{
std::transform(s.begin(), s.end(), s.begin(), ::tolower);
test_expect( s.find("counter=123321") != s.npos );
test_expect( s.find("200 ok") != s.npos );
test_expect( s.find("post test body") != s.npos );
}
static void test_simple_request()
{
http::config.keep_alive = true;
http::config.server = "localhost";
http::config.log_level = http::log_debug;
http::config.log_callback = test_http::log_comment;
http::config.implicit_clear = false;
http::config.max_ram_content_length = 10000;
http::config.append_error_messages = true;
test_http::test_request("GET / HTTP/1.0\n\n", false, get_01_rq, get_01_chk);
test_http::test_request("", true, 0, get_badrq_chk);
test_http::test_request("GET / HTTP/1.0\n", true, 0, get_badrq_chk);
test_http::test_request("GET / HTTP/2.0\n\n", true, 0, get_badrq_chk);
test_http::test_request(
"POST http://www.example.com/post/?a=1&b=%20spaces%20 HTTP/1.1\n"
"Content-length: 10\n"
"Content-Type: text/plain\n"
"Cookie: a=1; b=\"a string\" ; c=a%20string"
"\n\n"
"0123456789" , false, post_rq, post_chk
);
}
// </editor-fold>
void test()
{
test_static_fn();
test_simple_request();
test_timestamp2string();
}
#include <http.hh>
#include <filesystem.hh>
#include <string.hh>
#include <iomanip>
#include <fcntl.h>
#if defined __MSC_VER
namespace { namespace std { template <typename T> bool isnan(T d) { return !!_isnan(d); } } }
#endif
namespace sw { namespace math {
template <typename vt>
vt random(vt min, double max) throw()
{
static bool is_init = false;
if(!is_init) { ::srand(::time(0)); is_init=true; }
double mn = min, mx = max;
if(mn>mx) { double t=mn; mn=mx; mx=t; }
mn += ((mx-mn)/(double)RAND_MAX * ::rand());
return (vt)mn;
}
}}
using namespace std;
using sw::fs;
using sw::trim;
using sw::rtrim;
using sw::to_lower;
typedef sw::detail::basic_http<sw::detail::http_ram_storage_provider<> > http;
// <editor-fold desc="statics" defaultstate="collapsed">
int test_content_mime_type()
{
#define chk(X,Y) { string s=http::mime_content_type(X); \
cout << "'" << X << "' --> '" << s << "' |--> " \
<< (s==Y ? "ok" : "FAILED") << endl; }
chk(string(""),"text/plain");
chk("z", "text/plain");
chk("_", "text/plain");
chk("txt", "text/plain");
chk("html", "text/html");
chk("mp3", "audio/mpeg");
chk("aac", "audio/aac");
chk("zip", "application/x-zip-compressed");
http::mime_content_type_add("aaa", "singsang");
chk("aaa", "singsang");
#undef chk
return 0;
}
int test_urlencode_urldecode()
{
// php -r 'echo urlencode("!@£$%^&") . "\n";'
cout << http::urlencode("abcdefghij_~.;,$(!@$%^&)") << endl;
cout << http::urldecode("abcdefghij_%7e.%3b%2c%24%28%21%40%24%25%5e%26%29") << endl;
for(int i=0; i<100; ++i) {
string in, enc, dec;
in.reserve(100);
for(int j=0; j<100; j++) {
in += sw::math::random<char>(30, 127);
if(sw::math::random<int>(0, 100) > 80) break;
}
enc = http::urlencode(in);
dec = http::urldecode(enc);
if(in != dec) {
cout << "Missmatch urlencode/decode [" << i <<"]: '" << in << "' --> '"
<< enc << "' --> '" << dec << "'" << endl;
}
}
return 0;
}
// </editor-fold>
// <editor-fold desc="log" defaultstate="collapsed">
string logstr;
void dolog(const string & s, int level)
{
(void) level;
cerr << "LOG" << s << endl;
logstr += s;
logstr += "\n";
}
// </editor-fold>
int test_http(bool getindex)
{
http::config.keep_alive = true;
http::config.server = "localhost";
http::config.log_level = http::log_debug;
http::config.log_callback = dolog;
http::config.implicit_clear = false;
http::config.max_ram_content_length = 10000;
//http::connection_t prov; http ht(prov);
http ht;
string base_path;
int r=1;
if(getindex) {
cout << "GET / HTTP/1.1" << endl << endl;
base_path = "/http/htdocs";
stringstream ss;
#define ssrecv(X) { string s = X; r = ht.recv(s.data(), s.length()); }
// ss << "GET /test/chunked/ HTTP/1.1\n\n";
// ss << "GET /index.html HTTP/1.1\n\n";
// ss << "GET /test/chunked/ HTTP/1.1\nCookie: test=aber\n\n";
// 3.1415926535897932384626433832795029
ss << "POST /test/post/?a=1&b=%20no%20 HTTP/1.1" << endl
<< "Content-length:100" << endl << endl << "1234";
ssrecv(ss.str());
ssrecv("56789012345678901234567890123456789012");
ssrecv("34567890123456789012345");
ssrecv("67890123456789012345678901234567890");
ssrecv("IGNORED_IGNORED_IGNORED_IGNORED_IGNORED");
#undef ssrecv
//cerr << endl << ht.dump() << endl;
} else {
while(r>0) r = ht.recv();
}
if(r == -1) return 1; // Fatal -> exit immediately, nothing we can send to the client
if(r < 0) { ht.send(); return 1; } // Error where we can inform the client
string path = fs::cwd() + base_path + ht.request().path;
path = fs::realpath(path);
if(fs::is_dir(path)) path = rtrim(path, "/") + "/index.html";
if(path.find("/.") != string::npos) {
ht.send(404);
} else if(!fs::is_file(path)) {
if(ht.request().path == "/test/post/") {
ht.response().content_type = "text/html; charset=utf-8";
ht.response().cache_ctrl = http::cache_ctrl_no_store;
stringstream ss;
double d = ht.post_dbl("post_double");
string s = http::htmlspecialchars(ht.post("post_string"));
ss << "<html><head></head><body>" << endl
<< "<h1>Post test</h1>" << endl
<< "<form method=\"post\" action=\"" << ht.request().path << "\">" << endl
<< " <input type=\"text\" name=\"post_string\" value=\"" << s << "\"/>" << endl
<< " <input type=\"text\" name=\"post_double\" value=\"" << setprecision(12) << d << "\"/>" << endl
<< " <input type=\"submit\" name=\"post_submit\" value=\"submit\"/>" << endl
<< "</form>" << endl
<< "<br/><br/><pre>" << endl
<< "post_string = " << s << endl
<< "post_double = " << setprecision(12) << d << endl
<< endl
<< "is RAM handled = " << ht.modified() << endl
<< "</pre></body></html>" << endl;
ht.send(ss.str(), 200);
} else if(ht.request().path == "/test/chunked/") {
ht.response().transf_encoding = http::tfe_chunked;
ht.response().content_type = "text/plain; charset=utf-8";
ht.response().cache_ctrl = http::cache_ctrl_no_store;
double d = ht.cookie_dbl("count");
if(std::isnan(d)) d = 0;
d += 1;
ht.cookie("count", d, time(0)+3600);
stringstream ss;
ss << "Counter = " << d << endl
<< "get[a] = " << ht.get_dbl("a") << endl
<< "get[b] = " << ht.get_dbl("b") << endl
<< logstr
;
ht.send(ss.str(), 200);
} else {
ht.send(404);
}
} else if(!fs::is_readable(path)) {
ht.send(403);
} else {
ht.send_file(path);
}
while(ht.send() > 0); // Flush buffer
return 0;
}
int main(int argc, char** argv)
{
if(argc < 2 || !argv[1]) {
return test_http(true); // default: GET index.html
} else {
string cmd = trim(to_lower(string(argv[1])));
if(cmd == "--urlencode") {
return test_urlencode_urldecode();
} else if(cmd == "--mime") {
return test_content_mime_type();
} else if(cmd == "--httpd") {
return test_http(false);
} else {
cout << "Usage: " << argv[0] << " [--urlencode|--mime|--httpd]" << endl;
}
}
return 1;
}
#include <http.hh>
#include <string.hh>
#include <iomanip>
#include <cassert>
using namespace std;
using namespace sw;
//
// Here some requests you can pipe in with cat.
//
#ifdef FILE_CONTENTS_TO_PIPE_INTO_THIS_EXAMPLE1
POST /test1/?a=1&b=hello&c=%20space%20 HTTP/1.1
Content-length:100
1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890
#endif
#ifdef FILE_CONTENTS_TO_PIPE_INTO_THIS_EXAMPLE2
POST /test2/?a=1&b=hello&c=%20space%20 HTTP/1.1
Content-length: 11
Content-type: application/x-www-form-urlencoded
Cookie: var1=10; var2=%20NONO%20
ccc=1&bbb=3
#endif
// Logging function, used with sw::http::config.log_callback = dolog;
void dolog(const string &txt, int level) {
cerr << "[" << level << "]:" << txt << endl;
}
int main()
{
// The http class has a static configuration that applies to
// all instances. You need to set this stuff only once.
// Note we are using namespace sw, http is in that namespace.
http::config.keep_alive = true; // Default header Connection: keep-alive
http::config.server = "localhost"; // Default header Server: localhost
http::config.log_level = http::log_debug; // Log level
http::config.log_callback = dolog; // Assign logging callback
http::config.implicit_clear = false; // Free memory as soon as possible
http::config.max_ram_content_length = 10000; // Maximum POST/PUT length directly handled in RAM
// Instantiate a new http object for a new request.
sw::http ht;
// The object has a state that describes what phase of the request we're in,
// that starts with receiving the header. When ht.recv() is called, the object
// reads from the specified source stream using the virtual read(...) method.
// When the header is received, or on error, the state switches accordingly.
assert(ht.state() == http::st_r_header);
// The default specialisation sw::http reads from STDIN and writes to STDOUT, but using it with
// a socket is simple: overload the methods
//
// - virtual long read(void* data, unsigned long sz)
// - virtual long write(const void* data, unsigned long sz)
//
// to implement the data source/sink to whatever you like (for sockets send and recv).
// But important: The methods must behave like ::read and ::write, means return the number
// of bytes read/written, 0 on EOF, <0 on error. The http class checks EWOULDBLOCK/EAGAIN.
// recv returns 1 when it wants more data to proceed, 0 when it does not need more data, and
// negative on error. If the error is <-100 it is a HTTP error code that can be responded to
// directly.
int n_received;
do { n_received = ht.recv(); } while(n_received > 0); // Reads stdin until EOF, enough or error
if(n_received < 0) {
if(n_received < 100) {
// The response code is already set, an error page will be sent automatically
// Send returns > 0 when more data are in the buffer that shall be sent, so we
// loop. Also, the error will be logged.
while(ht.send() > 0);
return ht.response().status;
} else {
// Nothing we can send back, the error is fatal. E.g. socket closed, broken pipe etc.
// The error is logged. So, here we just exit with 1 === fatal.
return 1;
}
}
// At this point the request read. Either it is completely in RAM (e.g. if it is a GET
// request or the request content-length smaller than http::config.max_ram_content_length)
// OR the request body is stored using the storage_provider (more about that later).
// We definitely have the request header, and we can set the response header:
assert(ht.state() >= http::st_r_done);
// The dump() method helps to analyse what we got now:
// cerr << "ht object after input: " << ht.dump() << endl << endl;
// Let's work with some request values:
if(ht.request().method == http::rq_get) { // it's a GET request
// That is the default if the client accepts it
ht.response().content_type = "text/html";
// That is also default, we don't need to set it. Local time!
ht.response().date = time(NULL);
// E.g. we set Cache-Control: private
ht.response().cache_ctrl = http::cache_ctrl_private;
// If that nonempty 'Etag: "...."' will be set in the response header, otherwise omitted
ht.response().etag = "0123-abcd-0815";
// Local time of last modified, GMT conversion is done by the http class
ht.response().last_modified = time(NULL);
// Force "Connection: close". Normally http checks the HTTP version and what the
// client prefers, and sets "Connection: keep-alive" or "Connection: close" automatically.
// Note that if you use a socket, set the keepalive option, too. Otherwise that makes no sense.
ht.response().keep_alive = false;
// You can enable or disable if you accept content ranges
ht.response().accept_ranges = false;
// The status is something you MUST set the send() methods allow to do this implicitly.
ht.response().status = 200;
// Here we set "Transfer-Encoding: chunked". You can also say http::tfe_gzip, deflate etc,
// but the http class does not zip/deflate it for you - the data you push in must already be.
ht.response().transf_encoding = http::tfe_chunked;
// You can set it explicitly or leave it -1 (it is already -1)
ht.response().content_length = -1;
// ht.response().headers is a string map. But you better use the header() method:
ht.response().headers["x-poweredby"] = "me";
// We overwrite it with the proper header method. Note that the keys are always STORED
// LOWERCASE. When sending the header they will be converted to "Proper-Case".
ht.header("x-poweredby", "you");
// And that's the getter.
assert(ht.header("x-poweredby") == "you");
// COOKIE getters FROM THE REQUEST
string text = ht.cookie("text"); // Empty if not set
double counter = ht.cookie_dbl("counter"); // NAN if not set
if(std::isnan(counter)) counter = 0;
if(text.empty()) text = "a text";
// COOKIE setters TO RESPONSE HEADER
ht.cookie("text", text, time(0)+3600); // Expires in an hour, set string
ht.cookie("counter", counter, time(0)+3600); // Expires in an hour, set double
// GET getters from request:
string a = ht.get("a");
string b = ht.get("b");
string c = ht.get("c");
// Our response body:
std::stringstream ss;
// http::htmlspecialchars escapes html characters, ht.request().path is the requested
// resource without query string
ss << "<h1>Index of " << http::htmlspecialchars( ht.request().path ) << " </h1><br/>" << endl
<< "Query string is '" << ht.request().query_string << "' <br/>" << endl
<< "Referer is '" << ht.request().referer << "' <br/>" << endl
<< "if-modified-since is '" << ht.request().if_modified_since << "' <br/>" << endl
<< endl
<< "GET[a] = '" << http::htmlspecialchars(a) << "' <br/>" << endl
<< "GET[b] = '" << http::htmlspecialchars(b) << "' <br/>" << endl
<< "GET[c] = '" << http::htmlspecialchars(c) << "' <br/>" << endl
;
// And we send it
ht.send(ss.str(), 200);
} else if(ht.request().method == http::rq_post) {
// it's a POST request
// GET getters from request:
string a = ht.get("a");
string b = ht.get("b");
string c = ht.get("c");
// POST
string text = ht.post("text"); // If the content type not form-urlencoded, the data is in "text".
double ccc = ht.post_dbl("bbb"); // Fetch double from POST (NAN if missing or invalid)
string ddd = ht.post("ccc"); // Fetch string from POST
// Response body
std::stringstream ss;
ss << "<h1>POST of " << http::htmlspecialchars( ht.request().path ) << " </h1><br/>" << endl
<< "GET[a] = '" << http::htmlspecialchars(a) << "' <br/>" << endl
<< "GET[b] = '" << http::htmlspecialchars(b) << "' <br/>" << endl
<< "GET[c] = '" << http::htmlspecialchars(c) << "' <br/>" << endl
<< endl
<< "POST[text]= '" << http::htmlspecialchars(text) << "' <br/>" << endl
<< "POST[ccc]= " << ccc << " <br/>" << endl
<< "POST[ddd]= '" << http::htmlspecialchars(ddd) << "' <br/>" << endl
;
ht.response().cache_ctrl = http::cache_ctrl_no_store;
ht.send(ss.str(), 200);
}
while(ht.send() > 0); // We ensure that all data are sent. (flush buffer)
return 0;
}
Beispiel: Mit Temp-Storage-Provider und Dateiuplod (multipart POST)
Example: With temp storage provider and file upload (multipart POST)
#include <http.hh>
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <sstream>
using namespace std;
string default_input = string()
+ "POST http://localhost/test/multipart/ HTTP/1.1\n"
+ "User-Agent: Mozilla/5.0 Firefox/12.0\n"
+ "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\n"
+ "Accept-Language: en-us,en;q=0.5\n"
+ "Accept-Encoding: gzip, deflate\n"
+ "Content-Type: multipart/form-data; boundary = ---------------------------41184676334\n"
+ "Content-Length:636\n"
+ "\n"
+ "-----------------------------41184676334\n"
+ "Content-Disposition: form-data; name=\"caption\"\n"
+ "\n"
+ "Images to send\n"
+ "-----------------------------41184676334\n"
+ "Content-Disposition: form-data; name=\"description\"\n"
+ "Content-Type: text/plain\n"
+ "\n"
+ "Those are images that are sent ...\n"
+ "-----------------------------41184676334\n"
+ "Content-Disposition: form-data; name=\"image1\"; filename=\"The-Image1.png\"\n"
+ "Content-Type: image/png\n"
+ "\n"
+ "01234567890\n"
+ "-----------------------------41184676334\n"
+ "Content-Disposition: form-data; filename=\"The-Image2.png\" ; name=\"image2\"\n"
+ "Content-Type: image/png\n"
+ "Content-Transfer-Encoding: binary\n"
+ "\n"
+ "01234567890\n"
+ "-----------------------------41184676334--";
// We use the http class with the http_ram_storage_provider, even for big files.
typedef sw::detail::basic_http<sw::detail::http_ram_storage_provider<> > http;
// Logging function
std::stringstream logstream;
void dolog(const string & s, int level)
{ logstream << "[" << level << "] " << s << endl; }
// And go
int main(int argc, char** argv)
{
// Config
http::config.keep_alive = true; // Default header Connection: keep-alive
http::config.server = "localhost"; // Default header Server: localhost
http::config.implicit_clear = false; // Free memory as soon as possible
http::config.max_ram_content_length = 10000; // Maximum POST/PUT length directly handled in RAM
http::config.log_level = http::log_debug; // Log level
http::config.log_callback = dolog; // Used logging function
http::config.append_error_messages = true; // Append error messages to error pages.
// Instance
http ht;
// Input
if(argc >= 2 && argv[1]) {
// file input
FILE* fp = ::fopen(argv[1], "rb");
if(!fp) {
ht.send("Failed to open input file", 404);
return 404;
}
int r=1, fr=1;
char buf[256];
while(fr>0 && r>0) {
fr = ::fread(buf, sizeof(char), 256, fp);
if(fr < 0) break;
r = ht.recv(buf, fr);
}
::fclose(fp);
r = ht.recv(buf, 0); // Ensure the object sees that it is EOF.
if(r < 0) { ht.send(); return -r; };
} else {
// default input
ht.recv(default_input.data(), default_input.length());
int r = ht.recv(default_input.data(), 0); // Ensure the object sees the EOF.
if(r < 0) { ht.send(); return -r; }
}
// Dump and send
ht.response().content_type = "text/plain"; // Content-Type: text/plain
ht.response().cache_ctrl = http::cache_ctrl_no_store; // Cache-Control: no-cache, no-store ...
std::stringstream out;
out << "Dump after receiving a multi part request:" << endl
<< endl
<< ht.dump() << endl
<< endl;
for(http::upload_files_t::const_iterator it=ht.files().begin();
it != ht.files().end(); ++it)
{
const http::upload_file_t &file = it->second;
out << endl << "========= DATA OF " << file.name << " ======================" << endl;
if(file.start < 0) {
// Data is in the instance variable "data"
out << file.data << endl;
} else {
// Data is in the storage, means normally in a file we can copy these data into
// a target file or, as here, print it.
char c;
ht.storage().seek(file.start);
for(unsigned long i=0; i<file.size; ++i) {
ht.storage().read(&c, sizeof(char));
out << c;
}
out << endl;
}
}
out << endl << "========= LOG ====================================" << endl << logstream.str();
ht.send(out.str(), 200);
return 0;
}
Beispiel: Kleiner Server
Example: Small server
#include <http.hh>
#include <filesystem.hh>
#include <string>
#include <string.hh>
#include <socket.hh>
#include <iostream>
#include <iomanip>
#include <fcntl.h>
#include <signal.h>
#define log_verbose(X) { std::cerr << X << std::endl; }
#define log_verbose2(X) { std::cerr << X << std::endl; }
using namespace std;
using sw::fs;
using sw::rtrim;
using sw::trim;
#ifdef __MSC_VER
namespace { template <typename T> static int isnan(T d) { return _isnan(d); } }
#else
namespace { template <typename T> static int isnan(T d) { return std::isnan(d); } }
#endif
// <editor-fold desc="signal" defaultstate="collapsed">
volatile bool stop = false;
void signal_stop(int sig) {
if(stop) ::exit(1);
stop = true;
switch(sig) {
case SIGINT: log_verbose("\n[SIGINT]\n"); break;
case SIGQUIT: log_verbose("\n[SIGQUIT]\n"); break;
case SIGTRAP: log_verbose("\n[SIGTRAP]\n"); break;
case SIGABRT: log_verbose("\n[SIGABRT]\n"); ::exit(1); break;
}
}
// </editor-fold>
// <editor-fold desc="log" defaultstate="collapsed">
//
// Logging function for http::config
//
void dolog(const string & s, int level)
{ (void) level; log_verbose(s); }
// </editor-fold>
// <editor-fold desc="acceptor_stats" defaultstate="collapsed">
//
// This class is only for statistics. It is not really related to the functionality
// of the http class.
//
class acceptor_stats
{
public:
acceptor_stats(): nreceived(0), nsent(0), status(0)
{ id = curr_id++; gettime(created); connected=created; received=created; closed=created; }
/**
* Destructor
*/
virtual ~acceptor_stats()
{ ; }
/**
* Get the time
*/
static void gettime(struct timeval & what)
{ ::gettimeofday(&what, NULL); }
/**
* Evaluate and write the statistic
*/
void push_me()
{
connected.tv_sec -= created.tv_sec; connected.tv_usec -= created.tv_usec;
received.tv_sec -= created.tv_sec; received.tv_usec -= created.tv_usec;
closed.tv_sec -= created.tv_sec; closed.tv_usec -= created.tv_usec;
double t0 = ((double)connected.tv_sec + ((double) connected.tv_usec)/1e6) ;
double t1 = ((double)received.tv_sec + ((double) received.tv_usec)/1e6) ;
double t2 = ((double)closed.tv_sec + ((double) closed.tv_usec)/1e6) ;
{
sw::sync::autolock al(_lock); // we lock before writing to ensure everything is line-by-line
printf(
"%6lu, %10.3f, %10.3f, %10.3f, %10u, %10u, %4d\n",
id, t0*1000, t1*1000, t2*1000, nreceived, nsent, status
);
fflush(stdout);
}
}
// And some variables to save stuff in until it's written
unsigned long id;
unsigned nreceived;
unsigned nsent;
int status;
struct timeval created;
struct timeval connected;
struct timeval received;
struct timeval closed;
static unsigned long curr_id;
static sw::sync::lock _lock;
};
unsigned long acceptor_stats::curr_id;
sw::sync::lock acceptor_stats::_lock;
// </editor-fold>
// <editor-fold desc="http_acceptor" defaultstate="collapsed">
//
// This is the http class we refer to, not sw::http, but this one. The difference is
// that we use our own storage provider:
//
typedef sw::detail::basic_http<sw::detail::http_ram_storage_provider<> > http;
//
// Here the interesting stuff takes place: This class inherits from the class http
// and the class sw::sock::tcp_acceptor, which is a communication handler class for
// the server. For each request the server class will instantiate new objects of that
// class and shovels the client requests to them.
//
template <typename T=int>
class http_acceptor : public sw::sock::tcp_acceptor, protected http
{
typedef sw::sock::tcp_acceptor socket_t; // Convenience typedef
typedef http http_t; // Convenience typedef
public:
/**
* Constructor
*/
http_acceptor() : socket_t(), http_t()
{ }
/**
* Destructor, here we write out out statistics:
*/
virtual ~http_acceptor()
{ stats.push_me(); }
protected:
/**
* This is the overloaded of our http read() method:
* All we need to do is to glue to the recv method of the socket class.
*/
long read(void* data, unsigned long sz)
{
long r = socket_t::recv(data, sz);
if(r > 0) stats.nreceived += r; // And we set some statistics
return r;
}
/**
* This is the overload of our htttp write(): We pass the data to the
* socket send() method:
*/
long write(const void* data, unsigned long sz)
{
long r = socket_t::send(data, sz);
if(r > 0) stats.nsent += r;
return r;
}
/**
* That one is called when the socket is connected.
* We just save when.
*/
void on_connected()
{ stats.gettime(stats.connected); }
/**
* This is a socket overload, too - and our main handler ... read below
*/
void on_receive()
{
long r;
// When this method is called, there are data to receive, so
// we invoke the http recv method, which then invokes our glue code
// read() above, and fetches the data internally. It tells us with
// the return code if there is an error, or if no more data are
// required.
if((r=http_t::recv()) > 0) {
// http_t::recv() has read all avaliable data and wants more
return;
} else if(r < -100) {
// Error where we can inform the client (http status)
stats.gettime(stats.received);
http_t::send();
socket_t::close();
return;
} else if(r < 0) {
// That would be -1 == fatal, nothing to send, just quit
stats.gettime(stats.received);
socket_t::close();
return;
}
// For the stats
stats.gettime(stats.received);
// Here we set the socket keepalive according to the client's preferences and HTTP version.
socket_t::keepalive(http_t::request().keep_alive);
// And here we evaluate the request itself:
string path = base_path + request().path;
path = fs::realpath(path);
if(fs::is_dir(path)) path = rtrim(path, "/") + "/index.html";
if(path.find("/.") != string::npos) {
// Rudimentary prevention of escaping the doocument root with ../../ or
// the like. It's really basic security and I do not recommend relying
// only on that.
http_t::send(404);
socket_t::close();
} else if(request().path == "/") {
// Server root location path
// We send a simple POST form back:
response().content_type = "text/html; charset=utf-8";
response().cache_ctrl = http::cache_ctrl_no_store;
stringstream ss;
ss << "<html><head></head><body>" << endl
<< "<h1>MEM /</h1>" << endl
<< "<form method=\"post\" action=\"" << request().path << "\">" << endl
<< " <input type=\"text\" name=\"post_string\" value=\"" << "\"/>" << endl
<< " <input type=\"text\" name=\"post_double\" value=\"" << "\"/>" << endl
<< " <input type=\"submit\" name=\"post_submit\" value=\"submit\"/>" << endl
<< "</form>" << endl
<< "<br/><br/><pre>" << endl
<< endl
<< "is RAM handled = " << modified() << endl
<< "</pre></body></html>" << endl;
http_t::send(ss.str(), 200);
} else if(request().path == "/test/post/") {
// Location /test/post/
response().content_type = "text/html; charset=utf-8";
response().cache_ctrl = http::cache_ctrl_no_store;
stringstream ss;
double d = post_dbl("post_double");
string s = http::htmlspecialchars(post("post_string"));
ss << "<html><head></head><body>" << endl
<< "<h1>Post test</h1>" << endl
<< "<form method=\"post\" action=\"" << request().path << "\">" << endl
<< " <input type=\"text\" name=\"post_string\" value=\"" << s << "\"/>" << endl
<< " <input type=\"text\" name=\"post_double\" value=\"" << setprecision(12) << d << "\"/>" << endl
<< " <input type=\"submit\" name=\"post_submit\" value=\"submit\"/>" << endl
<< "</form>" << endl
<< "<br/><br/><pre>" << endl
<< "post_string = " << s << endl
<< "post_double = " << setprecision(12) << d << endl
<< endl
<< "is RAM handled = " << modified() << endl
<< "</pre></body></html>" << endl;
http_t::send(ss.str(), 200);
} else if(request().path == "/test/chunked/") {
// Here we send chunked data back
response().transf_encoding = http::tfe_chunked;
response().content_type = "text/plain; charset=utf-8";
response().cache_ctrl = http::cache_ctrl_no_store;
double d = cookie_dbl("count");
if(std::isnan(d)) d = 0;
d += 1;
cookie("count", d, time(0)+3600);
stringstream ss;
ss << "Counter = " << d << endl
<< "get[a] = " << get_dbl("a") << endl
<< "get[b] = " << get_dbl("b") << endl
;
http_t::send(ss.str(), 200);
} else if(!fs::is_file(path)) {
// File not found
http_t::send(404);
socket_t::close();
} else if(!fs::is_readable(path)) {
// File not readable -> send 403
http_t::send(403);
socket_t::close();
} else {
// Found and readable: send the file
http_t::send_file(path);
}
// Flush buffer
while(http_t::send() > 0);
// And close when we're done.
socket_t::close();
}
/**
* Socket overload. Called after the socket was closed and the thread is just about to exit.
*/
void on_close()
{
stats.gettime(stats.closed);
stats.status = response().status;
}
public:
/**
* Instance variables
*/
acceptor_stats stats;
static string base_path;
};
template <typename T>
string http_acceptor<T>::base_path = string();
// </editor-fold>
// <editor-fold desc="http_server" defaultstate="collapsed">
//
// The server class. This one only listens and accepts with instances
// of our http acceptor implementation.
//
// To see what happens some tracings are added:
//
template <typename acceptor_type=http_acceptor<> >
class http_server : public sw::sock::tcp_server<acceptor_type>
{
typedef acceptor_type acceptor_t;
typedef sw::sock::tcp_server<acceptor_type> server_t;
public:
http_server(): server_t()
{ log_verbose("http_server::http_server()"); }
virtual ~http_server()
{ log_verbose("http_server::~http_server()"); }
private:
void on_started()
{ log_verbose("http_server::on_started()"); }
void on_stopped()
{ log_verbose("http_server::on_stopped()"); }
bool on_request(acceptor_t & a)
{
// Special case: We don't refuse HTTP requests actively, instead we leave them
// in the queue and clean after finished requests. If the server is overencumbered
// the systems socket subsystem will let the client know.
// The only thing we do it to wait one ms, so that we can maybe catch this
// accept in the current loop.
if(server_t::num_connected() >= server_t::max_clients()) {
server_t::thread_t::usleep(1000);
server_t::clear_closed();
}
log_verbose2("http_server::on_request(" << a.ip().str() << ")");
return true;
}
void on_error(long no, const char* text)
{ (void) no; log_verbose("http_server::on_error(" << text << ")\n"); }
void on_update()
{ server_t::clear_closed(); }
};
// </editor-fold>
// <editor-fold desc="main" defaultstate="collapsed">
//
// Last and least: main().
//
int main(int argc, char** argv)
{
(void) argc; (void) argv;
// Process signals
::signal(SIGINT, signal_stop);
::signal(SIGQUIT, signal_stop);
// Http config
http::config.keep_alive = true;
http::config.server = "localhost";
http::config.log_level = http::log_debug;
http::config.log_callback = dolog;
http::config.implicit_clear = false;
http::config.max_ram_content_length = 1<<20;
http_acceptor<>::base_path = fs::cwd() + "/http/htdocs";
// Instance of our server
http_server<> svr;
// With IP and port
svr.ip("127.0.0.1:8080");
// Maximum simultaneously running acceptors
svr.max_clients(128);
// The number of clients that the system keeps in the queue until refusing new connections.
svr.max_clients_pending(256);
// And GO
svr.start();
// Wait until server stopped / or process signal
while(svr.running()) { if(stop) svr.stop(); sw::thread::usleep(1000); }
// Tell if we had an error on the listening socket
if(svr.error()) log_verbose("EXIT ERROR: " << svr.error_text());
// And that was it.
return 0;
}
// </editor-fold>
Quelltext
Source code
// <editor-fold desc="Doc/license" defaultstate="collapsed">
/**
* @package de.atwillys.cc.swl
* @license BSD (simplified)
* @author Stefan Wilhelm (stfwi)
*
* @file http.hh
* @ccflags
* @ldflags
* @platform linux, bsd, windows
* @standard >= c++11
*
* -----------------------------------------------------------------------------
*
* Simple HTTP protocol handling class for use in modules that shall be able
* to read HTTP requests and generate HTTP responses.
*
* This is NOT a web server, more a capability base to implement request handlers
* or CLI programs that can act optionally as CGI.
*
* Annotations:
*
* - Headers are no string-vector-maps, but string-maps, means you always replace
* headers. Use comma separation (RFC2616) like "Cache-Control: no-cache, no-store"
* to concatenate header fields.
*
* - Data are read and written binary. Possible encodings in header and body are
* therefore forwarded to the API / the output stream.
*
* -----------------------------------------------------------------------------
* +++ BSD license header +++
* Copyright (c) 2010-2014, Stefan Wilhelm (stfwi, <cerbero s@atwilly s.de>)
* All rights reserved.
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met: (1) Redistributions
* of source code must retain the above copyright notice, this list of conditions
* and the following disclaimer. (2) Redistributions in binary form must reproduce
* the above copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the distribution.
* (3) Neither the name of atwillys.de nor the names of its contributors may be
* used to endorse or promote products derived from this software without specific
* prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS
* AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
* BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER
* OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
* OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
* WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
* DAMAGE.
* -----------------------------------------------------------------------------
*/
// </editor-fold>
#ifndef SW_HTTP_HH
#define SW_HTTP_HH
// <editor-fold desc="Includes/macros" defaultstate="collapsed">
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <map>
#include <limits>
#include <sstream>
#include <deque>
#include <algorithm>
#include <cctype>
#include <ctime>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cerrno>
#include <cmath>
#ifdef __MSC_VER
#include <sys\stat.h>
#include <sys\types.h>
#include <stdint.h>
namespace { namespace std { template <typename T> bool isnan(T d) { return !!_isnan(d); } } }
#ifndef ssize_t
#define ssize_t int
#endif
#ifndef S_ISREG
#define S_ISREG(M) (((M)&_S_IFREG)==_S_IFREG)
#endif
#else
#include <sys/stat.h>
#include <unistd.h>
#include <inttypes.h>
#endif
// </editor-fold>
namespace sw { namespace detail {
// <editor-fold desc="http_tmpstorage_provider" defaultstate="collapsed">
/**
* Storage class for POST/PUT input exceeding the RAM storage limit.
* THIS IS THE TEMPLATE FOR EXTERNALLY IMPLEMENTED STORAGE CLASSES.
* The functions are similar to standard C basic I/O functions, except that the
* basic_http<> class is not interested in internal variables like file pointers,
* stream refs or handles. It's basically an interface not requiring RTTI.
*
* The main reason requiring this provider template is the platform dependent disk i/o
* handling, more specifically locking the temporary files.
*
* Note: The basic_http<> class does not catch exceptions of this provider. The methods must not
* throw. Instead return negative.
*
* Note: The template return_t must be signed.
*/
template <typename return_t=int>
class http_tmpstorage_provider
{
public:
/**
* Constructors / Destructor
*/
inline http_tmpstorage_provider()
{ ; }
/**
* Will not be used by basic_http<>
*/
inline http_tmpstorage_provider(const http_tmpstorage_provider&)
{ ; }
/**
* Will not be used by basic_http<>
*/
inline http_tmpstorage_provider & operator = (http_tmpstorage_provider&)
{ return *this; }
/**
* Note: close and delete the tmp file.
*/
virtual ~http_tmpstorage_provider()
{ close(); }
/**
* Open a storage, return a negative value on error, 0 on success.
* The argument sz is informational and tells how many bytes will be required - good to
* e.g. deny (return e.g -1) or reserve space.
*
* Note: You should always lock the file.
* Note: I/O MUST NOT be non-blocking. EAGAIN/EWOULDBLOCK is not differed from other errors.
* @return return_t
*/
virtual return_t open(unsigned long) throw()
{ return -1; }
/**
* Write to the storage, return negative on error, positive or 0 for number of bytes written.
* This function must write all bytes or return an error (not non-blocking).
* @param const void* d
* @param unsigned long sz
* @return return_t
*/
virtual return_t write(const void*, unsigned long) throw()
{ return -1; }
/**
* Read from storage, return negative on error, positive or 0 for number of bytes read.
* @param void* d
* @param unsigned long sz
* @return return_t
*/
virtual return_t read(void*, unsigned long) throw()
{ return -1; }
/**
* Set the current position the byte specified with pos from the beginning of the file.
* Return negative on error, 0 on success.
* @return return_t
*/
virtual return_t seek(unsigned long) throw()
{ return -1; }
/**
* Return the size in bytes of the storage, 0 on error.
* @return unsigned long
*/
virtual unsigned long size() throw()
{ return 0; }
/**
* Has to return true if the end of storage is reached or the storage is not open / invalid state.
* @return bool
*/
virtual bool eof() const throw()
{ return true; }
/**
* Close and cleanup (e.g. delete temporary file). Must not throw
* @return void
*/
virtual void close() throw()
{ }
};
// </editor-fold>
// <editor-fold desc="http_ram_storage_provider" defaultstate="collapsed">
template <typename return_t=int>
class http_ram_storage_provider : public http_tmpstorage_provider<return_t>
{
public:
/**
* Constructor
*/
inline http_ram_storage_provider() : buf_(), pos_(0)
{ ; }
/**
* Copy constuctor
*/
inline http_ram_storage_provider(const http_ram_storage_provider & o) : buf_(o.buf_), pos_(o.pos_)
{ ; }
/**
* Copy assignment
*/
inline http_ram_storage_provider & operator = (http_ram_storage_provider & o)
{ buf_=o.buf_; pos_=o.pos_; return *this; }
/**
* Destructor
*/
virtual ~http_ram_storage_provider()
{ http_ram_storage_provider::close(); }
/**
* Open the storage
*/
return_t open(unsigned long sz) throw()
{ try { buf_.clear(); buf_.reserve(sz); return 0; } catch(...) { return -1; } }
/**
* Write to the storage
*/
return_t write(const void* data, unsigned long sz) throw()
{
if(!data) return -1;
if(sz == 0) return 0;
try { buf_.reserve(buf_.length()+sz); } catch(...) { return -1; }
const char* end = (const char*) data+sz;
const char* p = (const char*) data;
for(; p<end; p++) buf_ += (char)(*p);
return sz;
}
/**
* Read from the storage
*/
return_t read(void* data, unsigned long sz) throw()
{
if(!data) return -1;
if(!sz) return 0;
if(pos_ >= buf_.length()) return 0;
if(pos_+sz > buf_.length()) sz = buf_.length() - pos_;
memcpy(data, buf_.data()+pos_, sz);
pos_ += sz;
return (return_t) sz;
}
/**
* Seek to a position for reading / writing
*/
return_t seek(unsigned long pos) throw()
{ pos_ = pos; if(pos_ >= buf_.length()) { pos_ =buf_.length(); return -1; } else return 0; }
/**
* Check if EOF
*/
bool eof() const throw()
{ return pos_ >= buf_.length(); }
/**
* Get the total size of the data in the storage
*/
unsigned long size() throw()
{ return buf_.length(); }
/**
* Close it
*/
void close() throw()
{ std::string s; buf_.swap(s); pos_=0; }
public:
// This method is not part of the storage_provider interface, but
// a special case used in basic_http POST parsing.
void swap_string_buffer_with(std::string & s)
{ buf_.swap(s); pos_=0; }
private:
/**
* The buffer used
*/
std::string buf_;
/**
* Current position
*/
unsigned long pos_;
};
// </editor-fold>
/**
* Http main class template, uses detail for a connection provider class (e.g. for
* stdio/socket/unix socket) and storage provider class (normally for disk i/o).
*/
template <class Storage_Type = http_tmpstorage_provider<int> >
class basic_http
{
public:
// <editor-fold desc="read/write overloads" defaultstate="collapsed">
/**
* Reading method, overload e.g. for socket use. Return the number of bytes read
* or negative on error.
* @param void* data
* @param unsigned long sz
* @return long
*/
virtual long read(void* data, unsigned long sz)
{
#ifdef __MSC_VER
return _read(_fileno(stdin), data, sz);
#else
return ::read(::fileno(stdin), data, sz);
#endif
}
/**
* Writing method, overload e.g. for socket use. Return the number of bytes written
* or negative on error.
* @param const void* data
* @param unsigned long sz
* @return long
*/
virtual long write(const void* data, unsigned long sz)
{
#ifdef __MSC_VER
return _write(_fileno(stdout), data, sz);
#else
return ::write(::fileno(stdout), data, sz);
#endif
}
// </editor-fold>
public:
// <editor-fold desc="Types/enums" defaultstate="collapsed">
//////////////////////////////////////////////////////////////////////////////
// Types
typedef std::string str_t;
typedef char char_t;
typedef std::map<str_t, str_t> str_map_t;
typedef typename str_t::size_type sz_t;
typedef std::basic_ostream<char_t> os_t;
typedef std::basic_istream<char_t> is_t;
typedef std::basic_stringstream<char_t> ss_t;
typedef std::vector<std::pair<int64_t,int64_t> > range_t; // range elements: from -> to
typedef Storage_Type storage_t;
struct upload_file_t;
typedef std::map<str_t, upload_file_t> upload_files_t;
/**
* HTTP request methods
*/
typedef enum {
rq_get=0, rq_head, rq_post, rq_put, rq_delete, rq_options, rq_move, rq_copy, rq_mkcol,
rq_lock, rq_unlock, rq_trace, rq_invalid
} request_method_t;
/**
* Processor state
*/
typedef enum { st_r_header=0, st_r_body, st_r_done, st_r_error, st_s_header,
st_s_body, st_done, st_error } state_t;
/**
* Logging level note: you need to set a callback in the config, too in order to log.
*/
typedef enum {
log_crit=0, log_error, log_warn, log_notice, log_debug
} log_level_t;
/**
* Content type identifier. Used to choose how parse POST.
*/
typedef enum {
cont_tp_any=0, cont_tp_text_plain, cont_tp_urlenc, cont_tp_multipart
} content_type_id_t;
/**
* Response cache control
*/
typedef enum {
cache_ctrl_none=0, // No info
cache_ctrl_public=0x1, // Cache-Control: public
cache_ctrl_private=0x2, // Cache-Control: private
cache_ctrl_no_cache=0x4, // Cache-Control: no-cache --> max_age=0
cache_ctrl_no_store=0x8 // Cache-Control: no-store --> no-cache max_age=0
} cache_ctrl_t;
/**
* Response transfer-encoding setting. Note: Chunked is done in this class, deflate and
* gzip are only set in the response header, the input data has to be zipped/deflated already.
*/
typedef enum {
tfe_omit=0, tfe_chunked, tfe_deflate, tfe_gzip
} transfer_enc_t;
// </editor-fold>
// <editor-fold desc="config_t" defaultstate="collapsed">
/**
* Configuration
*/
struct config_t
{
/**
* Log level
*/
log_level_t log_level;
/**
* Log callback
*/
void (*log_callback)(const str_t &, int);
/**
* Maximum total size of the header. Note that the header is always copied into the
* RAM before processing it.
*/
unsigned max_request_header_size;
/**
* Maximum length of the URI including query string and hash string
*/
unsigned max_request_path_length;
/**
* The maximum string size of GET/POST variable names
*/
unsigned max_arg_key_size;
/**
* The maximum string size of GET/POST variable values
*/
uint32_t max_arg_val_size;
/**
* Defines the maximum specified request content length
*/
uint64_t max_content_length;
/**
* Defines the maximum specified request content length that the parser will implicitly
* store in RAM. If exceeded, the body content will be redirected into a temporary file.
* Note: max_content_length is still respected, so max_ram_content_length must be smaller than
* max_content_length, otherwise temporary files are never used.
*/
uint32_t max_ram_content_length;
/**
* Default max-age for Cache-Control (-1 == omit, 0 --> "no-cache" if cache_ctrl not set)
*/
int32_t max_age;
/**
* Default response cache control setting
*/
cache_ctrl_t cache_ctrl;
/**
* Host that will be shown in the header field. Note, if you like to check the host matching
* the requested host, you have to do this yourself.
*/
str_t server;
/**
* Default setting for Connection: keep-alive/close header field.
*/
bool keep_alive;
/**
* Specifies if the request and response structures shall be freed (as much as possible)to
* save memory when sending the header information to the client.
*/
bool implicit_clear;
/**
* Specifies weather error messages - if they are set before - shall be added to
* error pages sent. That applies only if no custom error pages are sent (response data
* empty).
*/
bool append_error_messages;
/**
* Initialisation
*/
inline config_t() : log_level(log_crit), log_callback(0), max_request_header_size(1u<<14),
max_request_path_length(1u<<13), max_arg_key_size(64), max_arg_val_size(1ul<<14),
max_content_length(1ul<<30), max_ram_content_length(1ul<<15), max_age(-1),
cache_ctrl(cache_ctrl_none), server(), keep_alive(0), implicit_clear(0),
append_error_messages(false)
{ ; }
/**
* Checks and sanatises config entries. Returns 0 if all was ok, >0 if entries were modified,
* and <0 on error.
* @return int
*/
int check_sanatize()
{
/// stfwi: add logging for individual errors
size_t sz;
int nerrs = 0, nchanged=0;
if(log_level < log_crit) { ++nerrs; log_level = log_crit; }
if(log_level > log_debug) { ++nerrs; log_level = log_debug; }
if(max_request_header_size < 256) { ++nerrs; max_request_header_size = 256; }
if(max_request_path_length < 32) { ++nerrs; max_request_path_length = 32; }
if(max_arg_key_size < 8) { ++nchanged; max_arg_key_size = 32; }
if(max_arg_val_size < 8) { ++nchanged; max_arg_val_size = 32; }
if(max_age <-1) { ++nerrs; max_age=-1; }
if(cache_ctrl < 0) { ++nerrs; cache_ctrl = cache_ctrl_none; }
// max_ram_content_length can be 0 or > max_content_length
// max_content_length can be 0 if e.g. only get/head shall be accepted
sz = server.length(); server = tr(server); if(sz != server.length()) nchanged++;
return nerrs > 0 ? -nerrs : nchanged;
}
};
// </editor-fold>
// <editor-fold desc="buffer_t" defaultstate="collapsed">
/**
* Small buffer class allowing to quickly push data and implicitly freeing allocated memory.
*/
class buffer_t
{
private:
typedef std::deque<std::vector<char> > buf_t;
buf_t buf_;
long sz_;
long r_; // read position
char no_; // used to always provide a valid pointer, not NULL
public:
buffer_t() : buf_(), sz_(0), r_(0), no_(0)
{ ; }
virtual ~buffer_t()
{ ; }
inline bool empty() const
{ return buf_.empty(); }
inline void clear()
{ buf_.clear(); sz_=r_=0; }
inline sz_t size() const
{ return (sz_t) sz_; }
inline const char* chunk() const
{ return (!sz_) ? (&no_) : buf_.front().data()+r_; }
inline sz_t chunk_size() const
{ return (sz_<=0) ? 0 : buf_.front().size() - r_; }
void advance(sz_t n)
{
if(!sz_) return;
if((long)n >= sz_) { clear(); return; }
long bs = (long) buf_.front().size(); // already caught by sz_ type limits above
sz_ -= n;
if((r_ += n) < bs) return;
buf_.pop_front();
r_ -= bs; // 0 or positive, read position in next chunk
if(buf_.empty()) clear(); // double check
}
void chunk(const char *c, sz_t sz)
{
if(!c || !sz) return; // no error, we just don't do it
buf_.push_back(std::vector<char>(sz));
memcpy(buf_.back().data(), c, sz);
sz_ += sz;
}
void chunk(const str_t & s)
{ chunk(s.data(), s.length()); }
void prepend(const str_t & s)
{
if(!s.length()) return;
buf_.push_front(std::vector<char>(s.length()));
memcpy(buf_.front().data(), s.c_str(), s.length());
sz_ += s.length();
}
};
// </editor-fold>
// <editor-fold desc="upload_file_t" defaultstate="collapsed">
struct upload_file_t
{
inline upload_file_t() : start(-1), size(0), name(), filename(), content_type(),
content_encoding(), data()
{;}
virtual ~upload_file_t()
{;}
inline upload_file_t(const upload_file_t& f) : start(f.start), size(f.size), name(f.name),
filename(f.filename), content_type(f.content_type), content_encoding(), data(f.data)
{;}
upload_file_t& operator=(const upload_file_t& f)
{ start=f.start; size=f.size; name=f.name; filename=f.filename; content_type=f.content_type;
content_encoding=f.content_encoding; data=f.data; return *this; }
int32_t start;
unsigned long size;
str_t name, filename, content_type, content_encoding, data;
};
// </editor-fold>
// <editor-fold desc="request_t" defaultstate="collapsed">
/**
* Request data type
*/
struct request_t
{
request_method_t method; // Enum parsed request method ("GET" = rq_get etc)
str_t service; // If given, e.g. "http", "https", "ws" ...
str_t host; // Host part, from URL or header "Host:"
str_t path; // Path of URI
str_t query_string; // URI after "?" and before "#"
str_t query_hash; // URI after "#"
int http_version; // Int of METHOD URI/PATH HTTP1.<http_version>
str_t accept; // Header "Accept:"
long content_length; // Header "Content-Length:"
str_t content_type; // Header "Content-Type:"
content_type_id_t content_type_id; // Content type identifier for parsing POST
str_map_t headers; // Additional headers not listed above
str_map_t get; // GET query variables
str_map_t post; // POST variables
str_map_t cookie; // Cookie from header, to response
upload_files_t files; // Information about uploaded files
time_t if_modified_since; // For sending a 304 header (Not modified)
time_t if_unmodified_since; // For sending a 412 header (Precondition failed) (force abort request)
range_t range; // Range: bytes ...
str_t referer; // Referer/Referrer:
str_t user_agent; // User-agent:
std::string data; // POST/PUT, use str_t::data() function for binary access
bool keep_alive:1; // Connection: keep-alive (or close), what the client prefers
bool no_cache:1; // Cache-Control: no-cache set by client
/**
* Default constructor
*/
inline request_t() : method(rq_invalid), service(), host(), path(), query_string(), query_hash(),
http_version(), accept(), content_length(0), content_type(), content_type_id(cont_tp_any),
headers(), if_modified_since(0), if_unmodified_since(0), range(), data(), keep_alive(false),
no_cache(false)
{ ; }
/**
* Frees container memory where possible.
*/
void clear()
{
headers.clear(); get.clear(); post.clear(); cookie.clear(); files.clear(); sc(content_type);
sc(accept); sc(query_string); sc(path); sc(service); sc(host); sc(referer); sc(user_agent);
sc(query_hash);
}
};
// </editor-fold>
// <editor-fold desc="response_t" defaultstate="collapsed">
/**
* Response data type
*/
struct response_t
{
uint16_t status; // Response status (default=0 ---> will send 500 )
time_t date; // Date (default -1 === auto)
time_t last_modified; // Local last modified timestamp of the resource (-1===omit)
int32_t content_length; // Content length in bytes of the resource
str_t content_type; // Mime content type
str_t etag; // Etag of the resource
str_map_t cookie; // Cookies to set keys are variable names
str_map_t headers; // Additional headers not listed above
buffer_t data; // Buffer for header and text data
int32_t max_age; // Cache-Control: max-age=<seconds>
cache_ctrl_t cache_ctrl; // Cache-Control: ... settings
transfer_enc_t transf_encoding; // Compression used in Content-Encoding header (read compression_t !)
bool keep_alive:1; // Connection: keep-alive (or close)
bool accept_ranges:1; // Add "Accept-Ranges: bytes" to header if true
inline response_t() : status(0), date(0), last_modified(0), content_length(-1), content_type(),
etag(), headers(), data(), max_age(config.max_age), cache_ctrl(config.cache_ctrl),
transf_encoding(tfe_omit), keep_alive(config.keep_alive), accept_ranges(0)
{ ; }
void clear(bool with_data=false)
{ headers.clear(); sc(content_type); cookie.clear(); if(with_data) data.clear(); }
};
// </editor-fold>
public:
// <editor-fold desc="Constructors/destructor" defaultstate="collapsed">
//////////////////////////////////////////////////////////////////////////////
// Instance
/**
* Constructors
*/
basic_http() : state_(st_r_header), rlen_(0), lch_(0), n_sent_(0), rx_(), rq_(), rs_(),
storage_(), error_(0), error_text_()
{ ; }
explicit basic_http(const storage_t & st) :
state_(st_r_header), rlen_(0), lch_(0), n_sent_(0), rq_(), rs_(), rx_(), storage_(st),
error_(0), error_text_()
{ ; }
/**
* Destructor
*/
virtual ~basic_http()
{ ; }
// No copy
basic_http(const basic_http & ht) = delete;
basic_http& operator = (const basic_http & ht) = delete;
// </editor-fold>
public:
// <editor-fold desc="Getters/setters" defaultstate="collapsed">
/**
* Returns the process state
* @return state_t
*/
inline state_t state() const throw()
{ return state_; }
/**
* Returns the currently set error code, 0 if no error is set.
* @return int
*/
inline int error() const throw()
{ return error_; }
/**
* Returns the currently set error message or empty if not set yet.
* @return const str_t &
*/
inline const str_t & error_msg() const throw()
{ return error_text_; }
/**
* Returns the request
* @return const request_t &
*/
inline const request_t & request() const throw()
{ return rq_; }
/**
* Returns the request
* @return request_t &
*/
inline request_t & request() throw()
{ return rq_; }
/**
* Returns the response
* @return const response_t &
*/
inline const response_t & response() const throw()
{ return rs_; }
/**
* Returns the response
* @return response_t &
*/
inline response_t & response() throw()
{ return rs_; }
/**
* Returns a reference to the storage provider object
* @return storage_t &
*/
inline storage_t & storage() throw()
{ return storage_; }
/**
* Returns a reference to the storage provider object
* @return const storage_t &
*/
inline const storage_t & storage() const throw()
{ return storage_; }
/**
* Returns the list of url-decoded GET variables
* @return const str_map_t &
*/
inline const str_map_t & get() const throw()
{ return rq_.get; }
/**
* Returns the value of a GET variable or an empty string if not found
* @param const str_t & var
* @return const str_t &
*/
inline const str_t & get(const str_t & var) const throw()
{
try {
str_map_t::const_iterator it = rq_.get.find(var);
return it==rq_.get.end() ? nstr : it->second;
} catch(...) {
return nstr;
}
}
/**
* Convenience numeric return of get(const str_t & var). Returns NAN if key not found or
* conversion failed.
* @param const str_t & var
* @return double
*/
inline double get_dbl(const str_t & var) const throw()
{ const str_t & s = get(var); return s.empty() ? nan : s2d(s); }
/**
* Returns the list of url-decoded POST variables
* @return const str_map_t &
*/
inline const str_map_t & post() const throw()
{ return rq_.post; }
/**
* Returns the value of a POST variable or an empty string if not found
* @param const str_t & var
* @return const str_t &
*/
inline const str_t & post(const str_t & var) const throw()
{
try {
str_map_t::const_iterator it = rq_.post.find(var);
return it==rq_.post.end() ? nstr : it->second;
} catch(...) {
return nstr;
}
}
/**
* Convenience numeric return of post(const str_t & var). Returns NAN if key not found or
* conversion failed.
* @param const str_t & var
* @return double
*/
inline double post_dbl(const str_t & var) const throw()
{ const str_t & s = post(var); return s.empty() ? nan : s2d(s); }
/**
* Returns the list of cookies sent with the request
* @return const str_map_t &
*/
inline const str_map_t & cookie() const throw()
{ return rq_.cookie; }
/**
* Returns the value of a COOKIE variable or an empty string if not found
* @param const str_t & var
* @return const str_t &
*/
inline const str_t & cookie(const str_t & var) const throw()
{
str_map_t::const_iterator it = rq_.cookie.find(var);
return (it==rq_.cookie.end()) ? nstr : it->second;
}
/**
* Convenience numeric return of cookie(const str_t & var). Returns NAN if key not found or
* conversion failed.
* @param const str_t & var
* @return double
*/
inline double cookie_dbl(const str_t & var) const throw()
{ const str_t & s = cookie(var); return s.empty() ? nan : s2d(s); }
/**
* Set a cookie in the response header
*/
basic_http & cookie(const str_t & name, const str_t & value, time_t expires=0, const str_t & domain="",
const str_t & path="", bool secure=false, bool httponly=false)
{
if(!name.length()) return *this;
str_t ck;
ck.reserve(name.length()+value.length()+32);
ck += urlencode(name); ck += "="; ck += urlencode(value);
if(expires > 0) { ck += "; expires="; ck += timestamp2string(expires); }
if(domain.length() > 0) { ck += "; domain="; ck += domain; }
if(path.length() > 0) { ck += "; path="; ck += path; }
if(secure) { ck += "; Secure"; }
if(httponly) { ck += "; HttpOnly"; }
rs_.cookie[name] = ck;
return *this;
}
/**
* Set a cookie in the response header
*/
basic_http & cookie(const str_t & name, double d, time_t expires=0, const str_t & domain="",
const str_t & path="", bool secure=false, bool httponly=false)
{ return std::isnan(d) ? cookie(name,"") : cookie(name,d2s(d),expires,domain,path,secure,httponly); }
/**
* Returns the map of uploaded files
* @return const upload_files_t &
*/
const upload_files_t & files() const
{ return rq_.files; }
/**
* Returns the number of bytes that have to be still pushed into the response().data buffer
* to reach the amount of bytes specified in the Content-Length response header field.
* If no Content-Length is specified (-1) returns 0.
* @return sz_t
*/
inline sz_t num_send_bytes_expected_left() const throw()
{ return (rs_.content_length<0) ? 0 : (n_sent_<=0 ? rs_.content_length :
(rs_.content_length-(n_sent_+rs_.data.size()))); }
/**
* Returns the number of bytes the parser expects until the request data is completely submitted.
* @return sz_t
*/
inline sz_t num_read_bytes_expected_left() const throw()
{ return rlen_ < 0 ? 0 : (sz_t)rlen_; }
/**
* Returns if the request content body did not have to be stored with the storage provider
* object.
* @return bool
*/
inline bool ram_handled() const throw()
{ return (unsigned long)rq_.content_length <= config.max_ram_content_length; }
/**
* Returns false if the `response().last_modified` is set and matches the conditions of
* if-modified-since/if-unmodified-since OR `response().etag` is set and matches the conditions
* of If-None-Match.
*/
bool modified() const throw()
{
if(rq_.no_cache) {
return true; // Client wants a fresh version
}
if(rs_.last_modified && rq_.if_modified_since) {
return rs_.last_modified > rq_.if_modified_since;
}
if(!rs_.etag.empty()) {
try {
typename str_map_t::const_iterator it;
if((it = rq_.headers.find("if-none-match")) != rq_.headers.end()) {
return tr(it->second, "\"\t ") != rs_.etag;
}
} catch(...) {
return true;
}
}
return true;
}
// </editor-fold>
// <editor-fold desc="header()" defaultstate="collapsed">
/**
* Set or remove a response header, so that it will appear in the header as
* "key: value\n". Empty keys are ignored, empty values cause to delete a header.
*
* @param const str_t& key
* @param const str_t& value
* @return basic_http &
*/
inline basic_http & header(const str_t& key, const str_t& value)
{
if(key.empty()) return *this;
if(!value.empty()) { rs_.headers[lc(key)] = value; } else { rs_.headers.erase(lc(key)); }
return *this;
}
/**
* Returns a header setting or "" if not found.
* @param const str_t& key
* @return const str_t &
*/
inline const str_t & header(const str_t& key) const throw()
{
try {
str_map_t::const_iterator it = rs_.headers.find(lc(key));
return it == rs_.headers.end() ? nstr : it->second;
} catch(...) {
return nstr;
}
}
/**
* Compose the response header text
* @return str_t
*/
str_t header() const
{
const char* nl = "\r\n";
str_t hd; hd.reserve(256+128);
hd += "HTTP/1."; hd += ('0' + rq_.http_version); hd += " ";
hd += reponse_status_text(rs_.status); hd += nl;
hd += str_t("Date: ") + timestamp2string((!rs_.date) ? time(0) : rs_.date) + nl;
if(config.server.length()) hd += str_t("Server: ") + config.server + nl;
if(rs_.status < 200 || rs_.status == 304 || rs_.status == 204) {
hd += str_t("Connection: close") + nl;
} else {
if(rs_.content_type.length()) hd += str_t("Content-Type: ") + rs_.content_type + nl;
if(rs_.content_length >= 0) hd += str_t("Content-Length: ") + l2s(rs_.content_length) + nl;
if(rs_.last_modified > 0) hd += str_t("Last-Modified: ") + timestamp2string(rs_.last_modified) + nl;
if(!rs_.etag.empty()) hd += str_t("Etag: \"") + rs_.etag + '\"' + nl;
if(rs_.max_age >= 0 || rs_.cache_ctrl) {
str_t max_age = rs_.max_age >= 0 ? (str_t(", max-age=") + l2s(rs_.max_age)) : str_t();
cache_ctrl_t cc = rs_.cache_ctrl;
if(!cc) cc = (rs_.max_age==0) ? cache_ctrl_no_cache : cache_ctrl_public;
switch(cc) {
case cache_ctrl_no_store:
hd += str_t("Cache-Control: no-cache, no-store, max-age=0, must-revalidate") + nl;
if(rq_.http_version == 0) hd += str_t("pragma: no-cache") + nl;
break;
case cache_ctrl_no_cache:
hd += str_t("Cache-Control: no-cache, max-age=0") + nl;
if(rq_.http_version == 0) hd += str_t("pragma: no-cache") + nl;
break;
case cache_ctrl_private:
hd += str_t("Cache-Control: private") + max_age + nl;
break;
case cache_ctrl_public:
hd += str_t("Cache-Control: public") + max_age + nl;
break;
default:
// Default public with max-age
if(rs_.max_age > 0) hd += str_t("Cache-control: public") + max_age + nl;
}
}
if(rq_.http_version > 0 && rs_.accept_ranges) {
hd += str_t("Accept-Ranges: Bytes") + nl;
}
if(rs_.transf_encoding) {
hd += str_t("Transfer-Encoding: ");
switch(rs_.transf_encoding) {
case tfe_chunked: hd += str_t("chunked")+nl; break;
case tfe_gzip: hd += str_t("gzip")+nl; break;
case tfe_deflate: hd += str_t("deflate")+nl; break;
default: hd += nl;
}
}
if(rq_.http_version < 1 || !rs_.keep_alive) { // HTTP/1.1 is default keep-alive
hd += str_t("Connection: ") + (rs_.keep_alive ? "keep-alive" : "close") + nl;
}
for(str_map_t::const_iterator it=rs_.cookie.begin(); it != rs_.cookie.end(); ++it) {
if(it->second.length()) { hd += "Set-Cookie: "; hd += it->second + nl; }
}
for(str_map_t::const_iterator it=rs_.headers.begin(); it != rs_.headers.end(); ++it) {
if(it->first.length() > 3 && !it->second.empty()) {
str_t s(it->first);
s[0] = ::toupper(s[0]);
str_t::iterator c = s.begin()+1, e = s.end()-1;
while(c != e) if(*c == '-') *(++c) = ::toupper(*(c)); else ++c;
hd += s + ": " + it->second + nl;
}
}
}
hd += nl;
return hd;
}
// </editor-fold>
// <editor-fold desc="recv()" defaultstate="collapsed">
/**
* Process by given file descriptor of an opened file/socket/pipe. If that is blocking or not etc
* needs to be set before calling this function. Returns ...
*
* 0 when all data are processed (EOF),
* 1 if more data are waited for (read said EAGAIN/EWOULDBLOCK), so you have to ::select()
* -1 on a connection or fatal read error - means you have to close the connection.
* -> Other negative values are inverted HTTP error status codes.
*
* @return int
*/
int recv()
{
if(error_ != 0) return -error_; // Don't continue doing anything if an error has occurred.
char s[128];
ssize_t n=0;
for(n = this->read(s, sizeof(s)); n > 0; n = this->read(s, sizeof(s))) {
int r = recv(s,n);
if(r <= 0) return r;
}
if(n > 0 || n == EWOULDBLOCK) {
return 1;
} else if(n < 0) {
str_t s = str_t("recv(): read() failed, error code is ") + l2s(n);
return -err(500, s.c_str()); // From EINTR over EBADF,EINVAL,..., they seem to be all fatal.
} else if(state_ < st_r_body) {
return -err(400, "recv(): read eof before header parsed");
} else {
return 0;
}
}
/**
* Process binary/text input data read from the socket or CGI pipe. Returns 1 if it needs more
* data, 0 if the input is complete, or negative on error. The input length sz==0 will be
* interpreted as end of input. Negative numbers are error codes corresponding to (negated)
* HTTP errors.
* @param const char* rq
* @param sz_t sz
* @return int
*/
int recv(const char* rq, sz_t sz)
{
if(error_ != 0) return -error_; // Don't continue doing anything if an error has occurred.
sz_t i = 0;
switch(state_) {
case st_r_header:
if(!rq || !sz) return -err(500);
rx_.reserve(rx_.length()+sz);
while(i < sz) {
char c = rq[i++];
rx_ += c;
if((!(i & 0xff)) && rx_.length() > config.max_request_header_size) {
return -err(413); // Entity Too Large, we check all 256 adding steps
} else if(c != '\n' && c != '\r') {
lch_ = 0;
} else {
// Check double newline --> eoh
const uint32_t cr='\r', lf='\n', crlf = (cr<<8)|lf;
const uint32_t crcr=(cr<<8)|cr;
const uint32_t lflf=(lf<<8)|lf;
const uint32_t clcl=((crlf<<16)|crlf);
lch_ = ((lch_<<8)|(c&0xff)) & 0xffffffff;
if((lch_ == crcr)
|| (lch_ == lflf)
|| (lch_ == clcl)
) {
// (PHASE 3) Header transferred
switch(rq_.method) {
case rq_get:
case rq_head:
case rq_delete:
case rq_trace:
state_ = st_r_done;
rlen_ = 0;
sc(rx_);
return 0;
case rq_post: // Body required
case rq_put:
if(rq_.content_length < 0) {
return -err(411); // Length required
}
case rq_mkcol: // Body optional
case rq_unlock:
case rq_lock:
case rq_copy:
case rq_move:
case rq_options:
if(rq_.content_length <= 0) {
// Post, Put, ...: no content body.
state_ = st_r_done; sc(rx_);
return 0;
}
if(rq_.content_type.length() == 0) rq_.content_type = "text/plain; charset=us-ascii"; // RFC822
rlen_ = rq_.content_length;
if(rlen_ < 0) { sz -= (-rlen_); rlen_ = 0; } // Not more input bytes than needed
if((unsigned long)rq_.content_length <= config.max_ram_content_length) {
// Input is copied into the RAM until the request is completely received, then
// parsed if applicable or left in the binary buffer.
rq_.data.reserve(rq_.content_length);
for(; i<sz && rlen_>0; ++i,--rlen_) rq_.data += rq[i]; // Copy rest of the buffer
} else {
// Input is redirected into a storage defined externally (normally a file).
rq_.data.reserve(sz-i+2);
for(; i<sz && rlen_>0; ++i,--rlen_) rq_.data += rq[i];
if(storage_.open(rq_.content_length) < 0) { // rq_.content_length>0 here
sc(rq_.data);
return -err(500, "Failed to open storage");
} else if(storage_.write(rq_.data.data(), rq_.data.length()) < 0) {
sc(rq_.data);
return -err(500, "Failed to write storage");
}
sc(rq_.data);
}
state_ = st_r_body;
break;
default:
log(str_t("Invalid request method enum ")+l2s(rq_.method));
}
} else if(rq_.method == rq_invalid) {
// (PHASE 1) That would be a head line ...
rx_ = rtr(rx_, "\r\n\t ");
std::string s; s.reserve(32);
std::string::size_type k,l, len=rx_.length();
for(k=0; k<len && !::isspace(rx_[k]); k++) s += ::toupper(rx_[k]);
if(k == len) return -err(400, "missing path and version");
if(s == "GET") rq_.method = rq_get;
else if(s == "POST") rq_.method = rq_post;
else if(s == "HEAD") rq_.method = rq_head;
else if(s == "PUT") rq_.method = rq_put;
else if(s == "DELETE") rq_.method = rq_delete;
else if(s == "OPTIONS") rq_.method = rq_options;
else if(s == "MOVE") rq_.method = rq_move;
else if(s == "COPY") rq_.method = rq_copy;
else if(s == "MKCOL") rq_.method = rq_mkcol;
else if(s == "LOCK") rq_.method = rq_lock;
else if(s == "UNLOCK") rq_.method = rq_unlock;
else if(s == "TRACE") rq_.method = rq_trace;
else return -err(400, "unknown method");
for(; k<len && ::isspace(rx_[k]); k++); // ommit spaces between method and path
rq_.path.reserve(32);
for(; k<len && !::isspace(rx_[k]); k++) rq_.path += rx_[k];
if(rq_.path.length() > config.max_request_path_length) return -err(414); // URI Too Long
if((l=rq_.path.find_first_of('#')) != str_t::npos) { // Also RFC3986 delim
rq_.query_hash = (l<rq_.path.length()-1) ? rq_.path.substr(l+1) : str_t();
rq_.path.erase(l);
}
if((l=rq_.path.find_first_of('?')) != str_t::npos) {
rq_.query_string = (l<rq_.path.length()-1) ? rq_.path.substr(l+1) : str_t();
rq_.path.erase(l);
int i;
if((i = parse_url_encoded(rq_.query_string, rq_.get, false, config.max_arg_key_size,
config.max_arg_val_size)) < 0) return -err(-i, "Parsing url-encoded GET failed");
}
if((l=rq_.path.find("://")) != str_t::npos) { // If full URI given
if(l < 1 || l > rq_.path.length()-4) return -err(400, "URI: Invalid use of '://)'");
rq_.service = lc(rq_.path.substr(0, l));
if(rq_.service == "http") sc(rq_.service); // That is the default anyway.
rq_.path = rq_.path.substr(l+3);
if((l=rq_.path.find('/')) == str_t::npos || l == 0 || l >= rq_.path.length()-1) {
return -err(400, "URI: Missing path or host after service");
}
rq_.host = rq_.path.substr(0, l);
rq_.path = rq_.path.substr(l); // Path not empty because "rq_.path.length()-4"
}
if(k == len || rq_.path.length() == 0) return -err(400, "Path length zero");
rq_.path = urldecode(rq_.path);
for(; k<len && ::isspace(rx_[k]); k++); // omit spaces
s.clear(); s.reserve(32);
for(; k<len && !::isspace(rx_[k]); k++) s += ::toupper(rx_[k]);
if(s.length() != 8 || s.find("HTTP/1.") == str_t::npos) {
return -err(400, "http version invalid"); // on http 1.x that will be wrong
}
rq_.http_version = (s[7]-'0');
if(rq_.http_version > 9) {
return -err(400, "http version invalid"); // in case someone sends HTTP/1.Z or the like
}
rq_.keep_alive = rq_.http_version > 0; // HTTP/1.1 default preferred is keep-alive
rx_.clear();
} else {
// (PHASE 2) Header field line
rx_ = tr(rx_, "\r\n\t ");
std::string::size_type k=0, len=rx_.length();
str_t key; key.reserve(32);
str_t val; val.reserve(64);
for(; k<len && rx_[k] != ':'; k++) key += rx_[k];
key = lc(tr(key, "\t "));
for(++k; k<len; k++) val += rx_[k];
if(!key.empty()) {
val = tr(val, "\t ");
switch(key[0]) {
case 'a':
if(key == "accept") {
rq_.accept = lc(val);
} else {
rq_.headers[key] = val;
}
break;
case 'c':
if(key == "content-length") {
if(!isuint(val)) return -err(400, "content-length no uint");
rq_.content_length = s2l(val);
if(rq_.content_length > (long)config.max_content_length) return -err(413); // Entity too long
} else if(key == "content-type") {
val = lc(val);
rq_.content_type = val;
if(rq_.method == rq_post) {
if(val.find("text") == 0) {
rq_.content_type_id = cont_tp_text_plain;
} else if(val.find("application/x-www-form-urlencoded") != str_t::npos) {
rq_.content_type_id = cont_tp_urlenc;
} else if(val.find("multipart/form-data") != str_t::npos) {
rq_.content_type_id = cont_tp_multipart;
}
}
} else if(key == "cookie") {
k = 0;
len = val.length();
str_t ck, cv; ck.reserve(len/2); cv.reserve(len/2);
while(k<len) {
ck.clear(); cv.clear();
while(k<len && val[k]!='=' && val[k]!=';') ck += val[k++];
if(k>=len) break;
if((ck = lc(tr(urldecode(ck)))).length() > 0) {
if(val[k++] != ';') {
if(val[k] == '"') {
// version 1 cookie, quoted string
for(++k; k<len-1; ++k) {
if(val[k] == '\\' && val[k+1] == '"') {
k += 2; cv += '"';
} else if(val[k] == '"') {
break;
} else {
cv += val[k];
}
}
rq_.cookie[ck] = cv;
while(k<len && val[++k] != ';') ; // No errors, ignore tailing chars
} else {
// version 0 cookie, urldecode
while(k<len && val[k]!=';') cv += val[k++];
rq_.cookie[ck] = urldecode(tr(cv), false);
}
}
} else {
k++;
}
}
} else if(key == "cache-control") {
val = lc(val);
if(val.find("no-cache") != str_t::npos) {
rq_.no_cache = true;
} else {
rq_.headers[key] = val;
}
} else if(key == "connection") {
if(!val.empty()) rq_.keep_alive = ::tolower(val[0]) != 'c'; // "[c]lose" == !keep-alive
} else {
rq_.headers[key] = val;
}
break;
case 'h':
if(key == "host") {
if(!val.empty()) rq_.host = tr(val);
} else {
rq_.headers[key] = val;
}
break;
case 'i':
if(key == "if-modified-since") {
rq_.if_modified_since = string2timestamp(val);
} else if(key == "if-unmodified-since") {
rq_.if_unmodified_since = string2timestamp(val);
} else {
rq_.headers[key] = val;
}
break;
case 'r':
if(key == "referrer" || key == "referer") {
rq_.referer = val;
} else if(key == "range") {
if(val.find("bytes=") != 0 || val.length() < 7) {
rq_.headers[key] = val; // that would be principally a bad request
} else {
bool bad = false;
std::deque<str_t> q;
do {
str_t s; s.reserve(val.length());
bool hasdash = false;
for(str_t::const_iterator it=val.begin()+6; it!=val.end(); ++it) {
if(::isspace(*it)) continue;
if(*it == ',') {
if(!hasdash || s.length()<2) { bad=true; break; }
q.push_back(s);
s.clear();
hasdash = false;
} else if(*it == '-') {
if(hasdash) { bad=true; break; }
hasdash = true;
s += *it;
} else if(!::isdigit(*it)) {
bad = true; break;
} else {
s += (*it == ',') ? ' ' : *it;
}
}
if(!s.empty()) q.push_back(s);
} while(0);
rq_.range.reserve(q.size());
while(!q.empty()) {
str_t s; s.swap(q.front()); q.pop_front();
if(s[0] == '-') {
std::basic_stringstream<char_t> ss(s.substr(1));
int64_t l; ss >> l; // That should be ok, pre-checked
rq_.range.push_back(typename range_t::value_type(-1, l));
} else if(s[s.length()-1] == '-') {
s.resize(s.length()-1);
std::basic_stringstream<char_t> ss(s);
int64_t l; ss >> l;
rq_.range.push_back(typename range_t::value_type(l, -1));
} else {
sz_t i = s.find('-');
int64_t l0, l1;
{ std::basic_stringstream<char_t> ss(s.substr(0,i)); ss >> l0; }
{ std::basic_stringstream<char_t> ss(s.substr(i+1)); ss >> l1; }
if(l0 > l1) { bad=true; break; }
rq_.range.push_back(typename range_t::value_type(l0, l1));
}
}
if(bad) return -err(400, "range header field invalid");
}
} else {
rq_.headers[key] = val;
}
break;
case 'u':
if(key == "user-agent") {
rq_.user_agent = lc(val);
} else {
rq_.headers[key] = val;
}
break;
default:
rq_.headers[key] = val;
}
}
rx_.clear();
}
}
}
if(state_ > st_r_body) return 0; // all current sz processed, and we don't need more
if(state_ < st_r_body) return 1;
case st_r_body:
if(this->ram_handled()) {
for(; i<sz && rlen_>0; ++i,--rlen_) rq_.data += rq[i]; // Copy rest of the buffer
if(sz == 0 && rlen_ > 0) return -err(400, "Client did not send all announced body bytes.");
if(rlen_ > 0) return 1;
state_ = st_r_done;
// Parse input in case of known text data
if(rq_.method == rq_post) {
switch(rq_.content_type_id) {
case cont_tp_text_plain:
rq_.post["text"] = rq_.data;
sc(rq_.data); // free memory of request data
break;
case cont_tp_urlenc:
do { // Block for local int l.
int l = parse_url_encoded(rq_.data, rq_.post, true, config.max_arg_key_size,
config.max_arg_val_size); // Parse all in one go
if(l < 0) return -err(-l, "Parsing POST urlencoded failed");
sc(rq_.data);
} while(0);
break;
case cont_tp_multipart:
do {
http_ram_storage_provider<> ram;
ram.swap_string_buffer_with(rq_.data);
int l = parse_body_post_multipart_formdata(ram);
ram.close();
if(l < 0) return l; // error already set
} while(0);
break;
default:
break; // no parsing
}
}
} else {
if((long)sz > rlen_) sz = rlen_;
if(storage_.write(rq, sz) < 0) return -err(500, "Failed to write storage");
if(sz == 0 && rlen_ > 0) return -err(400, "Client did not send all announced body bytes.");
if((rlen_ -= sz) > 0) return 1;
// All data received, parse what can be parsed. That is only
// POST multipart. Other cases are either short enough to be RAM handled (above),
// or the method is not POST, or the content length is too large to parse url-encoded
// data to RAM (menas the rq_.post map).
if(rq_.method == rq_post && rq_.content_type_id == cont_tp_multipart) {
do {
int l = parse_body_post_multipart_formdata(storage_);
if(l < 0) return l; // error already set
} while(0);
break;
}
}
// return 0 branch
state_ = st_r_done;
case st_r_done:
return 0;
case st_r_error:
case st_error:
return -error_;
default:
return -err(500, "recv: invalid state");
}
return -err(500, "recv: state block did not return");
}
/**
* Parses form url encoded data and adds the results to the POST map of this object.
* @param const str_t &
* @return int
*/
int parse_body_post_form_url_encoded(const str_t & s)
{
int r = parse_url_encoded(s, rq_.post, true, config.max_arg_key_size, config.max_arg_val_size);
if(r < 0) return -err(-r, "Failed to parse form-url-encoded data"); else return r; // num added
}
/**
* Parses POST multipart formdata, adds
* @param const str_t &
* @return int
*/
int parse_body_post_multipart_formdata(http_tmpstorage_provider<> & st)
{
std::deque<upload_file_t> files; // List if found boundaries
bool with_cr = false; // correction indicator for sizes
// (1) Find boundaries
do {
str_t boundary = rq_.content_type;
do {
str_t::iterator it = std::remove_if(boundary.begin(), boundary.end(), ::isspace);
boundary.erase(it, boundary.end());
} while(0);
sz_t boundlen = boundary.find("boundary="); // e.g. boundary=aBcDe
if(boundlen == str_t::npos || boundlen+10 >= boundary.length()) {
return -err(400, "Form multipart data: missing 'boundary='");
}
boundary = str_t("--") + boundary.substr(boundlen+9); // e.g. --> --aBcDe
do {
rq_.content_type.resize(boundlen);
boundlen = rq_.content_type.find(';');
if(boundlen != str_t::npos && boundlen > 0) rq_.content_type.resize(boundlen);
rq_.content_type = tr(rq_.content_type); // you never know
} while(0);
boundlen = boundary.length();
if(boundlen > 255) {
return -err(400, "Form multipart data: boundary string too long");
}
st.seek(0);
if(!st.size() || st.eof()) {
return -err(500, "Form multipart data parser: no content in tmp storage");
} else if(st.size() < ((2*boundlen)+3)) { // start '--aBcDe', end '\n--aBcDe--'
return -err(400, "Form multipart data parser: content too short to hold boundaries");
}
// Maybe not the most efficient way, but reading goes byte-per-byte
char c; // read character
long r; // read result
str_t buf; // compare buffer for matching boundaries
unsigned long end=(st.size()-boundlen-2); // not including '\n--aBcDe--'
unsigned long p; // current position
buf.reserve(boundlen+3);
// Double boundary at check end of stream
st.seek(end); // position after last \n --> must be the boundary string + '--'
while((r=st.read(&c, sizeof(char))) > 0) {
if(!::isspace(c)) buf += c;
}
if(buf.find(boundary) != 0) return -err(400, "multipart parser: missing closing boundary");
// Check start of stream
st.seek(0);
buf.clear();
for(p=0; (r=st.read(&c, sizeof(char)))>0 && p<boundlen+5 && c!='\n'; ++p) {
if(c == '\r') with_cr = true;
if(!::isspace(c)) buf += c;
}
if(buf != boundary) return -err(400, "multipart parser: missing starting boundary");
// Search more boundaries
upload_file_t file;
file.start = p+1;
buf.clear();
for(; (r=st.read(&c, sizeof(char)))>0 && ++p<end;) {
if(!::isspace(c) && buf.length() < boundlen) buf += c;
if(c == '\n') {
if(buf.length() == boundlen && buf == boundary) {
file.size = p - boundlen - file.start - (with_cr ? 2 : 1);
files.push_back(file);
file.size = 0;
file.start = p+1;
}
buf.clear();
}
}
if(r < 0) return -err(500, "multipart parser: storage i/o failed");
file.size = p - file.start - 1; // push last item, cr/lr already chopped because of 'end'
files.push_back(file);
} while(0);
// (2) Split boundary header and start seek correction
do {
str_t header, line;
header.reserve(256);
line.reserve(64);
for(typename std::deque<upload_file_t>::iterator it= files.begin(); it != files.end(); ++it) {
header.clear();
line.clear();
st.seek(it->start);
char c;
bool was_nl = false;
unsigned long i;
for(i=0; i < it->size; ++i) {
st.read(&c, sizeof(char));
if(c=='\r') continue;
if(c=='\n' && was_nl) break;
was_nl = c=='\n';
if(was_nl || !::isspace(c)) header += c;
}
it->start += i+1;
if(i+1+(with_cr ? 1:0) >= it->size) it->size = 0; else it->size -= i+1+(with_cr ? 1:0);
sz_t p = header.find_last_not_of('\n');
if(p != str_t::npos) header.resize(p+1);
while(!header.empty()) {
if((p = header.rfind('\n')) == str_t::npos) {
line = header;
header.clear();
} else {
line = header.substr(p+1); // \n is trimmed already, so can't be at the end
header.resize(p);
}
str_t s = lc(line);
if(s.find("content-disposition:") == 0) {
// Before modifying s check for filename=, so that it is case correct.
if((p = s.find(";filename=")) != str_t::npos) { // quote checked separately
p += 10;
if(p >= s.length()-2 || s[p] != '"') { // two quotes, at least one char for the name
return -err(400, "Multipart header: file name not quoted.");
}
sz_t p1 = s.find('"', p+1);
if(p1 == str_t::npos || p1 < p+3 ) {
return -err(400, "Multipart header: file name not quoted.");
}
it->filename = line.substr(p+1, p1-p-1);
}
// The rest is lowercase, name inclusively
line = s.substr(20);
if(line.find("form-data;") != 0 ) {
return -err(400, "Multipart: Disposition not form-data.");
}
line = line.substr(10);
// Normally there are no leading / tailing ";" but you never know:
while(!line.empty() && line[line.length()-1] == ';') line.resize(line.length()-1);
while(!line.empty() && line[0] == ';') line = line.substr(1);
// Line chunks can be in random order
while(!line.empty()) {
if((p = line.rfind(';')) != str_t::npos) {
s = line.substr(p+1); line.resize(p);
} else {
s = line; line.clear();
}
if(s.find("name=\"") == 0) {
if(s.length() < 8) return -err(400, "Multipart header: Bad entity name");
s.resize(s.length()-1);
s = s.substr(6);
for(sz_t i=0; i<s.length(); ++i) {
if(s[i] < 30) return -err(400, "Multipart header: Bad entity name");
}
it->name = s;
}
// Others may follow
}
} else if(s.find("content-type:") == 0) {
it->content_type = s.substr(13);
} else if(s.find("content-transfer-encoding:") == 0) {
it->content_encoding = s.substr(26);
} else {
log(str_t("Multipart header line ignored: ")+line, log_debug);
}
}
if(it->name.empty()) return -err(400, "Multipart header: No entity name for boundary");
}
} while(0);
// (3) Transfer data to rq_.post and rq_.files
for(typename std::deque<upload_file_t>::iterator it= files.begin(); it != files.end(); ++it) {
if(it->filename.empty()) {
if(it->size > config.max_arg_val_size && !ram_handled()) {
return -err(400, "Multipart header: Missing file name for large data");
} else if(!it->content_type.empty() && (it->content_type.find("text")!=0)) {
return -err(400, "Multipart header: Missing file name for non-text data");
}
// Stored in POST
if(it->size == 0) {
rq_.post[it->name] = str_t();
} else {
str_t s;
s.reserve(it->size);
st.seek(it->start);
char c;
for(unsigned long i=0; i<it->size; ++i) {
st.read(&c, 1); // maybe check that, too
s += c;
}
rq_.post[it->name] = s;
}
} else if(it->size < config.max_arg_val_size || ram_handled()) {
// File data is saved in file.data
if(it->size > 0) {
str_t s;
s.reserve(it->size);
st.seek(it->start);
char c;
for(unsigned long i=0; i<it->size; ++i) {
st.read(&c, sizeof(char)); s += c;
}
it->data.swap(s);
}
it->start = -1; // indicator that the data is in ram, not as storage position
rq_.files[it->name] = *it;
} else {
// Data can be found in the storage, file.start and file.size say where to find it.
rq_.files[it->name] = *it;
}
}
return 0;
}
// </editor-fold>
// <editor-fold desc="send()" defaultstate="collapsed">
/**
* Sends the header data and data set in the `response().data`. If a content_length is specified,
* it will be used, weather it exceeds the data length or not.
*
* - The content length will be modified if it is not -1 and it is smaller than the
* `response().data` length.
*
* - If the request method is HEAD, all data will be omitted, but the content-length added to
* the header (if not -1). As well for 204 and 304, where no body is allowed.
*
* - If the status represents a status code >= 300 and no data nor content-length is given, a
* short message is generated if the accepted type is *%*, text/plain or text/html. Otherwise
* the message is sent without content.
*
* - Transforms invalid status codes (such as 0) into a 500 status.
*
* This function is generally used to send the header before sending other data, such as file
* contents or any other information.
*
* @return int
*/
int send()
{
if(state_ >= st_done) {
return 0;
} else if(state_ <= st_s_header) {
if(rs_.status < 100 || rs_.status >= 600) {
str_t s = str_t("Invalid response status code ") + l2s(rs_.status);
err(500, s.c_str());
}
if(rq_.http_version < 1 && rs_.status < 200) {
log(str_t("HTTP/1.0 does not know 1xx status"), log_warn);
}
if(rq_.method == rq_head) {
// Do nothing, header will be added below
} else if(rs_.status < 200 || rs_.status == 304 || rs_.status == 204) {
// Codes where no body is allowed
rs_.content_length = 0;
rs_.content_type = "";
rs_.keep_alive = false;
rs_.transf_encoding = tfe_omit;
} else if(rs_.status >= 300) {
// Codes where content is allowed and may have to be automatically generated.
// The content is not variable. Redirections and errors should close the connection.
rs_.keep_alive = false;
if(rs_.data.size() == 0) {
// Messages are only generated if no response data is set and no
// content length defined.
rs_.transf_encoding = tfe_omit;
if(rq_.accept.empty() || rq_.accept.find("html") != str_t::npos
|| rq_.accept.find("*/*") != str_t::npos) {
rs_.content_type = "text/html";
rs_.data.chunk(str_t("<h1>") + reponse_status_text(rs_.status) + "</h1>\n");
if(config.append_error_messages && !error_text_.empty()) {
rs_.data.chunk(str_t("<br/>") + htmlspecialchars(error_text_) + "\n");
}
rs_.content_length = rs_.data.size();
} else if(rq_.accept.find("text/plain") != str_t::npos) {
rs_.content_type = "text/plain";
rs_.data.chunk(str_t(reponse_status_text(rs_.status)) + "\n");
if(config.append_error_messages && !error_text_.empty()) {
rs_.data.chunk(str_t("\n") + error_text_ + "\n");
}
rs_.content_length = rs_.data.size();
}
} else {
// Data set, we leave the content-length as it is because we might want to send more than
// only these data. -1 is ignored anyway in header(). Only exception: incorrect length:
rs_.content_length = rs_.data.size();
}
} else {
// 2xx codes, normally with content. Content length==-1 means we don't know the content length,
// hence omitted.
if(rs_.content_length >= 0 && (sz_t)rs_.content_length < rs_.data.size()) {
rs_.content_length = rs_.data.size();
}
}
rs_.data.prepend(header());
// 1st chunk size == header size. n_sent_ passes 0 at the first body byte.
n_sent_ = -((long)rs_.data.chunk_size());
if(config.implicit_clear) { rq_.clear(); rs_.clear(false); }
state_ = st_s_body;
}
// Send data contents. Return 1 until rs_.data is empty, then 0 (-1 = error).
if(rs_.data.empty()) {
if(n_sent_ >= rs_.content_length) {
if(rs_.transf_encoding == tfe_chunked) this->write("0\r\n\r\n", 5); // Chunked: finish
state_ = st_done;
}
return 0; // This function has no more data to send
}
ssize_t r = 1; // send return variable
while(r > 0 && !rs_.data.empty()) {
if(rs_.transf_encoding != tfe_chunked || n_sent_ < 0) { // not chunked or still sending header
r = this->write(rs_.data.chunk(), rs_.data.chunk_size());
} else {
str_t s = ul2hex(rs_.data.chunk_size()) + str_t("\r\n");
ssize_t r2;
r = this->write(s.data(), s.length());
if(r > 0) r = this->write(rs_.data.chunk(), rs_.data.chunk_size());
if(r > 0 && (r2 = this->write("\r\n", 2)) < 0) r = r2;
}
if(r > 0) {
n_sent_ += r;
rs_.data.advance((sz_t)r);
} else if(r != EWOULDBLOCK) {
state_ = st_error;
rs_.data.clear();
str_t s = str_t("send(): Failed to write data: ") + l2s(r);
return -err(1, s.c_str()); // == EBADF EFBIG EINTR EIO ENOSPC EPIPE EINVAL
}
}
return 1; // This function did send data
}
/**
* Send a status, dependent on the status code, a small notice body is added.
* @param int status
* @return int
*/
int send(int status)
{
if(state_ <= st_s_header) {
rs_.status = status;
rs_.data.clear();
} else {
log("Headers already sent", log_warn);
}
return send();
}
/**
* Send a string. Default content-type=text/plain.
* @param const str_t & s
* @return int
*/
int send(const str_t & s, int status=200, bool implicit_content_length=true)
{ return send_binary(s.data(), s.length(), status, implicit_content_length); }
/**
* Send binary.
* @param const void* data
* @param sz_t sz
* @param int status=200
* @param bool implicit_content_length=true
* @return int
*/
int send_binary(const void *data, sz_t sz, int status=200, bool implicit_content_length=true)
{
if(state_ <= st_s_header) {
rs_.status = status;
rs_.data.chunk((const char*) data, sz);
if(implicit_content_length) rs_.content_length = rs_.data.size();
if(rs_.content_type.length() == 0) rs_.content_type = "text/plain"; // charset=client guess
} else if(sz > 0) {
if(rs_.content_length >= 0 && n_sent_+(long)sz > rs_.content_length) {
log("send_binary(): Trimmed data size to fit content-length", log_warn);
sz = rs_.content_length - n_sent_;
}
rs_.data.chunk((const char*) data, sz);
}
return send();
}
/**
* Send a file.
*
* @param const str_t & filename
* @param int status=200
* @return int
*/
int send_file(const str_t & filename, int status=200)
{
struct stat st;
status = status <= 0 ? 500 : status;
switch(stat(filename.c_str(), &st)) {
case 0:
if(!S_ISREG(st.st_mode)) { // no links
return send(404);
} else {
{
str_t::size_type n = filename.find_last_of('.');
if(n != str_t::npos && n < filename.length()-1) {
str_t s = mime_content_type(lc(tr(filename.substr(n+1))));
rs_.content_type = (s.length() > 0) ? s : str_t("application/octet-stream");
}
rs_.last_modified = st.st_mtime;
rs_.content_length = st.st_size;
rs_.etag = mketag(filename, st);
}
if(!modified()) {
send(304);
} else {
std::ifstream fs;
fs.open(filename.c_str(), std::ifstream::binary);
if(!fs.good()) {
if(fs.eof()) { send(204); } else { send(403); } // That needs to be double checked ...
} else {
rs_.status = status; // check if 200 is always correct
send();
char s[256];
sz_t n;
while(fs.good()) {
fs.read(s, sizeof(s));
n = (sz_t)fs.gcount();
if(n == 0) { while(send() > 0) {;} break; } // flush
if(send_binary(s, n) < 0) break;
while((!rs_.data.empty()) && (send() > 0)); // in case of EAGAIN
if(state_ == st_done) break;
}
if(num_send_bytes_expected_left() > 0) {
log(str_t("Sending '") + filename + "' did not send the last " +
l2s((long)num_send_bytes_expected_left()) + " bytes.");
}
}
fs.close();
}
}
break;
case EACCES:
return send(403);
case ENOENT:
case ENAMETOOLONG:
case ENOTDIR:
return send(404);
case EOVERFLOW:
default:
return send(500);
}
return -err(1, "send_file: not all cases covered.");
}
// </editor-fold>
public:
// <editor-fold desc="Public static auxiliaries" defaultstate="collapsed">
//////////////////////////////////////////////////////////////////////////////
// Public static functions
/**
* Log something
* @param str_t txt
* @return void
*/
static void log(str_t txt, int level = log_notice)
{
static const char* lvls[] = { "CRT", "ERR ", "WRN", "INF", "DBG", "!!!" };
if(!config.log_callback || level < 0 || level > log_debug || level > config.log_level) return;
txt = str_t() + ul2s(::time(NULL)) + " [" + lvls[level] + "] " + txt + "\n";
config.log_callback(txt, level);
}
/**
* Decodes url encoded data (RFC 3986), normally for url/GET or optionally
* for POST form data accepting "+" as well to mark spaces.
* @param const str_t &in
* @param bool form=false
* @return str_t
*/
static str_t urldecode(const str_t &in, bool form=false)
{
str_t out; out.reserve(in.length());
int n = in.length();
typename str_t::const_iterator p = in.begin();
for(; --n >= 2; ++p) {
if(*p == '+' && form) {
out += ' ';
} else if(*p == '%' && std::isxdigit(*(p+1)) && std::isxdigit(*(p+2))) {
char_t c1=*(++p), c0=*(++p);
out += hex2char(c1, c0); n-=2;
} else {
out += *p;
}
}
for(; n-- >= 0; ++p) {
out += (*p == '+' && form) ? '+' : *p;
}
return out;
}
/**
* Encodes a string RFC 3986 compliant.
* @param const str_t &in
* @return str_t
*/
static str_t urlencode(const str_t &in)
{
str_t out; out.reserve(in.length()*2);
typename str_t::const_iterator p = in.begin();
typename str_t::const_iterator e = in.end();
for(; p!=e; ++p) {
if(std::isalnum(*p) || *p=='-' || *p=='_' || *p=='.') {
out += *p;
} else {
out += "%"; out += char2hex(*p);
}
}
return out;
}
/**
* Escapes HTML entities < > and &
* @param const str_t & s
* @param bool with_quotes=true
* @return str_t
*/
static str_t htmlspecialchars(const str_t & s, bool with_quotes=true)
{
str_t ss;
if(with_quotes) {
do {
sz_t n = s.length();
for(str_t::const_iterator it=s.begin(); it!=s.end(); ++it) {
switch(*it) {
case '>': // >
case '<': n+= 3; break; // <
case '&': n+= 4; break; // &
case '"': n+= 5; break; // "
case '\'':n+= 5; break; // '
}
}
ss.reserve(n);
} while(0);
for(str_t::const_iterator it=s.begin(); it!=s.end(); ++it) {
switch(*it) {
case '>' : ss += str_t(">"); break;
case '<' : ss += str_t("<"); break;
case '&' : ss += str_t("&"); break;
case '"' : ss += str_t("""); break;
case '\'': ss += str_t("'"); break;
default : ss += *it;
}
}
} else {
do {
sz_t n = s.length();
for(str_t::const_iterator it=s.begin(); it!=s.end(); ++it) {
switch(*it) {
case '>': // >
case '<': n+= 3; break; // <
case '&': n+= 4; break; // &
}
}
ss.reserve(n);
} while(0);
for(str_t::const_iterator it=s.begin(); it!=s.end(); ++it) {
switch(*it) {
case '>' : ss += str_t(">"); break;
case '<' : ss += str_t("<"); break;
case '&' : ss += str_t("&"); break;
default : ss += *it;
}
}
}
return ss;
}
/**
* Returns the mime type for a given extension (without additional tags
* like ";charset=...").
* @param const str_t &ext
* @return str_t
*/
static str_t mime_content_type(const str_t &ext)
{
static const char_t *text_plain = "text/plain";
static const char_t *lut[] = { // sorted lookup table
"aac","audio/aac", "asf","video/x-ms-asf", "avi","video/x-msvideo",
"bmp","image/bmp", "css","text/css", "csv","text/csv",
"doc","application/msword", "eps","application/postscript",
"exe","application/octet-stream", "gif","image/gif",
"gz","application/x-gunzip", "htm","text/html", "html","text/html",
"ico","image/x-icon", "jpeg","image/jpeg", "jpg","image/jpeg",
"js","application/javascript", "json","application/json",
"m4v","video/x-m4v", "mid","audio/x-midi", "mov","video/quicktime",
"mp3","audio/mpeg", "mp4","video/mp4", "mpeg","video/mpeg",
"mpg","video/mpeg", "oga","audio/ogg", "ogg","audio/ogg",
"ogv","video/ogg", "pdf","application/pdf", "png","image/png",
"ps","application/postscript", "rar","application/x-arj-compressed",
"rtf","application/rtf", "sgm","text/sgml",
"shtm","text/html", "shtml","text/html", "svg","image/svg+xml",
"swf","application/x-shockwave-flash", "tar","application/x-tar",
"tgz","application/x-tar-gz", "tif","image/tiff", "tiff","image/tiff",
"txt","text/plain", "wav","audio/x-wav", "webm","video/webm",
"wrl","model/vrml", "xhtml","application/xhtml+xml",
"xls","application/x-msexcel", "xml","text/xml", "xsl","application/xml",
"xslt","application/xml", "zip","application/x-zip-compressed",
};
if(ext.empty()) return text_plain;
{ str_t s = mime_content_type_add(ext.c_str(), 0); if(!s.empty()) return s; }
int n = (sizeof(lut)/sizeof(const char_t*)), p;
#ifdef assert
assert(!(sizeof(lut)/sizeof(const char_t*)&0x1));
#endif
while(n > 0) {
n = (n)>>1u;
p = n & 0xfffe;
const char* lv = lut[p];
if(ext.compare(lv)) { p+=n; continue; }
if(ext == lv) return lut[p+1];
p -= n;
}
for (p = (p-=2)<0 ? 0:p, n=sizeof(lut)/sizeof(const char_t*); p<n; p+=2) {
if(ext==lut[p]) return lut[p+1];
}
return text_plain;
}
/**
* Add/get a user defined mime type. Added mime types will be automatically used
* in mime_content_type(). Setting mime to NULL will return a user added mime type
* or an empty string if the lookup did not find it.
* @param const char_t* ext
* @param const char_t* mime
* @return str_t
*/
static str_t mime_content_type_add(const char_t* ext, const char_t* mime)
{
static std::map<str_t, str_t> lut;
if(!ext || !ext[0]) return "";
if(!mime) {
typename std::map<str_t, str_t>::const_iterator it = lut.find(ext);
return (it==lut.end()) ? "" : it->second;
} else {
str_t e = lc(ext);
str_t m = lc(mime);
if(!m.empty()) { lut[e] = m; }
return m;
}
}
/**
* Returns the text version of a response code for use in the response header.
* Contains two versions of the status text table. The compiler selects/drops one
* dependent on the template parameter With_All_Status_Code_Strings.
* @param unsigned code
* @return const char *
*/
static const char * reponse_status_text(unsigned code)
{
typedef struct { unsigned c; const char* h; } rc_t;
static const rc_t rc[] = {
{100,"100 Continue"},{101,"101 Switching Protocols"},{102,"102 Processing"},
{200,"200 OK"},{201,"201 Created"},{202,"202 Accepted"},{203,"203 Non-Authoritative Information"},
{204,"204 No Content"},{205,"205 Reset Content"},{206,"206 Partial Content"},
{207,"207 Multi-Status"},{208,"208 Already Reported"},{226,"226 IM Used"},
{300,"300 Multiple Choices"},{301,"301 Moved Permanently"},{302,"302 Found"},
{303,"303 See Other"},{304,"304 Not Modified"},{305,"305 Use Proxy"},{306,"306 Switch Proxy"},
{307,"307 Temporary Redirect"},{308,"308 Permanent Redirect"},{400,"400 Bad Request"},
{401,"401 Unauthorized"},{402,"402 Payment Required"},{403,"403 Forbidden"},{404,"404 Not Found"},
{405,"405 Method Not Allowed"},{406,"406 Not Acceptable"},{407,"407 Proxy Authentication Required"},
{408,"408 Request Timeout"},{409,"409 Conflict"},{410,"410 Gone"},{411,"411 Length Required"},
{412,"412 Precondition Failed"},{413,"413 Request Entity Too Large"},{414,"414 Request-URI Too Long"},
{415,"415 Unsupported Media Type"},{416,"416 Requested Range Not Satisfiable"},
{417,"417 Expectation Failed"},{419,"419 Authentication Timeout"},
{420,"420 Enhance Your Calm"},{422,"422 Unprocessable Entity"},{423,"423 Locked"},
{424,"424 Failed Dependency"},{425,"425 Unordered Collection"},{426,"426 Upgrade Required"},
{428,"428 Precondition Required"},{429,"429 Too Many Requests"},
{431,"431 Request Header Fields Too Large"},{440,"440 Login Timeout"},{444,"444 No Response"},
{449,"449 Retry With"},{450,"450 Blocked by Windows Parental Controls"},
{451,"451 Unavailable For Legal Reasons"},/*{451,"451 Redirect"},*/{494,"494 Request Header Too Large"},
{495,"495 Cert Error"},{496,"496 No Cert"},{497,"497 HTTP to HTTPS"},{499,"499 Client Closed Request"},
{500,"500 Internal Server Error"},{501,"501 Not Implemented"},{502,"502 Bad Gateway"},
{503,"503 Service Unavailable"},{504,"504 Gateway Timeout"},{505,"505 HTTP Version Not Supported"},
{506,"506 Variant Also Negotiates"},{507,"507 Insufficient Storage"},
{508,"508 Loop Detected"},{509,"509 Bandwidth Limit Exceeded"},{510,"510 Not Extended"},
{511,"511 Network Authentication Required"},{520,"520 Origin Error"},{521,"521 Web server is down"},
{522,"522 Connection timed out"},{523,"523 Proxy Declined Request"},{524,"524 A timeout occurred"},
{598,"598 Network read timeout error"},{599,"599 Network connect timeout error"},
{0,"500 Internal Server Error"}
};
const unsigned rcsz = (sizeof(rc)/sizeof(rc_t))-1; // 81; {0,"500 error"}=82
switch(code) {
case 200: return "200 OK"; // that will be the same pointer as in the list.
case 404: return "404 Not Found";
case 500: return "500 Internal Server Error";
case 501: return "501 Not Implemented";
default:
if(code >= 600 || code < 100) return rc[rcsz].h;
unsigned i=rcsz>>1, k=rcsz>>1;
while(rc[i].c != code && k>0) i = (code > rc[i].c) ? i+(k>>=1) : i-(k>>=1);
while(code > rc[i].c) ++i;
while(code < rc[i].c) --i;
return code == rc[i].c ? rc[i].h : rc[rcsz].h;
}
}
/**
* Parses URL encoded data into a string map. Returns the number of added key-value pairs or
* negative on error.
* @param const str_t & inp
* @param str_map_t & out
* @param bool form_encoded=false
* @param unsigned max_key_sz=64
* @param unsigned max_val_sz=4096
* @return int
*/
static int parse_url_encoded(const str_t & inp, str_map_t & out, bool form_encoded=false,
unsigned max_key_sz=64, unsigned max_val_sz=4096)
{
sz_t qlen = inp.length();
int n=0;
for(sz_t l=0; l<qlen; l++) {
str_t key, val;
key.reserve(32);
val.reserve(64);
for(; l<qlen && inp[l] != '&' && inp[l] != '='; l++) key += inp[l];
if(inp[l] == '=') {
for(++l; l<qlen && inp[l] != '&'; l++) val += inp[l];
}
key = urldecode(tr(key), form_encoded);
val = urldecode(tr(val), form_encoded);
if(key.length() > max_key_sz) return -431;
if(val.length() > max_val_sz) return -431;
if(!key.empty()) { out[key] = val; n++; }
}
return n;
}
/**
* Returns a GMT time based string of a time stamp suitable to be used in header date information.
* @param time_t t
* @return str_t
*/
static str_t timestamp2string(time_t t)
{
char s[128];
struct ::tm tm;
memset(s, 0, sizeof(s));
memset(&tm, 0, sizeof(tm));
#if !defined(_WINDOWS_) && !defined(_WIN32) && !defined(__MSC_VER)
if(!gmtime_r(&t, &tm)) return "Thu, 01 Jan 1970 00:00:00 GMT";
#else
errno_t e = gmtime_s(&tm, &t);
if(e != 0) {
using namespace std;
cerr << ::strerror(e) << endl;
return "Thu, 01 Jan 1970 00:00:00 GMT";
}
#endif
::strftime(s, sizeof(s), "%a, %d %b %Y %H:%M:%S GMT", &tm);
return s;
}
/**
* Returns a GMT time based on a formatted string or 0 if the string could not be parsed.
* @param const str_t & stime
* @return time_t
*/
static time_t string2timestamp(const str_t & stime)
{ // no c++11 crono requirement, no plain strptime (unsafe) --> detect pattern, parse then
// bit of a pain, but well ...
if(stime.empty()) return 0;
str_t s, pattern;
do {
// Preconditioning the time string (lower, no double spaces, strip unknown characters)
s.reserve(48);
bool wassp=false;
for(str_t::const_iterator it=stime.begin(); it!=stime.end(); ++it) {
char_t c = *it;
if(::isalnum(c)) { wassp=false; s += ::tolower(c); continue; }
if(c == ',') c = ' '; // save some trouble later on
if(::isspace(c)) { if(!wassp) { wassp = true; s += ' '; } continue; }
if(str_t(",:+-./").find(c) != str_t::npos) wassp = true; else continue;
s += c;
}
s = tr(s);
} while(0);
do {
// Pattern
pattern.reserve(32);
char_t tl = 0;
for(str_t::const_iterator it=s.begin(); it!=s.end(); ++it) {
char_t tc = ::isdigit(*it) ? 'd' : (::isalpha(*it) ? 'a' : *it);
if(tc != tl) { tl = tc; pattern += tc; }
}
} while(0);
do {
int y=-1,m=-1,d=-1,h=0,min=0,sec=0;
str_t wds, tzs, ms;
char_t c;
if(pattern == "a d a d d:d:d a") { // Thu, 01 Jan 1970 00:00:00 GMT : RFC 822, RFC 1123
std::stringstream ss(s);
ss >> wds >> std::dec >> d >> ms >> y >> h >> c >> min >> c >> sec >> tzs;
} else if(pattern == "a d-a-d d:d:d a") { // Sunday, 06-Nov-94 08:49:37 GMT, RFC 850
for(str_t::iterator it=s.begin(); it != s.end(); ++it) if(*it == '-') *it = ' ';
std::stringstream ss(s);
ss >> wds >> d >> ms >> y >> h >> c >> min >> c >> sec >> tzs;
} else if(pattern == "a a d d:d:d d") { // Sun Nov 6 08:49:37 1994, asctime
std::stringstream ss(s);
ss >> wds >> ms >> d >> h >> c >> min >> c >> sec >> y;
}
if(y<0 || d<0 || h<0 || min<0 || sec<0 || ((m<0) && (ms.length()<3))) return 0;
if((!tzs.empty()) && (tzs != "gmt")) return 0; // for now only specified timezone accepted.
if(m<0) {
ms.resize(3);
const char* months[12] = {
"jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"
};
for(int i=0; i<12; ++i) if(ms == months[i]) { m=i+1; break; }
}
//if(y < 100) { if(y >=70) y += 1900; else if(y<8) y += 2000; else return 0; }
struct ::tm tm;
memset(&tm, 0, sizeof(struct ::tm));
tm.tm_year = y-1900;
tm.tm_mon = m-1;
tm.tm_mday = d;
tm.tm_hour = h;
tm.tm_min = min;
tm.tm_sec = sec;
#if !defined(_WINDOWS_) && !defined(__MINGW32__) && !defined(__MINGW64__) && !defined(__MSC_VER)
tm.tm_gmtoff = 0; // stfwi: double check
return ::timegm(&tm);
#else
return time_t(_mkgmtime(&tm));
#endif
} while(0);
return 0;
}
// </editor-fold>
protected:
// <editor-fold desc="Protected auxiliaries" defaultstate="collapsed">
//////////////////////////////////////////////////////////////////////////////
// Internal functions / methods
/**
* Switch to error state, set response code. Returns int cast for return of negative numbers.
* @param unsigned code
* @return int
*/
inline int err(unsigned code)
{
error_ = code;
if(state_ < st_s_header) {
state_ = st_r_error;
rs_.status = code;
} else {
state_ = st_error;
}
rlen_ = 0; // ensure no additional read actions
return (int) code;
}
/**
* Switch to error state, set response code. Returns int cast for return of negative numbers.
* @param unsigned code
* @param const char *msg
* @return int
*/
inline int err(unsigned code, const char *msg)
{ if(msg) { error_text_ = msg; log(msg, log_error); } return err(code); }
/**
* Returns a character value from two hex digits. Does not check if the
* digits are really hex.
* @param char_t c1 (higher nibble)
* @param char_t c0 (lower nibble)
* @return char_t
*/
static inline char_t hex2char(register char_t c1, register char_t c0)
{
c1 = std::tolower(c1); c1 -= (c1>'9' ? ('a'-10) : '0');
c0 = std::tolower(c0); c0 -= (c0>'9' ? ('a'-10) : '0');
return (char_t) ((c1)<<4) | ((c0)<<0);
}
/**
* Transforms a character value to hex (upper case)
* @param char_t c
* @return str_t
*/
static inline str_t char2hex(register char_t c)
{
static const char_t lut[17] = "0123456789abcdef";
return str_t(1, lut[(c>>4)&0xf]) + lut[(c>>0)&0xf];
}
/**
* Transforms positive number into a hex string
* @param uint64_t n
* @return str_t
*/
static inline str_t ul2hex(unsigned long n)
{
static const char_t lut[17] = "0123456789abcdef"; // The same pointer as above after compiling
static const sz_t l = 2*sizeof(unsigned long)+1; // note: one additional character in the buf
if(!n) return str_t("0");
str_t s(l, ' ');
sz_t i = l;
while(n && i>0) { s[--i] = lut[n & 0x0ful]; n >>= 4u; }
return s.substr(i);
}
/**
* Transform lower case
* @param str_t s
*/
static inline str_t lc(str_t s)
{ std::transform(s.begin(), s.end(), s.begin(), ::tolower); return s; }
/**
* Transform upper case
* @param str_t s
*/
static inline str_t uc(str_t s)
{ std::transform(s.begin(), s.end(), s.begin(), ::toupper); return s; }
/**
* Trim white spaces right
* @param str_t s
* @param const char *spacechars=" "
*/
static str_t rtr(const str_t & s, const char* spacechars=" ")
{
if(s.empty()) return s;
sz_t n = s.find_last_not_of(spacechars);
return (n == str_t::npos) ? "" : (n >= s.length()-1 ? s : s.substr(0, n+1));
}
/**
* Trim white spaces left
* @param str_t s
* @param const char *spacechars=" "
*/
static str_t ltr(const str_t & s, const char* spacechars=" ")
{
if(s.empty()) return s;
sz_t n = s.find_first_not_of(spacechars);
return (n == str_t::npos) ? "" : (n >= s.length() ? s : s.substr(n, str_t::npos));
}
/**
* Trim white spaces
* @param str_t s
* @param const char *spacechars=" "
*/
static inline str_t tr(const str_t & s, const char* spacechars=" ")
{ return rtr(ltr(s, spacechars), spacechars); }
/**
* Returns true if a given string represents an unsigned integer
* @param const str_t & s
* @return bool
*/
static inline bool isuint(const str_t & s)
{ return s.length()>0 && s.length()<11 && (unsigned) std::count_if(s.begin(), s.end(), ::isdigit)
+ (s[0]=='+' ? 1:0) == s.length(); }
/**
* Returns true if a given string represents an unsigned integer
* @param const str_t & s
* @return bool
*/
static inline bool isint(const str_t & s)
{ return s.length()>0 && s.length()<11 && (unsigned) std::count_if(s.begin(), s.end(), ::isdigit)
+ ((s[0]=='-' || s[0]=='+') ? 1:0) == s.length(); }
/**
* Alpha to long wrapper
* @param const str_t & s
* @return long
*/
static inline long s2l(const str_t & s)
{ return ::atol(s.c_str()); }
/**
* Long to string wrapper
* @return str_t
*/
static inline str_t l2s(long l)
{ std::stringstream ss; ss << l; return ss.str(); }
/**
* Unsigned long to string wrapper
* @return str_t
*/
static inline str_t ul2s(unsigned long ul)
{ std::stringstream ss; ss << ul; return ss.str(); }
/**
* String to double
* @param const str_t & s
* @return double
*/
static inline double s2d(const str_t & s)
{ std::stringstream ss(s); double d; return (!(ss>>d) || !ss.eof()) ?
std::numeric_limits<double>::quiet_NaN() : d; }
/**
* Double to string
* @return str_t
*/
static inline str_t d2s(double d)
{ std::stringstream ss; ss << d; return ss.str(); }
/**
* String clear with memory shrinking deallocation. c++11: shrinktofit, more general: swap.
* @param str_t s
*/
static inline void sc(str_t & s)
{ s.clear(); str_t().swap(s); }
/**
* Generate a simple etag from file path and data
* @param filename
* @param const struct stat & st
* @return str_t
*/
static str_t mketag(const str_t & filename, const struct stat & st)
{
static const char* enc = "0123456789abcdefghijklmnopqrstuv----";
static const str_t::size_type l = 32;
str_t s; s.reserve(l);
uint64_t v;
for(v=(uint64_t) st.st_mtime; v>0 && s.length()<l; v>>=5u) s += enc[v&((1u<<5)-1)];
for(v=(uint64_t) st.st_size; v>0 && s.length()<l; v>>=5u) s += enc[v&((1u<<5)-1)];
for(v=(uint64_t) st.st_ino; v>0 && s.length()<l; v>>=5u) s += enc[v&((1u<<5)-1)];
for(v=(uint64_t) filename.length(); v>0 && s.length()<l; v>>=5u) s += enc[v&((1u<<5)-1)];
v = 0;
for(str_t::const_iterator it=filename.begin(); it!=filename.end(); ++it) v += (uint64_t) *it;
for(; v>0 && s.length()<l; v>>=5u) s += enc[v&((1u<<5)-1)];
return s;
}
// </editor-fold>
protected:
// <editor-fold desc="Instance variables" defaultstate="collapsed">
//////////////////////////////////////////////////////////////////////////////
// Instance variables / internal types
state_t state_; // Current parser state
long rlen_; // Temporary, remaining length, used to match request content length.
uint32_t lch_; // Last characters used for input parsing, temporary
long n_sent_; // Number of sent content bytes (without header)
str_t rx_; // "request header text", temporary
request_t rq_; // Request
response_t rs_; // Response
storage_t storage_; // The storage provider
int error_; // Error number
str_t error_text_; // String of last error, if set
// </editor-fold>
public:
// <editor-fold desc="dump()" defaultstate="collapsed">
str_t dump()
{
std::stringstream ss;
const char endl = '\n';
ss << "basic_http<...> = {" << endl
<< " state : ";
switch(state()) {
case st_r_header: ss << "rx header"; break;
case st_r_body: ss << "rx body"; break;
case st_r_done: ss << "rx done"; break;
case st_r_error: ss << "rx error"; break;
case st_s_header: ss << "tx header"; break;
case st_s_body: ss << "tx body"; break;
case st_done: ss << "done"; break;
case st_error: ss << "error"; break;
}
ss << endl
<< " modified() : " << modified() << endl
<< " num_read_bts_exp: " << num_read_bytes_expected_left() << endl
<< " num_send_bts_exp: " << num_send_bytes_expected_left() << endl
<< " request() {" << endl
<< " method : ";
switch(rq_.method) {
case rq_get: ss << "GET"; break;
case rq_head: ss << "HEAD"; break;
case rq_post: ss << "POST"; break;
case rq_put: ss << "PUT"; break;
case rq_delete: ss << "DELETE"; break;
case rq_options: ss << "OPTIONS"; break;
case rq_trace: ss << "TRACE"; break;
case rq_mkcol: ss << "MKCOL"; break;
case rq_move: ss << "MOVE"; break;
case rq_copy: ss << "COPY"; break;
case rq_lock: ss << "LOCK"; break;
case rq_unlock: ss << "UNLOCK"; break;
case rq_invalid: ss << "INVALID"; break;
}
ss << endl;
if(!rq_.service.empty()) ss << " service : " << rq_.service << endl;
if(!rq_.host.empty()) ss << " host : " << rq_.host << endl;
if(!rq_.path.empty()) ss << " path : " << rq_.path << endl;
if(!rq_.query_string.empty()) ss << " query_string : " << rq_.query_string << endl;
if(!rq_.query_hash.empty()) ss << " query_hash : " << rq_.query_hash << endl;
ss << " http_ver : 1." << rq_.http_version << endl;
if(!rq_.referer.empty()) ss << " referer : " << rq_.referer << endl;
if(!rq_.accept.empty()) ss << " accept: : " << rq_.accept << endl;
if(!rq_.user_agent.empty()) ss << " user_agent : " << rq_.user_agent << endl;
if(!rq_.content_type.empty()) ss << " content_type : " << rq_.content_type << endl;
if(rq_.content_length>-1) ss << " content_length: " << rq_.content_length << endl;
if(!rq_.if_modified_since) ss << " if_mod_since : " << rq_.if_modified_since << endl;
if(!rq_.if_unmodified_since) ss << " if_unmod_since: " << rq_.if_unmodified_since << endl;
ss << " keep_alive : " << rq_.keep_alive << endl;
ss << " no_cache : " << rq_.no_cache << endl;
if(!rq_.range.empty()) {
ss << " range : ";
for(range_t::const_iterator it=rq_.range.begin(); it!=rq_.range.end(); ++it) {
ss << "[" << it->first << "->" << it->second << "] ";
}
ss << endl;
}
ss << " data (ram) : " << rq_.data.length() << " bytes" << endl
<< " storage() : " << storage().size() << " bytes" << endl;
if(!rq_.headers.empty()) {
ss << " headers() {" << endl;
for(str_map_t::const_iterator it=rq_.headers.begin(); it != rq_.headers.end(); ++it) {
ss << " \"" << it->first << "\": \"" << it->second << "\"" << endl;
}
ss << " }" << endl;
}
if(!get().empty()) {
ss << " get() {" << endl;
for(str_map_t::const_iterator it = get().begin(); it != get().end(); ++it) {
ss << " \"" << it->first << "\": \"" << it->second << "\"" << endl;
}
ss << " }" << endl;
}
if(!post().empty()) {
ss << " post() {" << endl;
for(str_map_t::const_iterator it = post().begin(); it != post().end(); ++it) {
ss << " \"" << it->first << "\": \"" << it->second << "\"" << endl;
}
ss << " }" << endl;
}
if(!rq_.cookie.empty()) {
ss << " cookie() {" << endl;
for(str_map_t::const_iterator it = rq_.cookie.begin(); it != rq_.cookie.end(); ++it) {
ss << " \"" << it->first << "\": \"" << it->second << "\"" << endl;
}
ss << " }" << endl;
}
if(!rq_.files.empty()) {
ss << " files() {" << endl;
for(typename upload_files_t::const_iterator it= rq_.files.begin(); it!=rq_.files.end(); ++it){
ss << " \"" << it->first << "\": {" << endl
<< " filename : \"" << it->second.filename << "\"" << endl
<< " content-type: \"" << it->second.content_type << "\"" << endl
<< " encoding : \"" << it->second.content_encoding << "\"" << endl
<< " start : " << it->second.start << endl
<< " size : " << it->second.size << endl
<< " data : " << it->second.data.length() << " bytes" << endl
<< " }" << endl ;
}
}
ss << " }" << endl
<< " response() {" << endl
<< " status : " << response().status << endl
<< " date : " << response().date << endl
<< " content_type :" << response().content_type << endl
<< " content_length :" << response().content_length << endl
<< " etag :" << response().etag << endl
<< " data : " << response().data.size() << " bytes" << endl
<< " last_modified : " << response().last_modified << endl
<< " accept_ranges : " << response().accept_ranges << endl
<< " cache_ctrl : ";
switch(response().cache_ctrl) {
case cache_ctrl_none: ss << "cache_ctrl_none"; break;
case cache_ctrl_public: ss << "cache_ctrl_public"; break;
case cache_ctrl_private: ss << "cache_ctrl_private"; break;
case cache_ctrl_no_cache: ss << "cache_ctrl_no_cache"; break;
case cache_ctrl_no_store: ss << "cache_ctrl_no_store"; break;
}
ss << endl
<< " transf_encoding: ";
switch(response().transf_encoding) {
case tfe_omit: ss << "tfe_omit"; break;
case tfe_chunked: ss << "tfe_chunked"; break;
case tfe_deflate: ss << "tfe_deflate"; break;
case tfe_gzip: ss << "tfe_gzip"; break;
}
ss << endl
<< " keep_alive : " <<response().keep_alive << endl
<< " cookie() {" << endl;
for(str_map_t::const_iterator it=response().cookie.begin(); it != response().cookie.end(); ++it) {
ss << " \"" << it->first << "\": \"" << it->second << "\"" << endl;
}
ss << " }" << endl
<< " }" << endl
<< "}" << endl;
return ss.str();
}
// </editor-fold>
public:
// <editor-fold desc="Class variables" defaultstate="collapsed">
static config_t config; // On config to rule them all
static const double nan; // Convenience NaN
static const str_t nstr; // For referencing "not found" string in case of const str_t & returns.
// </editor-fold>
};
// <editor-fold desc="Statics & operators" defaultstate="collapsed">
////////////////////////////////////////////////////////////////////////////////
// Static variables
template <typename S>
typename basic_http<S>::config_t basic_http<S>::config;
template <typename S>
const typename basic_http<S>::str_t basic_http<S>::nstr;
template <typename S>
const double basic_http<S>::nan = std::numeric_limits<double>::quiet_NaN();
// </editor-fold>
}}
// <editor-fold desc="Specialisation" defaultstate="collapsed">
////////////////////////////////////////////////////////////////////////////////
// Default specialisation
namespace sw {
typedef detail::basic_http<> http;
}
// </editor-fold>
#endif