/* * Baltisot * Copyright (C) 1999-2007 Nicolas "Pixel" Noble * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ /* $Id: HttpServ.cc,v 1.59 2007-10-12 13:07:19 pixel Exp $ */ #include #ifdef HAVE_CONFIG_H #include "config.h" #endif #include "Socket.h" #include "Action.h" #include "HttpServ.h" #include "Buffer.h" #include "ReadJob.h" #include "CopyJob.h" #include "ChainTasks.h" #include "Task.h" #include "Base64.h" #include "Domain.h" #include "HashFunction.h" #include "gettext.h" #define MAXLEN 4*1024*1024 /* stuff we should support... Etags: Server: ETag: "27b42a-134-46cece08" Client: If-None-Match: "27b42a-146-46cecd32" Last-Modified: Server: Last-Modified: Fri, 24 Aug 2007 12:24:40 GMT Client: If-Modified-Since: Fri, 24 Aug 2007 12:21:06 GMT Multipart stuff: ----8<-----Example-------8<-------- Content-Type: multipart/form-data; boundary=---------------------------163944935918044553841983937859 Content-Length: 377 -----------------------------163944935918044553841983937859 Content-Disposition: form-data; name="textline" -----------------------------163944935918044553841983937859 Content-Disposition: form-data; name="datafile"; filename="tst-user-agent.txt" Content-Type: text/plain User-Agent: CFNetwork/129.20 -----------------------------163944935918044553841983937859-- ----8<-----Example-------8<-------- */ String endhl = "\r\n", endnl = "\n"; struct multipart_file { multipart_file(Buffer * _file, const String & _type, const String & _filename, const String & _varname) : file(_file), type(_type), filename(_filename), varname(_varname) { } ~multipart_file() { } Buffer * file; String type, filename, varname; }; typedef std::vector multipart_files; class ProcessRequest : public Task { public: ProcessRequest(Action * action, const Socket & out, const String & server_name, int server_port, const String & root = "/bin/start"); virtual ~ProcessRequest(); virtual String GetName(); protected: virtual int Do() throw (GeneralException); private: String GetMime(const String &); String UnMangle(const String &, int p = 0); bool ParseUri(String &, String &, String &, Handle *); void ParseVars(Handle *, int); void ParseMultipart(Handle *, int); void ShowError(Handle *); void SendHeads(Handle *, const String &, const String & = "", time_t = -1); void SendRedirect(Handle *); String file, domain, t, Method, Uri; Buffer b; Task * c, * a; Action * f; int len, localport; Action * p; Socket s; Domain * d; multipart_files m_files; String name, host, gvars, login, password, root, boundary; Variables * Vars, * Heads; bool bad, hasvars, post, multipart, content_type; HttpResponse response; }; ProcessRequest::ProcessRequest(Action * ap, const Socket & as, const String & aname, int aport, const String & aroot) : localport(aport), p(ap), s(as), name(aname), root(aroot) { SetBurst(); } ProcessRequest::~ProcessRequest() { std::vector::iterator i; for (i = m_files.begin(); i != m_files.end(); i++) { delete i->file; } } String ProcessRequest::GetName() { return _("Processing HTTP request"); } int ProcessRequest::Do() throw(GeneralException) { switch (current) { case 0: if (!s.IsConnected()) return TASK_DONE; c = new ReadJob(&s, &b); WaitFor(c); current = 1; Suspend(TASK_ON_HOLD); case 1: delete c; bad = false; // std::cerr << "---- Got a request from handle " << s.GetHandle() << " \n"; post = ParseUri(file, domain, gvars, &b); Heads = new Variables(); Vars = new Variables(); len = -1; content_type = false; do { int p; b >> t; // std::cerr << "Read Request (n): " << t << std::endl; if ((t.strstr("Content-Length: ") == 0) || (t.strstr("Content-length: ") == 0)) { // std::cerr << "Saw 'Content-Lenght:', reading length from '" << t.extract(16) << "'\n"; len = t.extract(16).to_int(); if (len > MAXLEN) len = -1; } if (t.strstr("Host: ") == 0) { host = t.extract(6); } if (t.strstr("Authorization: Basic ") == 0) { int l; char * credentials = (char *) Base64::decode(t.extract(21), &l); credentials[l] = 0; char * p = strchr(credentials, ':'); if (p) { *p = 0; login = credentials; password = p + 1; } free(credentials); } if (t.strstr("Content-Type: application/x-www-form-urlencoded") == 0) { // basic post method multipart = false; content_type = true; } if (t.strstr("Content-Type: multipart/form-data; boundary=") == 0) { multipart = true; content_type = true; boundary = "--" + t.extract(44); } if ((p = t.strchr(':')) >= 0) { String s = t.extract(0, p - 1); s += '='; s += t.extract(p + 2); Heads->Add(s); } } while (t.strlen()); // std::cerr << "---- Processing it.\n"; hasvars = false; if (post) { // On a pas eu de ligne 'Content-Length' mais on a eu une méthode POST. // Cela est une erreur. if (len == -1) { // std::cerr << "Error: method POST but no Content-Length\n"; bad = true; } else { // std::cerr << "Got a POST request. Parsing variables. (len = " << len << ")\n"; // Les variables seront initialisées ici. hasvars = true; } if (!content_type) { bad = true; } } current = 2; if (hasvars && len) { c = new CopyJob(&s, &b, len); WaitFor(c); Suspend(); } else { c = 0; } case 2: if (gvars != "") { Buffer b2; b2 << gvars; ParseVars(&b2, gvars.strlen()); } if (hasvars && len) { delete c; if (!multipart) { ParseVars(&b, len); } else { ParseMultipart(&b, len); } } std::cerr << " Domain = '" << domain << "' - File = '" << file << "'\n"; if (file == "favicon.ico") { domain = "/image"; } if (!bad) { // Nous vérifions le domaine. bad = true; // Les domaines par défaut valides sont '/', '/bin' et '/image'. if (domain == "/image") bad = false; if (domain == "/bin") bad = false; if (domain == "/") bad = false; // L'url sans domaine ni fichier est valide. (cela arrive sur certains navigateurs...) if (domain == "") bad = (file != ""); if ((d = Domain::find_domain(Uri))) bad = false; if (bad) { std::cerr << _("Error: bad domain.\n"); } } a = 0; if (bad) { ShowError(&b); } else { if (d) { HttpRequest request; request.vars = Vars; request.headers = Heads; request.uri = Uri; request.login = login; request.password = password; request.lip = s.GetAddr(); request.dip = s.GetDistantAddr(); request.lport = s.GetPort(); request.dport = s.GetDistantPort(); request.method = Method; d->Do(request, &response); a = response.BuildResponse(&s); } else if (((domain == "") || (domain == "/")) && (file == "")) { // Si le navigateur a demandé l'URL '/', alors on renvoie une notification // de redirection. SendRedirect(&b); } else if (domain == "/bin") { // Le domaine 'bin' est réservé aux actions. On cherche donc l'action à effectuer. if ((f = p->Look4URL(file))) { SendHeads(&b, "text/html"); a = f->Do(Vars, Heads, &s); } else { ShowError(&b); } } else if (domain == "/image") { // Dans tous les autres cas de domaine, on cherche le fichier dans le répertoire data. // On utilise try au cas où le fichier n'existe pas et donc que le constructeur // d'input renvoie une erreur. try { Handle * i = new Input(String("data/") + file); SendHeads(&b, GetMime(file), String("Accept-Ranges: bytes") + endhl + "Content-Length: " + (unsigned long long int) i->GetSize() + endhl, i->GetModif()); i->SetNonBlock(); a = new CopyJob(i, &s); std::cerr << _("File found, dumping.\n"); } catch (IOGeneral e) { ShowError(&b); std::cerr << _("File not found, error shown.\n"); } } else { ShowError(&b); } } if (a) a->Stop(); // std::cerr << "---- Sending header buffer.\n"; if (!d) { c = new CopyJob(&b, &s, -1, false); WaitFor(c); current = 3; Suspend(); } case 3: if (!d) delete c; if (a) { // std::cerr << "---- Sending contents.\n"; a->Restart(); WaitFor(a); current = 4; Suspend(); } case 4: if (a) delete a; delete Vars; delete Heads; // std::cerr << "---- End of Request.\n"; } return TASK_DONE; } void ProcessRequest::ParseMultipart(Handle * s, int len) { String t, crboundary, k, v, disposition, type; char redo[256], r, tmp[4]; int p, i, rl; bool done = false, in_headers = true, is_file = false; Buffer * out = 0; std::vector::iterator mf_p; Variables * dispos_headers; crboundary = "\r\n" + boundary; redo[0] = p = 0; i = 1; while (i < crboundary.strlen()) { if (crboundary[i] == crboundary[p]) { redo[i++] = ++p; } else if (p > 0) { p = redo[p - 1]; } else { redo[i++] = 0; } } p = 0; (*s) >> t; if (t != boundary) { return; } while (!done) { if (in_headers) { (*s) >> t; if (t != "") { // Content-Disposition: form-data; name="datafile"; filename="tst-user-agent.txt" // Content-Type: text/plain if (t.strstr("Content-Disposition: form-data; ") == 0) { disposition = t.extract(32); dispos_headers = new Variables(); // **** } else if (t.strstr("Content-Type: ") == 0) { type = t.extract(14); } else { printm(M_WARNING, "Unknown header in multipart chunk: " + t + "\n"); } } else { in_headers = false; if ((type != "") && ((*dispos_headers)["filename"] != "")) { is_file = true; out = new Buffer(); } else { k = (*dispos_headers)["name"]; } } } else { rl = 0; r = s->readU8(); if (crboundary[p] == r) { p++; rl = 1; } else { for (i = 0; i <= p; i++) { if (is_file) { out->writeU8(crboundary[i]); } else { v += crboundary[i]; } } if (p > 0) p = redo[p - 1]; } if (p == crboundary.strlen()) { p = 0; tmp[0] = s->readU8(); tmp[1] = s->readU8(); if ((t[0] == 13) && (t[1] == 10)) { in_headers = true; if (is_file) { m_files.push_back(multipart_file(out, type, (*dispos_headers)["filename"], (*dispos_headers)["name"])); out = 0; } else { Vars->Add(k + "=" + v); } k = ""; v = ""; type = ""; delete dispos_headers; } else if ((tmp[0] == '-') && (tmp[1] == '-')) { tmp[2] = s->readU8(); tmp[3] = s->readU8(); if ((tmp[2] == 13) && (tmp[3] == 10)) { // end of streams if (is_file) { m_files.push_back(multipart_file(out, type, (*dispos_headers)["filename"], (*dispos_headers)["name"])); out = 0; } else { Vars->Add(k + "=" + v); } return; } else { rl = 4; } } else { rl = 2; } } for (i = 0; i < rl; i++) { if (is_file) { out->writeU8(r); } else { v += r; } r = tmp[0]; tmp[0] = tmp[1]; tmp[1] = tmp[2]; tmp[2] = tmp[3]; } } } } void ProcessRequest::ParseVars(Handle * s, int len) { String t, v; char conv[3], l; int hconv, nbvars; ssize_t pos = 0, next; t = ""; for (int i = 0; i < len; i++) { s->read(&l, 1); t += l; } // std::cerr << "Post variables line: '" << t << "'\n"; // Les variables sont sous la forme 'var1=val1&var2=val2&val3=var3'. Donc le nombre d'occurences // du caractère '=' indique le nombre de variables. // WRONG! WRONG! nbvars = t.strchrcnt('='); for (int i = 0; i < nbvars; i++) { // Les variables sont sous la forme 'var1=val1&var2=val2&val3=var3'. Donc on cherche le caractère // & dans la chaine POST. next = t.strchr('&', pos); if (next < 0) next = t.strlen(); v = ""; while (pos != next) { switch (t[pos]) { // Le navigateur encode les caractères spéciaux à l'aide du format %XX où XX indique // la valeur hexadécimale du caractère. Nous encodons surtout les caractères // ' ', '=', '%', et '/' avec cette technique. case '%': pos++; conv[0] = t[pos++]; conv[1] = t[pos++]; conv[2] = '\0'; sscanf(conv, "%x", &hconv); v += ((char) hconv); break; // Certains navigateurs utilisent '+' pour indiquer ' ' (qui est illégal) au lieu // d'utiliser %20. case '+': v += ' '; pos++; break; default: v += t[pos++]; } } // std::cerr << "Pushing HTTP variable: " << v << std::endl; Vars->Add(v); pos++; } } /* * Cette fonction renverra true si la méthode est une méthode POST. * Les Strings domain et file seront modifiées afin de renvoyer le domaine * et le fichier lut. La string s doit donner la première ligne de la requète, * c'est à dire la méthode demandée par le client. */ bool ProcessRequest::ParseUri(String & file, String & domain, String & gvars, Handle * s) { String t; bool post = false; const char * p = 0; ssize_t sppos; *s >> t; std::cerr << _("Read Request (1): ") << t << std::endl; int IPos = t.strchr('?'); gvars = ""; if (IPos >= 0) { int HPos = t.strchr(' ', IPos); char * sdup = t.strdup(0, IPos - 1); gvars = t.extract(IPos + 1, HPos - 1); t = sdup; free(sdup); } // std::cerr << "New request: " << t << ", gvars = " << gvars << std::endl; bad = false; // p nous indiquera la position de la chaîne URL. switch (t[0]) { case 'P': /* POST? */ if (t.extract(1, 4) == "OST ") { p = t.to_charp(5); post = true; } else { // std::cerr << "Error: unknow request.\n"; bad = true; } Method = "POST"; break; case 'G': /* GET? */ if (t.extract(1, 3) == "ET ") { p = t.to_charp(4); } else { // std::cerr << "Error: unknow request.\n"; bad = true; } Method = "GET"; break; default: // std::cerr << "Error: unknow request.\n"; bad = true; Method = "Unknown"; } if (!bad) { ssize_t poshttp, posslash; Uri = p; sppos = Uri.strrchr(' '); p = Uri.to_charp(0, sppos - 1); Uri = p; // On enlève tout le host spécifié éventuellement dans la requete. if ((poshttp = Uri.strstr("http://")) > 0) { Uri = Uri.to_charp(poshttp + 7); posslash = Uri.strchr('/'); // Certains navigateurs indiqueront uniquement http://host comme URL. if (posslash >= 0) { host = Uri.extract(0, posslash - 1); Uri = Uri.to_charp(posslash); } else { host = Uri; Uri = ""; } } Uri = UnMangle(Uri); posslash = Uri.strrchr('/'); file = Uri.to_charp(posslash + 1); if (posslash > 0) { domain = Uri.to_charp(0, posslash - 1); } else { domain = ""; } } return post; } /* * Ceci sert à rediriger le navigateur vers l'url de démarrage. */ void ProcessRequest::SendRedirect(Handle * s) { *s << "HTTP/1.1 301 Moved Permanently" << endhl << "Server: " << name << endhl << "Location: http://" << host << root << endhl << "Cache-Control: no-cache" << endhl << "Connection: closed" << endhl << "Content-Type: text/html" << endhl << endhl << p->GetSkinner()->Redirect("http://" + host + root); } /* * Nous envoyons les entetes de réponse HTTP. */ void ProcessRequest::SendHeads(Handle * s, const String & mime, const String & extra, time_t lm) { time_t t = time(NULL); struct tm * ft = gmtime(&t); char buf[1025]; strftime(buf, 1024, "%a, %d %b %Y %H:%M:%S GMT", ft); *s << "HTTP/1.1 200 OK" << endhl << "Date: " << buf << endhl << "Server: " << name << endhl; if (lm >=0) { ft = gmtime(&lm); strftime(buf, 1024, "%a, %d %b %Y %H:%M:%S GMT", ft); } *s << "Last-Modified: " << buf << endhl << extra << "Connection: closed" << endhl << "Content-Type: " << mime << endhl << endhl; } /* * Affichage d'une erreur 404. */ void ProcessRequest::ShowError(Handle * s) { *s << "HTTP/1.1 404 Not Found" << endhl << "Server: " << name << endhl << "Cache-Control: no-cache" << endhl << "Connection: closed" << endhl << "Content-Type: text/html" << endhl << endhl << p->GetSkinner()->Error(""); } /* * Sert à déterminer le type mime à partir de l'extension du fichier. * Par défaut, nous mettons "text/plain". */ String ProcessRequest::GetMime(const String & f) { String ext; size_t ppos; ppos = f.strrchr('.'); if (ppos >= 0) { ext = f.extract(ppos + 1); if (ext == "jpg") return "image/jpeg"; if (ext == "jpeg") return "image/jpeg"; if (ext == "htm") return "text/html"; if (ext == "html") return "text/html"; if (ext == "gif") return "image/gif"; if (ext == "png") return "image/png"; if (ext == "class") return "application/octet-stream"; } return "text/plain"; } String ProcessRequest::UnMangle(const String & s, int p) { String r; char c1, c2, c[2] = "x"; if (s.strlen() <= p) return ""; switch (s[p]) { case '%': c1 = s[p + 1]; c2 = s[p + 2]; if ((((c1 >= '0') && (c1 <= '9')) || ((c1 >= 'A') && (c1 <= 'F')) || ((c1 >= 'a') && (c1 <= 'f'))) && (((c2 >= '0') && (c2 <= '9')) || ((c2 >= 'A') && (c2 <= 'F')) || ((c2 >= 'a') && (c2 <= 'f')))) { if ((c1 >= '0') && (c1 <= '9')) c1 -= '0'; if ((c2 >= '0') && (c2 <= '9')) c2 -= '0'; if ((c1 >= 'A') && (c1 <= 'F')) c1 -= 'A' - 10; if ((c2 >= 'A') && (c2 <= 'F')) c2 -= 'A' - 10; if ((c1 >= 'a') && (c1 <= 'f')) c1 -= 'a' - 10; if ((c2 >= 'a') && (c2 <= 'f')) c2 -= 'a' - 10; c[0] = c1 * 16 + c2; r = c; p += 3; } else { r = "%"; p++; } break; case '+': r = " "; p++; break; default: r = s[p]; p++; break; } return r + UnMangle(s, p); } HttpServ::HttpServ(Action * ap, int port, const String & nname) throw (GeneralException) : root("/bin/start") { bool r = true; p = ap; name = nname; localport = port; // std::cerr << "Initialising Mini HTTP-Server on port " << localport << std::endl; r = Listener.SetLocal("", port); if (!r) { throw GeneralException(_("Initialisation of the Mini HTTP-Server failed: can't bind")); } r = Listener.Listen(); if (!r) { throw GeneralException(_("Initialisation of the Mini HTTP-Server failed: can't listen")); } Listener.SetNonBlock(); WaitFor(&Listener, W4_STICKY | W4_READING); // std::cerr << "Mini HTTP-Server '" << name << "' ready and listening for port " << port << std::endl; } HttpServ::~HttpServ(void) { Listener.close(); } void HttpServ::SetRoot(const String & _root) { root = _root; } int HttpServ::Do() throw (GeneralException) { try { Socket s = Listener.Accept(); s.SetNonBlock(); new ProcessRequest(p, s, name, localport, root); } catch (GeneralException) { } return TASK_ON_HOLD; } String HttpServ::GetName() { return String("Mini HTTP-Server '") + name + "'"; } class BuildHttpResponse : public Task { public: BuildHttpResponse(HttpResponse * _hr, Handle * _out) : hr(_hr), out(_out) { SetBurst(); } virtual ~BuildHttpResponse() { } virtual String GetName() { return "BuildHttpResponse"; } virtual int Do() throw (GeneralException) { Buffer * b; switch (current) { case 0: if (hr->Prepared()) { b = &hr->contents; } else { b = new Buffer(); hr->PrepareResponse(b); t = new CopyJob(&hr->contents, b); t->DryRun(); delete t; } t = new CopyJob(b, out, -1, b != &hr->contents); current = 1; WaitFor(t); Suspend(TASK_ON_HOLD); case 1: delete t; } return TASK_DONE; } private: HttpResponse * hr; Handle * out; Task * t; }; HttpResponse::HttpResponse() : mime_type("text/html; charset=iso8859-1"), location(""), server_name("GruiK Server v0.2"), return_code(HTTP_200_OK), last_modified(time(NULL)), cache(true), already_prepared(false), builder(0) { } HttpResponse::~HttpResponse() { } Task * HttpResponse::BuildResponse(Handle * out) { ChainTasks::tasklist_t l; Task * t; t = new BuildHttpResponse(this, out); if (builder) { l.push_back(builder); l.push_back(t); t = new ChainTasks(l); } return t; } void HttpResponse::PrepareResponse(Handle * b) { if (already_prepared) return; if (!b) b = &contents; already_prepared = true; char buf[1025]; time_t cur_time = time(NULL); struct tm * ft = gmtime(&cur_time); strftime(buf, 1024, "%a, %d %b %Y %H:%M:%S GMT", ft); (*b) << "HTTP/1.1 " << return_code << " " << code_to_string() << "\r\n" << "Server: " << server_name << "\r\n" << "Date: " << buf << "\r\n"; if (!cache) (*b) << "Cache-Control: no-cache\r\n"; (*b) << "Connection: closed\r\n" << "Content-Type: " << mime_type << "\r\n"; if (location != "") { switch (return_code) { case HTTP_301_PERM_MOVED: case HTTP_302_FOUND: (*b) << "Location: " << location << "\r\n"; break; case HTTP_401_UNAUTHORIZED: if (domain != "") { SHA1 h; String digest; h.Update(location + ":" + ((Uint64) time(0)) + ":" + domain + ":" + rand()); digest = h.Finish(); (*b) << "WWW-Authenticate: Digest realm=\"" << location << "\", domain=\"" << domain << "\", nonce=\"" << digest << "\", algorithm=\"MD5\", qop=\"auth\"\r\n"; } else { (*b) << "WWW-Authenticate: Basic realm=\"" << location << "\"\r\n"; } break; } } if (last_modified >= 0) { ft = gmtime(&last_modified); strftime(buf, 1024, "%a, %d %b %Y %H:%M:%S GMT", ft); (*b) << "Last-Modified: " << buf << "\r\n"; } (*b) << "\r\n"; } bool HttpResponse::Prepared() { return already_prepared; } String HttpResponse::code_to_string() { switch (return_code) { case HTTP_200_OK: return "OK"; case HTTP_301_PERM_MOVED: return "Moved Permanently"; case HTTP_302_FOUND: return "Found"; case HTTP_400_BAD_REQUEST: return "Bad Request"; case HTTP_401_UNAUTHORIZED: return "Authorization Required"; case HTTP_403_FORBIDDEN: return "Forbidden"; case HTTP_404_NOT_FOUND: return "Not Found"; case HTTP_500_INTERNAL_ERROR: return "Internal Error"; case HTTP_503_SERVICE_UNAVAILABLE: return "Service Unavailable"; } }