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 rootFile = httpExchange.getHttpContext().getAttribute('rootFile'); 286 if (rootFile == null) { 287 throw new jls.lang.Exception('fileHandler: Root file not defined'); 288 } 289 var contentTypes = httpExchange.getHttpContext().getAttribute('contentTypes'); 290 if (contentTypes == null) { 291 contentTypes = {}; 292 httpExchange.getHttpContext().setAttribute('contentTypes', contentTypes); 293 } 294 var res = path.length == 0 ? rootFile : new jls.io.File(rootFile, path); 295 //jls.logger.trace('Checking "' + res.getPath() + '"'); 296 if (res.exists()) { 297 //jls.logger.info('Sending "' + res.getPath() + '"'); 298 if (res.isDirectory()) { 299 var index = new jls.io.File(res, 'index.html'); 300 if (index.isFile()) { 301 res = index; 302 } 303 } 304 if (res.isFile()) { 305 for (var ext in contentTypes) { 306 if (res.getPath().endsWith(ext)) { 307 httpExchange.getResponseHeaders().setField(jls.net.http.HttpHeader.HEADER_CONTENT_TYPE, 308 contentTypes[ext]); 309 break; 310 } 311 } 312 httpExchange.sendResponseHeaders(jls.net.http.HttpHeader.HTTP_OK, new jls.net.FileSelectionHandler(res)); 313 } else { 314 var createFolderPage = httpExchange.getHttpContext().getAttribute('createFolderPage'); 315 if (createFolderPage != null) { 316 var content = createFolderPage(reqPath, res.list()); 317 httpExchange.sendResponseHeaders(jls.net.http.HttpHeader.HTTP_OK, content); 318 } else { 319 //jls.logger.debug('Cannot browse folder'); 320 jls.net.http.HttpServer.defaultHandler(httpExchange); 321 } 322 } 323 } else { 324 //jls.logger.debug('Not found'); 325 jls.net.http.HttpServer.defaultHandler(httpExchange); 326 } 327 }, 328 zipHandler : function(httpExchange) { 329 var method = httpExchange.getRequestHeaders().getMethod(); 330 var reqPath = httpExchange.getRequestURI(); 331 var ctxPath = httpExchange.getHttpContext().getPath(); 332 var path = reqPath.substring(ctxPath.length); 333 jls.logger.info('Handle ' + method + ' "' + path + '"'); 334 //jls.logger.debug('fileHandler(URI: ' + reqPath + ', ' + ctxPath + ', ' + path + ')'); 335 336 var zipFile = httpExchange.getHttpContext().getAttribute('zipFile'); 337 if (zipFile == null) { 338 var file = httpExchange.getHttpContext().getAttribute('rootFile'); 339 if (file == null) { 340 throw new jls.lang.Exception('zipHandler: Root file not defined'); 341 } 342 jls.logger.info('Openning zip "' + file.getPath() + '"...'); 343 zipFile = new jls.util.zip.ZipFile(file); 344 httpExchange.getHttpContext().setAttribute('zipFile', zipFile); 345 } 346 var contentTypes = httpExchange.getHttpContext().getAttribute('contentTypes'); 347 if (contentTypes == null) { 348 contentTypes = {}; 349 httpExchange.getHttpContext().setAttribute('contentTypes', contentTypes); 350 } 351 path = path.substring(1); 352 if (zipFile.hasEntry(path)) { 353 var content = zipFile.getEntryContent(path); 354 for (var ext in contentTypes) { 355 if (path.endsWith(ext)) { 356 httpExchange.getResponseHeaders().setField(jls.net.http.HttpHeader.HEADER_CONTENT_TYPE, 357 contentTypes[ext]); 358 break; 359 } 360 } 361 httpExchange.sendResponseHeaders(jls.net.http.HttpHeader.HTTP_OK, new jls.net.BufferSelectionHandler(content)); 362 } else { 363 //jls.logger.debug('Not found'); 364 jls.net.http.HttpServer.defaultHandler(httpExchange); 365 } 366 } 367 }); 368