├── .gitignore ├── LICENSE ├── README.md ├── bower.json ├── dist └── fxos-web-server.js ├── example ├── directory │ ├── css │ │ └── app.css │ ├── img │ │ └── icons │ │ │ ├── icon.svg │ │ │ ├── icon128x128.png │ │ │ ├── icon16x16.png │ │ │ ├── icon48x48.png │ │ │ └── icon60x60.png │ ├── index.html │ ├── js │ │ ├── app.js │ │ └── storage.js │ ├── lib │ │ └── fxos-web-server.js │ └── manifest.webapp ├── p2p │ ├── css │ │ └── app.css │ ├── img │ │ └── icons │ │ │ ├── icon.svg │ │ │ ├── icon128x128.png │ │ │ ├── icon16x16.png │ │ │ ├── icon48x48.png │ │ │ └── icon60x60.png │ ├── index.html │ ├── js │ │ ├── app.js │ │ └── p2p-helper.js │ ├── lib │ │ └── fxos-web-server.js │ └── manifest.webapp ├── simple │ ├── css │ │ └── app.css │ ├── img │ │ ├── icons │ │ │ ├── icon.svg │ │ │ ├── icon128x128.png │ │ │ ├── icon16x16.png │ │ │ ├── icon48x48.png │ │ │ └── icon60x60.png │ │ └── image.jpg │ ├── index.html │ ├── js │ │ └── app.js │ ├── lib │ │ └── fxos-web-server.js │ └── manifest.webapp └── upload │ ├── css │ └── app.css │ ├── img │ └── icons │ │ ├── icon.svg │ │ ├── icon128x128.png │ │ ├── icon16x16.png │ │ ├── icon48x48.png │ │ └── icon60x60.png │ ├── index.html │ ├── js │ └── app.js │ ├── lib │ └── fxos-web-server.js │ └── manifest.webapp ├── package.json ├── src ├── binary-utils.js ├── event-target.js ├── http-request.js ├── http-response.js ├── http-server.js ├── http-status.js └── ip-utils.js └── test ├── karma.conf.js ├── mock └── navigator-moztcpsocket.js └── unit └── http-server.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | 30 | # Mac OS X temporary files 31 | .DS_Store 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Justin D'Arcangelo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Firefox OS Web Server 2 | 3 | > A basic HTTP server for Firefox OS, written in JavaScript! 4 | 5 | ## Installing it 6 | 7 | ```bash 8 | npm install git+https://github.com/justindarc/fxos-web-server.git 9 | ``` 10 | 11 | ## Using it 12 | 13 | ```javascript 14 | 15 | // Require the server code 16 | var HTTPServer = require('fxos-web-server'); 17 | 18 | // Make an instance listening in port 80 19 | var server = new HTTPServer(80); 20 | 21 | // Listen to request events 22 | server.addEventListener('request', function(evt) { 23 | 24 | // To make things simple, we'll respond by displaying the request path 25 | var responseText = evt.request.path; 26 | var response = evt.response; 27 | response.send(responseText); 28 | 29 | }); 30 | 31 | // Finally start the server 32 | server.start(); 33 | 34 | // (you can stop it afterwards with server.stop()) 35 | 36 | ``` 37 | 38 | You can also have a look at the examples in the `example` folder. 39 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fxos-web-server", 3 | "version": "0.1.0", 4 | "homepage": "https://github.com/justindarc/fxos-web-server", 5 | "authors": [ 6 | "Justin D'Arcangelo " 7 | ], 8 | "main": "dist/fxos-web-server.js", 9 | "license": "MIT", 10 | "ignore": [ 11 | "**/.*", 12 | "example", 13 | "node_modules", 14 | "src", 15 | "test", 16 | "bower.json", 17 | "LICENSE", 18 | "package.json", 19 | "README.md" 20 | ], 21 | "dependencies": {}, 22 | "devDependencies": {} 23 | } 24 | -------------------------------------------------------------------------------- /dist/fxos-web-server.js: -------------------------------------------------------------------------------- 1 | !function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.HTTPServer=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o { 75 | listener.call(this, data); 76 | }); 77 | }; 78 | 79 | EventTarget.prototype.addEventListener = function(name, listener) { 80 | var events = this._events = this._events || {}; 81 | var listeners = events[name] = events[name] || []; 82 | if (listeners.find(fn => fn === listener)) { 83 | return; 84 | } 85 | 86 | listeners.push(listener); 87 | }; 88 | 89 | EventTarget.prototype.removeEventListener = function(name, listener) { 90 | var events = this._events || {}; 91 | var listeners = events[name] || []; 92 | for (var i = listeners.length - 1; i >= 0; i--) { 93 | if (listeners[i] === listener) { 94 | listeners.splice(i, 1); 95 | return; 96 | } 97 | } 98 | }; 99 | 100 | return EventTarget; 101 | 102 | })(); 103 | 104 | },{}],3:[function(require,module,exports){ 105 | /*jshint esnext:true*/ 106 | /*exported HTTPRequest*/ 107 | 'use strict'; 108 | 109 | module.exports = window.HTTPRequest = (function() { 110 | 111 | var EventTarget = require('./event-target'); 112 | var BinaryUtils = require('./binary-utils'); 113 | 114 | const CRLF = '\r\n'; 115 | 116 | function HTTPRequest(socket) { 117 | var parts = []; 118 | var receivedLength = 0; 119 | 120 | var checkRequestComplete = () => { 121 | var contentLength = parseInt(this.headers['Content-Length'], 10); 122 | if (isNaN(contentLength)) { 123 | this.complete = true; 124 | this.dispatchEvent('complete', this); 125 | return; 126 | } 127 | 128 | if (receivedLength < contentLength) { 129 | return; 130 | } 131 | 132 | BinaryUtils.mergeArrayBuffers(parts, (data) => { 133 | this.body = parseBody(this.headers['Content-Type'], data); 134 | this.complete = true; 135 | this.dispatchEvent('complete', this); 136 | }); 137 | 138 | socket.ondata = null; 139 | }; 140 | 141 | socket.ondata = (event) => { 142 | var data = event.data; 143 | 144 | if (parts.length > 0) { 145 | parts.push(data); 146 | receivedLength += data.byteLength; 147 | checkRequestComplete(); 148 | return; 149 | } 150 | 151 | var firstPart = parseHeader(this, data); 152 | if (this.invalid) { 153 | this.dispatchEvent('error', this); 154 | 155 | socket.close(); 156 | socket.ondata = null; 157 | return; 158 | } 159 | 160 | if (firstPart) { 161 | parts.push(firstPart); 162 | receivedLength += firstPart.byteLength; 163 | } 164 | 165 | checkRequestComplete(); 166 | }; 167 | } 168 | 169 | HTTPRequest.prototype = new EventTarget(); 170 | 171 | HTTPRequest.prototype.constructor = HTTPRequest; 172 | 173 | function parseHeader(request, data) { 174 | if (!data) { 175 | request.invalid = true; 176 | return null; 177 | } 178 | 179 | data = BinaryUtils.arrayBufferToString(data); 180 | 181 | var requestParts = data.split(CRLF + CRLF); 182 | 183 | var header = requestParts.shift(); 184 | var body = requestParts.join(CRLF + CRLF); 185 | 186 | var headerLines = header.split(CRLF); 187 | var requestLine = headerLines.shift().split(' '); 188 | 189 | var method = requestLine[0]; 190 | var uri = requestLine[1]; 191 | var version = requestLine[2]; 192 | 193 | if (version !== HTTPServer.HTTP_VERSION) { 194 | request.invalid = true; 195 | return null; 196 | } 197 | 198 | var uriParts = uri.split('?'); 199 | 200 | var path = uriParts.shift(); 201 | var params = parseURLEncodedString(uriParts.join('?')); 202 | 203 | var headers = {}; 204 | headerLines.forEach((headerLine) => { 205 | var parts = headerLine.split(': '); 206 | if (parts.length !== 2) { 207 | return; 208 | } 209 | 210 | var name = parts[0]; 211 | var value = parts[1]; 212 | 213 | headers[name] = value; 214 | }); 215 | 216 | request.method = method; 217 | request.path = path; 218 | request.params = params; 219 | request.headers = headers; 220 | 221 | if (headers['Content-Length']) { 222 | // request.body = parseBody(headers['Content-Type'], body); 223 | return BinaryUtils.stringToArrayBuffer(body); 224 | } 225 | 226 | return null; 227 | } 228 | 229 | function setOrAppendValue(object, name, value) { 230 | var existingValue = object[name]; 231 | if (existingValue === undefined) { 232 | object[name] = value; 233 | } else { 234 | if (Array.isArray(existingValue)) { 235 | existingValue.push(value); 236 | } else { 237 | object[name] = [existingValue, value]; 238 | } 239 | } 240 | } 241 | 242 | function parseURLEncodedString(string) { 243 | var values = {}; 244 | 245 | string.split('&').forEach((pair) => { 246 | if (!pair) { 247 | return; 248 | } 249 | 250 | var parts = decodeURIComponent(pair).split('='); 251 | 252 | var name = parts.shift(); 253 | var value = parts.join('='); 254 | 255 | setOrAppendValue(values, name, value); 256 | }); 257 | 258 | return values; 259 | } 260 | 261 | function parseMultipartFormDataString(string, boundary) { 262 | var values = {}; 263 | 264 | string.split('--' + boundary).forEach((data) => { 265 | data = data.replace(/^\r\n/, '').replace(/\r\n$/, ''); 266 | 267 | if (!data || data === '--') { 268 | return; 269 | } 270 | 271 | var parts = data.split(CRLF + CRLF); 272 | 273 | var header = parts.shift(); 274 | var value = { 275 | headers: {}, 276 | metadata: {}, 277 | value: parts.join(CRLF + CRLF) 278 | }; 279 | 280 | var name; 281 | 282 | var headers = header.split(CRLF); 283 | headers.forEach((header) => { 284 | var headerParams = header.split(';'); 285 | var headerParts = headerParams.shift().split(': '); 286 | 287 | var headerName = headerParts[0]; 288 | var headerValue = headerParts[1]; 289 | 290 | if (headerName !== 'Content-Disposition' || 291 | headerValue !== 'form-data') { 292 | value.headers[headerName] = headerValue; 293 | return; 294 | } 295 | 296 | headerParams.forEach((param) => { 297 | var paramParts = param.trim().split('='); 298 | 299 | var paramName = paramParts[0]; 300 | var paramValue = paramParts[1]; 301 | 302 | paramValue = paramValue.replace(/\"(.*?)\"/, '$1') || paramValue; 303 | 304 | if (paramName === 'name') { 305 | name = paramValue; 306 | } 307 | 308 | else { 309 | value.metadata[paramName] = paramValue; 310 | } 311 | }); 312 | }); 313 | 314 | if (name) { 315 | setOrAppendValue(values, name, value); 316 | } 317 | }); 318 | 319 | return values; 320 | } 321 | 322 | function parseBody(contentType, data) { 323 | contentType = contentType || 'text/plain'; 324 | 325 | var contentTypeParams = contentType.replace(/\s/g, '').split(';'); 326 | var mimeType = contentTypeParams.shift(); 327 | 328 | var body = BinaryUtils.arrayBufferToString(data); 329 | 330 | var result; 331 | 332 | try { 333 | switch (mimeType) { 334 | case 'application/x-www-form-urlencoded': 335 | result = parseURLEncodedString(body); 336 | break; 337 | case 'multipart/form-data': 338 | contentTypeParams.forEach((contentTypeParam) => { 339 | var parts = contentTypeParam.split('='); 340 | 341 | var name = parts[0]; 342 | var value = parts[1]; 343 | 344 | if (name === 'boundary') { 345 | result = parseMultipartFormDataString(body, value); 346 | } 347 | }); 348 | break; 349 | case 'application/json': 350 | result = JSON.parse(body); 351 | break; 352 | case 'application/xml': 353 | result = new DOMParser().parseFromString(body, 'text/xml'); 354 | break; 355 | default: 356 | break; 357 | } 358 | } catch (exception) { 359 | console.log('Unable to parse HTTP request body with Content-Type: ' + contentType); 360 | } 361 | 362 | return result || body; 363 | } 364 | 365 | return HTTPRequest; 366 | 367 | })(); 368 | 369 | },{"./binary-utils":1,"./event-target":2}],4:[function(require,module,exports){ 370 | /*jshint esnext:true*/ 371 | /*exported HTTPResponse*/ 372 | 'use strict'; 373 | 374 | module.exports = window.HTTPResponse = (function() { 375 | 376 | var EventTarget = require('./event-target'); 377 | var BinaryUtils = require('./binary-utils'); 378 | var HTTPStatus = require('./http-status'); 379 | 380 | const CRLF = '\r\n'; 381 | const BUFFER_SIZE = 64 * 1024; 382 | 383 | function HTTPResponse(socket, timeout) { 384 | this.socket = socket; 385 | this.timeout = timeout; 386 | 387 | this.headers = {}; 388 | this.headers['Content-Type'] = 'text/html'; 389 | this.headers['Connection'] = 'close'; 390 | 391 | if (this.timeout) { 392 | this.timeoutHandler = setTimeout(() => { 393 | this.send(null, 500); 394 | }, this.timeout); 395 | } 396 | } 397 | 398 | HTTPResponse.prototype = new EventTarget(); 399 | 400 | HTTPResponse.prototype.constructor = HTTPResponse; 401 | 402 | HTTPResponse.prototype.send = function(body, status) { 403 | return createResponse(body, status, this.headers, (response) => { 404 | var offset = 0; 405 | var remaining = response.byteLength; 406 | 407 | var sendNextPart = () => { 408 | var length = Math.min(remaining, BUFFER_SIZE); 409 | 410 | var bufferFull = this.socket.send(response, offset, length); 411 | 412 | offset += length; 413 | remaining -= length; 414 | 415 | if (remaining > 0) { 416 | if (!bufferFull) { 417 | sendNextPart(); 418 | } 419 | } 420 | 421 | else { 422 | clearTimeout(this.timeoutHandler); 423 | 424 | this.socket.close(); 425 | this.dispatchEvent('complete'); 426 | } 427 | }; 428 | 429 | this.socket.ondrain = sendNextPart; 430 | 431 | sendNextPart(); 432 | }); 433 | }; 434 | 435 | HTTPResponse.prototype.sendFile = function(fileOrPath, status) { 436 | if (fileOrPath instanceof File) { 437 | BinaryUtils.blobToArrayBuffer(fileOrPath, (arrayBuffer) => { 438 | this.send(arrayBuffer, status); 439 | }); 440 | 441 | return; 442 | } 443 | 444 | var xhr = new XMLHttpRequest(); 445 | xhr.open('GET', fileOrPath, true); 446 | xhr.responseType = 'arraybuffer'; 447 | xhr.onload = () => { 448 | this.send(xhr.response, status); 449 | }; 450 | 451 | xhr.send(null); 452 | }; 453 | 454 | function createResponseHeader(status, headers) { 455 | var header = HTTPStatus.getStatusLine(status); 456 | 457 | for (var name in headers) { 458 | header += name + ': ' + headers[name] + CRLF; 459 | } 460 | 461 | return header; 462 | } 463 | 464 | function createResponse(body, status, headers, callback) { 465 | body = body || ''; 466 | status = status || 200; 467 | headers = headers || {}; 468 | 469 | headers['Content-Length'] = body.length || body.byteLength; 470 | 471 | var response = new Blob([ 472 | createResponseHeader(status, headers), 473 | CRLF, 474 | body 475 | ]); 476 | 477 | return BinaryUtils.blobToArrayBuffer(response, callback); 478 | } 479 | 480 | return HTTPResponse; 481 | 482 | })(); 483 | 484 | },{"./binary-utils":1,"./event-target":2,"./http-status":6}],5:[function(require,module,exports){ 485 | /*jshint esnext:true*/ 486 | /*exported HTTPServer*/ 487 | 'use strict'; 488 | 489 | module.exports = window.HTTPServer = (function() { 490 | 491 | var EventTarget = require('./event-target'); 492 | var HTTPRequest = require('./http-request'); 493 | var HTTPResponse = require('./http-response'); 494 | var IPUtils = require('./ip-utils'); 495 | 496 | const DEFAULT_PORT = 8080; 497 | const DEFAULT_TIMEOUT = 20000; 498 | 499 | const CRLF = '\r\n'; 500 | 501 | function HTTPServer(port, options) { 502 | this.port = port || DEFAULT_PORT; 503 | 504 | options = options || {}; 505 | for (var option in options) { 506 | this[option] = options[option]; 507 | } 508 | 509 | this.running = false; 510 | } 511 | 512 | HTTPServer.HTTP_VERSION = 'HTTP/1.1'; 513 | 514 | HTTPServer.prototype = new EventTarget(); 515 | 516 | HTTPServer.prototype.constructor = HTTPServer; 517 | 518 | HTTPServer.prototype.timeout = DEFAULT_TIMEOUT; 519 | 520 | HTTPServer.prototype.start = function() { 521 | if (this.running) { 522 | return; 523 | } 524 | 525 | console.log('Starting HTTP server on port ' + this.port); 526 | 527 | var socket = navigator.mozTCPSocket.listen(this.port, { 528 | binaryType: 'arraybuffer' 529 | }); 530 | 531 | socket.onconnect = (connectEvent) => { 532 | var socket = connectEvent.socket || connectEvent; 533 | var request = new HTTPRequest(socket); 534 | 535 | request.addEventListener('complete', () => { 536 | var response = new HTTPResponse(socket, this.timeout); 537 | 538 | this.dispatchEvent('request', { 539 | request: request, 540 | response: response, 541 | socket: socket 542 | }); 543 | }); 544 | 545 | request.addEventListener('error', () => { 546 | console.warn('Invalid request received'); 547 | }); 548 | }; 549 | 550 | this.socket = socket; 551 | this.running = true; 552 | }; 553 | 554 | HTTPServer.prototype.stop = function() { 555 | if (!this.running) { 556 | return; 557 | } 558 | 559 | console.log('Shutting down HTTP server on port ' + this.port); 560 | 561 | this.socket.close(); 562 | 563 | this.running = false; 564 | }; 565 | 566 | return HTTPServer; 567 | 568 | })(); 569 | 570 | },{"./event-target":2,"./http-request":3,"./http-response":4,"./ip-utils":7}],6:[function(require,module,exports){ 571 | /*jshint esnext:true*/ 572 | /*exported HTTPStatus*/ 573 | 'use strict'; 574 | 575 | module.exports = window.HTTPStatus = (function() { 576 | 577 | const CRLF = '\r\n'; 578 | 579 | var HTTPStatus = {}; 580 | 581 | HTTPStatus.STATUS_CODES = { 582 | 100: 'Continue', 583 | 101: 'Switching Protocols', 584 | 102: 'Processing', 585 | 200: 'OK', 586 | 201: 'Created', 587 | 202: 'Accepted', 588 | 203: 'Non Authoritative Information', 589 | 204: 'No Content', 590 | 205: 'Reset Content', 591 | 206: 'Partial Content', 592 | 207: 'Multi-Status', 593 | 300: 'Mutliple Choices', 594 | 301: 'Moved Permanently', 595 | 302: 'Moved Temporarily', 596 | 303: 'See Other', 597 | 304: 'Not Modified', 598 | 305: 'Use Proxy', 599 | 307: 'Temporary Redirect', 600 | 400: 'Bad Request', 601 | 401: 'Unauthorized', 602 | 402: 'Payment Required', 603 | 403: 'Forbidden', 604 | 404: 'Not Found', 605 | 405: 'Method Not Allowed', 606 | 406: 'Not Acceptable', 607 | 407: 'Proxy Authentication Required', 608 | 408: 'Request Timeout', 609 | 409: 'Conflict', 610 | 410: 'Gone', 611 | 411: 'Length Required', 612 | 412: 'Precondition Failed', 613 | 413: 'Request Entity Too Large', 614 | 414: 'Request-URI Too Long', 615 | 415: 'Unsupported Media Type', 616 | 416: 'Requested Range Not Satisfiable', 617 | 417: 'Expectation Failed', 618 | 419: 'Insufficient Space on Resource', 619 | 420: 'Method Failure', 620 | 422: 'Unprocessable Entity', 621 | 423: 'Locked', 622 | 424: 'Failed Dependency', 623 | 500: 'Server Error', 624 | 501: 'Not Implemented', 625 | 502: 'Bad Gateway', 626 | 503: 'Service Unavailable', 627 | 504: 'Gateway Timeout', 628 | 505: 'HTTP Version Not Supported', 629 | 507: 'Insufficient Storage' 630 | }; 631 | 632 | HTTPStatus.getStatusLine = function(status) { 633 | var reason = HTTPStatus.STATUS_CODES[status] || 'Unknown'; 634 | 635 | return [HTTPServer.HTTP_VERSION, status, reason].join(' ') + CRLF; 636 | }; 637 | 638 | return HTTPStatus; 639 | 640 | })(); 641 | 642 | },{}],7:[function(require,module,exports){ 643 | /*jshint esnext:true*/ 644 | /*exported IPUtils*/ 645 | 'use strict'; 646 | 647 | module.exports = window.IPUtils = (function() { 648 | 649 | const CRLF = '\r\n'; 650 | 651 | var IPUtils = { 652 | getAddresses: function(callback) { 653 | if (typeof callback !== 'function') { 654 | console.warn('No callback provided'); 655 | return; 656 | } 657 | 658 | var addresses = { 659 | '0.0.0.0': true 660 | }; 661 | 662 | var RTCPeerConnection = window.RTCPeerConnection || 663 | window.mozRTCPeerConnection; 664 | 665 | var rtc = new RTCPeerConnection({ iceServers: [] }); 666 | rtc.createDataChannel('', { reliable: false }); 667 | 668 | rtc.onicecandidate = function(evt) { 669 | if (evt.candidate) { 670 | parseSDP('a=' + evt.candidate.candidate); 671 | } 672 | }; 673 | 674 | rtc.createOffer((description) => { 675 | parseSDP(description.sdp); 676 | rtc.setLocalDescription(description, noop, noop); 677 | }, (error) => { 678 | console.warn('Unable to create offer', error); 679 | }); 680 | 681 | function addAddress(address) { 682 | if (addresses[address]) { 683 | return; 684 | } 685 | 686 | addresses[address] = true; 687 | callback(address); 688 | } 689 | 690 | function parseSDP(sdp) { 691 | sdp.split(CRLF).forEach((line) => { 692 | var parts = line.split(' '); 693 | 694 | if (line.indexOf('a=candidate') !== -1) { 695 | if (parts[7] === 'host') { 696 | addAddress(parts[4]); 697 | } 698 | } 699 | 700 | else if (line.indexOf('c=') !== -1) { 701 | addAddress(parts[2]); 702 | } 703 | }); 704 | } 705 | } 706 | }; 707 | 708 | function noop() {} 709 | 710 | return IPUtils; 711 | 712 | })(); 713 | 714 | },{}]},{},[5])(5) 715 | }); -------------------------------------------------------------------------------- /example/directory/css/app.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 0; 3 | padding: 0.5rem; 4 | } 5 | 6 | body { 7 | font-size: 1rem; 8 | background: #fff; 9 | } 10 | 11 | h1 { 12 | margin: 0 0 1rem 0; 13 | } 14 | 15 | p { 16 | font-size: inherit; 17 | } 18 | 19 | button { 20 | font-size: 1rem; 21 | padding: 0.5rem 1rem; 22 | } -------------------------------------------------------------------------------- /example/directory/img/icons/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 19 | 21 | 22 | 24 | image/svg+xml 25 | 27 | 28 | 29 | 30 | 50 | 51 | empty 53 | Created with Sketch. 55 | 57 | 59 | 63 | 67 | 68 | 77 | 78 | 88 | 93 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /example/directory/img/icons/icon128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justindarc/fxos-web-server/d2d3d40acc88c04d48743137f2d279fc684dd45d/example/directory/img/icons/icon128x128.png -------------------------------------------------------------------------------- /example/directory/img/icons/icon16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justindarc/fxos-web-server/d2d3d40acc88c04d48743137f2d279fc684dd45d/example/directory/img/icons/icon16x16.png -------------------------------------------------------------------------------- /example/directory/img/icons/icon48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justindarc/fxos-web-server/d2d3d40acc88c04d48743137f2d279fc684dd45d/example/directory/img/icons/icon48x48.png -------------------------------------------------------------------------------- /example/directory/img/icons/icon60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justindarc/fxos-web-server/d2d3d40acc88c04d48743137f2d279fc684dd45d/example/directory/img/icons/icon60x60.png -------------------------------------------------------------------------------- /example/directory/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Directory Web Server 6 | 7 | 8 | 9 | 10 | 11 |
12 |

