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