/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. * * This 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 software 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 software; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, * USA. */ #include #include #include #include using namespace rfb; using namespace rdr; static LogWriter vlog("HTTPServer"); const int clientWaitTimeMillis = 20000; const int idleTimeoutSecs = 5 * 60; // // -=- LineReader // Helper class which is repeatedly called until a line has been read // (lines end in \n or \r\n). // Returns true when line complete, and resets internal state so that // next read() call will start reading a new line. // Only one buffer is kept - process line before reading next line! // class LineReader : public CharArray { public: LineReader(InStream& is_, int l) : CharArray(l), is(is_), pos(0), len(l), bufferOverrun(false) {} // Returns true if line complete, false otherwise bool read() { while (is.checkNoWait(1)) { char c = is.readU8(); if (c == '\n') { if (pos && (buf[pos-1] == '\r')) pos--; bufferOverrun = false; buf[pos++] = 0; pos = 0; return true; } if (pos == (len-1)) { bufferOverrun = true; buf[pos] = 0; return true; } buf[pos++] = c; } return false; } bool didBufferOverrun() const {return bufferOverrun;} protected: InStream& is; int pos, len; bool bufferOverrun; }; // // -=- HTTPServer::Session // Manages the internal state for an HTTP session. // processHTTP returns true when request has completed, // indicating that socket & session data can be deleted. // class rfb::HTTPServer::Session { public: Session(network::Socket& s, rfb::HTTPServer& srv) : contentType(0), contentLength(-1), lastModified(-1), line(s.inStream(), 256), sock(s), server(srv), state(ReadRequestLine), lastActive(time(0)) { } ~Session() { } void writeResponse(int result, const char* text); bool writeResponse(int code); bool processHTTP(); network::Socket* getSock() const {return &sock;} int checkIdleTimeout(); protected: CharArray uri; const char* contentType; int contentLength; time_t lastModified; LineReader line; network::Socket& sock; rfb::HTTPServer& server; enum {ReadRequestLine, ReadHeaders, WriteResponse} state; enum {GetRequest, HeadRequest} request; time_t lastActive; }; // - Internal helper routines void copyStream(InStream& is, OutStream& os) { try { while (1) { os.writeU8(is.readU8()); } } catch (rdr::EndOfStream) { } } void writeLine(OutStream& os, const char* text) { os.writeBytes(text, strlen(text)); os.writeBytes("\r\n", 2); } // - Write an HTTP-compliant response to the client void HTTPServer::Session::writeResponse(int result, const char* text) { char buffer[1024]; if (strlen(text) > 512) throw new rdr::Exception("Internal error - HTTP response text too big"); sprintf(buffer, "%s %d %s", "HTTP/1.1", result, text); OutStream& os=sock.outStream(); writeLine(os, buffer); writeLine(os, "Server: TigerVNC/4.0"); time_t now = time(0); struct tm* tm = gmtime(&now); strftime(buffer, 1024, "Date: %a, %d %b %Y %H:%M:%S GMT", tm); writeLine(os, buffer); if (lastModified == (time_t)-1 || lastModified == 0) lastModified = now; tm = gmtime(&lastModified); strftime(buffer, 1024, "Last-Modified: %a, %d %b %Y %H:%M:%S GMT", tm); writeLine(os, buffer); if (contentLength != -1) { sprintf(buffer,"Content-Length: %d",contentLength); writeLine(os, buffer); } writeLine(os, "Connection: close"); os.writeBytes("Content-Type: ", 14); if (result == 200) { if (!contentType) contentType = guessContentType(uri.buf, "text/html"); os.writeBytes(contentType, strlen(contentType)); } else { os.writeBytes("text/html", 9); } os.writeBytes("\r\n", 2); writeLine(os, ""); if (result != 200) { writeLine(os, ""); writeLine(os, ""); sprintf(buffer, "%d %s", result, text); writeLine(os, buffer); writeLine(os, "

"); writeLine(os, text); writeLine(os, "

