mirror of
https://github.com/kasmtech/KasmVNC.git
synced 2024-11-26 18:13:16 +01:00
425 lines
11 KiB
C++
425 lines
11 KiB
C++
/* 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: KasmVNC/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;
|
|
}
|