blob: 94f06090503b7d7fb0d9216445b344aa236eaae1 [file] [log] [blame]
Constantin Kaplinskya2adc8d2006-05-25 05:01:55 +00001/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved.
2 *
3 * This is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This software is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License
14 * along with this software; if not, write to the Free Software
15 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
16 * USA.
17 */
18
19#include <rfb/HTTPServer.h>
20#include <rfb/LogWriter.h>
21#include <rfb/util.h>
22#include <rdr/MemOutStream.h>
23
24#ifdef WIN32
25#define strcasecmp _stricmp
26#endif
27
28
29using namespace rfb;
30using namespace rdr;
31
32static LogWriter vlog("HTTPServer");
33
34const int clientWaitTimeMillis = 20000;
35const int idleTimeoutSecs = 5 * 60;
36
37
38//
39// -=- LineReader
40// Helper class which is repeatedly called until a line has been read
41// (lines end in \n or \r\n).
42// Returns true when line complete, and resets internal state so that
43// next read() call will start reading a new line.
44// Only one buffer is kept - process line before reading next line!
45//
46
47class LineReader : public CharArray {
48public:
49 LineReader(InStream& is_, int l)
50 : CharArray(l), is(is_), pos(0), len(l), bufferOverrun(false) {}
51
52 // Returns true if line complete, false otherwise
53 bool read() {
54 while (is.checkNoWait(1)) {
55 char c = is.readU8();
56
57 if (c == '\n') {
58 if (pos && (buf[pos-1] == '\r'))
59 pos--;
60 bufferOverrun = false;
61 buf[pos++] = 0;
62 pos = 0;
63 return true;
64 }
65
66 if (pos == (len-1)) {
67 bufferOverrun = true;
68 buf[pos] = 0;
69 return true;
70 }
71
72 buf[pos++] = c;
73 }
74
75 return false;
76 }
77 bool didBufferOverrun() const {return bufferOverrun;}
78protected:
79 InStream& is;
80 int pos, len;
81 bool bufferOverrun;
82};
83
84
85//
86// -=- HTTPServer::Session
87// Manages the internal state for an HTTP session.
88// processHTTP returns true when request has completed,
89// indicating that socket & session data can be deleted.
90//
91
92class rfb::HTTPServer::Session {
93public:
94 Session(network::Socket& s, rfb::HTTPServer& srv)
95 : contentType(0), contentLength(-1), lastModified(-1),
96 line(s.inStream(), 256), sock(s),
97 server(srv), state(ReadRequestLine), lastActive(time(0)) {
98 }
99 ~Session() {
100 }
101
102 void writeResponse(int result, const char* text);
103 bool writeResponse(int code);
104
105 bool processHTTP();
106
107 network::Socket* getSock() const {return &sock;}
108
109 int checkIdleTimeout();
110protected:
111 CharArray uri;
112 const char* contentType;
113 int contentLength;
114 time_t lastModified;
115 LineReader line;
116 network::Socket& sock;
117 rfb::HTTPServer& server;
118 enum {ReadRequestLine, ReadHeaders, WriteResponse} state;
119 enum {GetRequest, HeadRequest} request;
120 time_t lastActive;
121};
122
123
124// - Internal helper routines
125
126void
127copyStream(InStream& is, OutStream& os) {
128 try {
129 while (1) {
130 os.writeU8(is.readU8());
131 }
132 } catch (rdr::EndOfStream) {
133 }
134}
135
136void writeLine(OutStream& os, const char* text) {
137 os.writeBytes(text, strlen(text));
138 os.writeBytes("\r\n", 2);
139}
140
141
142// - Write an HTTP-compliant response to the client
143
144
145void
146HTTPServer::Session::writeResponse(int result, const char* text) {
147 char buffer[1024];
148 if (strlen(text) > 512)
149 throw new rdr::Exception("Internal error - HTTP response text too big");
150 sprintf(buffer, "%s %d %s", "HTTP/1.1", result, text);
151 OutStream& os=sock.outStream();
152 writeLine(os, buffer);
Peter Åstrand4eacc022009-02-27 10:12:14 +0000153 writeLine(os, "Server: TigerVNC/4.0");
Constantin Kaplinskya2adc8d2006-05-25 05:01:55 +0000154 time_t now = time(0);
155 struct tm* tm = gmtime(&now);
156 strftime(buffer, 1024, "Date: %a, %d %b %Y %H:%M:%S GMT", tm);
157 writeLine(os, buffer);
158 if (lastModified == (time_t)-1 || lastModified == 0)
159 lastModified = now;
160 tm = gmtime(&lastModified);
161 strftime(buffer, 1024, "Last-Modified: %a, %d %b %Y %H:%M:%S GMT", tm);
162 writeLine(os, buffer);
163 if (contentLength != -1) {
164 sprintf(buffer,"Content-Length: %d",contentLength);
165 writeLine(os, buffer);
166 }
167 writeLine(os, "Connection: close");
168 os.writeBytes("Content-Type: ", 14);
169 if (result == 200) {
170 if (!contentType)
171 contentType = guessContentType(uri.buf, "text/html");
172 os.writeBytes(contentType, strlen(contentType));
173 } else {
174 os.writeBytes("text/html", 9);
175 }
176 os.writeBytes("\r\n", 2);
177 writeLine(os, "");
178 if (result != 200) {
179 writeLine(os, "<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML 2.0//EN\">");
180 writeLine(os, "<HTML><HEAD>");
181 sprintf(buffer, "<TITLE>%d %s</TITLE>", result, text);
182 writeLine(os, buffer);
183 writeLine(os, "</HEAD><BODY><H1>");
184 writeLine(os, text);
185 writeLine(os, "</H1></BODY></HTML>");
186 sock.outStream().flush();
187 }
188}
189
190bool
191HTTPServer::Session::writeResponse(int code) {
192 switch (code) {
193 case 200: writeResponse(code, "OK"); break;
194 case 400: writeResponse(code, "Bad Request"); break;
195 case 404: writeResponse(code, "Not Found"); break;
196 case 501: writeResponse(code, "Not Implemented"); break;
197 default: writeResponse(500, "Unknown Error"); break;
198 };
199
200 // This return code is passed straight out of processHTTP().
201 // true indicates that the request has been completely processed.
202 return true;
203}
204
205// - Main HTTP request processing routine
206
207bool
208HTTPServer::Session::processHTTP() {
209 lastActive = time(0);
210
211 while (sock.inStream().checkNoWait(1)) {
212
213 switch (state) {
214
215 // Reading the Request-Line
216 case ReadRequestLine:
217
218 // Either read a line, or run out of incoming data
219 if (!line.read())
220 return false;
221
222 // We have read a line! Skip it if it's blank
223 if (strlen(line.buf) == 0)
224 continue;
225
226 // The line contains a request to process.
227 {
228 char method[16], path[128], version[16];
229 int matched = sscanf(line.buf, "%15s%127s%15s",
230 method, path, version);
231 if (matched != 3)
232 return writeResponse(400);
233
234 // Store the required "method"
235 if (strcmp(method, "GET") == 0)
236 request = GetRequest;
237 else if (strcmp(method, "HEAD") == 0)
238 request = HeadRequest;
239 else
240 return writeResponse(501);
241
242 // Store the URI to the "document"
243 uri.buf = strDup(path);
244 }
245
246 // Move on to reading the request headers
247 state = ReadHeaders;
248 break;
249
250 // Reading the request headers
251 case ReadHeaders:
252
253 // Try to read a line
254 if (!line.read())
255 return false;
256
257 // Skip headers until we hit a blank line
258 if (strlen(line.buf) != 0)
259 continue;
260
261 // Headers ended - write the response!
262 {
263 CharArray address(sock.getPeerAddress());
264 vlog.info("getting %s for %s", uri.buf, address.buf);
265 contentLength = -1;
266 lastModified = -1;
267 InStream* data = server.getFile(uri.buf, &contentType, &contentLength,
268 &lastModified);
269 if (!data)
270 return writeResponse(404);
271
272 try {
273 writeResponse(200);
274 if (request == GetRequest)
275 copyStream(*data, sock.outStream());
276 sock.outStream().flush();
277 } catch (rdr::Exception& e) {
278 vlog.error("error writing HTTP document:%s", e.str());
279 }
280 delete data;
281 }
282
283 // The operation is complete!
284 return true;
285
286 default:
287 throw rdr::Exception("invalid HTTPSession state!");
288 };
289
290 }
291
292 // Indicate that we're still processing the HTTP request.
293 return false;
294}
295
296int HTTPServer::Session::checkIdleTimeout() {
297 time_t now = time(0);
298 int timeout = (lastActive + idleTimeoutSecs) - now;
299 if (timeout > 0)
300 return secsToMillis(timeout);
301 sock.shutdown();
302 return 0;
303}
304
305// -=- Constructor / destructor
306
307HTTPServer::HTTPServer() {
308}
309
310HTTPServer::~HTTPServer() {
311 std::list<Session*>::iterator i;
312 for (i=sessions.begin(); i!=sessions.end(); i++)
313 delete *i;
314}
315
316
317// -=- SocketServer interface implementation
318
319void
320HTTPServer::addSocket(network::Socket* sock, bool) {
321 Session* s = new Session(*sock, *this);
322 if (!s) {
323 sock->shutdown();
324 } else {
325 sock->inStream().setTimeout(clientWaitTimeMillis);
326 sock->outStream().setTimeout(clientWaitTimeMillis);
327 sessions.push_front(s);
328 }
329}
330
331void
332HTTPServer::removeSocket(network::Socket* sock) {
333 std::list<Session*>::iterator i;
334 for (i=sessions.begin(); i!=sessions.end(); i++) {
335 if ((*i)->getSock() == sock) {
336 delete *i;
337 sessions.erase(i);
338 return;
339 }
340 }
341}
342
343void
344HTTPServer::processSocketEvent(network::Socket* sock) {
345 std::list<Session*>::iterator i;
346 for (i=sessions.begin(); i!=sessions.end(); i++) {
347 if ((*i)->getSock() == sock) {
348 try {
349 if ((*i)->processHTTP()) {
350 vlog.info("completed HTTP request");
351 sock->shutdown();
352 }
353 } catch (rdr::Exception& e) {
354 vlog.error("untrapped: %s", e.str());
355 sock->shutdown();
356 }
357 return;
358 }
359 }
360 throw rdr::Exception("invalid Socket in HTTPServer");
361}
362
363void HTTPServer::getSockets(std::list<network::Socket*>* sockets)
364{
365 sockets->clear();
366 std::list<Session*>::iterator ci;
367 for (ci = sessions.begin(); ci != sessions.end(); ci++) {
368 sockets->push_back((*ci)->getSock());
369 }
370}
371
372int HTTPServer::checkTimeouts() {
373 std::list<Session*>::iterator ci;
374 int timeout = 0;
375 for (ci = sessions.begin(); ci != sessions.end(); ci++) {
376 soonestTimeout(&timeout, (*ci)->checkIdleTimeout());
377 }
378 return timeout;
379}
380
381
382// -=- Default getFile implementation
383
384InStream*
385HTTPServer::getFile(const char* name, const char** contentType,
386 int* contentLength, time_t* lastModified)
387{
388 return 0;
389}
390
391const char*
392HTTPServer::guessContentType(const char* name, const char* defType) {
393 CharArray file, ext;
394 if (!strSplit(name, '.', &file.buf, &ext.buf))
395 return defType;
396 if (strcasecmp(ext.buf, "html") == 0 ||
397 strcasecmp(ext.buf, "htm") == 0) {
398 return "text/html";
399 } else if (strcasecmp(ext.buf, "txt") == 0) {
400 return "text/plain";
401 } else if (strcasecmp(ext.buf, "gif") == 0) {
402 return "image/gif";
403 } else if (strcasecmp(ext.buf, "jpg") == 0) {
404 return "image/jpeg";
405 } else if (strcasecmp(ext.buf, "jar") == 0) {
406 return "application/java-archive";
407 } else if (strcasecmp(ext.buf, "exe") == 0) {
408 return "application/octet-stream";
409 }
410 return defType;
411}