| /* 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 <rfb/HTTPServer.h> |
| #include <rfb/LogWriter.h> |
| #include <rfb/util.h> |
| #include <rdr/MemOutStream.h> |
| |
| |
| 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, "<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML 2.0//EN\">"); |
| writeLine(os, "<HTML><HEAD>"); |
| sprintf(buffer, "<TITLE>%d %s</TITLE>", result, text); |
| writeLine(os, buffer); |
| writeLine(os, "</HEAD><BODY><H1>"); |
| writeLine(os, text); |
| writeLine(os, "</H1></BODY></HTML>"); |
| 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<Session*>::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<Session*>::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<Session*>::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<Session*>::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<network::Socket*>* sockets) |
| { |
| sockets->clear(); |
| std::list<Session*>::iterator ci; |
| for (ci = sessions.begin(); ci != sessions.end(); ci++) { |
| sockets->push_back((*ci)->getSock()); |
| } |
| } |
| |
| int HTTPServer::checkTimeouts() { |
| std::list<Session*>::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; |
| } |