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