Directory Web Server

13 |

Status: Stopped

14 |

IP Address:

15 |

Port:

16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /example/directory/js/app.js: -------------------------------------------------------------------------------- 1 | var httpServer = new HTTPServer(8080); 2 | var storage = new Storage('sdcard'); 3 | 4 | httpServer.addEventListener('request', function(evt) { 5 | var request = evt.request; 6 | var response = evt.response; 7 | 8 | if (request.path.substr(-1) === '/') { 9 | request.path = request.path.substring(0, request.path.length - 1); 10 | } 11 | 12 | console.log(request); 13 | 14 | var path = decodeURIComponent(request.path) || '/'; 15 | 16 | storage.list(path, function(directory) { 17 | if (directory instanceof File) { 18 | response.headers['Content-Type'] = directory.type; 19 | response.sendFile(directory); 20 | return; 21 | } 22 | 23 | var baseHref = request.path; 24 | if (baseHref !== '/') { 25 | baseHref += '/'; 26 | } 27 | 28 | var rows = []; 29 | for (var name in directory) { 30 | rows.push('' + 31 | '' + 32 | '' + 33 | name + 34 | '' + 35 | '' + 36 | '' + 37 | (directory[name] instanceof File ? 'File' : 'Folder') + 38 | '' + 39 | '' + 40 | (directory[name] instanceof File ? directory[name].size : '--') + 41 | '' + 42 | '' + 43 | (directory[name] instanceof File ? directory[name].lastModifiedDate.toLocaleFormat() : '--') + 44 | '' + 45 | ''); 46 | } 47 | 48 | rows = rows.join(''); 49 | 50 | var body = 51 | ` 52 | 53 | 54 | Firefox OS Web Server 55 | 59 | 60 | 61 |

Index of ${path}

62 |

63 | Up to parent level 64 |

65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | ${rows} 75 |
NameTypeSizeLast Modified
76 | 77 | `; 78 | 79 | response.send(body); 80 | }); 81 | }); 82 | 83 | window.addEventListener('load', function() { 84 | var status = document.getElementById('status'); 85 | var ip = document.getElementById('ip'); 86 | var port = document.getElementById('port'); 87 | var start = document.getElementById('start'); 88 | var stop = document.getElementById('stop'); 89 | 90 | IPUtils.getAddresses(function(ipAddress) { 91 | ip.textContent = ip.textContent || ipAddress; 92 | }); 93 | 94 | port.textContent = httpServer.port; 95 | 96 | start.addEventListener('click', function() { 97 | httpServer.start(); 98 | status.textContent = 'Running'; 99 | }); 100 | 101 | stop.addEventListener('click', function() { 102 | httpServer.stop(); 103 | status.textContent = 'Stopped'; 104 | }); 105 | }); 106 | 107 | window.addEventListener('beforeunload', function() { 108 | httpServer.stop(); 109 | }); 110 | -------------------------------------------------------------------------------- /example/directory/js/storage.js: -------------------------------------------------------------------------------- 1 | window.Storage = (function() { 2 | 3 | function Storage(name) { 4 | this.ds = navigator.getDeviceStorage(name); 5 | } 6 | 7 | Storage.prototype.constructor = Storage; 8 | 9 | Storage.prototype.list = function(path, callback) { 10 | if (typeof callback !== 'function') { 11 | return; 12 | } 13 | 14 | var root = '/' + this.ds.storageName + '/'; 15 | var tree = {}; 16 | 17 | var cursor = this.ds.enumerate(); 18 | cursor.onsuccess = function() { 19 | var file = this.result; 20 | if (!file) { 21 | callback(resolvePathForTree(path, tree)); 22 | return; 23 | } 24 | 25 | var parent = tree; 26 | 27 | var keys = file.name.substring(root.length).split('/'); 28 | var length = keys.length; 29 | for (var i = 0, key; i < length; i++) { 30 | key = keys[i]; 31 | parent = parent[key] = (i === length - 1) ? file : (parent[key] || {}); 32 | } 33 | 34 | this.continue(); 35 | }; 36 | }; 37 | 38 | function resolvePathForTree(path, tree) { 39 | if (path.indexOf('/') === 0) { 40 | path = path.substring(1); 41 | } 42 | 43 | if (!path) { 44 | return tree; 45 | } 46 | 47 | var keys = path.split('/'); 48 | var length = keys.length; 49 | for (var i = 0, key; i < length; i++) { 50 | key = keys[i]; 51 | tree = tree[key]; 52 | 53 | if (!tree) { 54 | return null; 55 | } 56 | } 57 | 58 | return tree; 59 | } 60 | 61 | return Storage; 62 | 63 | })(); 64 | -------------------------------------------------------------------------------- /example/directory/lib/fxos-web-server.js: -------------------------------------------------------------------------------- 1 | !function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.HTTPServer=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o { 75 | listener.call(this, data); 76 | }); 77 | }; 78 | 79 | EventTarget.prototype.addEventListener = function(name, listener) { 80 | var events = this._events = this._events || {}; 81 | var listeners = events[name] = events[name] || []; 82 | if (listeners.find(fn => fn === listener)) { 83 | return; 84 | } 85 | 86 | listeners.push(listener); 87 | }; 88 | 89 | EventTarget.prototype.removeEventListener = function(name, listener) { 90 | var events = this._events || {}; 91 | var listeners = events[name] || []; 92 | for (var i = listeners.length - 1; i >= 0; i--) { 93 | if (listeners[i] === listener) { 94 | listeners.splice(i, 1); 95 | return; 96 | } 97 | } 98 | }; 99 | 100 | return EventTarget; 101 | 102 | })(); 103 | 104 | },{}],3:[function(require,module,exports){ 105 | /*jshint esnext:true*/ 106 | /*exported HTTPRequest*/ 107 | 'use strict'; 108 | 109 | module.exports = window.HTTPRequest = (function() { 110 | 111 | var EventTarget = require('./event-target'); 112 | var BinaryUtils = require('./binary-utils'); 113 | 114 | const CRLF = '\r\n'; 115 | 116 | function HTTPRequest(socket) { 117 | var parts = []; 118 | var receivedLength = 0; 119 | 120 | var checkRequestComplete = () => { 121 | var contentLength = parseInt(this.headers['Content-Length'], 10); 122 | if (isNaN(contentLength)) { 123 | this.complete = true; 124 | this.dispatchEvent('complete', this); 125 | return; 126 | } 127 | 128 | if (receivedLength < contentLength) { 129 | return; 130 | } 131 | 132 | BinaryUtils.mergeArrayBuffers(parts, (data) => { 133 | this.body = parseBody(this.headers['Content-Type'], data); 134 | this.complete = true; 135 | this.dispatchEvent('complete', this); 136 | }); 137 | 138 | socket.ondata = null; 139 | }; 140 | 141 | socket.ondata = (event) => { 142 | var data = event.data; 143 | 144 | if (parts.length > 0) { 145 | parts.push(data); 146 | receivedLength += data.byteLength; 147 | checkRequestComplete(); 148 | return; 149 | } 150 | 151 | var firstPart = parseHeader(this, data); 152 | if (this.invalid) { 153 | this.dispatchEvent('error', this); 154 | 155 | socket.close(); 156 | socket.ondata = null; 157 | return; 158 | } 159 | 160 | if (firstPart) { 161 | parts.push(firstPart); 162 | receivedLength += firstPart.byteLength; 163 | } 164 | 165 | checkRequestComplete(); 166 | }; 167 | } 168 | 169 | HTTPRequest.prototype = new EventTarget(); 170 | 171 | HTTPRequest.prototype.constructor = HTTPRequest; 172 | 173 | function parseHeader(request, data) { 174 | if (!data) { 175 | request.invalid = true; 176 | return null; 177 | } 178 | 179 | data = BinaryUtils.arrayBufferToString(data); 180 | 181 | var requestParts = data.split(CRLF + CRLF); 182 | 183 | var header = requestParts.shift(); 184 | var body = requestParts.join(CRLF + CRLF); 185 | 186 | var headerLines = header.split(CRLF); 187 | var requestLine = headerLines.shift().split(' '); 188 | 189 | var method = requestLine[0]; 190 | var uri = requestLine[1]; 191 | var version = requestLine[2]; 192 | 193 | if (version !== HTTPServer.HTTP_VERSION) { 194 | request.invalid = true; 195 | return null; 196 | } 197 | 198 | var uriParts = uri.split('?'); 199 | 200 | var path = uriParts.shift(); 201 | var params = parseURLEncodedString(uriParts.join('?')); 202 | 203 | var headers = {}; 204 | headerLines.forEach((headerLine) => { 205 | var parts = headerLine.split(': '); 206 | if (parts.length !== 2) { 207 | return; 208 | } 209 | 210 | var name = parts[0]; 211 | var value = parts[1]; 212 | 213 | headers[name] = value; 214 | }); 215 | 216 | request.method = method; 217 | request.path = path; 218 | request.params = params; 219 | request.headers = headers; 220 | 221 | if (headers['Content-Length']) { 222 | // request.body = parseBody(headers['Content-Type'], body); 223 | return BinaryUtils.stringToArrayBuffer(body); 224 | } 225 | 226 | return null; 227 | } 228 | 229 | function setOrAppendValue(object, name, value) { 230 | var existingValue = object[name]; 231 | if (existingValue === undefined) { 232 | object[name] = value; 233 | } else { 234 | if (Array.isArray(existingValue)) { 235 | existingValue.push(value); 236 | } else { 237 | object[name] = [existingValue, value]; 238 | } 239 | } 240 | } 241 | 242 | function parseURLEncodedString(string) { 243 | var values = {}; 244 | 245 | string.split('&').forEach((pair) => { 246 | if (!pair) { 247 | return; 248 | } 249 | 250 | var parts = decodeURIComponent(pair).split('='); 251 | 252 | var name = parts.shift(); 253 | var value = parts.join('='); 254 | 255 | setOrAppendValue(values, name, value); 256 | }); 257 | 258 | return values; 259 | } 260 | 261 | function parseMultipartFormDataString(string, boundary) { 262 | var values = {}; 263 | 264 | string.split('--' + boundary).forEach((data) => { 265 | data = data.replace(/^\r\n/, '').replace(/\r\n$/, ''); 266 | 267 | if (!data || data === '--') { 268 | return; 269 | } 270 | 271 | var parts = data.split(CRLF + CRLF); 272 | 273 | var header = parts.shift(); 274 | var value = { 275 | headers: {}, 276 | metadata: {}, 277 | value: parts.join(CRLF + CRLF) 278 | }; 279 | 280 | var name; 281 | 282 | var headers = header.split(CRLF); 283 | headers.forEach((header) => { 284 | var headerParams = header.split(';'); 285 | var headerParts = headerParams.shift().split(': '); 286 | 287 | var headerName = headerParts[0]; 288 | var headerValue = headerParts[1]; 289 | 290 | if (headerName !== 'Content-Disposition' || 291 | headerValue !== 'form-data') { 292 | value.headers[headerName] = headerValue; 293 | return; 294 | } 295 | 296 | headerParams.forEach((param) => { 297 | var paramParts = param.trim().split('='); 298 | 299 | var paramName = paramParts[0]; 300 | var paramValue = paramParts[1]; 301 | 302 | paramValue = paramValue.replace(/\"(.*?)\"/, '$1') || paramValue; 303 | 304 | if (paramName === 'name') { 305 | name = paramValue; 306 | } 307 | 308 | else { 309 | value.metadata[paramName] = paramValue; 310 | } 311 | }); 312 | }); 313 | 314 | if (name) { 315 | setOrAppendValue(values, name, value); 316 | } 317 | }); 318 | 319 | return values; 320 | } 321 | 322 | function parseBody(contentType, data) { 323 | contentType = contentType || 'text/plain'; 324 | 325 | var contentTypeParams = contentType.replace(/\s/g, '').split(';'); 326 | var mimeType = contentTypeParams.shift(); 327 | 328 | var body = BinaryUtils.arrayBufferToString(data); 329 | 330 | var result; 331 | 332 | try { 333 | switch (mimeType) { 334 | case 'application/x-www-form-urlencoded': 335 | result = parseURLEncodedString(body); 336 | break; 337 | case 'multipart/form-data': 338 | contentTypeParams.forEach((contentTypeParam) => { 339 | var parts = contentTypeParam.split('='); 340 | 341 | var name = parts[0]; 342 | var value = parts[1]; 343 | 344 | if (name === 'boundary') { 345 | result = parseMultipartFormDataString(body, value); 346 | } 347 | }); 348 | break; 349 | case 'application/json': 350 | result = JSON.parse(body); 351 | break; 352 | case 'application/xml': 353 | result = new DOMParser().parseFromString(body, 'text/xml'); 354 | break; 355 | default: 356 | break; 357 | } 358 | } catch (exception) { 359 | console.log('Unable to parse HTTP request body with Content-Type: ' + contentType); 360 | } 361 | 362 | return result || body; 363 | } 364 | 365 | return HTTPRequest; 366 | 367 | })(); 368 | 369 | },{"./binary-utils":1,"./event-target":2}],4:[function(require,module,exports){ 370 | /*jshint esnext:true*/ 371 | /*exported HTTPResponse*/ 372 | 'use strict'; 373 | 374 | module.exports = window.HTTPResponse = (function() { 375 | 376 | var EventTarget = require('./event-target'); 377 | var BinaryUtils = require('./binary-utils'); 378 | var HTTPStatus = require('./http-status'); 379 | 380 | const CRLF = '\r\n'; 381 | const BUFFER_SIZE = 64 * 1024; 382 | 383 | function HTTPResponse(socket, timeout) { 384 | this.socket = socket; 385 | this.timeout = timeout; 386 | 387 | this.headers = {}; 388 | this.headers['Content-Type'] = 'text/html'; 389 | this.headers['Connection'] = 'close'; 390 | 391 | if (this.timeout) { 392 | this.timeoutHandler = setTimeout(() => { 393 | this.send(null, 500); 394 | }, this.timeout); 395 | } 396 | } 397 | 398 | HTTPResponse.prototype = new EventTarget(); 399 | 400 | HTTPResponse.prototype.constructor = HTTPResponse; 401 | 402 | HTTPResponse.prototype.send = function(body, status) { 403 | return createResponse(body, status, this.headers, (response) => { 404 | var offset = 0; 405 | var remaining = response.byteLength; 406 | 407 | var sendNextPart = () => { 408 | var length = Math.min(remaining, BUFFER_SIZE); 409 | 410 | var bufferFull = this.socket.send(response, offset, length); 411 | 412 | offset += length; 413 | remaining -= length; 414 | 415 | if (remaining > 0) { 416 | if (!bufferFull) { 417 | sendNextPart(); 418 | } 419 | } 420 | 421 | else { 422 | clearTimeout(this.timeoutHandler); 423 | 424 | this.socket.close(); 425 | this.dispatchEvent('complete'); 426 | } 427 | }; 428 | 429 | this.socket.ondrain = sendNextPart; 430 | 431 | sendNextPart(); 432 | }); 433 | }; 434 | 435 | HTTPResponse.prototype.sendFile = function(fileOrPath, status) { 436 | if (fileOrPath instanceof File) { 437 | BinaryUtils.blobToArrayBuffer(fileOrPath, (arrayBuffer) => { 438 | this.send(arrayBuffer, status); 439 | }); 440 | 441 | return; 442 | } 443 | 444 | var xhr = new XMLHttpRequest(); 445 | xhr.open('GET', fileOrPath, true); 446 | xhr.responseType = 'arraybuffer'; 447 | xhr.onload = () => { 448 | this.send(xhr.response, status); 449 | }; 450 | 451 | xhr.send(null); 452 | }; 453 | 454 | function createResponseHeader(status, headers) { 455 | var header = HTTPStatus.getStatusLine(status); 456 | 457 | for (var name in headers) { 458 | header += name + ': ' + headers[name] + CRLF; 459 | } 460 | 461 | return header; 462 | } 463 | 464 | function createResponse(body, status, headers, callback) { 465 | body = body || ''; 466 | status = status || 200; 467 | headers = headers || {}; 468 | 469 | headers['Content-Length'] = body.length || body.byteLength; 470 | 471 | var response = new Blob([ 472 | createResponseHeader(status, headers), 473 | CRLF, 474 | body 475 | ]); 476 | 477 | return BinaryUtils.blobToArrayBuffer(response, callback); 478 | } 479 | 480 | return HTTPResponse; 481 | 482 | })(); 483 | 484 | },{"./binary-utils":1,"./event-target":2,"./http-status":6}],5:[function(require,module,exports){ 485 | /*jshint esnext:true*/ 486 | /*exported HTTPServer*/ 487 | 'use strict'; 488 | 489 | module.exports = window.HTTPServer = (function() { 490 | 491 | var EventTarget = require('./event-target'); 492 | var HTTPRequest = require('./http-request'); 493 | var HTTPResponse = require('./http-response'); 494 | var IPUtils = require('./ip-utils'); 495 | 496 | const DEFAULT_PORT = 8080; 497 | const DEFAULT_TIMEOUT = 20000; 498 | 499 | const CRLF = '\r\n'; 500 | 501 | function HTTPServer(port, options) { 502 | this.port = port || DEFAULT_PORT; 503 | 504 | options = options || {}; 505 | for (var option in options) { 506 | this[option] = options[option]; 507 | } 508 | 509 | this.running = false; 510 | } 511 | 512 | HTTPServer.HTTP_VERSION = 'HTTP/1.1'; 513 | 514 | HTTPServer.prototype = new EventTarget(); 515 | 516 | HTTPServer.prototype.constructor = HTTPServer; 517 | 518 | HTTPServer.prototype.timeout = DEFAULT_TIMEOUT; 519 | 520 | HTTPServer.prototype.start = function() { 521 | if (this.running) { 522 | return; 523 | } 524 | 525 | console.log('Starting HTTP server on port ' + this.port); 526 | 527 | var socket = navigator.mozTCPSocket.listen(this.port, { 528 | binaryType: 'arraybuffer' 529 | }); 530 | 531 | socket.onconnect = (connectEvent) => { 532 | var socket = connectEvent.socket || connectEvent; 533 | var request = new HTTPRequest(socket); 534 | 535 | request.addEventListener('complete', () => { 536 | var response = new HTTPResponse(socket, this.timeout); 537 | 538 | this.dispatchEvent('request', { 539 | request: request, 540 | response: response, 541 | socket: socket 542 | }); 543 | }); 544 | 545 | request.addEventListener('error', () => { 546 | console.warn('Invalid request received'); 547 | }); 548 | }; 549 | 550 | this.socket = socket; 551 | this.running = true; 552 | }; 553 | 554 | HTTPServer.prototype.stop = function() { 555 | if (!this.running) { 556 | return; 557 | } 558 | 559 | console.log('Shutting down HTTP server on port ' + this.port); 560 | 561 | this.socket.close(); 562 | 563 | this.running = false; 564 | }; 565 | 566 | return HTTPServer; 567 | 568 | })(); 569 | 570 | },{"./event-target":2,"./http-request":3,"./http-response":4,"./ip-utils":7}],6:[function(require,module,exports){ 571 | /*jshint esnext:true*/ 572 | /*exported HTTPStatus*/ 573 | 'use strict'; 574 | 575 | module.exports = window.HTTPStatus = (function() { 576 | 577 | const CRLF = '\r\n'; 578 | 579 | var HTTPStatus = {}; 580 | 581 | HTTPStatus.STATUS_CODES = { 582 | 100: 'Continue', 583 | 101: 'Switching Protocols', 584 | 102: 'Processing', 585 | 200: 'OK', 586 | 201: 'Created', 587 | 202: 'Accepted', 588 | 203: 'Non Authoritative Information', 589 | 204: 'No Content', 590 | 205: 'Reset Content', 591 | 206: 'Partial Content', 592 | 207: 'Multi-Status', 593 | 300: 'Mutliple Choices', 594 | 301: 'Moved Permanently', 595 | 302: 'Moved Temporarily', 596 | 303: 'See Other', 597 | 304: 'Not Modified', 598 | 305: 'Use Proxy', 599 | 307: 'Temporary Redirect', 600 | 400: 'Bad Request', 601 | 401: 'Unauthorized', 602 | 402: 'Payment Required', 603 | 403: 'Forbidden', 604 | 404: 'Not Found', 605 | 405: 'Method Not Allowed', 606 | 406: 'Not Acceptable', 607 | 407: 'Proxy Authentication Required', 608 | 408: 'Request Timeout', 609 | 409: 'Conflict', 610 | 410: 'Gone', 611 | 411: 'Length Required', 612 | 412: 'Precondition Failed', 613 | 413: 'Request Entity Too Large', 614 | 414: 'Request-URI Too Long', 615 | 415: 'Unsupported Media Type', 616 | 416: 'Requested Range Not Satisfiable', 617 | 417: 'Expectation Failed', 618 | 419: 'Insufficient Space on Resource', 619 | 420: 'Method Failure', 620 | 422: 'Unprocessable Entity', 621 | 423: 'Locked', 622 | 424: 'Failed Dependency', 623 | 500: 'Server Error', 624 | 501: 'Not Implemented', 625 | 502: 'Bad Gateway', 626 | 503: 'Service Unavailable', 627 | 504: 'Gateway Timeout', 628 | 505: 'HTTP Version Not Supported', 629 | 507: 'Insufficient Storage' 630 | }; 631 | 632 | HTTPStatus.getStatusLine = function(status) { 633 | var reason = HTTPStatus.STATUS_CODES[status] || 'Unknown'; 634 | 635 | return [HTTPServer.HTTP_VERSION, status, reason].join(' ') + CRLF; 636 | }; 637 | 638 | return HTTPStatus; 639 | 640 | })(); 641 | 642 | },{}],7:[function(require,module,exports){ 643 | /*jshint esnext:true*/ 644 | /*exported IPUtils*/ 645 | 'use strict'; 646 | 647 | module.exports = window.IPUtils = (function() { 648 | 649 | const CRLF = '\r\n'; 650 | 651 | var IPUtils = { 652 | getAddresses: function(callback) { 653 | if (typeof callback !== 'function') { 654 | console.warn('No callback provided'); 655 | return; 656 | } 657 | 658 | var addresses = { 659 | '0.0.0.0': true 660 | }; 661 | 662 | var RTCPeerConnection = window.RTCPeerConnection || 663 | window.mozRTCPeerConnection; 664 | 665 | var rtc = new RTCPeerConnection({ iceServers: [] }); 666 | rtc.createDataChannel('', { reliable: false }); 667 | 668 | rtc.onicecandidate = function(evt) { 669 | if (evt.candidate) { 670 | parseSDP('a=' + evt.candidate.candidate); 671 | } 672 | }; 673 | 674 | rtc.createOffer((description) => { 675 | parseSDP(description.sdp); 676 | rtc.setLocalDescription(description, noop, noop); 677 | }, (error) => { 678 | console.warn('Unable to create offer', error); 679 | }); 680 | 681 | function addAddress(address) { 682 | if (addresses[address]) { 683 | return; 684 | } 685 | 686 | addresses[address] = true; 687 | callback(address); 688 | } 689 | 690 | function parseSDP(sdp) { 691 | sdp.split(CRLF).forEach((line) => { 692 | var parts = line.split(' '); 693 | 694 | if (line.indexOf('a=candidate') !== -1) { 695 | if (parts[7] === 'host') { 696 | addAddress(parts[4]); 697 | } 698 | } 699 | 700 | else if (line.indexOf('c=') !== -1) { 701 | addAddress(parts[2]); 702 | } 703 | }); 704 | } 705 | } 706 | }; 707 | 708 | function noop() {} 709 | 710 | return IPUtils; 711 | 712 | })(); 713 | 714 | },{}]},{},[5])(5) 715 | }); -------------------------------------------------------------------------------- /example/directory/manifest.webapp: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.1", 3 | "name": "Directory Web Server", 4 | "description": "A directory HTTP server for Firefox OS", 5 | "launch_path": "/index.html", 6 | "icons": { 7 | "16": "/img/icons/icon16x16.png", 8 | "48": "/img/icons/icon48x48.png", 9 | "60": "/img/icons/icon60x60.png", 10 | "128": "/img/icons/icon128x128.png" 11 | }, 12 | "developer": { 13 | "name": "Justin D'Arcangelo", 14 | "url": "https://github.com/justindarc" 15 | }, 16 | "type": "privileged", 17 | "permissions": { 18 | "tcp-socket": {}, 19 | "device-storage:sdcard": { "access": "readonly" } 20 | }, 21 | "installs_allowed_from": [ 22 | "*" 23 | ], 24 | "locales": {}, 25 | "default_locale": "en" 26 | } 27 | -------------------------------------------------------------------------------- /example/p2p/css/app.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 0; 3 | padding: 0.5rem; 4 | } 5 | 6 | body { 7 | font-size: 1rem; 8 | background: #fff; 9 | } 10 | 11 | h1 { 12 | margin: 0 0 1rem 0; 13 | } 14 | 15 | p { 16 | font-size: inherit; 17 | } 18 | 19 | button { 20 | font-size: 1rem; 21 | padding: 0.5rem 1rem; 22 | } 23 | 24 | ul { 25 | list-style: none; 26 | padding: 0; 27 | } 28 | 29 | li > a { 30 | background-color: #eee; 31 | color: #000; 32 | text-decoration: none; 33 | display: block; 34 | margin: 0.1rem 0; 35 | padding: 1rem 1.5rem 1rem 1rem; 36 | } 37 | 38 | li > a:after { 39 | content: '>'; 40 | font-size: 2rem; 41 | display: inline-block; 42 | float: right; 43 | margin: -0.5rem -1rem 0 0; 44 | } 45 | 46 | header { 47 | position: absolute; 48 | top: 0; 49 | left: 0; 50 | width: 100%; 51 | height: 3rem; 52 | } 53 | 54 | header > a { 55 | background-color: #eee; 56 | color: #000; 57 | font-size: 0.8rem; 58 | text-decoration: none; 59 | display: inline-block; 60 | padding: 1rem; 61 | } 62 | 63 | iframe { 64 | border: none; 65 | position: absolute; 66 | top: 3rem; 67 | left: 0; 68 | width: 100%; 69 | height: calc(100% - 3rem); 70 | } 71 | 72 | #remote { 73 | display: none; 74 | } 75 | -------------------------------------------------------------------------------- /example/p2p/img/icons/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 19 | 21 | 22 | 24 | image/svg+xml 25 | 27 | 28 | 29 | 30 | 50 | 51 | empty 53 | Created with Sketch. 55 | 57 | 59 | 63 | 67 | 68 | 77 | 78 | 88 | 93 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /example/p2p/img/icons/icon128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justindarc/fxos-web-server/d2d3d40acc88c04d48743137f2d279fc684dd45d/example/p2p/img/icons/icon128x128.png -------------------------------------------------------------------------------- /example/p2p/img/icons/icon16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justindarc/fxos-web-server/d2d3d40acc88c04d48743137f2d279fc684dd45d/example/p2p/img/icons/icon16x16.png -------------------------------------------------------------------------------- /example/p2p/img/icons/icon48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justindarc/fxos-web-server/d2d3d40acc88c04d48743137f2d279fc684dd45d/example/p2p/img/icons/icon48x48.png -------------------------------------------------------------------------------- /example/p2p/img/icons/icon60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justindarc/fxos-web-server/d2d3d40acc88c04d48743137f2d279fc684dd45d/example/p2p/img/icons/icon60x60.png -------------------------------------------------------------------------------- /example/p2p/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | P2P Web Server 6 | 7 | 8 | 9 | 10 | 11 |
12 |

P2P Web Server

13 |

Status: Stopped

14 | 15 | 16 |

Message

17 | 18 |

Peers

19 |
    20 |
21 |
22 | 23 |
24 |
25 | Back 26 |
27 | 28 |
29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /example/p2p/js/app.js: -------------------------------------------------------------------------------- 1 | var httpServer = new HTTPServer(8080); 2 | 3 | httpServer.addEventListener('request', function(evt) { 4 | var request = evt.request; 5 | var response = evt.response; 6 | 7 | var message = document.getElementById('message').value; 8 | 9 | var body = 10 | ` 11 | 12 | 13 | Firefox OS Web Server 14 | 15 | 16 |

Hello World!

17 |

If you can read this, the Firefox OS Web Server is operational!

18 |

The path you requested is: ${request.path}

19 |

Message

20 |
${message}
21 | 22 | `; 23 | 24 | response.send(body); 25 | }); 26 | 27 | window.addEventListener('load', function() { 28 | if (!window.P2PHelper) { 29 | alert('WiFi Direct is not available on this device'); 30 | window.close(); 31 | return; 32 | } 33 | 34 | var peers = document.getElementById('peers'); 35 | 36 | P2PHelper.addEventListener('peerlistchange', function(evt) { 37 | peers.innerHTML = ''; 38 | 39 | evt.peerList.forEach(function(peer) { 40 | var li = document.createElement('li'); 41 | li.dataset.address = peer.address; 42 | li.dataset.status = peer.connectionStatus; 43 | 44 | var a = document.createElement('a'); 45 | a.href = '#' + peer.address; 46 | a.textContent = peer.name; 47 | 48 | li.appendChild(a); 49 | peers.appendChild(li); 50 | }); 51 | }); 52 | 53 | var reloadInterval; 54 | 55 | P2PHelper.addEventListener('connected', function(evt) { 56 | frame.src = 'http://' + evt.groupOwner.ipAddress + ':8080'; 57 | 58 | reloadInterval = setInterval(function() { 59 | frame.reload(); 60 | }, 1000); 61 | }); 62 | 63 | P2PHelper.addEventListener('disconnected', function(evt) { 64 | frame.src = ''; 65 | 66 | clearInterval(reloadInterval); 67 | }); 68 | 69 | // Set the device name that will be shown to nearby peers. 70 | P2PHelper.setDisplayName('P2P Web Server ' + P2PHelper.localAddress); 71 | 72 | // Start scanning for nearby peers. 73 | P2PHelper.startScan(); 74 | 75 | var home = document.getElementById('home'); 76 | var remote = document.getElementById('remote'); 77 | var frame = document.getElementById('frame'); 78 | 79 | window.addEventListener('hashchange', function(evt) { 80 | var address = window.location.hash.substring(1); 81 | if (!address) { 82 | home.style.display = 'block'; 83 | remote.style.display = 'none'; 84 | 85 | P2PHelper.disconnect(); 86 | return; 87 | } 88 | 89 | home.style.display = 'none'; 90 | remote.style.display = 'block'; 91 | 92 | P2PHelper.connect(address); 93 | }); 94 | 95 | var status = document.getElementById('status'); 96 | var start = document.getElementById('start'); 97 | var stop = document.getElementById('stop'); 98 | 99 | start.addEventListener('click', function() { 100 | httpServer.start(); 101 | status.textContent = 'Running'; 102 | }); 103 | 104 | stop.addEventListener('click', function() { 105 | httpServer.stop(); 106 | status.textContent = 'Stopped'; 107 | }); 108 | }); 109 | 110 | window.addEventListener('visibilitychange', function(evt) { 111 | P2PHelper.restartScan(); 112 | }); 113 | 114 | window.addEventListener('beforeunload', function() { 115 | httpServer.stop(); 116 | 117 | P2PHelper.disconnect(); 118 | P2PHelper.stopScan(); 119 | }); 120 | -------------------------------------------------------------------------------- /example/p2p/js/p2p-helper.js: -------------------------------------------------------------------------------- 1 | !function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.P2PHelper=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o { 71 | listener.call(this, data); 72 | }); 73 | }, 74 | 75 | addEventListener: function(name, listener) { 76 | var events = this._events = this._events || {}; 77 | var listeners = events[name] = events[name] || []; 78 | if (listeners.find(fn => fn === listener)) { 79 | return; 80 | } 81 | 82 | listeners.push(listener); 83 | }, 84 | 85 | removeEventListener: function(name, listener) { 86 | var events = this._events || {}; 87 | var listeners = events[name] || []; 88 | for (var i = listeners.length - 1; i >= 0; i--) { 89 | if (listeners[i] === listener) { 90 | listeners.splice(i, 1); 91 | return; 92 | } 93 | } 94 | } 95 | }; 96 | 97 | wifiP2pManager.addEventListener('statuschange', (evt) => { 98 | console.log('wifiP2pManager::statuschange', evt); 99 | 100 | P2PHelper.dispatchEvent('statuschange'); 101 | 102 | if (groupOwner && !wifiP2pManager.groupOwner) { 103 | groupOwner = null; 104 | P2PHelper.dispatchEvent('disconnected'); 105 | return; 106 | } 107 | 108 | groupOwner = wifiP2pManager.groupOwner; 109 | 110 | if (groupOwner) { 111 | P2PHelper.dispatchEvent('connected', { 112 | groupOwner: groupOwner 113 | }); 114 | } 115 | }); 116 | 117 | wifiP2pManager.addEventListener('peerinfoupdate', (evt) => { 118 | console.log('wifiP2pManager::peerinfoupdate', evt); 119 | 120 | var request = wifiP2pManager.getPeerList(); 121 | request.onsuccess = function() { 122 | P2PHelper.dispatchEvent('peerlistchange', { 123 | peerList: request.result 124 | }); 125 | }; 126 | request.onerror = function() { 127 | console.warn('Unable to get peer list', request.error); 128 | }; 129 | }); 130 | 131 | navigator.mozSetMessageHandler('wifip2p-pairing-request', (evt) => { 132 | console.log('wifip2p-pairing-request', evt); 133 | 134 | var accepted = true; 135 | var pin = ''; // optional 136 | 137 | P2PHelper.dispatchEvent('pairingrequest'); 138 | 139 | wifiP2pManager.setPairingConfirmation(accepted, pin); 140 | }); 141 | 142 | return P2PHelper; 143 | 144 | })(); 145 | 146 | },{}]},{},[1])(1) 147 | }); -------------------------------------------------------------------------------- /example/p2p/lib/fxos-web-server.js: -------------------------------------------------------------------------------- 1 | !function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.HTTPServer=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o { 75 | listener.call(this, data); 76 | }); 77 | }; 78 | 79 | EventTarget.prototype.addEventListener = function(name, listener) { 80 | var events = this._events = this._events || {}; 81 | var listeners = events[name] = events[name] || []; 82 | if (listeners.find(fn => fn === listener)) { 83 | return; 84 | } 85 | 86 | listeners.push(listener); 87 | }; 88 | 89 | EventTarget.prototype.removeEventListener = function(name, listener) { 90 | var events = this._events || {}; 91 | var listeners = events[name] || []; 92 | for (var i = listeners.length - 1; i >= 0; i--) { 93 | if (listeners[i] === listener) { 94 | listeners.splice(i, 1); 95 | return; 96 | } 97 | } 98 | }; 99 | 100 | return EventTarget; 101 | 102 | })(); 103 | 104 | },{}],3:[function(require,module,exports){ 105 | /*jshint esnext:true*/ 106 | /*exported HTTPRequest*/ 107 | 'use strict'; 108 | 109 | module.exports = window.HTTPRequest = (function() { 110 | 111 | var EventTarget = require('./event-target'); 112 | var BinaryUtils = require('./binary-utils'); 113 | 114 | const CRLF = '\r\n'; 115 | 116 | function HTTPRequest(socket) { 117 | var parts = []; 118 | var receivedLength = 0; 119 | 120 | var checkRequestComplete = () => { 121 | var contentLength = parseInt(this.headers['Content-Length'], 10); 122 | if (isNaN(contentLength)) { 123 | this.complete = true; 124 | this.dispatchEvent('complete', this); 125 | return; 126 | } 127 | 128 | if (receivedLength < contentLength) { 129 | return; 130 | } 131 | 132 | BinaryUtils.mergeArrayBuffers(parts, (data) => { 133 | this.body = parseBody(this.headers['Content-Type'], data); 134 | this.complete = true; 135 | this.dispatchEvent('complete', this); 136 | }); 137 | 138 | socket.ondata = null; 139 | }; 140 | 141 | socket.ondata = (event) => { 142 | var data = event.data; 143 | 144 | if (parts.length > 0) { 145 | parts.push(data); 146 | receivedLength += data.byteLength; 147 | checkRequestComplete(); 148 | return; 149 | } 150 | 151 | var firstPart = parseHeader(this, data); 152 | if (this.invalid) { 153 | this.dispatchEvent('error', this); 154 | 155 | socket.close(); 156 | socket.ondata = null; 157 | return; 158 | } 159 | 160 | if (firstPart) { 161 | parts.push(firstPart); 162 | receivedLength += firstPart.byteLength; 163 | } 164 | 165 | checkRequestComplete(); 166 | }; 167 | } 168 | 169 | HTTPRequest.prototype = new EventTarget(); 170 | 171 | HTTPRequest.prototype.constructor = HTTPRequest; 172 | 173 | function parseHeader(request, data) { 174 | if (!data) { 175 | request.invalid = true; 176 | return null; 177 | } 178 | 179 | data = BinaryUtils.arrayBufferToString(data); 180 | 181 | var requestParts = data.split(CRLF + CRLF); 182 | 183 | var header = requestParts.shift(); 184 | var body = requestParts.join(CRLF + CRLF); 185 | 186 | var headerLines = header.split(CRLF); 187 | var requestLine = headerLines.shift().split(' '); 188 | 189 | var method = requestLine[0]; 190 | var uri = requestLine[1]; 191 | var version = requestLine[2]; 192 | 193 | if (version !== HTTPServer.HTTP_VERSION) { 194 | request.invalid = true; 195 | return null; 196 | } 197 | 198 | var uriParts = uri.split('?'); 199 | 200 | var path = uriParts.shift(); 201 | var params = parseURLEncodedString(uriParts.join('?')); 202 | 203 | var headers = {}; 204 | headerLines.forEach((headerLine) => { 205 | var parts = headerLine.split(': '); 206 | if (parts.length !== 2) { 207 | return; 208 | } 209 | 210 | var name = parts[0]; 211 | var value = parts[1]; 212 | 213 | headers[name] = value; 214 | }); 215 | 216 | request.method = method; 217 | request.path = path; 218 | request.params = params; 219 | request.headers = headers; 220 | 221 | if (headers['Content-Length']) { 222 | // request.body = parseBody(headers['Content-Type'], body); 223 | return BinaryUtils.stringToArrayBuffer(body); 224 | } 225 | 226 | return null; 227 | } 228 | 229 | function setOrAppendValue(object, name, value) { 230 | var existingValue = object[name]; 231 | if (existingValue === undefined) { 232 | object[name] = value; 233 | } else { 234 | if (Array.isArray(existingValue)) { 235 | existingValue.push(value); 236 | } else { 237 | object[name] = [existingValue, value]; 238 | } 239 | } 240 | } 241 | 242 | function parseURLEncodedString(string) { 243 | var values = {}; 244 | 245 | string.split('&').forEach((pair) => { 246 | if (!pair) { 247 | return; 248 | } 249 | 250 | var parts = decodeURIComponent(pair).split('='); 251 | 252 | var name = parts.shift(); 253 | var value = parts.join('='); 254 | 255 | setOrAppendValue(values, name, value); 256 | }); 257 | 258 | return values; 259 | } 260 | 261 | function parseMultipartFormDataString(string, boundary) { 262 | var values = {}; 263 | 264 | string.split('--' + boundary).forEach((data) => { 265 | data = data.replace(/^\r\n/, '').replace(/\r\n$/, ''); 266 | 267 | if (!data || data === '--') { 268 | return; 269 | } 270 | 271 | var parts = data.split(CRLF + CRLF); 272 | 273 | var header = parts.shift(); 274 | var value = { 275 | headers: {}, 276 | metadata: {}, 277 | value: parts.join(CRLF + CRLF) 278 | }; 279 | 280 | var name; 281 | 282 | var headers = header.split(CRLF); 283 | headers.forEach((header) => { 284 | var headerParams = header.split(';'); 285 | var headerParts = headerParams.shift().split(': '); 286 | 287 | var headerName = headerParts[0]; 288 | var headerValue = headerParts[1]; 289 | 290 | if (headerName !== 'Content-Disposition' || 291 | headerValue !== 'form-data') { 292 | value.headers[headerName] = headerValue; 293 | return; 294 | } 295 | 296 | headerParams.forEach((param) => { 297 | var paramParts = param.trim().split('='); 298 | 299 | var paramName = paramParts[0]; 300 | var paramValue = paramParts[1]; 301 | 302 | paramValue = paramValue.replace(/\"(.*?)\"/, '$1') || paramValue; 303 | 304 | if (paramName === 'name') { 305 | name = paramValue; 306 | } 307 | 308 | else { 309 | value.metadata[paramName] = paramValue; 310 | } 311 | }); 312 | }); 313 | 314 | if (name) { 315 | setOrAppendValue(values, name, value); 316 | } 317 | }); 318 | 319 | return values; 320 | } 321 | 322 | function parseBody(contentType, data) { 323 | contentType = contentType || 'text/plain'; 324 | 325 | var contentTypeParams = contentType.replace(/\s/g, '').split(';'); 326 | var mimeType = contentTypeParams.shift(); 327 | 328 | var body = BinaryUtils.arrayBufferToString(data); 329 | 330 | var result; 331 | 332 | try { 333 | switch (mimeType) { 334 | case 'application/x-www-form-urlencoded': 335 | result = parseURLEncodedString(body); 336 | break; 337 | case 'multipart/form-data': 338 | contentTypeParams.forEach((contentTypeParam) => { 339 | var parts = contentTypeParam.split('='); 340 | 341 | var name = parts[0]; 342 | var value = parts[1]; 343 | 344 | if (name === 'boundary') { 345 | result = parseMultipartFormDataString(body, value); 346 | } 347 | }); 348 | break; 349 | case 'application/json': 350 | result = JSON.parse(body); 351 | break; 352 | case 'application/xml': 353 | result = new DOMParser().parseFromString(body, 'text/xml'); 354 | break; 355 | default: 356 | break; 357 | } 358 | } catch (exception) { 359 | console.log('Unable to parse HTTP request body with Content-Type: ' + contentType); 360 | } 361 | 362 | return result || body; 363 | } 364 | 365 | return HTTPRequest; 366 | 367 | })(); 368 | 369 | },{"./binary-utils":1,"./event-target":2}],4:[function(require,module,exports){ 370 | /*jshint esnext:true*/ 371 | /*exported HTTPResponse*/ 372 | 'use strict'; 373 | 374 | module.exports = window.HTTPResponse = (function() { 375 | 376 | var EventTarget = require('./event-target'); 377 | var BinaryUtils = require('./binary-utils'); 378 | var HTTPStatus = require('./http-status'); 379 | 380 | const CRLF = '\r\n'; 381 | const BUFFER_SIZE = 64 * 1024; 382 | 383 | function HTTPResponse(socket, timeout) { 384 | this.socket = socket; 385 | this.timeout = timeout; 386 | 387 | this.headers = {}; 388 | this.headers['Content-Type'] = 'text/html'; 389 | this.headers['Connection'] = 'close'; 390 | 391 | if (this.timeout) { 392 | this.timeoutHandler = setTimeout(() => { 393 | this.send(null, 500); 394 | }, this.timeout); 395 | } 396 | } 397 | 398 | HTTPResponse.prototype = new EventTarget(); 399 | 400 | HTTPResponse.prototype.constructor = HTTPResponse; 401 | 402 | HTTPResponse.prototype.send = function(body, status) { 403 | return createResponse(body, status, this.headers, (response) => { 404 | var offset = 0; 405 | var remaining = response.byteLength; 406 | 407 | var sendNextPart = () => { 408 | var length = Math.min(remaining, BUFFER_SIZE); 409 | 410 | var bufferFull = this.socket.send(response, offset, length); 411 | 412 | offset += length; 413 | remaining -= length; 414 | 415 | if (remaining > 0) { 416 | if (!bufferFull) { 417 | sendNextPart(); 418 | } 419 | } 420 | 421 | else { 422 | clearTimeout(this.timeoutHandler); 423 | 424 | this.socket.close(); 425 | this.dispatchEvent('complete'); 426 | } 427 | }; 428 | 429 | this.socket.ondrain = sendNextPart; 430 | 431 | sendNextPart(); 432 | }); 433 | }; 434 | 435 | HTTPResponse.prototype.sendFile = function(fileOrPath, status) { 436 | if (fileOrPath instanceof File) { 437 | BinaryUtils.blobToArrayBuffer(fileOrPath, (arrayBuffer) => { 438 | this.send(arrayBuffer, status); 439 | }); 440 | 441 | return; 442 | } 443 | 444 | var xhr = new XMLHttpRequest(); 445 | xhr.open('GET', fileOrPath, true); 446 | xhr.responseType = 'arraybuffer'; 447 | xhr.onload = () => { 448 | this.send(xhr.response, status); 449 | }; 450 | 451 | xhr.send(null); 452 | }; 453 | 454 | function createResponseHeader(status, headers) { 455 | var header = HTTPStatus.getStatusLine(status); 456 | 457 | for (var name in headers) { 458 | header += name + ': ' + headers[name] + CRLF; 459 | } 460 | 461 | return header; 462 | } 463 | 464 | function createResponse(body, status, headers, callback) { 465 | body = body || ''; 466 | status = status || 200; 467 | headers = headers || {}; 468 | 469 | headers['Content-Length'] = body.length || body.byteLength; 470 | 471 | var response = new Blob([ 472 | createResponseHeader(status, headers), 473 | CRLF, 474 | body 475 | ]); 476 | 477 | return BinaryUtils.blobToArrayBuffer(response, callback); 478 | } 479 | 480 | return HTTPResponse; 481 | 482 | })(); 483 | 484 | },{"./binary-utils":1,"./event-target":2,"./http-status":6}],5:[function(require,module,exports){ 485 | /*jshint esnext:true*/ 486 | /*exported HTTPServer*/ 487 | 'use strict'; 488 | 489 | module.exports = window.HTTPServer = (function() { 490 | 491 | var EventTarget = require('./event-target'); 492 | var HTTPRequest = require('./http-request'); 493 | var HTTPResponse = require('./http-response'); 494 | var IPUtils = require('./ip-utils'); 495 | 496 | const DEFAULT_PORT = 8080; 497 | const DEFAULT_TIMEOUT = 20000; 498 | 499 | const CRLF = '\r\n'; 500 | 501 | function HTTPServer(port, options) { 502 | this.port = port || DEFAULT_PORT; 503 | 504 | options = options || {}; 505 | for (var option in options) { 506 | this[option] = options[option]; 507 | } 508 | 509 | this.running = false; 510 | } 511 | 512 | HTTPServer.HTTP_VERSION = 'HTTP/1.1'; 513 | 514 | HTTPServer.prototype = new EventTarget(); 515 | 516 | HTTPServer.prototype.constructor = HTTPServer; 517 | 518 | HTTPServer.prototype.timeout = DEFAULT_TIMEOUT; 519 | 520 | HTTPServer.prototype.start = function() { 521 | if (this.running) { 522 | return; 523 | } 524 | 525 | console.log('Starting HTTP server on port ' + this.port); 526 | 527 | var socket = navigator.mozTCPSocket.listen(this.port, { 528 | binaryType: 'arraybuffer' 529 | }); 530 | 531 | socket.onconnect = (connectEvent) => { 532 | var socket = connectEvent.socket || connectEvent; 533 | var request = new HTTPRequest(socket); 534 | 535 | request.addEventListener('complete', () => { 536 | var response = new HTTPResponse(socket, this.timeout); 537 | 538 | this.dispatchEvent('request', { 539 | request: request, 540 | response: response, 541 | socket: socket 542 | }); 543 | }); 544 | 545 | request.addEventListener('error', () => { 546 | console.warn('Invalid request received'); 547 | }); 548 | }; 549 | 550 | this.socket = socket; 551 | this.running = true; 552 | }; 553 | 554 | HTTPServer.prototype.stop = function() { 555 | if (!this.running) { 556 | return; 557 | } 558 | 559 | console.log('Shutting down HTTP server on port ' + this.port); 560 | 561 | this.socket.close(); 562 | 563 | this.running = false; 564 | }; 565 | 566 | return HTTPServer; 567 | 568 | })(); 569 | 570 | },{"./event-target":2,"./http-request":3,"./http-response":4,"./ip-utils":7}],6:[function(require,module,exports){ 571 | /*jshint esnext:true*/ 572 | /*exported HTTPStatus*/ 573 | 'use strict'; 574 | 575 | module.exports = window.HTTPStatus = (function() { 576 | 577 | const CRLF = '\r\n'; 578 | 579 | var HTTPStatus = {}; 580 | 581 | HTTPStatus.STATUS_CODES = { 582 | 100: 'Continue', 583 | 101: 'Switching Protocols', 584 | 102: 'Processing', 585 | 200: 'OK', 586 | 201: 'Created', 587 | 202: 'Accepted', 588 | 203: 'Non Authoritative Information', 589 | 204: 'No Content', 590 | 205: 'Reset Content', 591 | 206: 'Partial Content', 592 | 207: 'Multi-Status', 593 | 300: 'Mutliple Choices', 594 | 301: 'Moved Permanently', 595 | 302: 'Moved Temporarily', 596 | 303: 'See Other', 597 | 304: 'Not Modified', 598 | 305: 'Use Proxy', 599 | 307: 'Temporary Redirect', 600 | 400: 'Bad Request', 601 | 401: 'Unauthorized', 602 | 402: 'Payment Required', 603 | 403: 'Forbidden', 604 | 404: 'Not Found', 605 | 405: 'Method Not Allowed', 606 | 406: 'Not Acceptable', 607 | 407: 'Proxy Authentication Required', 608 | 408: 'Request Timeout', 609 | 409: 'Conflict', 610 | 410: 'Gone', 611 | 411: 'Length Required', 612 | 412: 'Precondition Failed', 613 | 413: 'Request Entity Too Large', 614 | 414: 'Request-URI Too Long', 615 | 415: 'Unsupported Media Type', 616 | 416: 'Requested Range Not Satisfiable', 617 | 417: 'Expectation Failed', 618 | 419: 'Insufficient Space on Resource', 619 | 420: 'Method Failure', 620 | 422: 'Unprocessable Entity', 621 | 423: 'Locked', 622 | 424: 'Failed Dependency', 623 | 500: 'Server Error', 624 | 501: 'Not Implemented', 625 | 502: 'Bad Gateway', 626 | 503: 'Service Unavailable', 627 | 504: 'Gateway Timeout', 628 | 505: 'HTTP Version Not Supported', 629 | 507: 'Insufficient Storage' 630 | }; 631 | 632 | HTTPStatus.getStatusLine = function(status) { 633 | var reason = HTTPStatus.STATUS_CODES[status] || 'Unknown'; 634 | 635 | return [HTTPServer.HTTP_VERSION, status, reason].join(' ') + CRLF; 636 | }; 637 | 638 | return HTTPStatus; 639 | 640 | })(); 641 | 642 | },{}],7:[function(require,module,exports){ 643 | /*jshint esnext:true*/ 644 | /*exported IPUtils*/ 645 | 'use strict'; 646 | 647 | module.exports = window.IPUtils = (function() { 648 | 649 | const CRLF = '\r\n'; 650 | 651 | var IPUtils = { 652 | getAddresses: function(callback) { 653 | if (typeof callback !== 'function') { 654 | console.warn('No callback provided'); 655 | return; 656 | } 657 | 658 | var addresses = { 659 | '0.0.0.0': true 660 | }; 661 | 662 | var RTCPeerConnection = window.RTCPeerConnection || 663 | window.mozRTCPeerConnection; 664 | 665 | var rtc = new RTCPeerConnection({ iceServers: [] }); 666 | rtc.createDataChannel('', { reliable: false }); 667 | 668 | rtc.onicecandidate = function(evt) { 669 | if (evt.candidate) { 670 | parseSDP('a=' + evt.candidate.candidate); 671 | } 672 | }; 673 | 674 | rtc.createOffer((description) => { 675 | parseSDP(description.sdp); 676 | rtc.setLocalDescription(description, noop, noop); 677 | }, (error) => { 678 | console.warn('Unable to create offer', error); 679 | }); 680 | 681 | function addAddress(address) { 682 | if (addresses[address]) { 683 | return; 684 | } 685 | 686 | addresses[address] = true; 687 | callback(address); 688 | } 689 | 690 | function parseSDP(sdp) { 691 | sdp.split(CRLF).forEach((line) => { 692 | var parts = line.split(' '); 693 | 694 | if (line.indexOf('a=candidate') !== -1) { 695 | if (parts[7] === 'host') { 696 | addAddress(parts[4]); 697 | } 698 | } 699 | 700 | else if (line.indexOf('c=') !== -1) { 701 | addAddress(parts[2]); 702 | } 703 | }); 704 | } 705 | } 706 | }; 707 | 708 | function noop() {} 709 | 710 | return IPUtils; 711 | 712 | })(); 713 | 714 | },{}]},{},[5])(5) 715 | }); -------------------------------------------------------------------------------- /example/p2p/manifest.webapp: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.1", 3 | "name": "P2P Web Server", 4 | "description": "A P2P HTTP server for Firefox OS", 5 | "launch_path": "/index.html", 6 | "icons": { 7 | "16": "/img/icons/icon16x16.png", 8 | "48": "/img/icons/icon48x48.png", 9 | "60": "/img/icons/icon60x60.png", 10 | "128": "/img/icons/icon128x128.png" 11 | }, 12 | "developer": { 13 | "name": "Justin D'Arcangelo", 14 | "url": "https://github.com/justindarc" 15 | }, 16 | "type": "certified", 17 | "permissions": { 18 | "tcp-socket": {}, 19 | "wifi-manage": {}, 20 | "browser": {} 21 | }, 22 | "installs_allowed_from": [ 23 | "*" 24 | ], 25 | "locales": {}, 26 | "default_locale": "en", 27 | "messages": [ 28 | "wifip2p-pairing-request" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /example/simple/css/app.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 0; 3 | padding: 0.5rem; 4 | } 5 | 6 | body { 7 | font-size: 1rem; 8 | background: #fff; 9 | } 10 | 11 | h1 { 12 | margin: 0 0 1rem 0; 13 | } 14 | 15 | p { 16 | font-size: inherit; 17 | } 18 | 19 | button { 20 | font-size: 1rem; 21 | padding: 0.5rem 1rem; 22 | } -------------------------------------------------------------------------------- /example/simple/img/icons/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 19 | 21 | 22 | 24 | image/svg+xml 25 | 27 | 28 | 29 | 30 | 50 | 51 | empty 53 | Created with Sketch. 55 | 57 | 59 | 63 | 67 | 68 | 77 | 78 | 88 | 93 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /example/simple/img/icons/icon128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justindarc/fxos-web-server/d2d3d40acc88c04d48743137f2d279fc684dd45d/example/simple/img/icons/icon128x128.png -------------------------------------------------------------------------------- /example/simple/img/icons/icon16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justindarc/fxos-web-server/d2d3d40acc88c04d48743137f2d279fc684dd45d/example/simple/img/icons/icon16x16.png -------------------------------------------------------------------------------- /example/simple/img/icons/icon48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justindarc/fxos-web-server/d2d3d40acc88c04d48743137f2d279fc684dd45d/example/simple/img/icons/icon48x48.png -------------------------------------------------------------------------------- /example/simple/img/icons/icon60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justindarc/fxos-web-server/d2d3d40acc88c04d48743137f2d279fc684dd45d/example/simple/img/icons/icon60x60.png -------------------------------------------------------------------------------- /example/simple/img/image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justindarc/fxos-web-server/d2d3d40acc88c04d48743137f2d279fc684dd45d/example/simple/img/image.jpg -------------------------------------------------------------------------------- /example/simple/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Simple Web Server 6 | 7 | 8 | 9 | 10 | 11 |
12 |

Simple Web Server

13 |

Status: Stopped

14 |

IP Address:

15 |

Port:

16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /example/simple/js/app.js: -------------------------------------------------------------------------------- 1 | var httpServer = new HTTPServer(8080); 2 | 3 | httpServer.addEventListener('request', function(evt) { 4 | var request = evt.request; 5 | var response = evt.response; 6 | 7 | console.log(request); 8 | 9 | if (request.path === '/image.jpg') { 10 | response.headers['Content-Type'] = 'image/jpeg'; 11 | response.sendFile('/img/image.jpg'); 12 | return; 13 | } 14 | 15 | var paramsString = JSON.stringify(request.params, null, 2); 16 | var bodyString = JSON.stringify(request.body, null, 2); 17 | 18 | var firstName = (request.body && request.body.first_name) || ''; 19 | var lastName = (request.body && request.body.last_name) || ''; 20 | 21 | var body = 22 | ` 23 | 24 | 25 | Firefox OS Web Server 26 | 27 | 28 |

Hello World!

29 |

If you can read this, the Firefox OS Web Server is operational!

30 |

The path you requested is: ${request.path}

31 |
URL Parameters:
32 |
${paramsString}
33 |
POST Data:
34 |
${bodyString}
35 |

Sample Form

36 |
37 |

38 | 39 | 40 |

41 |

42 | 43 | 44 |

45 | 46 |
47 |

To see something really scary, click here :-)

48 | 49 | `; 50 | 51 | response.send(body); 52 | }); 53 | 54 | window.addEventListener('load', function() { 55 | var status = document.getElementById('status'); 56 | var ip = document.getElementById('ip'); 57 | var port = document.getElementById('port'); 58 | var start = document.getElementById('start'); 59 | var stop = document.getElementById('stop'); 60 | 61 | IPUtils.getAddresses(function(ipAddress) { 62 | ip.textContent = ip.textContent || ipAddress; 63 | }); 64 | 65 | port.textContent = httpServer.port; 66 | 67 | start.addEventListener('click', function() { 68 | httpServer.start(); 69 | status.textContent = 'Running'; 70 | }); 71 | 72 | stop.addEventListener('click', function() { 73 | httpServer.stop(); 74 | status.textContent = 'Stopped'; 75 | }); 76 | }); 77 | 78 | window.addEventListener('beforeunload', function() { 79 | httpServer.stop(); 80 | }); 81 | -------------------------------------------------------------------------------- /example/simple/lib/fxos-web-server.js: -------------------------------------------------------------------------------- 1 | !function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.HTTPServer=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o { 75 | listener.call(this, data); 76 | }); 77 | }; 78 | 79 | EventTarget.prototype.addEventListener = function(name, listener) { 80 | var events = this._events = this._events || {}; 81 | var listeners = events[name] = events[name] || []; 82 | if (listeners.find(fn => fn === listener)) { 83 | return; 84 | } 85 | 86 | listeners.push(listener); 87 | }; 88 | 89 | EventTarget.prototype.removeEventListener = function(name, listener) { 90 | var events = this._events || {}; 91 | var listeners = events[name] || []; 92 | for (var i = listeners.length - 1; i >= 0; i--) { 93 | if (listeners[i] === listener) { 94 | listeners.splice(i, 1); 95 | return; 96 | } 97 | } 98 | }; 99 | 100 | return EventTarget; 101 | 102 | })(); 103 | 104 | },{}],3:[function(require,module,exports){ 105 | /*jshint esnext:true*/ 106 | /*exported HTTPRequest*/ 107 | 'use strict'; 108 | 109 | module.exports = window.HTTPRequest = (function() { 110 | 111 | var EventTarget = require('./event-target'); 112 | var BinaryUtils = require('./binary-utils'); 113 | 114 | const CRLF = '\r\n'; 115 | 116 | function HTTPRequest(socket) { 117 | var parts = []; 118 | var receivedLength = 0; 119 | 120 | var checkRequestComplete = () => { 121 | var contentLength = parseInt(this.headers['Content-Length'], 10); 122 | if (isNaN(contentLength)) { 123 | this.complete = true; 124 | this.dispatchEvent('complete', this); 125 | return; 126 | } 127 | 128 | if (receivedLength < contentLength) { 129 | return; 130 | } 131 | 132 | BinaryUtils.mergeArrayBuffers(parts, (data) => { 133 | this.body = parseBody(this.headers['Content-Type'], data); 134 | this.complete = true; 135 | this.dispatchEvent('complete', this); 136 | }); 137 | 138 | socket.ondata = null; 139 | }; 140 | 141 | socket.ondata = (event) => { 142 | var data = event.data; 143 | 144 | if (parts.length > 0) { 145 | parts.push(data); 146 | receivedLength += data.byteLength; 147 | checkRequestComplete(); 148 | return; 149 | } 150 | 151 | var firstPart = parseHeader(this, data); 152 | if (this.invalid) { 153 | this.dispatchEvent('error', this); 154 | 155 | socket.close(); 156 | socket.ondata = null; 157 | return; 158 | } 159 | 160 | if (firstPart) { 161 | parts.push(firstPart); 162 | receivedLength += firstPart.byteLength; 163 | } 164 | 165 | checkRequestComplete(); 166 | }; 167 | } 168 | 169 | HTTPRequest.prototype = new EventTarget(); 170 | 171 | HTTPRequest.prototype.constructor = HTTPRequest; 172 | 173 | function parseHeader(request, data) { 174 | if (!data) { 175 | request.invalid = true; 176 | return null; 177 | } 178 | 179 | data = BinaryUtils.arrayBufferToString(data); 180 | 181 | var requestParts = data.split(CRLF + CRLF); 182 | 183 | var header = requestParts.shift(); 184 | var body = requestParts.join(CRLF + CRLF); 185 | 186 | var headerLines = header.split(CRLF); 187 | var requestLine = headerLines.shift().split(' '); 188 | 189 | var method = requestLine[0]; 190 | var uri = requestLine[1]; 191 | var version = requestLine[2]; 192 | 193 | if (version !== HTTPServer.HTTP_VERSION) { 194 | request.invalid = true; 195 | return null; 196 | } 197 | 198 | var uriParts = uri.split('?'); 199 | 200 | var path = uriParts.shift(); 201 | var params = parseURLEncodedString(uriParts.join('?')); 202 | 203 | var headers = {}; 204 | headerLines.forEach((headerLine) => { 205 | var parts = headerLine.split(': '); 206 | if (parts.length !== 2) { 207 | return; 208 | } 209 | 210 | var name = parts[0]; 211 | var value = parts[1]; 212 | 213 | headers[name] = value; 214 | }); 215 | 216 | request.method = method; 217 | request.path = path; 218 | request.params = params; 219 | request.headers = headers; 220 | 221 | if (headers['Content-Length']) { 222 | // request.body = parseBody(headers['Content-Type'], body); 223 | return BinaryUtils.stringToArrayBuffer(body); 224 | } 225 | 226 | return null; 227 | } 228 | 229 | function setOrAppendValue(object, name, value) { 230 | var existingValue = object[name]; 231 | if (existingValue === undefined) { 232 | object[name] = value; 233 | } else { 234 | if (Array.isArray(existingValue)) { 235 | existingValue.push(value); 236 | } else { 237 | object[name] = [existingValue, value]; 238 | } 239 | } 240 | } 241 | 242 | function parseURLEncodedString(string) { 243 | var values = {}; 244 | 245 | string.split('&').forEach((pair) => { 246 | if (!pair) { 247 | return; 248 | } 249 | 250 | var parts = decodeURIComponent(pair).split('='); 251 | 252 | var name = parts.shift(); 253 | var value = parts.join('='); 254 | 255 | setOrAppendValue(values, name, value); 256 | }); 257 | 258 | return values; 259 | } 260 | 261 | function parseMultipartFormDataString(string, boundary) { 262 | var values = {}; 263 | 264 | string.split('--' + boundary).forEach((data) => { 265 | data = data.replace(/^\r\n/, '').replace(/\r\n$/, ''); 266 | 267 | if (!data || data === '--') { 268 | return; 269 | } 270 | 271 | var parts = data.split(CRLF + CRLF); 272 | 273 | var header = parts.shift(); 274 | var value = { 275 | headers: {}, 276 | metadata: {}, 277 | value: parts.join(CRLF + CRLF) 278 | }; 279 | 280 | var name; 281 | 282 | var headers = header.split(CRLF); 283 | headers.forEach((header) => { 284 | var headerParams = header.split(';'); 285 | var headerParts = headerParams.shift().split(': '); 286 | 287 | var headerName = headerParts[0]; 288 | var headerValue = headerParts[1]; 289 | 290 | if (headerName !== 'Content-Disposition' || 291 | headerValue !== 'form-data') { 292 | value.headers[headerName] = headerValue; 293 | return; 294 | } 295 | 296 | headerParams.forEach((param) => { 297 | var paramParts = param.trim().split('='); 298 | 299 | var paramName = paramParts[0]; 300 | var paramValue = paramParts[1]; 301 | 302 | paramValue = paramValue.replace(/\"(.*?)\"/, '$1') || paramValue; 303 | 304 | if (paramName === 'name') { 305 | name = paramValue; 306 | } 307 | 308 | else { 309 | value.metadata[paramName] = paramValue; 310 | } 311 | }); 312 | }); 313 | 314 | if (name) { 315 | setOrAppendValue(values, name, value); 316 | } 317 | }); 318 | 319 | return values; 320 | } 321 | 322 | function parseBody(contentType, data) { 323 | contentType = contentType || 'text/plain'; 324 | 325 | var contentTypeParams = contentType.replace(/\s/g, '').split(';'); 326 | var mimeType = contentTypeParams.shift(); 327 | 328 | var body = BinaryUtils.arrayBufferToString(data); 329 | 330 | var result; 331 | 332 | try { 333 | switch (mimeType) { 334 | case 'application/x-www-form-urlencoded': 335 | result = parseURLEncodedString(body); 336 | break; 337 | case 'multipart/form-data': 338 | contentTypeParams.forEach((contentTypeParam) => { 339 | var parts = contentTypeParam.split('='); 340 | 341 | var name = parts[0]; 342 | var value = parts[1]; 343 | 344 | if (name === 'boundary') { 345 | result = parseMultipartFormDataString(body, value); 346 | } 347 | }); 348 | break; 349 | case 'application/json': 350 | result = JSON.parse(body); 351 | break; 352 | case 'application/xml': 353 | result = new DOMParser().parseFromString(body, 'text/xml'); 354 | break; 355 | default: 356 | break; 357 | } 358 | } catch (exception) { 359 | console.log('Unable to parse HTTP request body with Content-Type: ' + contentType); 360 | } 361 | 362 | return result || body; 363 | } 364 | 365 | return HTTPRequest; 366 | 367 | })(); 368 | 369 | },{"./binary-utils":1,"./event-target":2}],4:[function(require,module,exports){ 370 | /*jshint esnext:true*/ 371 | /*exported HTTPResponse*/ 372 | 'use strict'; 373 | 374 | module.exports = window.HTTPResponse = (function() { 375 | 376 | var EventTarget = require('./event-target'); 377 | var BinaryUtils = require('./binary-utils'); 378 | var HTTPStatus = require('./http-status'); 379 | 380 | const CRLF = '\r\n'; 381 | const BUFFER_SIZE = 64 * 1024; 382 | 383 | function HTTPResponse(socket, timeout) { 384 | this.socket = socket; 385 | this.timeout = timeout; 386 | 387 | this.headers = {}; 388 | this.headers['Content-Type'] = 'text/html'; 389 | this.headers['Connection'] = 'close'; 390 | 391 | if (this.timeout) { 392 | this.timeoutHandler = setTimeout(() => { 393 | this.send(null, 500); 394 | }, this.timeout); 395 | } 396 | } 397 | 398 | HTTPResponse.prototype = new EventTarget(); 399 | 400 | HTTPResponse.prototype.constructor = HTTPResponse; 401 | 402 | HTTPResponse.prototype.send = function(body, status) { 403 | return createResponse(body, status, this.headers, (response) => { 404 | var offset = 0; 405 | var remaining = response.byteLength; 406 | 407 | var sendNextPart = () => { 408 | var length = Math.min(remaining, BUFFER_SIZE); 409 | 410 | var bufferFull = this.socket.send(response, offset, length); 411 | 412 | offset += length; 413 | remaining -= length; 414 | 415 | if (remaining > 0) { 416 | if (!bufferFull) { 417 | sendNextPart(); 418 | } 419 | } 420 | 421 | else { 422 | clearTimeout(this.timeoutHandler); 423 | 424 | this.socket.close(); 425 | this.dispatchEvent('complete'); 426 | } 427 | }; 428 | 429 | this.socket.ondrain = sendNextPart; 430 | 431 | sendNextPart(); 432 | }); 433 | }; 434 | 435 | HTTPResponse.prototype.sendFile = function(fileOrPath, status) { 436 | if (fileOrPath instanceof File) { 437 | BinaryUtils.blobToArrayBuffer(fileOrPath, (arrayBuffer) => { 438 | this.send(arrayBuffer, status); 439 | }); 440 | 441 | return; 442 | } 443 | 444 | var xhr = new XMLHttpRequest(); 445 | xhr.open('GET', fileOrPath, true); 446 | xhr.responseType = 'arraybuffer'; 447 | xhr.onload = () => { 448 | this.send(xhr.response, status); 449 | }; 450 | 451 | xhr.send(null); 452 | }; 453 | 454 | function createResponseHeader(status, headers) { 455 | var header = HTTPStatus.getStatusLine(status); 456 | 457 | for (var name in headers) { 458 | header += name + ': ' + headers[name] + CRLF; 459 | } 460 | 461 | return header; 462 | } 463 | 464 | function createResponse(body, status, headers, callback) { 465 | body = body || ''; 466 | status = status || 200; 467 | headers = headers || {}; 468 | 469 | headers['Content-Length'] = body.length || body.byteLength; 470 | 471 | var response = new Blob([ 472 | createResponseHeader(status, headers), 473 | CRLF, 474 | body 475 | ]); 476 | 477 | return BinaryUtils.blobToArrayBuffer(response, callback); 478 | } 479 | 480 | return HTTPResponse; 481 | 482 | })(); 483 | 484 | },{"./binary-utils":1,"./event-target":2,"./http-status":6}],5:[function(require,module,exports){ 485 | /*jshint esnext:true*/ 486 | /*exported HTTPServer*/ 487 | 'use strict'; 488 | 489 | module.exports = window.HTTPServer = (function() { 490 | 491 | var EventTarget = require('./event-target'); 492 | var HTTPRequest = require('./http-request'); 493 | var HTTPResponse = require('./http-response'); 494 | var IPUtils = require('./ip-utils'); 495 | 496 | const DEFAULT_PORT = 8080; 497 | const DEFAULT_TIMEOUT = 20000; 498 | 499 | const CRLF = '\r\n'; 500 | 501 | function HTTPServer(port, options) { 502 | this.port = port || DEFAULT_PORT; 503 | 504 | options = options || {}; 505 | for (var option in options) { 506 | this[option] = options[option]; 507 | } 508 | 509 | this.running = false; 510 | } 511 | 512 | HTTPServer.HTTP_VERSION = 'HTTP/1.1'; 513 | 514 | HTTPServer.prototype = new EventTarget(); 515 | 516 | HTTPServer.prototype.constructor = HTTPServer; 517 | 518 | HTTPServer.prototype.timeout = DEFAULT_TIMEOUT; 519 | 520 | HTTPServer.prototype.start = function() { 521 | if (this.running) { 522 | return; 523 | } 524 | 525 | console.log('Starting HTTP server on port ' + this.port); 526 | 527 | var socket = navigator.mozTCPSocket.listen(this.port, { 528 | binaryType: 'arraybuffer' 529 | }); 530 | 531 | socket.onconnect = (connectEvent) => { 532 | var socket = connectEvent.socket || connectEvent; 533 | var request = new HTTPRequest(socket); 534 | 535 | request.addEventListener('complete', () => { 536 | var response = new HTTPResponse(socket, this.timeout); 537 | 538 | this.dispatchEvent('request', { 539 | request: request, 540 | response: response, 541 | socket: socket 542 | }); 543 | }); 544 | 545 | request.addEventListener('error', () => { 546 | console.warn('Invalid request received'); 547 | }); 548 | }; 549 | 550 | this.socket = socket; 551 | this.running = true; 552 | }; 553 | 554 | HTTPServer.prototype.stop = function() { 555 | if (!this.running) { 556 | return; 557 | } 558 | 559 | console.log('Shutting down HTTP server on port ' + this.port); 560 | 561 | this.socket.close(); 562 | 563 | this.running = false; 564 | }; 565 | 566 | return HTTPServer; 567 | 568 | })(); 569 | 570 | },{"./event-target":2,"./http-request":3,"./http-response":4,"./ip-utils":7}],6:[function(require,module,exports){ 571 | /*jshint esnext:true*/ 572 | /*exported HTTPStatus*/ 573 | 'use strict'; 574 | 575 | module.exports = window.HTTPStatus = (function() { 576 | 577 | const CRLF = '\r\n'; 578 | 579 | var HTTPStatus = {}; 580 | 581 | HTTPStatus.STATUS_CODES = { 582 | 100: 'Continue', 583 | 101: 'Switching Protocols', 584 | 102: 'Processing', 585 | 200: 'OK', 586 | 201: 'Created', 587 | 202: 'Accepted', 588 | 203: 'Non Authoritative Information', 589 | 204: 'No Content', 590 | 205: 'Reset Content', 591 | 206: 'Partial Content', 592 | 207: 'Multi-Status', 593 | 300: 'Mutliple Choices', 594 | 301: 'Moved Permanently', 595 | 302: 'Moved Temporarily', 596 | 303: 'See Other', 597 | 304: 'Not Modified', 598 | 305: 'Use Proxy', 599 | 307: 'Temporary Redirect', 600 | 400: 'Bad Request', 601 | 401: 'Unauthorized', 602 | 402: 'Payment Required', 603 | 403: 'Forbidden', 604 | 404: 'Not Found', 605 | 405: 'Method Not Allowed', 606 | 406: 'Not Acceptable', 607 | 407: 'Proxy Authentication Required', 608 | 408: 'Request Timeout', 609 | 409: 'Conflict', 610 | 410: 'Gone', 611 | 411: 'Length Required', 612 | 412: 'Precondition Failed', 613 | 413: 'Request Entity Too Large', 614 | 414: 'Request-URI Too Long', 615 | 415: 'Unsupported Media Type', 616 | 416: 'Requested Range Not Satisfiable', 617 | 417: 'Expectation Failed', 618 | 419: 'Insufficient Space on Resource', 619 | 420: 'Method Failure', 620 | 422: 'Unprocessable Entity', 621 | 423: 'Locked', 622 | 424: 'Failed Dependency', 623 | 500: 'Server Error', 624 | 501: 'Not Implemented', 625 | 502: 'Bad Gateway', 626 | 503: 'Service Unavailable', 627 | 504: 'Gateway Timeout', 628 | 505: 'HTTP Version Not Supported', 629 | 507: 'Insufficient Storage' 630 | }; 631 | 632 | HTTPStatus.getStatusLine = function(status) { 633 | var reason = HTTPStatus.STATUS_CODES[status] || 'Unknown'; 634 | 635 | return [HTTPServer.HTTP_VERSION, status, reason].join(' ') + CRLF; 636 | }; 637 | 638 | return HTTPStatus; 639 | 640 | })(); 641 | 642 | },{}],7:[function(require,module,exports){ 643 | /*jshint esnext:true*/ 644 | /*exported IPUtils*/ 645 | 'use strict'; 646 | 647 | module.exports = window.IPUtils = (function() { 648 | 649 | const CRLF = '\r\n'; 650 | 651 | var IPUtils = { 652 | getAddresses: function(callback) { 653 | if (typeof callback !== 'function') { 654 | console.warn('No callback provided'); 655 | return; 656 | } 657 | 658 | var addresses = { 659 | '0.0.0.0': true 660 | }; 661 | 662 | var RTCPeerConnection = window.RTCPeerConnection || 663 | window.mozRTCPeerConnection; 664 | 665 | var rtc = new RTCPeerConnection({ iceServers: [] }); 666 | rtc.createDataChannel('', { reliable: false }); 667 | 668 | rtc.onicecandidate = function(evt) { 669 | if (evt.candidate) { 670 | parseSDP('a=' + evt.candidate.candidate); 671 | } 672 | }; 673 | 674 | rtc.createOffer((description) => { 675 | parseSDP(description.sdp); 676 | rtc.setLocalDescription(description, noop, noop); 677 | }, (error) => { 678 | console.warn('Unable to create offer', error); 679 | }); 680 | 681 | function addAddress(address) { 682 | if (addresses[address]) { 683 | return; 684 | } 685 | 686 | addresses[address] = true; 687 | callback(address); 688 | } 689 | 690 | function parseSDP(sdp) { 691 | sdp.split(CRLF).forEach((line) => { 692 | var parts = line.split(' '); 693 | 694 | if (line.indexOf('a=candidate') !== -1) { 695 | if (parts[7] === 'host') { 696 | addAddress(parts[4]); 697 | } 698 | } 699 | 700 | else if (line.indexOf('c=') !== -1) { 701 | addAddress(parts[2]); 702 | } 703 | }); 704 | } 705 | } 706 | }; 707 | 708 | function noop() {} 709 | 710 | return IPUtils; 711 | 712 | })(); 713 | 714 | },{}]},{},[5])(5) 715 | }); -------------------------------------------------------------------------------- /example/simple/manifest.webapp: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.1", 3 | "name": "Simple Web Server", 4 | "description": "A simple HTTP server for Firefox OS", 5 | "launch_path": "/index.html", 6 | "icons": { 7 | "16": "/img/icons/icon16x16.png", 8 | "48": "/img/icons/icon48x48.png", 9 | "60": "/img/icons/icon60x60.png", 10 | "128": "/img/icons/icon128x128.png" 11 | }, 12 | "developer": { 13 | "name": "Justin D'Arcangelo", 14 | "url": "https://github.com/justindarc" 15 | }, 16 | "type": "privileged", 17 | "permissions": { 18 | "tcp-socket": {} 19 | }, 20 | "installs_allowed_from": [ 21 | "*" 22 | ], 23 | "locales": {}, 24 | "default_locale": "en" 25 | } 26 | -------------------------------------------------------------------------------- /example/upload/css/app.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 0; 3 | padding: 0.5rem; 4 | } 5 | 6 | body { 7 | font-size: 1rem; 8 | background: #fff; 9 | } 10 | 11 | h1 { 12 | margin: 0 0 1rem 0; 13 | } 14 | 15 | p { 16 | font-size: inherit; 17 | } 18 | 19 | button { 20 | font-size: 1rem; 21 | padding: 0.5rem 1rem; 22 | } -------------------------------------------------------------------------------- /example/upload/img/icons/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 19 | 21 | 22 | 24 | image/svg+xml 25 | 27 | 28 | 29 | 30 | 50 | 51 | empty 53 | Created with Sketch. 55 | 57 | 59 | 63 | 67 | 68 | 77 | 78 | 88 | 93 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /example/upload/img/icons/icon128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justindarc/fxos-web-server/d2d3d40acc88c04d48743137f2d279fc684dd45d/example/upload/img/icons/icon128x128.png -------------------------------------------------------------------------------- /example/upload/img/icons/icon16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justindarc/fxos-web-server/d2d3d40acc88c04d48743137f2d279fc684dd45d/example/upload/img/icons/icon16x16.png -------------------------------------------------------------------------------- /example/upload/img/icons/icon48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justindarc/fxos-web-server/d2d3d40acc88c04d48743137f2d279fc684dd45d/example/upload/img/icons/icon48x48.png -------------------------------------------------------------------------------- /example/upload/img/icons/icon60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justindarc/fxos-web-server/d2d3d40acc88c04d48743137f2d279fc684dd45d/example/upload/img/icons/icon60x60.png -------------------------------------------------------------------------------- /example/upload/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Upload Web Server 6 | 7 | 8 | 9 | 10 | 11 |
12 |

Upload Web Server

13 |

Status: Stopped

14 |

IP Address:

15 |

Port:

16 | 17 | 18 |

File Upload

19 | 20 |
21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /example/upload/js/app.js: -------------------------------------------------------------------------------- 1 | var httpServer = new HTTPServer(8080); 2 | 3 | httpServer.addEventListener('request', function(evt) { 4 | var request = evt.request; 5 | var response = evt.response; 6 | 7 | console.log(request); 8 | 9 | var image = document.getElementById('image'); 10 | 11 | var file = request.body && request.body.file && request.body.file.value; 12 | if (file) { 13 | file = BinaryUtils.stringToArrayBuffer(file); 14 | image.src = URL.createObjectURL(new Blob([file])); 15 | } 16 | 17 | var body = 18 | ` 19 | 20 | 21 | Firefox OS Web Server 22 | 23 | 24 |

Hello World!

25 |

If you can read this, the Firefox OS Web Server is operational!

26 |
27 |

28 | 29 | 30 |

31 |

32 | 33 | 34 |

35 | 36 |
37 | 38 | `; 39 | 40 | response.send(body); 41 | }); 42 | 43 | window.addEventListener('load', function() { 44 | var status = document.getElementById('status'); 45 | var ip = document.getElementById('ip'); 46 | var port = document.getElementById('port'); 47 | var start = document.getElementById('start'); 48 | var stop = document.getElementById('stop'); 49 | 50 | IPUtils.getAddresses(function(ipAddress) { 51 | ip.textContent = ip.textContent || ipAddress; 52 | }); 53 | 54 | port.textContent = httpServer.port; 55 | 56 | start.addEventListener('click', function() { 57 | httpServer.start(); 58 | status.textContent = 'Running'; 59 | }); 60 | 61 | stop.addEventListener('click', function() { 62 | httpServer.stop(); 63 | status.textContent = 'Stopped'; 64 | }); 65 | }); 66 | 67 | window.addEventListener('beforeunload', function() { 68 | httpServer.stop(); 69 | }); 70 | -------------------------------------------------------------------------------- /example/upload/lib/fxos-web-server.js: -------------------------------------------------------------------------------- 1 | !function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.HTTPServer=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o { 75 | listener.call(this, data); 76 | }); 77 | }; 78 | 79 | EventTarget.prototype.addEventListener = function(name, listener) { 80 | var events = this._events = this._events || {}; 81 | var listeners = events[name] = events[name] || []; 82 | if (listeners.find(fn => fn === listener)) { 83 | return; 84 | } 85 | 86 | listeners.push(listener); 87 | }; 88 | 89 | EventTarget.prototype.removeEventListener = function(name, listener) { 90 | var events = this._events || {}; 91 | var listeners = events[name] || []; 92 | for (var i = listeners.length - 1; i >= 0; i--) { 93 | if (listeners[i] === listener) { 94 | listeners.splice(i, 1); 95 | return; 96 | } 97 | } 98 | }; 99 | 100 | return EventTarget; 101 | 102 | })(); 103 | 104 | },{}],3:[function(require,module,exports){ 105 | /*jshint esnext:true*/ 106 | /*exported HTTPRequest*/ 107 | 'use strict'; 108 | 109 | module.exports = window.HTTPRequest = (function() { 110 | 111 | var EventTarget = require('./event-target'); 112 | var BinaryUtils = require('./binary-utils'); 113 | 114 | const CRLF = '\r\n'; 115 | 116 | function HTTPRequest(socket) { 117 | var parts = []; 118 | var receivedLength = 0; 119 | 120 | var checkRequestComplete = () => { 121 | var contentLength = parseInt(this.headers['Content-Length'], 10); 122 | if (isNaN(contentLength)) { 123 | this.complete = true; 124 | this.dispatchEvent('complete', this); 125 | return; 126 | } 127 | 128 | if (receivedLength < contentLength) { 129 | return; 130 | } 131 | 132 | BinaryUtils.mergeArrayBuffers(parts, (data) => { 133 | this.body = parseBody(this.headers['Content-Type'], data); 134 | this.complete = true; 135 | this.dispatchEvent('complete', this); 136 | }); 137 | 138 | socket.ondata = null; 139 | }; 140 | 141 | socket.ondata = (event) => { 142 | var data = event.data; 143 | 144 | if (parts.length > 0) { 145 | parts.push(data); 146 | receivedLength += data.byteLength; 147 | checkRequestComplete(); 148 | return; 149 | } 150 | 151 | var firstPart = parseHeader(this, data); 152 | if (this.invalid) { 153 | this.dispatchEvent('error', this); 154 | 155 | socket.close(); 156 | socket.ondata = null; 157 | return; 158 | } 159 | 160 | if (firstPart) { 161 | parts.push(firstPart); 162 | receivedLength += firstPart.byteLength; 163 | } 164 | 165 | checkRequestComplete(); 166 | }; 167 | } 168 | 169 | HTTPRequest.prototype = new EventTarget(); 170 | 171 | HTTPRequest.prototype.constructor = HTTPRequest; 172 | 173 | function parseHeader(request, data) { 174 | if (!data) { 175 | request.invalid = true; 176 | return null; 177 | } 178 | 179 | data = BinaryUtils.arrayBufferToString(data); 180 | 181 | var requestParts = data.split(CRLF + CRLF); 182 | 183 | var header = requestParts.shift(); 184 | var body = requestParts.join(CRLF + CRLF); 185 | 186 | var headerLines = header.split(CRLF); 187 | var requestLine = headerLines.shift().split(' '); 188 | 189 | var method = requestLine[0]; 190 | var uri = requestLine[1]; 191 | var version = requestLine[2]; 192 | 193 | if (version !== HTTPServer.HTTP_VERSION) { 194 | request.invalid = true; 195 | return null; 196 | } 197 | 198 | var uriParts = uri.split('?'); 199 | 200 | var path = uriParts.shift(); 201 | var params = parseURLEncodedString(uriParts.join('?')); 202 | 203 | var headers = {}; 204 | headerLines.forEach((headerLine) => { 205 | var parts = headerLine.split(': '); 206 | if (parts.length !== 2) { 207 | return; 208 | } 209 | 210 | var name = parts[0]; 211 | var value = parts[1]; 212 | 213 | headers[name] = value; 214 | }); 215 | 216 | request.method = method; 217 | request.path = path; 218 | request.params = params; 219 | request.headers = headers; 220 | 221 | if (headers['Content-Length']) { 222 | // request.body = parseBody(headers['Content-Type'], body); 223 | return BinaryUtils.stringToArrayBuffer(body); 224 | } 225 | 226 | return null; 227 | } 228 | 229 | function setOrAppendValue(object, name, value) { 230 | var existingValue = object[name]; 231 | if (existingValue === undefined) { 232 | object[name] = value; 233 | } else { 234 | if (Array.isArray(existingValue)) { 235 | existingValue.push(value); 236 | } else { 237 | object[name] = [existingValue, value]; 238 | } 239 | } 240 | } 241 | 242 | function parseURLEncodedString(string) { 243 | var values = {}; 244 | 245 | string.split('&').forEach((pair) => { 246 | if (!pair) { 247 | return; 248 | } 249 | 250 | var parts = decodeURIComponent(pair).split('='); 251 | 252 | var name = parts.shift(); 253 | var value = parts.join('='); 254 | 255 | setOrAppendValue(values, name, value); 256 | }); 257 | 258 | return values; 259 | } 260 | 261 | function parseMultipartFormDataString(string, boundary) { 262 | var values = {}; 263 | 264 | string.split('--' + boundary).forEach((data) => { 265 | data = data.replace(/^\r\n/, '').replace(/\r\n$/, ''); 266 | 267 | if (!data || data === '--') { 268 | return; 269 | } 270 | 271 | var parts = data.split(CRLF + CRLF); 272 | 273 | var header = parts.shift(); 274 | var value = { 275 | headers: {}, 276 | metadata: {}, 277 | value: parts.join(CRLF + CRLF) 278 | }; 279 | 280 | var name; 281 | 282 | var headers = header.split(CRLF); 283 | headers.forEach((header) => { 284 | var headerParams = header.split(';'); 285 | var headerParts = headerParams.shift().split(': '); 286 | 287 | var headerName = headerParts[0]; 288 | var headerValue = headerParts[1]; 289 | 290 | if (headerName !== 'Content-Disposition' || 291 | headerValue !== 'form-data') { 292 | value.headers[headerName] = headerValue; 293 | return; 294 | } 295 | 296 | headerParams.forEach((param) => { 297 | var paramParts = param.trim().split('='); 298 | 299 | var paramName = paramParts[0]; 300 | var paramValue = paramParts[1]; 301 | 302 | paramValue = paramValue.replace(/\"(.*?)\"/, '$1') || paramValue; 303 | 304 | if (paramName === 'name') { 305 | name = paramValue; 306 | } 307 | 308 | else { 309 | value.metadata[paramName] = paramValue; 310 | } 311 | }); 312 | }); 313 | 314 | if (name) { 315 | setOrAppendValue(values, name, value); 316 | } 317 | }); 318 | 319 | return values; 320 | } 321 | 322 | function parseBody(contentType, data) { 323 | contentType = contentType || 'text/plain'; 324 | 325 | var contentTypeParams = contentType.replace(/\s/g, '').split(';'); 326 | var mimeType = contentTypeParams.shift(); 327 | 328 | var body = BinaryUtils.arrayBufferToString(data); 329 | 330 | var result; 331 | 332 | try { 333 | switch (mimeType) { 334 | case 'application/x-www-form-urlencoded': 335 | result = parseURLEncodedString(body); 336 | break; 337 | case 'multipart/form-data': 338 | contentTypeParams.forEach((contentTypeParam) => { 339 | var parts = contentTypeParam.split('='); 340 | 341 | var name = parts[0]; 342 | var value = parts[1]; 343 | 344 | if (name === 'boundary') { 345 | result = parseMultipartFormDataString(body, value); 346 | } 347 | }); 348 | break; 349 | case 'application/json': 350 | result = JSON.parse(body); 351 | break; 352 | case 'application/xml': 353 | result = new DOMParser().parseFromString(body, 'text/xml'); 354 | break; 355 | default: 356 | break; 357 | } 358 | } catch (exception) { 359 | console.log('Unable to parse HTTP request body with Content-Type: ' + contentType); 360 | } 361 | 362 | return result || body; 363 | } 364 | 365 | return HTTPRequest; 366 | 367 | })(); 368 | 369 | },{"./binary-utils":1,"./event-target":2}],4:[function(require,module,exports){ 370 | /*jshint esnext:true*/ 371 | /*exported HTTPResponse*/ 372 | 'use strict'; 373 | 374 | module.exports = window.HTTPResponse = (function() { 375 | 376 | var EventTarget = require('./event-target'); 377 | var BinaryUtils = require('./binary-utils'); 378 | var HTTPStatus = require('./http-status'); 379 | 380 | const CRLF = '\r\n'; 381 | const BUFFER_SIZE = 64 * 1024; 382 | 383 | function HTTPResponse(socket, timeout) { 384 | this.socket = socket; 385 | this.timeout = timeout; 386 | 387 | this.headers = {}; 388 | this.headers['Content-Type'] = 'text/html'; 389 | this.headers['Connection'] = 'close'; 390 | 391 | if (this.timeout) { 392 | this.timeoutHandler = setTimeout(() => { 393 | this.send(null, 500); 394 | }, this.timeout); 395 | } 396 | } 397 | 398 | HTTPResponse.prototype = new EventTarget(); 399 | 400 | HTTPResponse.prototype.constructor = HTTPResponse; 401 | 402 | HTTPResponse.prototype.send = function(body, status) { 403 | return createResponse(body, status, this.headers, (response) => { 404 | var offset = 0; 405 | var remaining = response.byteLength; 406 | 407 | var sendNextPart = () => { 408 | var length = Math.min(remaining, BUFFER_SIZE); 409 | 410 | var bufferFull = this.socket.send(response, offset, length); 411 | 412 | offset += length; 413 | remaining -= length; 414 | 415 | if (remaining > 0) { 416 | if (!bufferFull) { 417 | sendNextPart(); 418 | } 419 | } 420 | 421 | else { 422 | clearTimeout(this.timeoutHandler); 423 | 424 | this.socket.close(); 425 | this.dispatchEvent('complete'); 426 | } 427 | }; 428 | 429 | this.socket.ondrain = sendNextPart; 430 | 431 | sendNextPart(); 432 | }); 433 | }; 434 | 435 | HTTPResponse.prototype.sendFile = function(fileOrPath, status) { 436 | if (fileOrPath instanceof File) { 437 | BinaryUtils.blobToArrayBuffer(fileOrPath, (arrayBuffer) => { 438 | this.send(arrayBuffer, status); 439 | }); 440 | 441 | return; 442 | } 443 | 444 | var xhr = new XMLHttpRequest(); 445 | xhr.open('GET', fileOrPath, true); 446 | xhr.responseType = 'arraybuffer'; 447 | xhr.onload = () => { 448 | this.send(xhr.response, status); 449 | }; 450 | 451 | xhr.send(null); 452 | }; 453 | 454 | function createResponseHeader(status, headers) { 455 | var header = HTTPStatus.getStatusLine(status); 456 | 457 | for (var name in headers) { 458 | header += name + ': ' + headers[name] + CRLF; 459 | } 460 | 461 | return header; 462 | } 463 | 464 | function createResponse(body, status, headers, callback) { 465 | body = body || ''; 466 | status = status || 200; 467 | headers = headers || {}; 468 | 469 | headers['Content-Length'] = body.length || body.byteLength; 470 | 471 | var response = new Blob([ 472 | createResponseHeader(status, headers), 473 | CRLF, 474 | body 475 | ]); 476 | 477 | return BinaryUtils.blobToArrayBuffer(response, callback); 478 | } 479 | 480 | return HTTPResponse; 481 | 482 | })(); 483 | 484 | },{"./binary-utils":1,"./event-target":2,"./http-status":6}],5:[function(require,module,exports){ 485 | /*jshint esnext:true*/ 486 | /*exported HTTPServer*/ 487 | 'use strict'; 488 | 489 | module.exports = window.HTTPServer = (function() { 490 | 491 | var EventTarget = require('./event-target'); 492 | var HTTPRequest = require('./http-request'); 493 | var HTTPResponse = require('./http-response'); 494 | var IPUtils = require('./ip-utils'); 495 | 496 | const DEFAULT_PORT = 8080; 497 | const DEFAULT_TIMEOUT = 20000; 498 | 499 | const CRLF = '\r\n'; 500 | 501 | function HTTPServer(port, options) { 502 | this.port = port || DEFAULT_PORT; 503 | 504 | options = options || {}; 505 | for (var option in options) { 506 | this[option] = options[option]; 507 | } 508 | 509 | this.running = false; 510 | } 511 | 512 | HTTPServer.HTTP_VERSION = 'HTTP/1.1'; 513 | 514 | HTTPServer.prototype = new EventTarget(); 515 | 516 | HTTPServer.prototype.constructor = HTTPServer; 517 | 518 | HTTPServer.prototype.timeout = DEFAULT_TIMEOUT; 519 | 520 | HTTPServer.prototype.start = function() { 521 | if (this.running) { 522 | return; 523 | } 524 | 525 | console.log('Starting HTTP server on port ' + this.port); 526 | 527 | var socket = navigator.mozTCPSocket.listen(this.port, { 528 | binaryType: 'arraybuffer' 529 | }); 530 | 531 | socket.onconnect = (connectEvent) => { 532 | var socket = connectEvent.socket || connectEvent; 533 | var request = new HTTPRequest(socket); 534 | 535 | request.addEventListener('complete', () => { 536 | var response = new HTTPResponse(socket, this.timeout); 537 | 538 | this.dispatchEvent('request', { 539 | request: request, 540 | response: response, 541 | socket: socket 542 | }); 543 | }); 544 | 545 | request.addEventListener('error', () => { 546 | console.warn('Invalid request received'); 547 | }); 548 | }; 549 | 550 | this.socket = socket; 551 | this.running = true; 552 | }; 553 | 554 | HTTPServer.prototype.stop = function() { 555 | if (!this.running) { 556 | return; 557 | } 558 | 559 | console.log('Shutting down HTTP server on port ' + this.port); 560 | 561 | this.socket.close(); 562 | 563 | this.running = false; 564 | }; 565 | 566 | return HTTPServer; 567 | 568 | })(); 569 | 570 | },{"./event-target":2,"./http-request":3,"./http-response":4,"./ip-utils":7}],6:[function(require,module,exports){ 571 | /*jshint esnext:true*/ 572 | /*exported HTTPStatus*/ 573 | 'use strict'; 574 | 575 | module.exports = window.HTTPStatus = (function() { 576 | 577 | const CRLF = '\r\n'; 578 | 579 | var HTTPStatus = {}; 580 | 581 | HTTPStatus.STATUS_CODES = { 582 | 100: 'Continue', 583 | 101: 'Switching Protocols', 584 | 102: 'Processing', 585 | 200: 'OK', 586 | 201: 'Created', 587 | 202: 'Accepted', 588 | 203: 'Non Authoritative Information', 589 | 204: 'No Content', 590 | 205: 'Reset Content', 591 | 206: 'Partial Content', 592 | 207: 'Multi-Status', 593 | 300: 'Mutliple Choices', 594 | 301: 'Moved Permanently', 595 | 302: 'Moved Temporarily', 596 | 303: 'See Other', 597 | 304: 'Not Modified', 598 | 305: 'Use Proxy', 599 | 307: 'Temporary Redirect', 600 | 400: 'Bad Request', 601 | 401: 'Unauthorized', 602 | 402: 'Payment Required', 603 | 403: 'Forbidden', 604 | 404: 'Not Found', 605 | 405: 'Method Not Allowed', 606 | 406: 'Not Acceptable', 607 | 407: 'Proxy Authentication Required', 608 | 408: 'Request Timeout', 609 | 409: 'Conflict', 610 | 410: 'Gone', 611 | 411: 'Length Required', 612 | 412: 'Precondition Failed', 613 | 413: 'Request Entity Too Large', 614 | 414: 'Request-URI Too Long', 615 | 415: 'Unsupported Media Type', 616 | 416: 'Requested Range Not Satisfiable', 617 | 417: 'Expectation Failed', 618 | 419: 'Insufficient Space on Resource', 619 | 420: 'Method Failure', 620 | 422: 'Unprocessable Entity', 621 | 423: 'Locked', 622 | 424: 'Failed Dependency', 623 | 500: 'Server Error', 624 | 501: 'Not Implemented', 625 | 502: 'Bad Gateway', 626 | 503: 'Service Unavailable', 627 | 504: 'Gateway Timeout', 628 | 505: 'HTTP Version Not Supported', 629 | 507: 'Insufficient Storage' 630 | }; 631 | 632 | HTTPStatus.getStatusLine = function(status) { 633 | var reason = HTTPStatus.STATUS_CODES[status] || 'Unknown'; 634 | 635 | return [HTTPServer.HTTP_VERSION, status, reason].join(' ') + CRLF; 636 | }; 637 | 638 | return HTTPStatus; 639 | 640 | })(); 641 | 642 | },{}],7:[function(require,module,exports){ 643 | /*jshint esnext:true*/ 644 | /*exported IPUtils*/ 645 | 'use strict'; 646 | 647 | module.exports = window.IPUtils = (function() { 648 | 649 | const CRLF = '\r\n'; 650 | 651 | var IPUtils = { 652 | getAddresses: function(callback) { 653 | if (typeof callback !== 'function') { 654 | console.warn('No callback provided'); 655 | return; 656 | } 657 | 658 | var addresses = { 659 | '0.0.0.0': true 660 | }; 661 | 662 | var RTCPeerConnection = window.RTCPeerConnection || 663 | window.mozRTCPeerConnection; 664 | 665 | var rtc = new RTCPeerConnection({ iceServers: [] }); 666 | rtc.createDataChannel('', { reliable: false }); 667 | 668 | rtc.onicecandidate = function(evt) { 669 | if (evt.candidate) { 670 | parseSDP('a=' + evt.candidate.candidate); 671 | } 672 | }; 673 | 674 | rtc.createOffer((description) => { 675 | parseSDP(description.sdp); 676 | rtc.setLocalDescription(description, noop, noop); 677 | }, (error) => { 678 | console.warn('Unable to create offer', error); 679 | }); 680 | 681 | function addAddress(address) { 682 | if (addresses[address]) { 683 | return; 684 | } 685 | 686 | addresses[address] = true; 687 | callback(address); 688 | } 689 | 690 | function parseSDP(sdp) { 691 | sdp.split(CRLF).forEach((line) => { 692 | var parts = line.split(' '); 693 | 694 | if (line.indexOf('a=candidate') !== -1) { 695 | if (parts[7] === 'host') { 696 | addAddress(parts[4]); 697 | } 698 | } 699 | 700 | else if (line.indexOf('c=') !== -1) { 701 | addAddress(parts[2]); 702 | } 703 | }); 704 | } 705 | } 706 | }; 707 | 708 | function noop() {} 709 | 710 | return IPUtils; 711 | 712 | })(); 713 | 714 | },{}]},{},[5])(5) 715 | }); -------------------------------------------------------------------------------- /example/upload/manifest.webapp: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.1", 3 | "name": "Upload Web Server", 4 | "description": "An HTTP server file upload example for Firefox OS", 5 | "launch_path": "/index.html", 6 | "icons": { 7 | "16": "/img/icons/icon16x16.png", 8 | "48": "/img/icons/icon48x48.png", 9 | "60": "/img/icons/icon60x60.png", 10 | "128": "/img/icons/icon128x128.png" 11 | }, 12 | "developer": { 13 | "name": "Justin D'Arcangelo", 14 | "url": "https://github.com/justindarc" 15 | }, 16 | "type": "privileged", 17 | "permissions": { 18 | "tcp-socket": {} 19 | }, 20 | "installs_allowed_from": [ 21 | "*" 22 | ], 23 | "locales": {}, 24 | "default_locale": "en" 25 | } 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fxos-web-server", 3 | "version": "0.1.0", 4 | "homepage": "https://github.com/justindarc/fxos-web-server", 5 | "license": "MIT", 6 | "main": "src/http-server.js", 7 | "directories": { 8 | "example": "examples", 9 | "test": "test" 10 | }, 11 | "dependencies": {}, 12 | "devDependencies": { 13 | "browserify": "^7.0.0", 14 | "karma": "^0.12.28", 15 | "karma-firefox-launcher": "^0.1.3", 16 | "karma-mocha": "^0.1.10", 17 | "karma-sinon-chai": "^0.2.0" 18 | }, 19 | "scripts": { 20 | "test": "./node_modules/karma/bin/karma start test/karma.conf.js --single-run", 21 | "build": "mkdir -p dist && ./node_modules/.bin/browserify src/http-server.js --outfile dist/fxos-web-server.js --standalone HTTPServer && mkdir -p example/simple/lib && cp -rf dist/fxos-web-server.js example/simple/lib/fxos-web-server.js && mkdir -p example/p2p/lib && cp -rf dist/fxos-web-server.js example/p2p/lib/fxos-web-server.js && mkdir -p example/directory/lib && cp -rf dist/fxos-web-server.js example/directory/lib/fxos-web-server.js && mkdir -p example/upload/lib && cp -rf dist/fxos-web-server.js example/upload/lib/fxos-web-server.js", 22 | "clean": "rm -rf dist/fxos-web-server.js && rm -rf example/lib/fxos-web-server.js" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "git://github.com/justindarc/fxos-web-server.git" 27 | }, 28 | "bugs": { 29 | "url": "https://github.com/justindarc/fxos-web-server/issues" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/binary-utils.js: -------------------------------------------------------------------------------- 1 | /*jshint esnext:true*/ 2 | /*exported BinaryUtils*/ 3 | 'use strict'; 4 | 5 | module.exports = window.BinaryUtils = (function() { 6 | 7 | var BinaryUtils = { 8 | stringToArrayBuffer: function(string) { 9 | var length = (string || '').length; 10 | var arrayBuffer = new ArrayBuffer(length); 11 | var uint8Array = new Uint8Array(arrayBuffer); 12 | for (var i = 0; i < length; i++) { 13 | uint8Array[i] = string.charCodeAt(i); 14 | } 15 | 16 | return arrayBuffer; 17 | }, 18 | 19 | arrayBufferToString: function(arrayBuffer) { 20 | var results = []; 21 | var uint8Array = new Uint8Array(arrayBuffer); 22 | 23 | for (var i = 0, length = uint8Array.length; i < length; i += 200000) { 24 | results.push(String.fromCharCode.apply(null, uint8Array.subarray(i, i + 200000))); 25 | } 26 | 27 | return results.join(''); 28 | }, 29 | 30 | blobToArrayBuffer: function(blob, callback) { 31 | var fileReader = new FileReader(); 32 | fileReader.onload = function() { 33 | if (typeof callback === 'function') { 34 | callback(fileReader.result); 35 | } 36 | }; 37 | fileReader.readAsArrayBuffer(blob); 38 | 39 | return fileReader.result; 40 | }, 41 | 42 | mergeArrayBuffers: function(arrayBuffers, callback) { 43 | return BinaryUtils.blobToArrayBuffer(new Blob(arrayBuffers), callback); 44 | } 45 | }; 46 | 47 | return BinaryUtils; 48 | 49 | })(); 50 | -------------------------------------------------------------------------------- /src/event-target.js: -------------------------------------------------------------------------------- 1 | /*jshint esnext:true*/ 2 | /*exported EventTarget*/ 3 | 'use strict'; 4 | 5 | module.exports = window.EventTarget = (function() { 6 | 7 | function EventTarget(object) { 8 | if (typeof object !== 'object') { 9 | return; 10 | } 11 | 12 | for (var property in object) { 13 | this[property] = object[property]; 14 | } 15 | } 16 | 17 | EventTarget.prototype.constructor = EventTarget; 18 | 19 | EventTarget.prototype.dispatchEvent = function(name, data) { 20 | var events = this._events || {}; 21 | var listeners = events[name] || []; 22 | listeners.forEach((listener) => { 23 | listener.call(this, data); 24 | }); 25 | }; 26 | 27 | EventTarget.prototype.addEventListener = function(name, listener) { 28 | var events = this._events = this._events || {}; 29 | var listeners = events[name] = events[name] || []; 30 | if (listeners.find(fn => fn === listener)) { 31 | return; 32 | } 33 | 34 | listeners.push(listener); 35 | }; 36 | 37 | EventTarget.prototype.removeEventListener = function(name, listener) { 38 | var events = this._events || {}; 39 | var listeners = events[name] || []; 40 | for (var i = listeners.length - 1; i >= 0; i--) { 41 | if (listeners[i] === listener) { 42 | listeners.splice(i, 1); 43 | return; 44 | } 45 | } 46 | }; 47 | 48 | return EventTarget; 49 | 50 | })(); 51 | -------------------------------------------------------------------------------- /src/http-request.js: -------------------------------------------------------------------------------- 1 | /*jshint esnext:true*/ 2 | /*exported HTTPRequest*/ 3 | 'use strict'; 4 | 5 | module.exports = window.HTTPRequest = (function() { 6 | 7 | var EventTarget = require('./event-target'); 8 | var BinaryUtils = require('./binary-utils'); 9 | 10 | const CRLF = '\r\n'; 11 | 12 | function HTTPRequest(socket) { 13 | var parts = []; 14 | var receivedLength = 0; 15 | 16 | var checkRequestComplete = () => { 17 | var contentLength = parseInt(this.headers['Content-Length'], 10); 18 | if (isNaN(contentLength)) { 19 | this.complete = true; 20 | this.dispatchEvent('complete', this); 21 | return; 22 | } 23 | 24 | if (receivedLength < contentLength) { 25 | return; 26 | } 27 | 28 | BinaryUtils.mergeArrayBuffers(parts, (data) => { 29 | this.body = parseBody(this.headers['Content-Type'], data); 30 | this.complete = true; 31 | this.dispatchEvent('complete', this); 32 | }); 33 | 34 | socket.ondata = null; 35 | }; 36 | 37 | socket.ondata = (event) => { 38 | var data = event.data; 39 | 40 | if (parts.length > 0) { 41 | parts.push(data); 42 | receivedLength += data.byteLength; 43 | checkRequestComplete(); 44 | return; 45 | } 46 | 47 | var firstPart = parseHeader(this, data); 48 | if (this.invalid) { 49 | this.dispatchEvent('error', this); 50 | 51 | socket.close(); 52 | socket.ondata = null; 53 | return; 54 | } 55 | 56 | if (firstPart) { 57 | parts.push(firstPart); 58 | receivedLength += firstPart.byteLength; 59 | } 60 | 61 | checkRequestComplete(); 62 | }; 63 | } 64 | 65 | HTTPRequest.prototype = new EventTarget(); 66 | 67 | HTTPRequest.prototype.constructor = HTTPRequest; 68 | 69 | function parseHeader(request, data) { 70 | if (!data) { 71 | request.invalid = true; 72 | return null; 73 | } 74 | 75 | data = BinaryUtils.arrayBufferToString(data); 76 | 77 | var requestParts = data.split(CRLF + CRLF); 78 | 79 | var header = requestParts.shift(); 80 | var body = requestParts.join(CRLF + CRLF); 81 | 82 | var headerLines = header.split(CRLF); 83 | var requestLine = headerLines.shift().split(' '); 84 | 85 | var method = requestLine[0]; 86 | var uri = requestLine[1]; 87 | var version = requestLine[2]; 88 | 89 | if (version !== HTTPServer.HTTP_VERSION) { 90 | request.invalid = true; 91 | return null; 92 | } 93 | 94 | var uriParts = uri.split('?'); 95 | 96 | var path = uriParts.shift(); 97 | var params = parseURLEncodedString(uriParts.join('?')); 98 | 99 | var headers = {}; 100 | headerLines.forEach((headerLine) => { 101 | var parts = headerLine.split(': '); 102 | if (parts.length !== 2) { 103 | return; 104 | } 105 | 106 | var name = parts[0]; 107 | var value = parts[1]; 108 | 109 | headers[name] = value; 110 | }); 111 | 112 | request.method = method; 113 | request.path = path; 114 | request.params = params; 115 | request.headers = headers; 116 | 117 | if (headers['Content-Length']) { 118 | // request.body = parseBody(headers['Content-Type'], body); 119 | return BinaryUtils.stringToArrayBuffer(body); 120 | } 121 | 122 | return null; 123 | } 124 | 125 | function setOrAppendValue(object, name, value) { 126 | var existingValue = object[name]; 127 | if (existingValue === undefined) { 128 | object[name] = value; 129 | } else { 130 | if (Array.isArray(existingValue)) { 131 | existingValue.push(value); 132 | } else { 133 | object[name] = [existingValue, value]; 134 | } 135 | } 136 | } 137 | 138 | function parseURLEncodedString(string) { 139 | var values = {}; 140 | 141 | string.split('&').forEach((pair) => { 142 | if (!pair) { 143 | return; 144 | } 145 | 146 | var parts = decodeURIComponent(pair).split('='); 147 | 148 | var name = parts.shift(); 149 | var value = parts.join('='); 150 | 151 | setOrAppendValue(values, name, value); 152 | }); 153 | 154 | return values; 155 | } 156 | 157 | function parseMultipartFormDataString(string, boundary) { 158 | var values = {}; 159 | 160 | string.split('--' + boundary).forEach((data) => { 161 | data = data.replace(/^\r\n/, '').replace(/\r\n$/, ''); 162 | 163 | if (!data || data === '--') { 164 | return; 165 | } 166 | 167 | var parts = data.split(CRLF + CRLF); 168 | 169 | var header = parts.shift(); 170 | var value = { 171 | headers: {}, 172 | metadata: {}, 173 | value: parts.join(CRLF + CRLF) 174 | }; 175 | 176 | var name; 177 | 178 | var headers = header.split(CRLF); 179 | headers.forEach((header) => { 180 | var headerParams = header.split(';'); 181 | var headerParts = headerParams.shift().split(': '); 182 | 183 | var headerName = headerParts[0]; 184 | var headerValue = headerParts[1]; 185 | 186 | if (headerName !== 'Content-Disposition' || 187 | headerValue !== 'form-data') { 188 | value.headers[headerName] = headerValue; 189 | return; 190 | } 191 | 192 | headerParams.forEach((param) => { 193 | var paramParts = param.trim().split('='); 194 | 195 | var paramName = paramParts[0]; 196 | var paramValue = paramParts[1]; 197 | 198 | paramValue = paramValue.replace(/\"(.*?)\"/, '$1') || paramValue; 199 | 200 | if (paramName === 'name') { 201 | name = paramValue; 202 | } 203 | 204 | else { 205 | value.metadata[paramName] = paramValue; 206 | } 207 | }); 208 | }); 209 | 210 | if (name) { 211 | setOrAppendValue(values, name, value); 212 | } 213 | }); 214 | 215 | return values; 216 | } 217 | 218 | function parseBody(contentType, data) { 219 | contentType = contentType || 'text/plain'; 220 | 221 | var contentTypeParams = contentType.replace(/\s/g, '').split(';'); 222 | var mimeType = contentTypeParams.shift(); 223 | 224 | var body = BinaryUtils.arrayBufferToString(data); 225 | 226 | var result; 227 | 228 | try { 229 | switch (mimeType) { 230 | case 'application/x-www-form-urlencoded': 231 | result = parseURLEncodedString(body); 232 | break; 233 | case 'multipart/form-data': 234 | contentTypeParams.forEach((contentTypeParam) => { 235 | var parts = contentTypeParam.split('='); 236 | 237 | var name = parts[0]; 238 | var value = parts[1]; 239 | 240 | if (name === 'boundary') { 241 | result = parseMultipartFormDataString(body, value); 242 | } 243 | }); 244 | break; 245 | case 'application/json': 246 | result = JSON.parse(body); 247 | break; 248 | case 'application/xml': 249 | result = new DOMParser().parseFromString(body, 'text/xml'); 250 | break; 251 | default: 252 | break; 253 | } 254 | } catch (exception) { 255 | console.log('Unable to parse HTTP request body with Content-Type: ' + contentType); 256 | } 257 | 258 | return result || body; 259 | } 260 | 261 | return HTTPRequest; 262 | 263 | })(); 264 | -------------------------------------------------------------------------------- /src/http-response.js: -------------------------------------------------------------------------------- 1 | /*jshint esnext:true*/ 2 | /*exported HTTPResponse*/ 3 | 'use strict'; 4 | 5 | module.exports = window.HTTPResponse = (function() { 6 | 7 | var EventTarget = require('./event-target'); 8 | var BinaryUtils = require('./binary-utils'); 9 | var HTTPStatus = require('./http-status'); 10 | 11 | const CRLF = '\r\n'; 12 | const BUFFER_SIZE = 64 * 1024; 13 | 14 | function HTTPResponse(socket, timeout) { 15 | this.socket = socket; 16 | this.timeout = timeout; 17 | 18 | this.headers = {}; 19 | this.headers['Content-Type'] = 'text/html'; 20 | this.headers['Connection'] = 'close'; 21 | 22 | if (this.timeout) { 23 | this.timeoutHandler = setTimeout(() => { 24 | this.send(null, 500); 25 | }, this.timeout); 26 | } 27 | } 28 | 29 | HTTPResponse.prototype = new EventTarget(); 30 | 31 | HTTPResponse.prototype.constructor = HTTPResponse; 32 | 33 | HTTPResponse.prototype.send = function(body, status) { 34 | return createResponse(body, status, this.headers, (response) => { 35 | var offset = 0; 36 | var remaining = response.byteLength; 37 | 38 | var sendNextPart = () => { 39 | var length = Math.min(remaining, BUFFER_SIZE); 40 | 41 | var bufferFull = this.socket.send(response, offset, length); 42 | 43 | offset += length; 44 | remaining -= length; 45 | 46 | if (remaining > 0) { 47 | if (!bufferFull) { 48 | sendNextPart(); 49 | } 50 | } 51 | 52 | else { 53 | clearTimeout(this.timeoutHandler); 54 | 55 | this.socket.close(); 56 | this.dispatchEvent('complete'); 57 | } 58 | }; 59 | 60 | this.socket.ondrain = sendNextPart; 61 | 62 | sendNextPart(); 63 | }); 64 | }; 65 | 66 | HTTPResponse.prototype.sendFile = function(fileOrPath, status) { 67 | if (fileOrPath instanceof File) { 68 | BinaryUtils.blobToArrayBuffer(fileOrPath, (arrayBuffer) => { 69 | this.send(arrayBuffer, status); 70 | }); 71 | 72 | return; 73 | } 74 | 75 | var xhr = new XMLHttpRequest(); 76 | xhr.open('GET', fileOrPath, true); 77 | xhr.responseType = 'arraybuffer'; 78 | xhr.onload = () => { 79 | this.send(xhr.response, status); 80 | }; 81 | 82 | xhr.send(null); 83 | }; 84 | 85 | function createResponseHeader(status, headers) { 86 | var header = HTTPStatus.getStatusLine(status); 87 | 88 | for (var name in headers) { 89 | header += name + ': ' + headers[name] + CRLF; 90 | } 91 | 92 | return header; 93 | } 94 | 95 | function createResponse(body, status, headers, callback) { 96 | body = body || ''; 97 | status = status || 200; 98 | headers = headers || {}; 99 | 100 | headers['Content-Length'] = body.length || body.byteLength; 101 | 102 | var response = new Blob([ 103 | createResponseHeader(status, headers), 104 | CRLF, 105 | body 106 | ]); 107 | 108 | return BinaryUtils.blobToArrayBuffer(response, callback); 109 | } 110 | 111 | return HTTPResponse; 112 | 113 | })(); 114 | -------------------------------------------------------------------------------- /src/http-server.js: -------------------------------------------------------------------------------- 1 | /*jshint esnext:true*/ 2 | /*exported HTTPServer*/ 3 | 'use strict'; 4 | 5 | module.exports = window.HTTPServer = (function() { 6 | 7 | var EventTarget = require('./event-target'); 8 | var HTTPRequest = require('./http-request'); 9 | var HTTPResponse = require('./http-response'); 10 | var IPUtils = require('./ip-utils'); 11 | 12 | const DEFAULT_PORT = 8080; 13 | const DEFAULT_TIMEOUT = 20000; 14 | 15 | const CRLF = '\r\n'; 16 | 17 | function HTTPServer(port, options) { 18 | this.port = port || DEFAULT_PORT; 19 | 20 | options = options || {}; 21 | for (var option in options) { 22 | this[option] = options[option]; 23 | } 24 | 25 | this.running = false; 26 | } 27 | 28 | HTTPServer.HTTP_VERSION = 'HTTP/1.1'; 29 | 30 | HTTPServer.prototype = new EventTarget(); 31 | 32 | HTTPServer.prototype.constructor = HTTPServer; 33 | 34 | HTTPServer.prototype.timeout = DEFAULT_TIMEOUT; 35 | 36 | HTTPServer.prototype.start = function() { 37 | if (this.running) { 38 | return; 39 | } 40 | 41 | console.log('Starting HTTP server on port ' + this.port); 42 | 43 | var socket = navigator.mozTCPSocket.listen(this.port, { 44 | binaryType: 'arraybuffer' 45 | }); 46 | 47 | socket.onconnect = (connectEvent) => { 48 | var socket = connectEvent.socket || connectEvent; 49 | var request = new HTTPRequest(socket); 50 | 51 | request.addEventListener('complete', () => { 52 | var response = new HTTPResponse(socket, this.timeout); 53 | 54 | this.dispatchEvent('request', { 55 | request: request, 56 | response: response, 57 | socket: socket 58 | }); 59 | }); 60 | 61 | request.addEventListener('error', () => { 62 | console.warn('Invalid request received'); 63 | }); 64 | }; 65 | 66 | this.socket = socket; 67 | this.running = true; 68 | }; 69 | 70 | HTTPServer.prototype.stop = function() { 71 | if (!this.running) { 72 | return; 73 | } 74 | 75 | console.log('Shutting down HTTP server on port ' + this.port); 76 | 77 | this.socket.close(); 78 | 79 | this.running = false; 80 | }; 81 | 82 | return HTTPServer; 83 | 84 | })(); 85 | -------------------------------------------------------------------------------- /src/http-status.js: -------------------------------------------------------------------------------- 1 | /*jshint esnext:true*/ 2 | /*exported HTTPStatus*/ 3 | 'use strict'; 4 | 5 | module.exports = window.HTTPStatus = (function() { 6 | 7 | const CRLF = '\r\n'; 8 | 9 | var HTTPStatus = {}; 10 | 11 | HTTPStatus.STATUS_CODES = { 12 | 100: 'Continue', 13 | 101: 'Switching Protocols', 14 | 102: 'Processing', 15 | 200: 'OK', 16 | 201: 'Created', 17 | 202: 'Accepted', 18 | 203: 'Non Authoritative Information', 19 | 204: 'No Content', 20 | 205: 'Reset Content', 21 | 206: 'Partial Content', 22 | 207: 'Multi-Status', 23 | 300: 'Mutliple Choices', 24 | 301: 'Moved Permanently', 25 | 302: 'Moved Temporarily', 26 | 303: 'See Other', 27 | 304: 'Not Modified', 28 | 305: 'Use Proxy', 29 | 307: 'Temporary Redirect', 30 | 400: 'Bad Request', 31 | 401: 'Unauthorized', 32 | 402: 'Payment Required', 33 | 403: 'Forbidden', 34 | 404: 'Not Found', 35 | 405: 'Method Not Allowed', 36 | 406: 'Not Acceptable', 37 | 407: 'Proxy Authentication Required', 38 | 408: 'Request Timeout', 39 | 409: 'Conflict', 40 | 410: 'Gone', 41 | 411: 'Length Required', 42 | 412: 'Precondition Failed', 43 | 413: 'Request Entity Too Large', 44 | 414: 'Request-URI Too Long', 45 | 415: 'Unsupported Media Type', 46 | 416: 'Requested Range Not Satisfiable', 47 | 417: 'Expectation Failed', 48 | 419: 'Insufficient Space on Resource', 49 | 420: 'Method Failure', 50 | 422: 'Unprocessable Entity', 51 | 423: 'Locked', 52 | 424: 'Failed Dependency', 53 | 500: 'Server Error', 54 | 501: 'Not Implemented', 55 | 502: 'Bad Gateway', 56 | 503: 'Service Unavailable', 57 | 504: 'Gateway Timeout', 58 | 505: 'HTTP Version Not Supported', 59 | 507: 'Insufficient Storage' 60 | }; 61 | 62 | HTTPStatus.getStatusLine = function(status) { 63 | var reason = HTTPStatus.STATUS_CODES[status] || 'Unknown'; 64 | 65 | return [HTTPServer.HTTP_VERSION, status, reason].join(' ') + CRLF; 66 | }; 67 | 68 | return HTTPStatus; 69 | 70 | })(); 71 | -------------------------------------------------------------------------------- /src/ip-utils.js: -------------------------------------------------------------------------------- 1 | /*jshint esnext:true*/ 2 | /*exported IPUtils*/ 3 | 'use strict'; 4 | 5 | module.exports = window.IPUtils = (function() { 6 | 7 | const CRLF = '\r\n'; 8 | 9 | var IPUtils = { 10 | getAddresses: function(callback) { 11 | if (typeof callback !== 'function') { 12 | console.warn('No callback provided'); 13 | return; 14 | } 15 | 16 | var addresses = { 17 | '0.0.0.0': true 18 | }; 19 | 20 | var RTCPeerConnection = window.RTCPeerConnection || 21 | window.mozRTCPeerConnection; 22 | 23 | var rtc = new RTCPeerConnection({ iceServers: [] }); 24 | rtc.createDataChannel('', { reliable: false }); 25 | 26 | rtc.onicecandidate = function(evt) { 27 | if (evt.candidate) { 28 | parseSDP('a=' + evt.candidate.candidate); 29 | } 30 | }; 31 | 32 | rtc.createOffer((description) => { 33 | parseSDP(description.sdp); 34 | rtc.setLocalDescription(description, noop, noop); 35 | }, (error) => { 36 | console.warn('Unable to create offer', error); 37 | }); 38 | 39 | function addAddress(address) { 40 | if (addresses[address]) { 41 | return; 42 | } 43 | 44 | addresses[address] = true; 45 | callback(address); 46 | } 47 | 48 | function parseSDP(sdp) { 49 | sdp.split(CRLF).forEach((line) => { 50 | var parts = line.split(' '); 51 | 52 | if (line.indexOf('a=candidate') !== -1) { 53 | if (parts[7] === 'host') { 54 | addAddress(parts[4]); 55 | } 56 | } 57 | 58 | else if (line.indexOf('c=') !== -1) { 59 | addAddress(parts[2]); 60 | } 61 | }); 62 | } 63 | } 64 | }; 65 | 66 | function noop() {} 67 | 68 | return IPUtils; 69 | 70 | })(); 71 | -------------------------------------------------------------------------------- /test/karma.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(config) { 4 | config.set({ 5 | frameworks: ['mocha', 'sinon-chai'], 6 | browsers: ['firefox_latest'], 7 | client: { 8 | captureConsole: true, 9 | mocha: { 'ui': 'tdd' } 10 | }, 11 | basePath: '../', 12 | 13 | customLaunchers: { 14 | firefox_latest: { 15 | base: 'FirefoxNightly', 16 | prefs: { 17 | 'dom.udpsocket.enabled': true 18 | } 19 | } 20 | }, 21 | 22 | files: [ 23 | 'dist/fxos-web-server.js', 24 | 'test/mock/navigator-moztcpsocket.js', 25 | 'test/unit/http-server.js' 26 | ] 27 | }); 28 | }; 29 | -------------------------------------------------------------------------------- /test/mock/navigator-moztcpsocket.js: -------------------------------------------------------------------------------- 1 | /*jshint esnext:true*/ 2 | /*exported MockMozTCPSocket*/ 3 | 'use strict'; 4 | 5 | window.MockMozTCPSocket = (function() { 6 | 7 | var MockMozTCPSocket = { 8 | listen: sinon.spy() 9 | }; 10 | 11 | return MockMozTCPSocket; 12 | 13 | })(); 14 | -------------------------------------------------------------------------------- /test/unit/http-server.js: -------------------------------------------------------------------------------- 1 | suite('HTTPServer', function() { 2 | /*jshint esnext:true*/ 3 | 'use strict'; 4 | 5 | suiteSetup(function() { 6 | 7 | }); 8 | 9 | suiteTeardown(function() { 10 | 11 | }); 12 | 13 | setup(function() { 14 | 15 | }); 16 | 17 | teardown(function() { 18 | 19 | }); 20 | 21 | suite('HTTPServer()', function() { 22 | test('Should set TCP port', function() { 23 | var httpServer = new HTTPServer(1234); 24 | 25 | assert.equal(httpServer.port, 1234); 26 | }); 27 | }); 28 | }); 29 | --------------------------------------------------------------------------------