1 jls.loader.provide('jls.net.http.HttpServer'); 2 3 jls.loader.require('jls.lang.Thread'); 4 jls.loader.require('jls.net.Socket'); 5 jls.loader.require('jls.net.ServerSocket'); 6 jls.loader.require('jls.net.SelectorThread'); 7 jls.loader.require('jls.net.BufferSelectionHandler'); 8 jls.loader.require('jls.net.http.HttpContext'); 9 jls.loader.require('jls.net.http.HttpExchange'); 10 jls.loader.require('jls.net.http.HttpRequestHeader'); 11 jls.loader.require('jls.net.FileSelectionHandler'); 12 jls.loader.require('jls.io.File'); 13 14 jls.net.http.HttpServer = jls.lang.Class.create(/** @lends jls.net.http.HttpServer.prototype */ 15 { 16 /** 17 * Creates a HTTP server. 18 * 19 * @constructs 20 * @class A HTTP server. 21 */ 22 initialize : function() { 23 this._selectorThread = new jls.net.SelectorThread(); 24 this._contexts = {}; 25 this._defaultContext = new jls.net.http.HttpContext(''); 26 this._defaultContext.setHandler(jls.net.http.HttpServer.defaultHandler); 27 }, 28 bind : function(addr, backlog) { 29 jls.logger.info('Start HTTP server on ' + addr.toString()); 30 this._serverSocket = new jls.net.ServerSocket(addr.getPort(), backlog, addr.getAddress()); 31 this._serverSocket.configureBlocking(false); 32 33 var key = this._selectorThread.register(this._serverSocket, jls.net.http.HttpServer._onAccept, jls.net.Selector.OP_ACCEPT); 34 key.httpServer = this; 35 }, 36 /** 37 * Creates a context at the given path. 38 * 39 * @param {String} path The path of the context. 40 * @param {Function} [handler] The handler of the context. 41 * @returns {jls.net.http.HttpContext} the created context. 42 */ 43 createContext : function(path, handler) { 44 jls.logger.debug('HttpServer.createContext("' + path + '")'); 45 var context = new jls.net.http.HttpContext(path); 46 if (handler) { 47 context.setHandler(handler); 48 } 49 this._contexts[path] = context; 50 return context; 51 }, 52 getHttpContext : function(path) { 53 jls.logger.debug('getHttpContext("' + path + '")'); 54 var maxLength = -1; 55 var context = null; 56 for (var p in this._contexts) { 57 jls.logger.trace('context "' + p + '"'); 58 if ((p.length > maxLength) && ((path.startsWith(p)) || (path + '/' == p))) { 59 jls.logger.info('Found matching context path "' + p + '" for "' + path + '"'); 60 maxLength = p.length; 61 context = this._contexts[p]; 62 } 63 } 64 if (context == null) { 65 jls.logger.info('No matching context found for "' + path + '"'); 66 return this._defaultContext; 67 } 68 return context; 69 }, 70 start : function() { 71 this._selectorThread.start(); 72 jls.logger.info('HTTP server started'); 73 }, 74 stop : function() { 75 this._selectorThread.stop(); 76 jls.logger.info('HTTP server stopped'); 77 } 78 }); 79 80 Object.extend(jls.net.http.HttpServer, /** @lends jls.net.http.HttpServer */ 81 { 82 /** 83 * Creates a HTTP server for a specified socket address. 84 * 85 * @param {jls.net.InetSocketAddress} addr The socket address. 86 * @param {Number} [backlog] The backlog. 87 * @returns {jls.net.http.HttpServer} the created HTTP server. 88 */ 89 create : function(addr, backlog) { 90 var httpServer = new jls.net.http.HttpServer(); 91 if (addr) { 92 httpServer.bind(addr, backlog); 93 } 94 return httpServer; 95 }, 96 _onAccept : function(key, inFlags, outFlags) { 97 jls.logger.info('Accept connection'); 98 99 var socket = key.socket.accept(); 100 socket.configureBlocking(false); 101 102 var readKey = key.selectorThread.register(socket, jls.net.http.HttpServer._onReadHeader, jls.net.Selector.OP_READ); 103 readKey.header = new jls.net.http.HttpRequestHeader(); 104 readKey.httpServer = key.httpServer; 105 }, 106 _onClose : function(key) { 107 jls.logger.info('Closing socket'); 108 key.selectorThread.lazyunregister(key); 109 key.socket.close(); 110 }, 111 _onReadHeader : function(key, inFlags, outFlags) { 112 var status = key.header.onRead(key.socket); 113 switch (status) { 114 case jls.net.SelectionHandler.STATUS_IN_PROGRESS: 115 break; 116 case jls.net.SelectionHandler.STATUS_DONE: 117 if (key.header.getUri() == '/admin/stop') { 118 jls.net.http.ServerMgr.getInstance().shutdown(); 119 jls.net.http.HttpServer._onClose(key); 120 return; 121 } 122 // The header has been proceed, create the http exchange 123 var context = key.httpServer.getHttpContext(key.header.getUri()); 124 var handle = context.getHandler(); 125 jls.logger.info('Request path is "' + context.getPath() + '"'); 126 jls.logger.info('header: ->' + key.header.toString() + '<-'); 127 key.httpExchange = new jls.net.http.HttpExchange(context, key.header, key.socket); 128 // Call handler 129 handle(key.httpExchange); 130 // Dump response 131 /*jls.lang.System.out.print('Response: ->'); 132 key.httpExchange.onWrite(jls.lang.System.out); 133 jls.lang.System.out.println('<-');*/ 134 // Switch socket handler 135 key.selectorThread.changeHandler(key, jls.net.http.HttpServer._onRead); 136 // call it 137 jls.net.http.HttpServer._onRead(key, inFlags, outFlags); 138 break; 139 default: 140 jls.logger.warn('Invalid selection handler status (' + status + ')'); 141 case jls.net.SelectionHandler.STATUS_FAILURE: 142 // No more data, closing socket 143 jls.net.http.HttpServer._onClose(key); 144 break; 145 } 146 }, 147 _onRead : function(key, inFlags, outFlags) { 148 jls.logger.info('HttpServer._onRead()'); 149 var status = key.httpExchange.onRead(key.socket); 150 switch (status) { 151 case jls.net.SelectionHandler.STATUS_IN_PROGRESS: 152 break; 153 case jls.net.SelectionHandler.STATUS_DONE: 154 key.httpExchange._responseHandler.reset(); 155 key.selectorThread.changeHandler(key, jls.net.http.HttpServer._onWrite); 156 key.selectorThread.changeMode(key, jls.net.Selector.OP_WRITE); 157 break; 158 default: 159 jls.logger.warn('Invalid selection handler status (' + status + ')'); 160 case jls.net.SelectionHandler.STATUS_FAILURE: 161 // No more data, closing socket 162 jls.net.http.HttpServer._onClose(key); 163 break; 164 } 165 }, 166 _onWrite : function(key, inFlags, outFlags) { 167 jls.logger.info('HttpServer._onWrite()'); 168 var status = key.httpExchange.onWrite(key.socket); 169 switch (status) { 170 case jls.net.SelectionHandler.STATUS_IN_PROGRESS: 171 break; 172 case jls.net.SelectionHandler.STATUS_DONE: 173 key.selectorThread.changeHandler(key, jls.net.http.HttpServer._onReadHeader); 174 key.selectorThread.changeMode(key, jls.net.Selector.OP_READ); 175 key.httpExchange = null; 176 break; 177 default: 178 jls.logger.warn('Invalid selection handler status (' + status + ')'); 179 case jls.net.SelectionHandler.STATUS_FAILURE: 180 // No more data, closing socket 181 jls.net.http.HttpServer._onClose(key); 182 break; 183 } 184 } 185 }); 186 187 /* 188 * Provide basic handler implementations. 189 */ 190 Object.extend(jls.net.http.HttpServer, /** @lends jls.net.http.HttpServer */ 191 { 192 /** 193 * A default handler implementation with the HTTP status: not found. 194 * 195 * @param {jls.net.http.HttpExchange} httpExchange The socket address. 196 */ 197 defaultHandler : function(httpExchange) { 198 jls.logger.info('onRequest(' + httpExchange.getRequestURI() + ')'); 199 //httpExchange.setRequestBodySelectionHandler(); 200 httpExchange.sendResponseHeaders(jls.net.http.HttpHeader.HTTP_NOT_FOUND, 'Resource "' + httpExchange.getRequestURI() + '" not found.'); 201 }, 202 /** 203 * A basic body handler implementation. 204 * Parameters:<dl> 205 * <dt>requestHandler</dt><dd>The function to call once the body has been read</dd> 206 * </dl> 207 * 208 * @param {jls.net.http.HttpExchange} httpExchange The socket address. 209 */ 210 bodyHandler : function(httpExchange) { 211 var requestHandler = httpExchange.getHttpContext().getAttribute('requestHandler'); 212 var contentLength = httpExchange.getRequestHeaders().getContentLength(); 213 // TODO use a buffer pool to reuse allocated buffers 214 var buffer = jls.lang.ByteBuffer.allocate(contentLength); 215 var sh = new jls.net.BufferSelectionHandler(buffer); 216 var previousOnRead = sh.onRead; 217 sh.onRead = function(channel) { 218 var status = previousOnRead.call(sh, channel); 219 if (status == jls.net.SelectionHandler.STATUS_DONE) { 220 sh.buffer().flip(); 221 requestHandler(httpExchange, sh.buffer()); 222 } 223 return status; 224 }; 225 httpExchange.setRequestBodySelectionHandler(sh); 226 }, 227 /** 228 * A basic folder page generation implementation. 229 * 230 * @param {String} reqPath The folder request path. 231 * @param {Array} filenames The file name list. 232 * @returns {String} The HTML content. 233 */ 234 createFolderPage : function(reqPath, filenames) { 235 if (! reqPath.endsWith('/')) { 236 reqPath += '/'; 237 } 238 var content = '<html><head></head><body>'; 239 for (var i = 0; i < filenames.length; i++) { 240 content += '<a href="' + reqPath + filenames[i] + '">' + filenames[i] + '</a><br>'; 241 } 242 content += '</body></html>'; 243 return content; 244 }, 245 /** 246 * Default content type objects. 247 */ 248 defaultContentTypes : { 249 '.html' : 'text/html; charset=UTF-8', 250 '.js' : 'text/plain; charset=UTF-8', 251 '.txt' : 'text/plain; charset=UTF-8', 252 '.xml' : 'text/xml; charset=UTF-8' 253 }, 254 /** 255 * A basic file handler implementation. 256 * Parameters:<dl> 257 * <dt>rootFile</dt><dd>The root path to look for files</dd> 258 * <dt>contentTypes</dt><dd>The extention / content type mapping to use</dd> 259 * <dt>createFolderPage</dt><dd>A function to generate a folder page</dd> 260 * </dl> 261 * 262 * @param {jls.net.http.HttpExchange} httpExchange The socket address. 263 */ 264 fileHandler : function(httpExchange) { 265 var reqPath = httpExchange.getRequestURI(); 266 var ctxPath = httpExchange.getHttpContext().getPath(); 267 var path = reqPath.substring(ctxPath.length); 268 jls.logger.info('fileHandler(URI: ' + reqPath + ', ' + ctxPath + ', ' + path + ')'); 269 var rootFile = httpExchange.getHttpContext().getAttribute('rootFile'); 270 if (rootFile == null) { 271 throw new jls.lang.Exception('fileHandler: Root file not defined'); 272 } 273 var contentTypes = httpExchange.getHttpContext().getAttribute('contentTypes'); 274 if (contentTypes == null) { 275 contentTypes = {}; 276 httpExchange.getHttpContext().setAttribute('contentTypes', contentTypes); 277 } 278 var res = path.length == 0 ? rootFile : new jls.io.File(rootFile, path); 279 jls.logger.info('Checking "' + res.getPath() + '"'); 280 if (res.exists()) { 281 jls.logger.info('Sending "' + res.getPath() + '"'); 282 if (res.isFile()) { 283 for (var ext in contentTypes) { 284 if (res.getPath().endsWith(ext)) { 285 httpExchange.getResponseHeaders().setField(jls.net.http.HttpHeader.HEADER_CONTENT_TYPE, 286 contentTypes[ext]); 287 break; 288 } 289 } 290 httpExchange.sendResponseHeaders(jls.net.http.HttpHeader.HTTP_OK, new jls.net.FileSelectionHandler(res)); 291 } else { 292 var createFolderPage = httpExchange.getHttpContext().getAttribute('createFolderPage'); 293 if (createFolderPage != null) { 294 var content = createFolderPage(reqPath, res.list()); 295 httpExchange.sendResponseHeaders(jls.net.http.HttpHeader.HTTP_OK, content); 296 } else { 297 jls.logger.info('Cannot browse folder'); 298 jls.net.http.HttpServer.defaultHandler(httpExchange); 299 } 300 } 301 } else { 302 jls.logger.info('Not found'); 303 jls.net.http.HttpServer.defaultHandler(httpExchange); 304 } 305 } 306 }); 307