"); sock.outStream().flush(); } } bool HTTPServer::Session::writeResponse(int code) { switch (code) { case 200: writeResponse(code, "OK"); break; case 400: writeResponse(code, "Bad Request"); break; case 404: writeResponse(code, "Not Found"); break; case 501: writeResponse(code, "Not Implemented"); break; default: writeResponse(500, "Unknown Error"); break; }; // This return code is passed straight out of processHTTP(). // true indicates that the request has been completely processed. return true; } // - Main HTTP request processing routine bool HTTPServer::Session::processHTTP() { lastActive = time(0); while (sock.inStream().checkNoWait(1)) { switch (state) { // Reading the Request-Line case ReadRequestLine: // Either read a line, or run out of incoming data if (!line.read()) return false; // We have read a line! Skip it if it's blank if (strlen(line.buf) == 0) continue; // The line contains a request to process. { char method[16], path[128], version[16]; int matched = sscanf(line.buf, "%15s%127s%15s", method, path, version); if (matched != 3) return writeResponse(400); // Store the required "method" if (strcmp(method, "GET") == 0) request = GetRequest; else if (strcmp(method, "HEAD") == 0) request = HeadRequest; else return writeResponse(501); // Store the URI to the "document" uri.buf = strDup(path); } // Move on to reading the request headers state = ReadHeaders; break; // Reading the request headers case ReadHeaders: // Try to read a line if (!line.read()) return false; // Skip headers until we hit a blank line if (strlen(line.buf) != 0) continue; // Headers ended - write the response! { CharArray address(sock.getPeerAddress()); vlog.info("getting %s for %s", uri.buf, address.buf); contentLength = -1; lastModified = -1; InStream* data = server.getFile(uri.buf, &contentType, &contentLength, &lastModified); if (!data) return writeResponse(404); try { writeResponse(200); if (request == GetRequest) copyStream(*data, sock.outStream()); sock.outStream().flush(); } catch (rdr::Exception& e) { vlog.error("error writing HTTP document:%s", e.str()); } delete data; } // The operation is complete! return true; default: throw rdr::Exception("invalid HTTPSession state!"); }; } // Indicate that we're still processing the HTTP request. return false; } int HTTPServer::Session::checkIdleTimeout() { time_t now = time(0); int timeout = (lastActive + idleTimeoutSecs) - now; if (timeout > 0) return secsToMillis(timeout); sock.shutdown(); return 0; } // -=- Constructor / destructor HTTPServer::HTTPServer() { } HTTPServer::~HTTPServer() { std::list::iterator i; for (i=sessions.begin(); i!=sessions.end(); i++) delete *i; } // -=- SocketServer interface implementation void HTTPServer::addSocket(network::Socket* sock, bool) { Session* s = new Session(*sock, *this); if (!s) { sock->shutdown(); } else { sock->inStream().setTimeout(clientWaitTimeMillis); sock->outStream().setTimeout(clientWaitTimeMillis); sessions.push_front(s); } } void HTTPServer::removeSocket(network::Socket* sock) { std::list::iterator i; for (i=sessions.begin(); i!=sessions.end(); i++) { if ((*i)->getSock() == sock) { delete *i; sessions.erase(i); return; } } } void HTTPServer::processSocketReadEvent(network::Socket* sock) { std::list::iterator i; for (i=sessions.begin(); i!=sessions.end(); i++) { if ((*i)->getSock() == sock) { try { if ((*i)->processHTTP()) { vlog.info("completed HTTP request"); sock->shutdown(); } } catch (rdr::Exception& e) { vlog.error("untrapped: %s", e.str()); sock->shutdown(); } return; } } throw rdr::Exception("invalid Socket in HTTPServer"); } void HTTPServer::processSocketWriteEvent(network::Socket* sock) { std::list::iterator i; for (i=sessions.begin(); i!=sessions.end(); i++) { if ((*i)->getSock() == sock) { try { sock->outStream().flush(); } catch (rdr::Exception& e) { vlog.error("untrapped: %s", e.str()); sock->shutdown(); } return; } } throw rdr::Exception("invalid Socket in HTTPServer"); } void HTTPServer::getSockets(std::list* sockets) { sockets->clear(); std::list::iterator ci; for (ci = sessions.begin(); ci != sessions.end(); ci++) { sockets->push_back((*ci)->getSock()); } } int HTTPServer::checkTimeouts() { std::list::iterator ci; int timeout = 0; for (ci = sessions.begin(); ci != sessions.end(); ci++) { soonestTimeout(&timeout, (*ci)->checkIdleTimeout()); } return timeout; } // -=- Default getFile implementation InStream* HTTPServer::getFile(const char* name, const char** contentType, int* contentLength, time_t* lastModified) { return 0; } const char* HTTPServer::guessContentType(const char* name, const char* defType) { CharArray file, ext; if (!strSplit(name, '.', &file.buf, &ext.buf)) return defType; if (strcasecmp(ext.buf, "html") == 0 || strcasecmp(ext.buf, "htm") == 0) { return "text/html"; } else if (strcasecmp(ext.buf, "txt") == 0) { return "text/plain"; } else if (strcasecmp(ext.buf, "gif") == 0) { return "image/gif"; } else if (strcasecmp(ext.buf, "jpg") == 0) { return "image/jpeg"; } else if (strcasecmp(ext.buf, "jar") == 0) { return "application/java-archive"; } else if (strcasecmp(ext.buf, "exe") == 0) { return "application/octet-stream"; } return defType; }