├── test
├── fixtures
│ ├── hello.txt
│ ├── hello.json
│ └── xhr2.png
├── src
│ ├── helpers
│ │ ├── browser_mocha_setup.coffee
│ │ ├── browser_mocha_runner.coffee
│ │ ├── setup.coffee
│ │ └── xhr_server.coffee
│ ├── responseurl_test.coffee
│ ├── status_test.coffee
│ ├── redirect_test.coffee
│ ├── event_target_test.coffee
│ ├── response_type_test.coffee
│ ├── xhr_test.coffee
│ ├── send_test.coffee
│ ├── nodejs_set_test.coffee
│ ├── headers_test.coffee
│ └── events_test.coffee
└── html
│ └── browser_test.html
├── lib
└── browser.js
├── .travis.yml
├── .vimrc
├── .gitignore
├── .npmignore
├── src
├── errors.coffee
├── progress_event.coffee
├── 000-xml_http_request_event_target.coffee
├── xml_http_request_upload.coffee
└── 001-xml_http_request.coffee
├── LICENSE.txt
├── CONTRIBUTING.md
├── package.json
├── README.md
└── Cakefile
/test/fixtures/hello.txt:
--------------------------------------------------------------------------------
1 | Hello world!
2 |
--------------------------------------------------------------------------------
/lib/browser.js:
--------------------------------------------------------------------------------
1 | module.exports = XMLHttpRequest;
2 |
--------------------------------------------------------------------------------
/test/fixtures/hello.json:
--------------------------------------------------------------------------------
1 | {"hello": "world", "answer": 42}
2 |
--------------------------------------------------------------------------------
/test/fixtures/xhr2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pwnall/node-xhr2/HEAD/test/fixtures/xhr2.png
--------------------------------------------------------------------------------
/test/src/helpers/browser_mocha_setup.coffee:
--------------------------------------------------------------------------------
1 | mocha.setup ui: 'bdd', slow: 150, timeout: 1000, bail: false
2 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | dist: focal
2 | language: node_js
3 | os: linux
4 | node_js:
5 | - "10"
6 | - "12"
7 | - "14"
8 | - "15"
9 |
--------------------------------------------------------------------------------
/.vimrc:
--------------------------------------------------------------------------------
1 | " Indentation settings for the project: 2-space indentation, no tabs.
2 | set tabstop=2
3 | set softtabstop=2
4 | set shiftwidth=2
5 | set expandtab
6 |
7 |
--------------------------------------------------------------------------------
/test/src/helpers/browser_mocha_runner.coffee:
--------------------------------------------------------------------------------
1 | window.addEventListener 'load', ->
2 | runner = null
3 | runner = mocha.run ->
4 | return if runner is null # Synchronous tests may call this spuriously.
5 | failures = runner.failures || 0
6 | total = runner.total || 0
7 | image = new Image()
8 | image.src = "/diediedie?failed=#{failures}&total=#{total}";
9 | image.onload = ->
10 | null
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Vim.
2 | *.swp
3 |
4 | # OSX
5 | .DS_Store
6 |
7 | # Npm modules.
8 | node_modules
9 |
10 | # Vendored javascript modules.
11 | test/vendor
12 |
13 | # Build output.
14 | lib/xhr2.js
15 | test/js
16 | tmp/*.js
17 |
18 | # Documentation output.
19 | doc/*.html
20 | doc/assets
21 | doc/classes
22 | doc/files
23 |
24 | # Node packaging output.
25 | xhr2-*.tgz
26 |
27 | # Potentially sensitive keys used during testing.
28 | test/ssl
29 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | # Vim.
2 | *.swp
3 | .vimrc
4 |
5 | # OSX
6 | .DS_Store
7 |
8 | # Git.
9 | .git
10 |
11 | # Travis.
12 | .travis.yml
13 |
14 | # Npm modules.
15 | node_modules
16 |
17 | # Vendored javascript modules.
18 | test/vendor
19 |
20 | # Test build output.
21 | test/js
22 | tmp/*.js
23 |
24 | # Documentation output.
25 | doc/*.html
26 | doc/assets
27 | doc/classes
28 | doc/files
29 |
30 | # Potentially sensitive credentials and keys used during testing.
31 | test/ssl
32 |
--------------------------------------------------------------------------------
/test/src/responseurl_test.coffee:
--------------------------------------------------------------------------------
1 | describe 'XMLHttpRequest', ->
2 | describe '#responseURL', ->
3 | beforeEach ->
4 | @xhr = new XMLHttpRequest
5 |
6 | it 'provies the URL of the response', (done) ->
7 | @xhr.open 'GET', 'http://localhost:8912/_/method'
8 | @xhr.onload = =>
9 | expect(@xhr.responseURL).to.equal("http://localhost:8912/_/method")
10 | done()
11 | @xhr.send()
12 |
13 | it 'ignores the hash fragment', (done) ->
14 | @xhr.open 'GET', 'http://localhost:8912/_/method#foo'
15 | @xhr.onload = =>
16 | expect(@xhr.responseURL).to.equal("http://localhost:8912/_/method")
17 | done()
18 | @xhr.send()
19 |
--------------------------------------------------------------------------------
/src/errors.coffee:
--------------------------------------------------------------------------------
1 | # This file defines the custom errors used in the XMLHttpRequest specification.
2 |
3 | # Thrown if the XHR security policy is violated.
4 | class SecurityError extends Error
5 | # @private
6 | constructor: -> super()
7 |
8 | # Thrown if the XHR security policy is violated.
9 | XMLHttpRequest.SecurityError = SecurityError
10 |
11 |
12 | # Usually thrown if the XHR is in the wrong readyState for an operation.
13 | class InvalidStateError extends Error
14 | # @private
15 | constructor: -> super()
16 |
17 | # Usually thrown if the XHR is in the wrong readyState for an operation.
18 | class InvalidStateError extends Error
19 | XMLHttpRequest.InvalidStateError = InvalidStateError
20 |
21 | # Thrown if there is a problem with the URL passed to the XHR.
22 | class NetworkError extends Error
23 | # @private
24 | constructor: -> super()
25 |
26 | # Thrown if parsing URLs errors out.
27 | XMLHttpRequest.SyntaxError = SyntaxError
28 |
29 | class SyntaxError extends Error
30 | # @private:
31 | constructor: -> super()
32 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2013 Victor Costan
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of
4 | this software and associated documentation files (the "Software"), to deal in
5 | the Software without restriction, including without limitation the rights to
6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
7 | of the Software, and to permit persons to whom the Software is furnished to do
8 | so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
20 |
--------------------------------------------------------------------------------
/test/html/browser_test.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | node-xhr2 browser tests
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/src/progress_event.coffee:
--------------------------------------------------------------------------------
1 | # http://xhr.spec.whatwg.org/#interface-progressevent
2 | class ProgressEvent
3 | # Creates a new event.
4 | #
5 | # @param {String} type the event type, e.g. 'readystatechange'; must be
6 | # lowercased
7 | constructor: (@type) ->
8 | @target = null
9 | @currentTarget = null
10 | @lengthComputable = false
11 | @loaded = 0
12 | @total = 0
13 | # Getting the time from the OS is expensive, skip on that for now.
14 | # @timeStamp = Date.now()
15 |
16 | # @property {Boolean} for compatibility with DOM events
17 | bubbles: false
18 |
19 | # @property {Boolean} for fompatibility with DOM events
20 | cancelable: false
21 |
22 | # @property {XMLHttpRequest} the request that caused this event
23 | target: null
24 |
25 | # @property {Number} number of bytes that have already been downloaded or
26 | # uploaded
27 | loaded: null
28 |
29 | # @property {Boolean} true if the Content-Length response header is available
30 | # and the value of the event's total property is meaningful
31 | lengthComputable: null
32 |
33 | # @property {Number} number of bytes that will be downloaded or uploaded by
34 | # the request that fired the event
35 | total: null
36 |
37 |
38 | # The XHR spec exports the ProgressEvent constructor.
39 | XMLHttpRequest.ProgressEvent = ProgressEvent
40 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # xhr2 Contribution Policy
2 |
3 | This is an open-source library and welcomes outside contributions.
4 |
5 | Please file bugs on
6 | [the GitHub issue page](https://github.com/pwnall/xhr2/issues).
7 |
8 | Please submit patches as
9 | [GitHub pull requests](https://help.github.com/articles/using-pull-requests).
10 | Please check the
11 | [existing pull requests](https://github.com/pwnall/xhr2/issues) to avoid
12 | duplicating effort.
13 |
14 |
15 | ## Pull Request Checklist
16 |
17 | * Do not modify the version in `package.json` or the commit history. Feel free
18 | to rebase your commits while the pull request is in progress.
19 | * If your patch adds new functionality, please make sure to include link to the
20 | relevant parts of the
21 | [W3C XMLHttpRequest specification](http://www.w3.org/TR/XMLHttpRequest/). Use
22 | the same style as existing source code.
23 | * Include tests whenever possible, so the functionality won't be broken by
24 | accident in future releases.
25 |
26 |
27 | ## Obligatory Legalese
28 |
29 | By submitting a contribution to the library, you grant Victor Costan
30 | (the library's author) a non-exclusive, irrevocable, perpetual, transferable,
31 | license to use, reproduce, modify, adapt, publish, translate, create derivative
32 | works from, distribute, perform and display your contribution (in whole or
33 | part) worldwide under the MIT license.
34 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "xhr2",
3 | "version": "0.2.1",
4 | "description": "XMLHttpRequest emulation for node.js",
5 | "keywords": [
6 | "xhr",
7 | "xmlhttprequest",
8 | "ajax",
9 | "browser"
10 | ],
11 | "homepage": "https://github.com/pwnall/node-xhr2",
12 | "author": "Victor Costan (http://www.costan.us)",
13 | "contributors": [
14 | "Alexander Black ",
15 | "Daniel Friedman ",
16 | "Eugen Mayer ",
17 | "Francois-Xavier Kowalski ",
18 | "Sébastien Nicouleaud"
19 | ],
20 | "license": "MIT",
21 | "repository": {
22 | "type": "git",
23 | "url": "https://github.com/pwnall/node-xhr2.git"
24 | },
25 | "bugs": {
26 | "url": "https://github.com/pwnall/node-xhr2/issues"
27 | },
28 | "engines": {
29 | "node": ">= 6"
30 | },
31 | "dependencies": {},
32 | "devDependencies": {
33 | "async": ">=3.0.1",
34 | "chai": ">=4.2.0",
35 | "codo": ">=2.1.2",
36 | "coffeescript": ">=2.4.1",
37 | "express": ">=4.17.1",
38 | "glob": ">=7.1.4",
39 | "mocha": ">=6.1.4",
40 | "open": ">=6.3.0",
41 | "remove": ">= 0.1.5",
42 | "sinon": ">=7.3.2",
43 | "sinon-chai": ">=3.3.0"
44 | },
45 | "main": "lib/xhr2.js",
46 | "browser": "lib/browser.js",
47 | "directories": {
48 | "doc": "doc",
49 | "lib": "lib",
50 | "src": "src",
51 | "test": "test"
52 | },
53 | "scripts": {
54 | "prepublish": "cake build",
55 | "test": "cake test"
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/test/src/helpers/setup.coffee:
--------------------------------------------------------------------------------
1 | if typeof window is 'undefined'
2 | # node.js
3 | global.XMLHttpRequest = require '../../../lib/xhr2'
4 | global.ProgressEvent = XMLHttpRequest.ProgressEvent
5 | global.NetworkError = XMLHttpRequest.NetworkError
6 | global.SecurityError = XMLHttpRequest.SecurityError
7 | global.InvalidStateError = XMLHttpRequest.InvalidStateError
8 |
9 | global.chai = require 'chai'
10 | global.assert = global.chai.assert
11 | global.expect = global.chai.expect
12 | global.sinon = require 'sinon'
13 | global.sinonChai = require 'sinon-chai'
14 |
15 | xhrServer = require './xhr_server'
16 | require './xhr2.png.js'
17 |
18 | https = require 'https'
19 | agent = new https.Agent
20 | agent.options.rejectUnauthorized = true
21 | agent.options.ca = xhrServer.https.sslCertificate()
22 | global.XMLHttpRequest.nodejsSet httpsAgent: agent
23 | global.XMLHttpRequest.nodejsSet(
24 | baseUrl: xhrServer.http.testUrl().replace('https://', 'http://'))
25 | else
26 | # browser
27 |
28 | # HACK(pwnall): the test is first loaded on https so the developer can bypass
29 | # the SSL interstitial; however, we need to run the test on http, because
30 | # Chrome blocks https -> http XHRs
31 | if window.location.href.indexOf('https://') is 0
32 | window.location.href = window.location.href.replace('https://', 'http://').
33 | replace(':8911', ':8912')
34 |
35 | window.NetworkError = window.Error
36 | window.SecurityError = window.Error
37 | window.assert = window.chai.assert
38 | window.expect = window.chai.expect
39 |
--------------------------------------------------------------------------------
/test/src/status_test.coffee:
--------------------------------------------------------------------------------
1 | describe 'XMLHttpRequest', ->
2 | beforeEach ->
3 | @xhr = new XMLHttpRequest
4 | @okUrl = 'http://localhost:8912/test/fixtures/hello.txt'
5 |
6 | @errorUrl = 'http://localhost:8912/_/response'
7 | @errorJson = JSON.stringify
8 | code: 401, status: 'Unauthorized',
9 | body: JSON.stringify(error: 'Credential error'),
10 | headers:
11 | 'Content-Type': 'application/json', 'Content-Length': '28'
12 |
13 | describe '#status', ->
14 | it 'is 200 for a normal request', (done) ->
15 | @xhr.open 'GET', @okUrl
16 | _done = false
17 | @xhr.addEventListener 'readystatechange', =>
18 | return if _done
19 | if @xhr.readyState < XMLHttpRequest.HEADERS_RECEIVED
20 | expect(@xhr.status).to.equal 0
21 | expect(@xhr.statusText).to.equal ''
22 | else
23 | expect(@xhr.status).to.equal 200
24 | expect(@xhr.statusText).to.be.ok
25 | expect(@xhr.statusText).to.not.equal ''
26 | if @xhr.readyState is XMLHttpRequest.DONE
27 | _done = true
28 | done()
29 | @xhr.send()
30 |
31 | it 'returns the server-reported status', (done) ->
32 | @xhr.open 'POST', @errorUrl
33 | _done = false
34 | @xhr.addEventListener 'readystatechange', =>
35 | return if _done
36 | if @xhr.readyState < XMLHttpRequest.HEADERS_RECEIVED
37 | expect(@xhr.status).to.equal 0
38 | expect(@xhr.statusText).to.equal ''
39 | else
40 | expect(@xhr.status).to.equal 401
41 | expect(@xhr.statusText).to.be.ok
42 | expect(@xhr.statusText).to.not.equal ''
43 | if @xhr.readyState is XMLHttpRequest.DONE
44 | _done = true
45 | done()
46 | @xhr.send @errorJson
47 |
48 |
--------------------------------------------------------------------------------
/test/src/redirect_test.coffee:
--------------------------------------------------------------------------------
1 | describe 'XMLHttpRequest', ->
2 | describe 'when redirected', ->
3 | beforeEach ->
4 | @xhr = new XMLHttpRequest
5 |
6 | it 'issues a GET for the next location', (done) ->
7 | @xhr.open 'POST', 'http://localhost:8912/_/redirect/302/method'
8 | @xhr.onload = =>
9 | expect(@xhr.responseText).to.match(/GET/i)
10 | done()
11 | @xhr.send 'This should be dropped during the redirect'
12 |
13 | it 'does not return the redirect headers', (done) ->
14 | @xhr.open 'GET', 'http://localhost:8912/_/redirect/302/method'
15 | @xhr.onload = =>
16 | expect(@xhr.getResponseHeader('Content-Type')).to.equal(
17 | 'text/plain; charset=utf-8')
18 | expect(@xhr.getResponseHeader('X-Redirect-Header')).not.to.be.ok
19 | done()
20 | @xhr.send()
21 |
22 | it 'persists custom request headers across redirects', (done) ->
23 | @xhr.open 'GET', 'http://localhost:8912/_/redirect/302/headers'
24 | @xhr.setRequestHeader 'X-Redirect-Test', 'should be preserved'
25 | @xhr.onload = =>
26 | expect(@xhr.responseText).to.match(/^\{.*\}$/)
27 | headers = JSON.parse @xhr.responseText
28 | expect(headers['connection']).to.equal 'keep-alive'
29 | expect(headers).to.have.property 'host'
30 | expect(headers['host']).to.equal 'localhost:8912'
31 | expect(headers).to.have.property 'x-redirect-test'
32 | expect(headers['x-redirect-test']).to.equal 'should be preserved'
33 | done()
34 | @xhr.send()
35 |
36 | it 'drops content-related headers across redirects', (done) ->
37 | @xhr.open 'POST', 'http://localhost:8912/_/redirect/302/headers'
38 | @xhr.setRequestHeader 'X-Redirect-Test', 'should be preserved'
39 | @xhr.onload = =>
40 | expect(@xhr.responseText).to.match(/^\{.*\}$/)
41 | headers = JSON.parse @xhr.responseText
42 | expect(headers['connection']).to.equal 'keep-alive'
43 | expect(headers).to.have.property 'host'
44 | expect(headers['host']).to.equal 'localhost:8912'
45 | expect(headers).to.have.property 'x-redirect-test'
46 | expect(headers['x-redirect-test']).to.equal 'should be preserved'
47 | expect(headers).not.to.have.property 'content-type'
48 | expect(headers).not.to.have.property 'content-length'
49 | done()
50 | @xhr.send 'This should be dropped during the redirect'
51 |
52 | it 'provides the final responseURL', (done) ->
53 | @xhr.open 'GET', 'http://localhost:8912/_/redirect/302/method'
54 | @xhr.onload = =>
55 | expect(@xhr.responseURL).to.equal("http://localhost:8912/_/method")
56 | done()
57 | @xhr.send()
58 |
--------------------------------------------------------------------------------
/test/src/event_target_test.coffee:
--------------------------------------------------------------------------------
1 | describe 'XMLHttpRequestEventTarget', ->
2 | describe 'dispatchEvent', ->
3 | beforeEach ->
4 | @xhr = new XMLHttpRequest
5 | @loadEvent = new ProgressEvent 'load'
6 |
7 | it 'works with a DOM0 listener', ->
8 | count = 0
9 | @xhr.onload = (event) ->
10 | count += 1
11 | @xhr.dispatchEvent @loadEvent
12 | expect(count).to.equal 1
13 |
14 | it 'works with a DOM2 listener', ->
15 | count = 0
16 | @xhr.addEventListener 'load', (event) ->
17 | count += 1
18 | @xhr.dispatchEvent @loadEvent
19 | expect(count).to.equal 1
20 |
21 | it 'removes a DOM2 listener correctly', ->
22 | count = 0
23 | listener = (event) ->
24 | count += 1
25 | @xhr.addEventListener 'load', listener
26 | @xhr.dispatchEvent @loadEvent
27 | expect(count).to.equal 1
28 |
29 | count = 0
30 | @xhr.removeEventListener 'load', listener
31 | @xhr.dispatchEvent @loadEvent
32 | expect(count).to.equal 0
33 |
34 | it 'binds this correctly in a DOM0 listener', ->
35 | eventThis = null
36 | @xhr.onload = (event) ->
37 | eventThis = @
38 | @xhr.dispatchEvent @loadEvent
39 | expect(eventThis).to.equal @xhr
40 |
41 | it 'binds this correctly in a DOM2 listener', ->
42 | eventThis = null
43 | @xhr.addEventListener 'load', (event) ->
44 | eventThis = @
45 | @xhr.dispatchEvent @loadEvent
46 | expect(eventThis).to.equal @xhr
47 |
48 | it 'sets target correctly in a DOM0 listener', ->
49 | eventTarget = null
50 | @xhr.onload = (event) ->
51 | eventTarget = event.target
52 | @xhr.dispatchEvent @loadEvent
53 | expect(eventTarget).to.equal @xhr
54 |
55 | it 'sets target correctly in a DOM2 listener', ->
56 | eventTarget = null
57 | @xhr.addEventListener 'load', (event) ->
58 | eventTarget = event.target
59 | @xhr.dispatchEvent @loadEvent
60 | expect(eventTarget).to.equal @xhr
61 |
62 | it 'works with a DOM0 and two DOM2 listeners', ->
63 | count = [0, 0, 0]
64 | @xhr.addEventListener 'load', (event) ->
65 | count[1] += 1
66 | @xhr.onload = (event) ->
67 | count[0] += 1
68 | @xhr.addEventListener 'load', (event) ->
69 | count[2] += 1
70 | @xhr.dispatchEvent @loadEvent
71 | expect(count).to.deep.equal [1, 1, 1]
72 |
73 | it 'does not invoke a DOM0 listener for a different event', ->
74 | count = 0
75 | @xhr.onerror = (event) ->
76 | count += 1
77 | @xhr.dispatchEvent @loadEvent
78 | expect(count).to.equal 0
79 |
80 | it 'does not invoke a DOM2 listener for a different event', ->
81 | count = 0
82 | @xhr.addEventListener 'error', (event) ->
83 | count += 1
84 | @xhr.dispatchEvent @loadEvent
85 | expect(count).to.equal 0
86 |
--------------------------------------------------------------------------------
/src/000-xml_http_request_event_target.coffee:
--------------------------------------------------------------------------------
1 | # This file's name is set up in such a way that it will always show up first in
2 | # the list of files given to coffee --join, so that the other files can assume
3 | # that XMLHttpRequestEventTarget was already defined.
4 |
5 |
6 | # The DOM EventTarget subclass used by XMLHttpRequest.
7 | #
8 | # @see http://xhr.spec.whatwg.org/#interface-xmlhttprequest
9 | class XMLHttpRequestEventTarget
10 | # @private
11 | # This is an abstract class and should not be instantiated directly.
12 | constructor: ->
13 | @onloadstart = null
14 | @onprogress = null
15 | @onabort = null
16 | @onerror = null
17 | @onload = null
18 | @ontimeout = null
19 | @onloadend = null
20 | @_listeners = {}
21 |
22 | # @property {function(ProgressEvent)} DOM level 0-style handler
23 | # for the 'loadstart' event
24 | onloadstart: null
25 |
26 | # @property {function(ProgressEvent)} DOM level 0-style handler
27 | # for the 'progress' event
28 | onprogress: null
29 |
30 | # @property {function(ProgressEvent)} DOM level 0-style handler
31 | # for the 'abort' event
32 | onabort: null
33 |
34 | # @property {function(ProgressEvent)} DOM level 0-style handler
35 | # for the 'error' event
36 | onerror: null
37 |
38 | # @property {function(ProgressEvent)} DOM level 0-style handler
39 | # for the 'load' event
40 | onload: null
41 |
42 | # @property {function(ProgressEvent)} DOM level 0-style handler
43 | # for the 'timeout' event
44 | ontimeout: null
45 |
46 | # @property {function(ProgressEvent)} DOM level 0-style handler
47 | # for the 'loadend' event
48 | onloadend: null
49 |
50 | # Adds a new-style listener for one of the XHR events.
51 | #
52 | # @see http://www.w3.org/TR/XMLHttpRequest/#events
53 | #
54 | # @param {String} eventType an XHR event type, such as 'readystatechange'
55 | # @param {function(ProgressEvent)} listener function that will be called when
56 | # the event fires
57 | # @return {undefined} undefined
58 | addEventListener: (eventType, listener) ->
59 | eventType = eventType.toLowerCase()
60 | @_listeners[eventType] ||= []
61 | @_listeners[eventType].push listener
62 | undefined
63 |
64 | # Removes an event listener added by calling addEventListener.
65 | #
66 | # @param {String} eventType an XHR event type, such as 'readystatechange'
67 | # @param {function(ProgressEvent)} listener the value passed in a previous
68 | # call to addEventListener.
69 | # @return {undefined} undefined
70 | removeEventListener: (eventType, listener) ->
71 | eventType = eventType.toLowerCase()
72 | if @_listeners[eventType]
73 | index = @_listeners[eventType].indexOf listener
74 | @_listeners[eventType].splice index, 1 if index isnt -1
75 | undefined
76 |
77 | # Calls all the listeners for an event.
78 | #
79 | # @param {ProgressEvent} event the event to be dispatched
80 | # @return {undefined} undefined
81 | dispatchEvent: (event) ->
82 | event.currentTarget = event.target = @
83 | eventType = event.type
84 | if listeners = @_listeners[eventType]
85 | for listener in listeners
86 | listener.call @, event
87 | if listener = @["on#{eventType}"]
88 | listener.call @, event
89 | undefined
90 |
--------------------------------------------------------------------------------
/src/xml_http_request_upload.coffee:
--------------------------------------------------------------------------------
1 | # @see http://xhr.spec.whatwg.org/#interface-xmlhttprequest
2 | class XMLHttpRequestUpload extends XMLHttpRequestEventTarget
3 | # @private
4 | # @param {XMLHttpRequest} the XMLHttpRequest that this upload object is
5 | # associated with
6 | constructor: (request) ->
7 | super()
8 | @_request = request
9 | @_reset()
10 |
11 | # Sets up this Upload to handle a new request.
12 | #
13 | # @private
14 | # @return {undefined} undefined
15 | _reset: ->
16 | @_contentType = null
17 | @_body = null
18 | undefined
19 |
20 | # Implements the upload-related part of the send() XHR specification.
21 | #
22 | # @private
23 | # @param {?String, ?Buffer, ?ArrayBufferView} data the argument passed to
24 | # XMLHttpRequest#send()
25 | # @return {undefined} undefined
26 | # @see step 4 of http://www.w3.org/TR/XMLHttpRequest/#the-send()-method
27 | _setData: (data) ->
28 | if typeof data is 'undefined' or data is null
29 | return
30 |
31 | if typeof data is 'string'
32 | # DOMString
33 | if data.length isnt 0
34 | @_contentType = 'text/plain;charset=UTF-8'
35 | @_body = Buffer.from data, 'utf8'
36 | else if Buffer.isBuffer data
37 | # node.js Buffer
38 | @_body = data
39 | else if data instanceof ArrayBuffer
40 | # ArrayBuffer arguments were supported in an old revision of the spec.
41 | body = Buffer.alloc data.byteLength
42 | view = new Uint8Array data
43 | body[i] = view[i] for i in [0...data.byteLength]
44 | @_body = body
45 | else if data.buffer and data.buffer instanceof ArrayBuffer
46 | # ArrayBufferView
47 | body = Buffer.alloc data.byteLength
48 | offset = data.byteOffset
49 | view = new Uint8Array data.buffer
50 | body[i] = view[i + offset] for i in [0...data.byteLength]
51 | @_body = body
52 | else
53 | # NOTE: diverging from the XHR specification of coercing everything else
54 | # to Strings via toString() because that behavior masks bugs and is
55 | # rarely useful
56 | throw new Error "Unsupported send() data #{data}"
57 |
58 | undefined
59 |
60 | # Updates the HTTP headers right before the request is sent.
61 | #
62 | # This is used to set data-dependent headers such as Content-Length and
63 | # Content-Type.
64 | #
65 | # @private
66 | # @param {Object} headers the HTTP headers to be sent
67 | # @param {Object} loweredHeaders maps lowercased HTTP header
68 | # names (e.g., 'content-type') to the actual names used in the headers
69 | # parameter (e.g., 'Content-Type')
70 | # @return {undefined} undefined
71 | _finalizeHeaders: (headers, loweredHeaders) ->
72 | if @_contentType
73 | unless 'content-type' of loweredHeaders
74 | headers['Content-Type'] = @_contentType
75 |
76 | if @_body
77 | # Restricted headers can't be set by the user, no need to check
78 | # loweredHeaders.
79 | headers['Content-Length'] = @_body.length.toString()
80 |
81 | undefined
82 |
83 | # Starts sending the HTTP request data.
84 | #
85 | # @private
86 | # @param {http.ClientRequest} request the HTTP request
87 | # @return {undefined} undefined
88 | _startUpload: (request) ->
89 | request.write @_body if @_body
90 | request.end()
91 |
92 | undefined
93 |
94 | # Export the XMLHttpRequestUpload constructor.
95 | XMLHttpRequest.XMLHttpRequestUpload = XMLHttpRequestUpload
96 |
--------------------------------------------------------------------------------
/test/src/response_type_test.coffee:
--------------------------------------------------------------------------------
1 | describe 'XMLHttpRequest', ->
2 | describe '#responseType', ->
3 | beforeEach ->
4 | @xhr = new XMLHttpRequest
5 | @jsonUrl = 'http://localhost:8912/test/fixtures/hello.json'
6 | @jsonString = '{"hello": "world", "answer": 42}\n'
7 | @imageUrl = 'http://localhost:8912/test/fixtures/xhr2.png'
8 |
9 | describe 'text', ->
10 | it 'reads a JSON file into a String', (done) ->
11 | @xhr.addEventListener 'load', =>
12 | expect(@xhr.response).to.equal @jsonString
13 | expect(@xhr.responseText).to.equal @jsonString
14 | done()
15 | @xhr.open 'GET', @jsonUrl
16 | @xhr.responseType = 'text'
17 | @xhr.send()
18 |
19 | describe 'json', ->
20 | it 'reads a JSON file into a parsed JSON object', (done) ->
21 | _done = false
22 | @xhr.addEventListener 'readystatechange', =>
23 | return if _done or @xhr.readyState isnt XMLHttpRequest.DONE
24 | _done = true
25 | expect(@xhr.response).to.deep.equal hello: 'world', answer: 42
26 | done()
27 | @xhr.open 'GET', @jsonUrl
28 | @xhr.responseType = 'json'
29 | @xhr.send()
30 |
31 | it 'produces null when reading a non-JSON file ', (done) ->
32 | @xhr.addEventListener 'loadend', =>
33 | expect(@xhr.response).to.equal null
34 | done()
35 | @xhr.open 'GET', 'http://localhost:8912/test/fixtures/hello.txt'
36 | @xhr.responseType = 'json'
37 | @xhr.send()
38 |
39 | describe 'arraybuffer', ->
40 | it 'reads a JSON file into an ArrayBuffer', (done) ->
41 | @xhr.addEventListener 'loadend', =>
42 | expect(@xhr.response).to.be.instanceOf ArrayBuffer
43 | view = new Uint8Array @xhr.response
44 | string = (String.fromCharCode(view[i]) for i in [0...view.length]).
45 | join ''
46 | expect(string).to.equal @jsonString
47 | done()
48 | @xhr.open 'GET', @jsonUrl
49 | @xhr.responseType = 'arraybuffer'
50 | @xhr.send()
51 |
52 | it 'reads a binary file into an ArrayBuffer', (done) ->
53 | @xhr.addEventListener 'loadend', =>
54 | expect(@xhr.response).to.be.instanceOf ArrayBuffer
55 | view = new Uint8Array @xhr.response
56 | bytes = (view[i] for i in [0...view.length])
57 | expect(bytes).to.deep.equal xhr2PngBytes
58 | done()
59 | @xhr.open 'GET', @imageUrl
60 | @xhr.responseType = 'arraybuffer'
61 | @xhr.send()
62 |
63 |
64 | describe 'buffer', ->
65 | it 'reads a JSON file into a node.js Buffer', (done) ->
66 | return done() if typeof Buffer is 'undefined'
67 | @xhr.addEventListener 'loadend', =>
68 | buffer = @xhr.response
69 | expect(buffer).to.be.instanceOf Buffer
70 | stringChars = for i in [0...buffer.length]
71 | String.fromCharCode buffer.readUInt8(i)
72 | expect(stringChars.join('')).to.equal @jsonString
73 | done()
74 | @xhr.open 'GET', @jsonUrl
75 | @xhr.responseType = 'buffer'
76 | @xhr.send()
77 |
78 | it 'reads a binary file into a node.js Buffer', (done) ->
79 | return done() if typeof Buffer is 'undefined'
80 | @xhr.addEventListener 'loadend', =>
81 | buffer = @xhr.response
82 | expect(buffer).to.be.instanceOf Buffer
83 | bytes = (buffer.readUInt8(i) for i in [0...buffer.length])
84 | expect(bytes).to.deep.equal xhr2PngBytes
85 | done()
86 | @xhr.open 'GET', @imageUrl
87 | @xhr.responseType = 'buffer'
88 | @xhr.send()
89 |
--------------------------------------------------------------------------------
/test/src/xhr_test.coffee:
--------------------------------------------------------------------------------
1 | describe 'XMLHttpRequest', ->
2 | beforeEach ->
3 | @xhr = new XMLHttpRequest
4 |
5 | describe 'constructor', ->
6 | it 'sets readyState to UNSENT', ->
7 | expect(@xhr.readyState).to.equal XMLHttpRequest.UNSENT
8 |
9 | it 'sets timeout to 0', ->
10 | expect(@xhr.timeout).to.equal 0
11 |
12 | it 'sets responseType to ""', ->
13 | expect(@xhr.responseType).to.equal ''
14 |
15 | it 'sets status to 0', ->
16 | expect(@xhr.status).to.equal 0
17 |
18 | it 'sets statusText to ""', ->
19 | expect(@xhr.statusText).to.equal ''
20 |
21 | describe '#open', ->
22 | it 'throws SecurityError on CONNECT', ->
23 | expect(=> @xhr.open 'CONNECT', 'http://localhost:8912/test').to.
24 | throw(SecurityError)
25 |
26 | describe 'with a GET for a local https request', ->
27 | beforeEach ->
28 | @xhr.open 'GET', 'https://localhost:8911/test/fixtures/hello.txt'
29 |
30 | it 'sets readyState to OPENED', ->
31 | expect(@xhr.readyState).to.equal XMLHttpRequest.OPENED
32 |
33 | it 'keeps status 0', ->
34 | expect(@xhr.status).to.equal 0
35 |
36 | it 'keeps statusText ""', ->
37 | expect(@xhr.statusText).to.equal ''
38 |
39 | describe '#send', ->
40 | describe 'on a local http GET', ->
41 | beforeEach ->
42 | @xhr.open 'GET', 'http://localhost:8912/test/fixtures/hello.txt'
43 |
44 | it 'kicks off the request', (done) ->
45 | @xhr.onload = (event) =>
46 | expect(@xhr.status).to.equal 200
47 | expect(@xhr.responseText).to.equal 'Hello world!\n'
48 | done()
49 | @xhr.send()
50 |
51 | describe 'on a local https GET', ->
52 | beforeEach ->
53 | @xhr.open 'GET', 'https://localhost:8911/test/fixtures/hello.txt'
54 |
55 | it 'kicks off the request', (done) ->
56 | @xhr.onload = (event) =>
57 | expect(@xhr.status).to.equal 200
58 | expect(@xhr.responseText).to.equal 'Hello world!\n'
59 | done()
60 | @xhr.send()
61 |
62 | describe 'on a local relative GET', ->
63 | beforeEach ->
64 | @xhr.open 'GET', '../fixtures/hello.txt'
65 |
66 | it 'kicks off the request', (done) ->
67 | @xhr.onload = (event) =>
68 | expect(@xhr.status).to.equal 200
69 | expect(@xhr.responseText).to.equal 'Hello world!\n'
70 | done()
71 | @xhr.send()
72 |
73 | describe 'on a local gopher GET', ->
74 | describe '#open + #send', ->
75 | it 'throw a NetworkError', ->
76 | expect(=>
77 | @xhr.open 'GET', 'gopher:localhost:8911'
78 | @xhr.send()
79 | ).to.throw(NetworkError)
80 |
81 | describe 'readyState constants', ->
82 | it 'UNSENT < OPENED', ->
83 | expect(XMLHttpRequest.UNSENT).to.be.below(XMLHttpRequest.OPENED)
84 |
85 | it 'OPENED < HEADERS_RECEIVED', ->
86 | expect(XMLHttpRequest.OPENED).to.be.
87 | below(XMLHttpRequest.HEADERS_RECEIVED)
88 |
89 | it 'HEADERS_RECEIVED < LOADING', ->
90 | expect(XMLHttpRequest.HEADERS_RECEIVED).to.be.
91 | below(XMLHttpRequest.LOADING)
92 |
93 | it 'LOADING < DONE', ->
94 | expect(XMLHttpRequest.LOADING).to.be.below(XMLHttpRequest.DONE)
95 |
96 | it 'XMLHttpRequest constants match the instance costants', ->
97 | expect(XMLHttpRequest.UNSENT).to.equal @xhr.UNSENT
98 | expect(XMLHttpRequest.OPENED).to.equal @xhr.OPENED
99 | expect(XMLHttpRequest.HEADERS_RECEIVED).to.equal @xhr.HEADERS_RECEIVED
100 | expect(XMLHttpRequest.LOADING).to.equal @xhr.LOADING
101 | expect(XMLHttpRequest.DONE).to.equal @xhr.DONE
102 |
103 |
--------------------------------------------------------------------------------
/test/src/send_test.coffee:
--------------------------------------------------------------------------------
1 | describe 'XMLHttpRequest', ->
2 | describe '#send', ->
3 | beforeEach ->
4 | @xhr = new XMLHttpRequest
5 | @xhr.open 'POST', 'http://localhost:8912/_/echo'
6 |
7 | @arrayBuffer = new ArrayBuffer xhr2PngBytes.length
8 | @arrayBufferView = new Uint8Array @arrayBuffer
9 | if typeof Buffer is 'undefined'
10 | @buffer = null
11 | else
12 | @buffer = Buffer.alloc xhr2PngBytes.length
13 |
14 | for i in [0...xhr2PngBytes.length]
15 | @arrayBufferView[i] = xhr2PngBytes[i]
16 | @buffer.writeUInt8 xhr2PngBytes[i], i if @buffer
17 |
18 | it 'works with ASCII DOMStrings', (done) ->
19 | @xhr.onload = =>
20 | expect(@xhr.getResponseHeader('content-type')).to.
21 | match(/^text\/plain(;\s?charset=UTF-8)?$/)
22 | expect(@xhr.responseText).to.equal 'Hello world!'
23 | done()
24 | @xhr.send "Hello world!"
25 |
26 | it 'works with UTF-8 DOMStrings', (done) ->
27 | @xhr.onloadend = =>
28 | expect(@xhr.getResponseHeader('content-type')).to.
29 | match(/^text\/plain(;\s?charset=UTF-8)?$/)
30 | expect(@xhr.responseText).to.equal '世界你好!'
31 | done()
32 | @xhr.send '世界你好!'
33 |
34 | it 'works with ArrayBufferViews', (done) ->
35 | @xhr.responseType = 'arraybuffer'
36 | @xhr.onload = =>
37 | expect(@xhr.getResponseHeader('content-type')).to.equal null
38 | responseView = new Uint8Array @xhr.response
39 | responseBytes = (responseView[i] for i in [0...responseView.length])
40 | expect(responseBytes).to.deep.equal xhr2PngBytes
41 | done()
42 | @xhr.send @arrayBufferView
43 |
44 | it 'works with ArrayBufferViews with set index and length', (done) ->
45 | @xhr.responseType = 'arraybuffer'
46 | @xhr.onload = =>
47 | expect(@xhr.getResponseHeader('content-type')).to.equal null
48 | responseView = new Uint8Array @xhr.response
49 | responseBytes = (responseView[i] for i in [0...responseView.length])
50 | expect(responseBytes).to.deep.equal xhr2PngBytes[10...52]
51 | done()
52 | arrayBufferView10 = new Uint8Array @arrayBuffer, 10, 42
53 | @xhr.send arrayBufferView10
54 |
55 | it 'works with ArrayBuffers', (done) ->
56 | @xhr.responseType = 'arraybuffer'
57 | @xhr.onload = =>
58 | expect(@xhr.getResponseHeader('content-type')).to.equal null
59 | responseView = new Uint8Array @xhr.response
60 | responseBytes = (responseView[i] for i in [0...responseView.length])
61 | expect(responseBytes).to.deep.equal xhr2PngBytes
62 | done()
63 | @xhr.send @arrayBuffer
64 |
65 | it 'works with node.js Buffers', (done) ->
66 | return done() unless @buffer
67 | # NOTE: using the same exact code as above, which is tested in a browser
68 | @xhr.responseType = 'arraybuffer'
69 | @xhr.onload = =>
70 | expect(@xhr.getResponseHeader('content-type')).to.equal null
71 | responseView = new Uint8Array @xhr.response
72 | responseBytes = (responseView[i] for i in [0...responseView.length])
73 | expect(responseBytes).to.deep.equal xhr2PngBytes
74 | done()
75 | @xhr.send @buffer
76 |
77 | it 'sets POST headers correctly when given null data', (done) ->
78 | @xhr.open 'POST', 'http://localhost:8912/_/headers'
79 | @xhr.responseType = 'text'
80 | @xhr.onload = =>
81 | expect(@xhr.responseText).to.match(/^\{.*\}$/)
82 | headers = JSON.parse @xhr.responseText
83 | expect(headers).to.have.property 'content-length'
84 | expect(headers['content-length']).to.equal '0'
85 | expect(headers).not.to.have.property 'content-type'
86 | done()
87 | @xhr.send()
88 |
89 |
90 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # XMLHttpRequest Emulation for node.js
2 |
3 | This is an [npm](https://npmjs.org/) package that implements the
4 | [W3C XMLHttpRequest](http://www.w3.org/TR/XMLHttpRequest/) specification on top
5 | of the [node.js](http://nodejs.org/) APIs.
6 |
7 |
8 | ## Supported Platforms
9 |
10 | This library is tested against the following platforms.
11 |
12 | * [node.js](http://nodejs.org/) 10
13 | * [node.js](http://nodejs.org/) 12
14 |
15 | Keep in mind that the versions above are not hard requirements.
16 |
17 |
18 | ## Installation and Usage
19 |
20 | The preferred installation method is to add the library to the `dependencies`
21 | section in your `package.json`.
22 |
23 | ```json
24 | {
25 | "dependencies": {
26 | "xhr2": "*"
27 | }
28 | }
29 | ```
30 |
31 | Alternatively, `npm` can be used to install the library directly.
32 |
33 | ```bash
34 | npm install xhr2
35 | ```
36 |
37 | Once the library is installed, `require`-ing it returns the `XMLHttpRequest`
38 | constructor.
39 |
40 | ```javascript
41 | var XMLHttpRequest = require('xhr2');
42 | ```
43 |
44 | The other objects that are usually defined in an XHR environment are hanging
45 | off of `XMLHttpRequest`.
46 |
47 | ```javascript
48 | var XMLHttpRequestUpload = XMLHttpRequest.XMLHttpRequestUpload;
49 | ```
50 |
51 | MDN (the Mozilla Developer Network) has a
52 | [great intro to XMLHttpRequest](https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest/Using_XMLHttpRequest).
53 |
54 | This library's [CoffeeDocs](http://coffeedoc.info/github/pwnall/node-xhr2/) can
55 | be used as quick reference to the XMLHttpRequest specification parts that were
56 | implemented.
57 |
58 |
59 | ## Features
60 |
61 | The following standard features are implemented.
62 |
63 | * `http` and `https` URI protocols
64 | * Basic authentication according to the XMLHttpRequest specification
65 | * request and response header management
66 | * `send()` accepts the following data types: String, ArrayBufferView,
67 | ArrayBuffer (deprecated in the standard)
68 | * `responseType` values: `text`, `json`, `arraybuffer`
69 | * `readystatechange` and download progress events
70 | * `overrideMimeType()`
71 | * `abort()`
72 | * `timeout`
73 | * automated redirection following
74 |
75 | The following node.js extensions are implemented.
76 |
77 | * `send()` accepts a node.js Buffer
78 | * Setting `responseType` to `buffer` produces a node.js Buffer
79 | * `nodejsSet` does XHR network configuration that is not exposed in browsers,
80 | for security reasons
81 |
82 | The following standard features are not implemented.
83 |
84 | * FormData
85 | * Blob
86 | * `file://` URIs
87 | * `data:` URIs
88 | * upload progress events
89 | * synchronous operation
90 | * Same-origin policy checks and CORS
91 | * cookie processing
92 |
93 |
94 | ## Versioning
95 |
96 | The library aims to implement the
97 | [W3C XMLHttpRequest](http://www.w3.org/TR/XMLHttpRequest/) specification, so
98 | the library's API will always be a (hopefully growing) subset of the API in the
99 | specification.
100 |
101 |
102 | ## Development
103 |
104 | The following commands will get the source tree in a `node-xhr2/` directory and
105 | build the library.
106 |
107 | ```bash
108 | git clone git://github.com/pwnall/node-xhr2.git
109 | cd node-xhr2
110 | npm install
111 | npm pack
112 | ```
113 |
114 | Installing CoffeeScript globally will let you type `cake` instead of
115 | `node_modules/.bin/cake`
116 |
117 | ```bash
118 | npm install -g coffeescript
119 | ```
120 |
121 | The library comes with unit tests that exercise the XMLHttpRequest API.
122 |
123 | ```bash
124 | cake test
125 | ```
126 |
127 | The tests themselves can be tested by running them in a browser environment,
128 | where a different XMLHttpRequest implementation is available. Both Google
129 | Chrome and Firefox deviate from the specification in small ways, so it's best
130 | to run the tests in both browsers and mentally compute an intersection of the
131 | failing tests.
132 |
133 | ```bash
134 | cake webtest
135 | BROWSER=firefox cake webtest
136 | ```
137 |
138 |
139 | ## Copyright and License
140 |
141 | The library is Copyright (c) 2013 Victor Costan, and distributed under the MIT
142 | License.
143 |
--------------------------------------------------------------------------------
/Cakefile:
--------------------------------------------------------------------------------
1 | async = require 'async'
2 | {spawn, exec} = require 'child_process'
3 | fs = require 'fs'
4 | glob = require 'glob'
5 | log = console.log
6 | path = require 'path'
7 | remove = require 'remove'
8 |
9 | # Node 0.6 compatibility hack.
10 | unless fs.existsSync
11 | fs.existsSync = (filePath) -> path.existsSync filePath
12 |
13 |
14 | task 'build', ->
15 | build()
16 |
17 | task 'test', ->
18 | vendor ->
19 | build ->
20 | ssl_cert ->
21 | test_cases = glob.sync 'test/js/**/*_test.js'
22 | test_cases.sort() # Consistent test case order.
23 | run 'node_modules/.bin/mocha --colors --slow 200 --timeout 1000 --exit ' +
24 | "--require test/js/helpers/setup.js #{test_cases.join(' ')}"
25 |
26 | task 'webtest', ->
27 | vendor ->
28 | build ->
29 | ssl_cert ->
30 | webtest()
31 |
32 | task 'cert', ->
33 | remove.removeSync 'test/ssl', ignoreMissing: true
34 | ssl_cert()
35 |
36 | task 'vendor', ->
37 | remove.removeSync './test/vendor', ignoreMissing: true
38 | vendor()
39 |
40 | task 'doc', ->
41 | run 'node_modules/.bin/codo --title "node-xhr API Documentation" src'
42 |
43 | build = (callback) ->
44 | commands = []
45 |
46 | # Ignoring ".coffee" when sorting.
47 | # We want "driver.coffee" to sort before "driver-browser.coffee"
48 | source_files = glob.sync 'src/**/*.coffee'
49 | source_files.sort (a, b) ->
50 | a.replace(/\.coffee$/, '').localeCompare b.replace(/\.coffee$/, '')
51 |
52 | # Compile without --join for decent error messages.
53 | commands.push 'node_modules/.bin/coffee --output tmp --compile ' +
54 | source_files.join(' ')
55 | commands.push 'node_modules/.bin/coffee --output lib --compile ' +
56 | "--join xhr2.js #{source_files.join(' ')}"
57 |
58 | # Tests are supposed to be independent, so the build order doesn't matter.
59 | test_dirs = glob.sync 'test/src/**/'
60 | for test_dir in test_dirs
61 | out_dir = test_dir.replace(/^test\/src\//, 'test/js/')
62 | test_files = glob.sync path.join(test_dir, '*.coffee')
63 | commands.push "node_modules/.bin/coffee --output #{out_dir} " +
64 | "--compile #{test_files.join(' ')}"
65 |
66 | async.forEachSeries commands, run, ->
67 | # Build the binary test image.
68 | buffer = fs.readFileSync 'test/fixtures/xhr2.png'
69 | bytes = (buffer.readUInt8(i) for i in [0...buffer.length])
70 | globalJs = '((function(){ return this.global || this; })())'
71 | js = "#{globalJs}.xhr2PngBytes = #{JSON.stringify(bytes)};"
72 | fs.writeFile 'test/js/helpers/xhr2.png.js', js, ->
73 | callback() if callback
74 |
75 | webtest = (callback) ->
76 | xhrServer = require './test/js/helpers/xhr_server.js'
77 | if 'BROWSER' of process.env
78 | if process.env['BROWSER'] is 'false'
79 | url = xhrServer.https.testUrl()
80 | console.log "Please open the URL below in your browser:\n #{url}"
81 | else
82 | xhrServer.https.openBrowser process.env['BROWSER']
83 | else
84 | xhrServer.https.openBrowser()
85 | callback() if callback?
86 |
87 | ssl_cert = (callback) ->
88 | if fs.existsSync 'test/ssl/cert.pem'
89 | callback() if callback?
90 | return
91 |
92 | fs.mkdirSync 'test/ssl' unless fs.existsSync 'test/ssl'
93 | run 'openssl req -new -x509 -days 365 -nodes -sha256 -newkey rsa:2048 ' +
94 | '-batch -out test/ssl/cert.pem -keyout test/ssl/cert.pem ' +
95 | '-subj /O=xhr2.js/OU=Testing/CN=localhost ', callback
96 |
97 | vendor = (callback) ->
98 | # All the files will be dumped here.
99 | fs.mkdirSync 'test/vendor' unless fs.existsSync 'test/vendor'
100 |
101 | downloads = [
102 | # chai.js ships different builds for browsers vs node.js
103 | ['https://www.chaijs.com/chai.js', 'test/vendor/chai.js'],
104 | # sinon.js also ships special builds for browsers
105 | ['https://sinonjs.org/releases/sinon.js', 'test/vendor/sinon.js'],
106 | ]
107 | async.forEachSeries downloads, download, ->
108 | callback() if callback
109 |
110 | run = (args...) ->
111 | for a in args
112 | switch typeof a
113 | when 'string' then command = a
114 | when 'object'
115 | if a instanceof Array then params = a
116 | else options = a
117 | when 'function' then callback = a
118 |
119 | command += ' ' + params.join ' ' if params?
120 | cmd = spawn '/bin/sh', ['-c', command], options
121 | cmd.stdout.on 'data', (data) -> process.stdout.write data
122 | cmd.stderr.on 'data', (data) -> process.stderr.write data
123 | process.on 'SIGHUP', -> cmd.kill()
124 | cmd.on 'exit', (code) -> callback() if callback? and code is 0
125 |
126 | download = ([url, file], callback) ->
127 | if fs.existsSync file
128 | callback() if callback?
129 | return
130 |
131 | run "curl -o #{file} #{url}", callback
132 |
--------------------------------------------------------------------------------
/test/src/helpers/xhr_server.coffee:
--------------------------------------------------------------------------------
1 | express = require 'express'
2 | fs = require 'fs'
3 | http = require 'http'
4 | https = require 'https'
5 | open = require 'open'
6 |
7 | # express.js server for testing the Web application.
8 | class XhrServer
9 | # Starts up a HTTP server.
10 | constructor: (@port, @useHttps) ->
11 | @createApp()
12 |
13 | # Opens the test URL in a browser.
14 | openBrowser: (appName) ->
15 | open @testUrl(), appName
16 |
17 | # The URL that should be used to start the tests.
18 | testUrl: ->
19 | "https://localhost:#{@port}/test/html/browser_test.html"
20 |
21 | # The self-signed certificate used by this server.
22 | sslCertificate: ->
23 | return null unless @useHttps
24 | keyMaterial = fs.readFileSync 'test/ssl/cert.pem', 'utf8'
25 | certIndex = keyMaterial.indexOf '-----BEGIN CERTIFICATE-----'
26 | keyMaterial.substring certIndex
27 |
28 | # The key for the self-signed certificate used by this server.
29 | sslKey: ->
30 | return null unless @useHttps
31 | keyMaterial = fs.readFileSync 'test/ssl/cert.pem', 'utf8'
32 | certIndex = keyMaterial.indexOf '-----BEGIN CERTIFICATE-----'
33 | keyMaterial.substring 0, certIndex
34 |
35 | # The server code.
36 | createApp: ->
37 | @app = express()
38 |
39 | ## Middleware.
40 |
41 | # CORS headers on everything, in case that ever gets implemented.
42 | @app.use (request, response, next) ->
43 | response.header 'Access-Control-Allow-Origin', '*'
44 | response.header 'Access-Control-Allow-Methods', 'DELETE,GET,POST,PUT'
45 | response.header 'Access-Control-Allow-Headers',
46 | 'Content-Type, Authorization'
47 | next()
48 |
49 | @app.use express.static(fs.realpathSync(__dirname + '/../../../'),
50 | { dotfiles: 'allow' })
51 |
52 | ## Routes
53 |
54 | @app.all '/_/method', (request, response) ->
55 | body = request.method
56 | response.header 'Content-Type', 'text/plain; charset=utf-8'
57 | response.header 'Content-Length', body.length.toString()
58 | response.end body
59 |
60 | # Echoes the request body. Used to test send(data).
61 | @app.post '/_/echo', (request, response) ->
62 | if request.headers['content-type']
63 | response.header 'Content-Type', request.headers['content-type']
64 | if request.headers['content-length']
65 | response.header 'Content-Length', request.headers['content-length']
66 |
67 | request.on 'data', (chunk) -> response.write chunk
68 | request.on 'end', -> response.end()
69 |
70 | # Lists the request headers. Used to test setRequestHeader().
71 | @app.all '/_/headers', (request, response) ->
72 | body = JSON.stringify request.headers
73 | response.header 'Content-Type', 'application/json'
74 | response.header 'Content-Length', body.length.toString()
75 | response.end body
76 |
77 | # Sets the response headers in the request. Used to test getResponse*().
78 | @app.post '/_/get_headers', (request, response) ->
79 | jsonString = ''
80 | request.on 'data', (chunk) -> jsonString += chunk
81 | request.on 'end', ->
82 | headers = JSON.parse jsonString
83 | for name, value of headers
84 | response.header name, value
85 | response.header 'Content-Length', '0'
86 | response.end ''
87 |
88 | # Sets every response detail. Used for error testing.
89 | @app.post '/_/response', (request, response) ->
90 | jsonString = ''
91 | request.on 'data', (chunk) -> jsonString += chunk
92 | request.on 'end', ->
93 | json = JSON.parse jsonString
94 | response.writeHead json.code, json.status, json.headers
95 | response.write json.body if json.body
96 | response.end()
97 |
98 | # Sends data in small chunks. Used for event testing.
99 | @app.post '/_/drip', (request, response) ->
100 | request.connection.setNoDelay()
101 | jsonString = ''
102 | request.on 'data', (chunk) -> jsonString += chunk
103 | request.on 'end', ->
104 | json = JSON.parse jsonString
105 | sentDrips = 0
106 | drip = new Array(json.size + 1).join '.'
107 | response.header 'Content-Type', 'text/plain'
108 | if json.length
109 | response.header 'Content-Length', (json.drips * json.size).toString()
110 | sendDrip = =>
111 | response.write drip
112 | sentDrips += 1
113 | if sentDrips >= json.drips
114 | response.end()
115 | else
116 | setTimeout sendDrip, json.ms
117 | sendDrip()
118 |
119 | # Returns a HTTP redirect. Used to test the redirection handling code.
120 | @app.all '/_/redirect/:status/:next_page', (request, response) =>
121 | response.statusCode = parseInt(request.params.status)
122 | response.header 'Location',
123 | "http://#{request.get('host')}/_/#{request.params.next_page}"
124 | body = "This is supposed to have a redirect link
"
125 | response.header 'Content-Type', 'text/html'
126 | response.header 'Content-Length', body.length.toString()
127 | response.header 'X-Redirect-Header', 'should not show up'
128 | response.end body
129 |
130 | # Requested when the browser test suite completes.
131 | @app.get '/diediedie', (request, response) =>
132 | if 'failed' of request.query
133 | failed = parseInt request.query['failed']
134 | else
135 | failed = 1
136 | total = parseInt request.query['total'] || 0
137 | passed = total - failed
138 | exitCode = if failed == 0 then 0 else 1
139 | console.log "#{passed} passed, #{failed} failed"
140 |
141 | response.header 'Content-Type', 'image/png'
142 | response.header 'Content-Length', '0'
143 | response.end ''
144 | unless 'NO_EXIT' of process.env
145 | @server.close()
146 | process.exit exitCode
147 |
148 | if @useHttps
149 | options = key: @sslKey(), cert: @sslCertificate()
150 | @server = https.createServer options, @app
151 | else
152 | @server = http.createServer @app
153 | @server.listen @port
154 |
155 | module.exports.https = new XhrServer 8911, true
156 | module.exports.http = new XhrServer 8912, false
157 |
--------------------------------------------------------------------------------
/test/src/nodejs_set_test.coffee:
--------------------------------------------------------------------------------
1 | describe 'XMLHttpRequest', ->
2 | describe '.nodejsSet', ->
3 | beforeEach ->
4 | @xhr = new XMLHttpRequest
5 | @customXhr = new XMLHttpRequest
6 |
7 | describe 'with a httpAgent option', ->
8 | beforeEach ->
9 | return unless XMLHttpRequest.nodejsSet # Skip in browsers.
10 |
11 | @customAgent = { custom: 'httpAgent' }
12 | @customXhr.nodejsHttpAgent = @customAgent
13 |
14 | @default = XMLHttpRequest::nodejsHttpAgent
15 | @agent = { mocking: 'httpAgent' }
16 | XMLHttpRequest.nodejsSet httpAgent: @agent
17 |
18 | it 'sets the default nodejsHttpAgent', ->
19 | return unless XMLHttpRequest.nodejsSet # Skip in browsers.
20 | expect(@xhr.nodejsHttpAgent).to.equal @agent
21 |
22 | it 'does not interfere with custom nodejsHttpAgent settings', ->
23 | return unless XMLHttpRequest.nodejsSet # Skip in browsers.
24 | expect(@customXhr.nodejsHttpAgent).to.equal @customAgent
25 |
26 | afterEach ->
27 | XMLHttpRequest.nodejsSet httpAgent: @default
28 |
29 | describe 'with a httpsAgent option', ->
30 | beforeEach ->
31 | return unless XMLHttpRequest.nodejsSet # Skip in browsers.
32 |
33 | @customAgent = { custom: 'httpsAgent' }
34 | @customXhr.nodejsHttpsAgent = @customAgent
35 |
36 | @default = XMLHttpRequest::nodejsHttpsAgent
37 | @agent = { mocking: 'httpsAgent' }
38 | XMLHttpRequest.nodejsSet httpsAgent: @agent
39 |
40 | it 'sets the default nodejsHttpsAgent', ->
41 | return unless XMLHttpRequest.nodejsSet # Skip in browsers.
42 | expect(@xhr.nodejsHttpsAgent).to.equal @agent
43 |
44 | it 'does not interfere with custom nodejsHttpsAgent settings', ->
45 | return unless XMLHttpRequest.nodejsSet # Skip in browsers.
46 | expect(@customXhr.nodejsHttpsAgent).to.equal @customAgent
47 |
48 | afterEach ->
49 | XMLHttpRequest.nodejsSet httpsAgent: @default
50 |
51 | describe 'with a baseUrl option', ->
52 | beforeEach ->
53 | return unless XMLHttpRequest.nodejsSet # Skip in browsers.
54 |
55 | @customBaseUrl = 'http://custom.url/base'
56 | @customXhr.nodejsBaseUrl = @customBaseUrl
57 |
58 | @default = XMLHttpRequest::nodejsBaseUrl
59 | @baseUrl = 'http://localhost/base'
60 | XMLHttpRequest.nodejsSet baseUrl: @baseUrl
61 |
62 | it 'sets the default nodejsBaseUrl', ->
63 | return unless XMLHttpRequest.nodejsSet # Skip in browsers.
64 | expect(@xhr.nodejsBaseUrl).to.equal @baseUrl
65 |
66 | it 'does not interfere with custom nodejsBaseUrl settings', ->
67 | return unless XMLHttpRequest.nodejsSet # Skip in browsers.
68 | expect(@customXhr.nodejsBaseUrl).to.equal @customBaseUrl
69 |
70 | afterEach ->
71 | XMLHttpRequest.nodejsSet baseUrl: @default
72 |
73 | describe '#nodejsSet', ->
74 | beforeEach ->
75 | @xhr = new XMLHttpRequest
76 | @customXhr = new XMLHttpRequest
77 |
78 | describe 'with a httpAgent option', ->
79 | beforeEach ->
80 | return unless XMLHttpRequest.nodejsSet # Skip in browsers.
81 |
82 | @customAgent = { custom: 'httpAgent' }
83 | @customXhr.nodejsSet httpAgent: @customAgent
84 |
85 | it 'sets nodejsHttpAgent on the XHR instance', ->
86 | return unless XMLHttpRequest.nodejsSet # Skip in browsers.
87 | expect(@customXhr.nodejsHttpAgent).to.equal @customAgent
88 |
89 | it 'does not interfere with default nodejsHttpAgent settings', ->
90 | return unless XMLHttpRequest.nodejsSet # Skip in browsers.
91 | expect(@xhr.nodejsHttpAgent).not.to.equal @customAgent
92 |
93 | describe 'with a httpsAgent option', ->
94 | beforeEach ->
95 | return unless XMLHttpRequest.nodejsSet # Skip in browsers.
96 |
97 | @customAgent = { custom: 'httpsAgent' }
98 | @customXhr.nodejsSet httpsAgent: @customAgent
99 |
100 | it 'sets nodejsHttpsAgent on the XHR instance', ->
101 | return unless XMLHttpRequest.nodejsSet # Skip in browsers.
102 | expect(@customXhr.nodejsHttpsAgent).to.equal @customAgent
103 |
104 | it 'does not interfere with default nodejsHttpsAgent settings', ->
105 | return unless XMLHttpRequest.nodejsSet # Skip in browsers.
106 | expect(@xhr.nodejsHttpsAgent).not.to.equal @customAgent
107 |
108 | describe 'base URL parsing', ->
109 | beforeEach ->
110 | @xhr = new XMLHttpRequest
111 |
112 | describe 'with null baseUrl', ->
113 | beforeEach ->
114 | return unless XMLHttpRequest.nodejsSet # Skip in browsers.
115 | @xhr.nodejsSet baseUrl: null
116 |
117 | it 'parses an absolute URL', ->
118 | return unless XMLHttpRequest.nodejsSet # Skip in browsers.
119 | parsedUrl = @xhr._parseUrl('http://www.domain.com/path')
120 | expect(parsedUrl).to.be.ok
121 | expect(parsedUrl).to.have.property 'href'
122 | expect(parsedUrl.href).to.equal 'http://www.domain.com/path'
123 |
124 | describe 'with a (protocol, domain, filePath) baseUrl', ->
125 | beforeEach ->
126 | return unless XMLHttpRequest.nodejsSet # Skip in browsers.
127 | @xhr.nodejsSet baseUrl: 'https://base.url/dir/file.html'
128 |
129 | it 'parses an absolute URL', ->
130 | return unless XMLHttpRequest.nodejsSet # Skip in browsers.
131 | parsedUrl = @xhr._parseUrl('http://www.domain.com/path')
132 | expect(parsedUrl).to.be.ok
133 | expect(parsedUrl).to.have.property 'href'
134 | expect(parsedUrl.href).to.equal 'http://www.domain.com/path'
135 |
136 | it 'parses a path-relative URL', ->
137 | return unless XMLHttpRequest.nodejsSet # Skip in browsers.
138 | parsedUrl = @xhr._parseUrl('path/to.js')
139 | expect(parsedUrl).to.be.ok
140 | expect(parsedUrl).to.have.property 'href'
141 | expect(parsedUrl.href).to.equal 'https://base.url/dir/path/to.js'
142 |
143 | it 'parses a path-relative URL with ..', ->
144 | return unless XMLHttpRequest.nodejsSet # Skip in browsers.
145 | parsedUrl = @xhr._parseUrl('../path/to.js')
146 | expect(parsedUrl).to.be.ok
147 | expect(parsedUrl).to.have.property 'href'
148 | expect(parsedUrl.href).to.equal 'https://base.url/path/to.js'
149 |
150 | it 'parses a host-relative URL', ->
151 | return unless XMLHttpRequest.nodejsSet # Skip in browsers.
152 | parsedUrl = @xhr._parseUrl('/path/to.js')
153 | expect(parsedUrl).to.be.ok
154 | expect(parsedUrl).to.have.property 'href'
155 | expect(parsedUrl.href).to.equal 'https://base.url/path/to.js'
156 |
157 | it 'parses a protocol-relative URL', ->
158 | return unless XMLHttpRequest.nodejsSet # Skip in browsers.
159 | parsedUrl = @xhr._parseUrl('//domain.com/path/to.js')
160 | expect(parsedUrl).to.be.ok
161 | expect(parsedUrl).to.have.property 'href'
162 | expect(parsedUrl.href).to.equal 'https://domain.com/path/to.js'
163 |
--------------------------------------------------------------------------------
/test/src/headers_test.coffee:
--------------------------------------------------------------------------------
1 | describe 'XMLHttpRequest', ->
2 | beforeEach ->
3 | @xhr = new XMLHttpRequest
4 |
5 | describe '#setRequestHeader', ->
6 | beforeEach ->
7 | @xhr.open 'POST', 'http://localhost:8912/_/headers'
8 | @xhr.responseType = 'text'
9 |
10 | describe 'with allowed headers', ->
11 | beforeEach ->
12 | @xhr.setRequestHeader 'Authorization', 'lol'
13 | @xhr.setRequestHeader 'User-Agent', 'toaster'
14 | @xhr.setRequestHeader 'X-Answer', '42'
15 | @xhr.setRequestHeader 'X-Header-Name', 'value'
16 |
17 | it 'should send the headers', (done) ->
18 | @xhr.onload = =>
19 | expect(@xhr.responseText).to.match(/^\{.*\}$/)
20 | headers = JSON.parse @xhr.responseText
21 | expect(headers).to.have.property 'authorization'
22 | expect(headers['authorization']).to.equal 'lol'
23 | expect(headers).to.have.property 'user-agent'
24 | expect(headers['user-agent']).to.equal 'toaster'
25 | expect(headers).to.have.property 'x-answer'
26 | expect(headers['x-answer']).to.equal '42'
27 | expect(headers).to.have.property 'x-header-name'
28 | expect(headers['x-header-name']).to.equal 'value'
29 | done()
30 | @xhr.send ''
31 |
32 | describe 'with a mix of allowed and forbidden headers', ->
33 | beforeEach ->
34 | @xhr.setRequestHeader 'Authorization', 'lol'
35 | @xhr.setRequestHeader 'Proxy-Authorization', 'evil:kitten'
36 | @xhr.setRequestHeader 'Sec-Breach', 'yes please'
37 | @xhr.setRequestHeader 'Host', 'www.google.com'
38 | @xhr.setRequestHeader 'Origin', 'https://www.google.com'
39 | @xhr.setRequestHeader 'X-Answer', '42'
40 |
41 | it 'should only send the allowed headers', (done) ->
42 | @xhr.onloadend = =>
43 | expect(@xhr.responseText).to.match(/^\{.*\}$/)
44 | headers = JSON.parse @xhr.responseText
45 | expect(headers).to.have.property 'authorization'
46 | expect(headers['authorization']).to.equal 'lol'
47 | expect(headers).not.to.have.property 'proxy-authorization'
48 | expect(headers).not.to.have.property 'sec-breach'
49 | expect(headers['origin']).not.to.match /www\.google\.com/
50 | expect(headers['host']).not.to.match /www\.google\.com/
51 | expect(headers).to.have.property 'x-answer'
52 | expect(headers['x-answer']).to.equal '42'
53 | done()
54 | @xhr.send ''
55 |
56 | describe 'with repeated headers', ->
57 | beforeEach ->
58 | @xhr.setRequestHeader 'Authorization', 'trol'
59 | @xhr.setRequestHeader 'Authorization', 'lol'
60 | @xhr.setRequestHeader 'Authorization', 'lol'
61 | @xhr.setRequestHeader 'X-Answer', '42'
62 |
63 | it 'should only send the allowed headers', (done) ->
64 | _done = false
65 | @xhr.onreadystatechange = =>
66 | return if _done or @xhr.readyState isnt XMLHttpRequest.DONE
67 | _done = true
68 | expect(@xhr.responseText).to.match(/^\{.*\}$/)
69 | headers = JSON.parse @xhr.responseText
70 | expect(headers).to.have.property 'authorization'
71 | expect(headers['authorization']).to.equal 'trol, lol, lol'
72 | expect(headers).to.have.property 'x-answer'
73 | expect(headers['x-answer']).to.equal '42'
74 | done()
75 | @xhr.send ''
76 |
77 | describe 'with no headers', ->
78 | beforeEach ->
79 | @xhr.open 'POST', 'http://localhost:8912/_/headers'
80 | @xhr.responseType = 'text'
81 |
82 | it 'should set the protected headers correctly', (done) ->
83 | @xhr.onload = =>
84 | expect(@xhr.responseText).to.match(/^\{.*\}$/)
85 | headers = JSON.parse @xhr.responseText
86 | expect(headers).to.have.property 'connection'
87 | expect(headers['connection']).to.equal 'keep-alive'
88 | expect(headers).to.have.property 'host'
89 | expect(headers['host']).to.equal 'localhost:8912'
90 | expect(headers).to.have.property 'user-agent'
91 | expect(headers['user-agent']).to.match(/^Mozilla\//)
92 | done()
93 | @xhr.send ''
94 |
95 | describe '#getResponseHeader', ->
96 | beforeEach ->
97 | @xhr.open 'POST', 'http://localhost:8912/_/get_headers'
98 | @headerJson =
99 | '''
100 | {"Accept-Ranges": "bytes",
101 | "Content-Type": "application/xhr2; charset=utf-1337",
102 | "Set-Cookie": "UserID=JohnDoe; Max-Age=3600; Version=1",
103 | "X-Header": "one, more, value"}
104 | '''
105 |
106 | it 'returns accessible headers', (done) ->
107 | @xhr.onloadend = =>
108 | expect(@xhr.getResponseHeader('AccEPt-RANgeS')).to.equal 'bytes'
109 | expect(@xhr.getResponseHeader('content-Type')).to.
110 | equal 'application/xhr2; charset=utf-1337'
111 | expect(@xhr.getResponseHeader('X-Header')).to.equal "one, more, value"
112 | done()
113 | @xhr.send @headerJson
114 |
115 | it 'returns null for private headers', (done) ->
116 | @xhr.onloadend = =>
117 | expect(@xhr.getResponseHeader('set-cookie')).to.equal null
118 | done()
119 | @xhr.send @headerJson
120 |
121 | it 'returns headers when the XHR enters HEADERS_RECEIVED', (done) ->
122 | _done = false
123 | @xhr.onreadystatechange = =>
124 | return if _done or @xhr.readyState isnt XMLHttpRequest.HEADERS_RECEIVED
125 | _done = true
126 | expect(@xhr.getResponseHeader('AccEPt-RANgeS')).to.equal 'bytes'
127 | done()
128 | @xhr.send @headerJson
129 |
130 | describe '#getAllResponseHeaders', ->
131 | beforeEach ->
132 | @xhr.open 'POST', 'http://localhost:8912/_/get_headers'
133 | @headerJson =
134 | '''
135 | {"Accept-Ranges": "bytes",
136 | "Content-Type": "application/xhr2; charset=utf-1337",
137 | "Set-Cookie": "UserID=JohnDoe; Max-Age=3600; Version=1",
138 | "X-Header": "one, more, value"}
139 | '''
140 |
141 | it 'contains accessible headers', (done) ->
142 | @xhr.onloadend = =>
143 | headers = @xhr.getAllResponseHeaders()
144 | expect(headers).to.match(/(\A|\r\n)accept-ranges: bytes(\r\n|\Z)/mi)
145 | expect(headers).to.match(
146 | /(\A|\r\n)content-type: application\/xhr2; charset=utf-1337(\r\n|\Z)/mi)
147 | expect(headers).to.match(/(\A|\r\n)X-Header: one, more, value(\r\n|\Z)/mi)
148 | done()
149 | @xhr.send @headerJson
150 |
151 | it 'does not contain private headers', (done) ->
152 | @xhr.onloadend = =>
153 | headers = @xhr.getAllResponseHeaders()
154 | expect(headers).not.to.match(/(\A|\r\n)set-cookie:/mi)
155 | done()
156 | @xhr.send @headerJson
157 |
158 | it 'returns headers when the XHR enters HEADERS_RECEIVED', (done) ->
159 | _done = false
160 | @xhr.onreadystatechange = =>
161 | return if _done or @xhr.readyState isnt XMLHttpRequest.HEADERS_RECEIVED
162 | _done = true
163 | headers = @xhr.getAllResponseHeaders()
164 | expect(headers).to.match(/(\A|\r\n)accept-ranges: bytes(\r\n|\Z)/mi)
165 | done()
166 | @xhr.send @headerJson
167 |
168 |
169 |
--------------------------------------------------------------------------------
/test/src/events_test.coffee:
--------------------------------------------------------------------------------
1 | describe 'XMLHttpRequest', ->
2 | beforeEach ->
3 | @xhr = new XMLHttpRequest
4 | @dripUrl = 'http://localhost:8912/_/drip'
5 | @dripJson = drips: 3, size: 1000, ms: 50, length: true
6 |
7 | describe 'level 2 events', ->
8 | beforeEach ->
9 | @events = []
10 | @endFired = false
11 | @events.check = -> null # replaced by tests
12 | @xhr.addEventListener 'loadstart', (event) =>
13 | expect(event.type).to.equal 'loadstart'
14 | expect(@endFired).to.equal false
15 | @events.push event
16 | @xhr.addEventListener 'progress', (event) =>
17 | expect(event.type).to.equal 'progress'
18 | expect(@endFired).to.equal false
19 | @events.push event
20 | @xhr.addEventListener 'load', (event) =>
21 | expect(event.type).to.equal 'load'
22 | expect(@endFired).to.equal false
23 | @events.push event
24 | @xhr.addEventListener 'loadend', (event) =>
25 | expect(event.type).to.equal 'loadend'
26 | expect(@endFired).to.equal false
27 | @endFired = 'loadend already fired'
28 | @events.push event
29 | @events.check()
30 | @xhr.addEventListener 'error', (event) =>
31 | expect(event.type).to.equal 'error'
32 | expect(@endFired).to.equal false
33 | @events.push event
34 | @xhr.addEventListener 'abort', (event) =>
35 | expect(event.type).to.equal 'abort'
36 | expect(@endFired).to.equal false
37 | @events.push event
38 |
39 | describe 'for a successful fetch with Content-Length set', ->
40 | beforeEach ->
41 | @xhr.open 'POST', @dripUrl
42 | @xhr.send JSON.stringify(@dripJson)
43 |
44 | it 'events have the correct target', (done) ->
45 | @events.check = =>
46 | for event in @events
47 | expect(event.target).to.equal @xhr
48 | done()
49 |
50 | it 'events have the correct bubbling setup', (done) ->
51 | @events.check = =>
52 | for event in @events
53 | expect(event.bubbles).to.equal false
54 | expect(event.cancelable).to.equal false
55 | done()
56 |
57 | it 'events have the correct progress info', (done) ->
58 | @events.check = =>
59 | for event in @events
60 | switch event.type
61 | when 'loadstart'
62 | expect(event.loaded).to.equal 0
63 | expect(event.lengthComputable).to.equal false
64 | expect(event.total).to.equal 0
65 | when 'load', 'loadend'
66 | expect(event.loaded).to.equal 3000
67 | expect(event.lengthComputable).to.equal true
68 | expect(event.total).to.equal 3000
69 | when 'progress'
70 | if event.lengthComputable
71 | expect(event.loaded).to.be.gte 0
72 | expect(event.loaded).to.be.lte 3000
73 | expect(event.total).to.equal 3000
74 | else
75 | expect(event.loaded).to.be.gte 0
76 | expect(event.total).to.equal 0
77 | done()
78 |
79 | it 'events include at least one intermediate progress event', (done) ->
80 | @events.check = =>
81 | found = 'no suitable progress event emitted'
82 | for event in @events
83 | continue unless event.type is 'progress'
84 | continue unless event.loaded > 0
85 | continue unless event.loaded < event.total
86 | found = true
87 | expect(found).to.equal true
88 | done()
89 |
90 | describe 'for a successful fetch without Content-Length set', ->
91 | beforeEach ->
92 | @xhr.open 'POST', @dripUrl
93 | @dripJson.length = false
94 | @xhr.send JSON.stringify(@dripJson)
95 |
96 | it 'events have the correct progress info', (done) ->
97 | @events.check = =>
98 | for event in @events
99 | expect(event.lengthComputable).to.equal false
100 | expect(event.total).to.equal 0
101 | switch event.type
102 | when 'loadstart'
103 | expect(event.loaded).to.equal 0
104 | when 'load', 'loadend'
105 | expect(event.loaded).to.equal 3000
106 | when 'progress'
107 | expect(event.loaded).to.be.gte 0
108 | done()
109 |
110 | it 'events include at least one intermediate progress event', (done) ->
111 | @events.check = =>
112 | found = 'no suitable progress event emitted'
113 | for event in @events
114 | continue unless event.type is 'progress'
115 | continue if event.loaded is 0
116 | continue if event.loaded is 3000
117 | found = true
118 | expect(found).to.equal true
119 | done()
120 |
121 | describe 'for a network error due to bad DNS', (done) ->
122 | beforeEach ->
123 | @xhr.open 'GET', 'https://broken.to.cause.an.xhrnetworkerror.com'
124 | @xhr.send()
125 |
126 | it 'no progress or load is emitted', (done) ->
127 | @events.check = =>
128 | for event in @events
129 | expect(event.type).not.to.equal 'load'
130 | expect(event.type).not.to.equal 'progress'
131 | done()
132 |
133 | it 'events include an error event', (done) ->
134 | @events.check = =>
135 | found = 'no suitable error emitted'
136 | for event in @events
137 | continue unless event.type is 'error'
138 | found = true
139 | expect(found).to.equal true
140 | done()
141 |
142 | describe 'readystatechange', ->
143 | beforeEach ->
144 | @events = []
145 | @states = []
146 | @doneFired = false
147 | @events.check = -> null # replaced by tests
148 | @xhr.addEventListener 'readystatechange', (event) =>
149 | expect(event.type).to.equal 'readystatechange'
150 | expect(@doneFired).to.equal false
151 | @events.push event
152 | @states.push event.target.readyState
153 | if event.target.readyState is XMLHttpRequest.DONE
154 | @doneFired = 'DONE already fired'
155 | @events.check()
156 |
157 | describe 'for a successful fetch with Content-Length set', ->
158 | beforeEach ->
159 | @xhr.open 'POST', @dripUrl
160 | @xhr.send JSON.stringify(@dripJson)
161 |
162 | it 'events have the correct target', (done) ->
163 | @events.check = =>
164 | for event in @events
165 | expect(event.target).to.equal @xhr
166 | done()
167 |
168 | it 'events have the correct bubbling setup', (done) ->
169 | @events.check = =>
170 | for event in @events
171 | expect(event.bubbles).to.equal false
172 | expect(event.cancelable).to.equal false
173 | done()
174 |
175 | it 'events states are in the correct order', (done) ->
176 | @events.check = =>
177 | expect(@states).to.deep.equal [XMLHttpRequest.OPENED,
178 | XMLHttpRequest.HEADERS_RECEIVED,
179 | XMLHttpRequest.LOADING, XMLHttpRequest.DONE]
180 | done()
181 |
182 | describe 'for a successful fetch without Content-Length set', ->
183 | beforeEach ->
184 | @xhr.open 'POST', @dripUrl
185 | @dripJson.length = false
186 | @xhr.send JSON.stringify(@dripJson)
187 |
188 | it 'events states are in the correct order', (done) ->
189 | @events.check = =>
190 | expect(@states).to.deep.equal [XMLHttpRequest.OPENED,
191 | XMLHttpRequest.HEADERS_RECEIVED, XMLHttpRequest.LOADING,
192 | XMLHttpRequest.DONE]
193 | done()
194 |
195 | describe 'for a network error due to bad DNS', (done) ->
196 | beforeEach ->
197 | @xhr.open 'GET', 'https://broken.to.cause.an.xhrnetworkerror.com'
198 | @xhr.send()
199 |
200 | it 'events states are in the correct order', (done) ->
201 | @events.check = =>
202 | expect(@states).to.deep.equal [XMLHttpRequest.OPENED,
203 | XMLHttpRequest.DONE]
204 | done()
205 |
--------------------------------------------------------------------------------
/src/001-xml_http_request.coffee:
--------------------------------------------------------------------------------
1 | # This file's name is set up in such a way that it will always show up second
2 | # in the list of files given to coffee --join, so it can use the
3 | # XMLHttpRequestEventTarget definition and so that the other files can assume
4 | # that XMLHttpRequest was already defined.
5 |
6 | http = require 'http'
7 | https = require 'https'
8 | os = require 'os'
9 | url = require 'url'
10 |
11 | # The ECMAScript HTTP API.
12 | #
13 | # @see http://www.w3.org/TR/XMLHttpRequest/#introduction
14 | class XMLHttpRequest extends XMLHttpRequestEventTarget
15 | # Creates a new request.
16 | #
17 | # @param {Object} options one or more of the options below
18 | # @option options {Boolean} anon if true, the request's anonymous flag
19 | # will be set
20 | # @see http://www.w3.org/TR/XMLHttpRequest/#constructors
21 | # @see http://www.w3.org/TR/XMLHttpRequest/#anonymous-flag
22 | constructor: (options) ->
23 | super()
24 | @onreadystatechange = null
25 |
26 | @_anonymous = options and options.anon
27 |
28 | @readyState = XMLHttpRequest.UNSENT
29 | @response = null
30 | @responseText = ''
31 | @responseType = ''
32 | @responseURL = ''
33 | @status = 0
34 | @statusText = ''
35 | @timeout = 0
36 | @upload = new XMLHttpRequestUpload @
37 |
38 | @_method = null # String
39 | @_url = null # Return value of url.parse()
40 | @_sync = false
41 | @_headers = null # Object
42 | @_loweredHeaders = null # Object
43 | @_mimeOverride = null
44 | @_request = null # http.ClientRequest
45 | @_response = null # http.ClientResponse
46 | @_responseParts = null # Array
47 | @_responseHeaders = null # Object
48 | @_aborting = null
49 | @_error = null
50 | @_loadedBytes = 0
51 | @_totalBytes = 0
52 | @_lengthComputable = false
53 |
54 | # @property {function(ProgressEvent)} DOM level 0-style handler for the
55 | # 'readystatechange' event
56 | onreadystatechange: null
57 |
58 | # @property {Number} the current state of the XHR object
59 | # @see http://www.w3.org/TR/XMLHttpRequest/#states
60 | readyState: null
61 |
62 | # @property {String, ArrayBuffer, Buffer, Object} processed XHR response
63 | # @see http://www.w3.org/TR/XMLHttpRequest/#the-response-attribute
64 | response: null
65 |
66 | # @property {String} response string, if responseType is '' or 'text'
67 | # @see http://www.w3.org/TR/XMLHttpRequest/#the-responsetext-attribute
68 | responseText: null
69 |
70 | # @property {String} sets the parsing method for the XHR response
71 | # @see http://www.w3.org/TR/XMLHttpRequest/#the-responsetype-attribute
72 | responseType: null
73 |
74 | # @property {Number} the HTTP
75 | # @see http://www.w3.org/TR/XMLHttpRequest/#the-status-attribute
76 | status: null
77 |
78 | # @property {Number} milliseconds to wait for the request to complete
79 | # @see http://www.w3.org/TR/XMLHttpRequest/#the-timeout-attribute
80 | timeout: null
81 |
82 | # @property {XMLHttpRequestUpload} the associated upload information
83 | # @see http://www.w3.org/TR/XMLHttpRequest/#the-upload-attribute
84 | upload: null
85 |
86 | # Sets the XHR's method, URL, synchronous flag, and authentication params.
87 | #
88 | # @param {String} method the HTTP method to be used
89 | # @param {String} url the URL that the request will be made to
90 | # @param {?Boolean} async if false, the XHR should be processed
91 | # synchronously; true by default
92 | # @param {?String} user the user credential to be used in HTTP basic
93 | # authentication
94 | # @param {?String} password the password credential to be used in HTTP basic
95 | # authentication
96 | # @return {undefined} undefined
97 | # @throw {SecurityError} method is not one of the allowed methods
98 | # @throw {SyntaxError} urlString is not a valid URL
99 | # @throw {Error} the URL contains an unsupported protocol; the supported
100 | # protocols are file, http and https
101 | # @see http://www.w3.org/TR/XMLHttpRequest/#the-open()-method
102 | open: (method, url, async, user, password) ->
103 | method = method.toUpperCase()
104 | if method of @_restrictedMethods
105 | throw new SecurityError "HTTP method #{method} is not allowed in XHR"
106 |
107 | xhrUrl = @_parseUrl url
108 | async = true if async is undefined
109 |
110 | switch @readyState
111 | when XMLHttpRequest.UNSENT, XMLHttpRequest.OPENED, XMLHttpRequest.DONE
112 | # Nothing to do here.
113 | null
114 | when XMLHttpRequest.HEADERS_RECEIVED, XMLHttpRequest.LOADING
115 | # TODO(pwnall): terminate abort(), terminate send()
116 | null
117 |
118 | @_method = method
119 | @_url = xhrUrl
120 | @_sync = !async
121 | @_headers = {}
122 | @_loweredHeaders = {}
123 | @_mimeOverride = null
124 | @_setReadyState XMLHttpRequest.OPENED
125 | @_request = null
126 | @_response = null
127 | @status = 0
128 | @statusText = ''
129 | @_responseParts = []
130 | @_responseHeaders = null
131 | @_loadedBytes = 0
132 | @_totalBytes = 0
133 | @_lengthComputable = false
134 | undefined
135 |
136 | # Appends a header to the list of author request headers.
137 | #
138 | # @param {String} name the HTTP header name
139 | # @param {String} value the HTTP header value
140 | # @return {undefined} undefined
141 | # @throw {InvalidStateError} readyState is not OPENED
142 | # @throw {SyntaxError} name is not a valid HTTP header name or value is not
143 | # a valid HTTP header value
144 | # @see http://www.w3.org/TR/XMLHttpRequest/#the-setrequestheader()-method
145 | setRequestHeader: (name, value) ->
146 | unless @readyState is XMLHttpRequest.OPENED
147 | throw new InvalidStateError "XHR readyState must be OPENED"
148 |
149 | loweredName = name.toLowerCase()
150 | if @_restrictedHeaders[loweredName] or /^sec\-/.test(loweredName) or
151 | /^proxy-/.test(loweredName)
152 | console.warn "Refused to set unsafe header \"#{name}\""
153 | return undefined
154 |
155 | value = value.toString()
156 | if loweredName of @_loweredHeaders
157 | # Combine value with the existing header value.
158 | name = @_loweredHeaders[loweredName]
159 | @_headers[name] = @_headers[name] + ', ' + value
160 | else
161 | # New header.
162 | @_loweredHeaders[loweredName] = name
163 | @_headers[name] = value
164 |
165 | undefined
166 |
167 | # Initiates the request.
168 | #
169 | # @param {?String, ?ArrayBufferView} data the data to be sent; ignored for
170 | # GET and HEAD requests
171 | # @return {undefined} undefined
172 | # @throw {InvalidStateError} readyState is not OPENED
173 | # @see http://www.w3.org/TR/XMLHttpRequest/#the-send()-method
174 | send: (data) ->
175 | unless @readyState is XMLHttpRequest.OPENED
176 | throw new InvalidStateError "XHR readyState must be OPENED"
177 |
178 | if @_request
179 | throw new InvalidStateError "send() already called"
180 |
181 | switch @_url.protocol
182 | when 'file:'
183 | @_sendFile data
184 | when 'http:', 'https:'
185 | @_sendHttp data
186 | else
187 | throw new NetworkError "Unsupported protocol #{@_url.protocol}"
188 |
189 | undefined
190 |
191 | # Cancels the network activity performed by this request.
192 | #
193 | # @return {undefined} undefined
194 | # @see http://www.w3.org/TR/XMLHttpRequest/#the-abort()-method
195 | abort: ->
196 | return unless @_request
197 |
198 | @_request.abort()
199 | @_setError()
200 | @_dispatchProgress 'abort'
201 | @_dispatchProgress 'loadend'
202 | undefined
203 |
204 | # Returns a header value in the HTTP response for this XHR.
205 | #
206 | # @param {String} name case-insensitive HTTP header name
207 | # @return {?String} value the value of the header whose name matches the
208 | # given name, or null if there is no such header
209 | # @see http://www.w3.org/TR/XMLHttpRequest/#the-getresponseheader()-method
210 | getResponseHeader: (name) ->
211 | return null unless @_responseHeaders
212 |
213 | loweredName = name.toLowerCase()
214 | if loweredName of @_responseHeaders
215 | @_responseHeaders[loweredName]
216 | else
217 | null
218 |
219 | # Returns all the HTTP headers in this XHR's response.
220 | #
221 | # @return {String} header lines separated by CR LF, where each header line
222 | # has the name and value separated by a ": " (colon, space); the empty
223 | # string is returned if the headers are not available
224 | # @see http://www.w3.org/TR/XMLHttpRequest/#the-getallresponseheaders()-method
225 | getAllResponseHeaders: ->
226 | return '' unless @_responseHeaders
227 |
228 | lines = ("#{name}: #{value}" for name, value of @_responseHeaders)
229 | lines.join "\r\n"
230 |
231 | # Overrides the Content-Type
232 | #
233 | # @return {undefined} undefined
234 | # @see http://www.w3.org/TR/XMLHttpRequest/#the-overridemimetype()-method
235 | overrideMimeType: (newMimeType) ->
236 | if @readyState is XMLHttpRequest.LOADING or
237 | @readyState is XMLHttpRequest.DONE
238 | throw new InvalidStateError(
239 | "overrideMimeType() not allowed in LOADING or DONE")
240 |
241 | @_mimeOverride = newMimeType.toLowerCase()
242 | undefined
243 |
244 | # Network configuration not exposed in the XHR API.
245 | #
246 | # Although the XMLHttpRequest specification calls itself "ECMAScript HTTP",
247 | # it assumes that requests are always performed in the context of a browser
248 | # application, where some network parameters are set by the browser user and
249 | # should not be modified by Web applications. This API provides access to
250 | # these network parameters.
251 | #
252 | # NOTE: this is not in the XMLHttpRequest API, and will not work in
253 | # browsers. It is a stable node-xhr2 API.
254 | #
255 | # @param {Object} options one or more of the options below
256 | # @option options {?http.Agent} httpAgent the value for the nodejsHttpAgent
257 | # property (the agent used for HTTP requests)
258 | # @option options {?https.Agent} httpsAgent the value for the
259 | # nodejsHttpsAgent property (the agent used for HTTPS requests)
260 | # @return {undefined} undefined
261 | nodejsSet: (options) ->
262 | if 'httpAgent' of options
263 | @nodejsHttpAgent = options.httpAgent
264 | if 'httpsAgent' of options
265 | @nodejsHttpsAgent = options.httpsAgent
266 | if 'baseUrl' of options
267 | baseUrl = options.baseUrl
268 | unless baseUrl is null
269 | parsedUrl = url.parse baseUrl, false, true
270 | unless parsedUrl.protocol
271 | throw new SyntaxError("baseUrl must be an absolute URL")
272 | @nodejsBaseUrl = baseUrl
273 |
274 | undefined
275 |
276 | # Default settings for the network configuration not exposed in the XHR API.
277 | #
278 | # NOTE: this is not in the XMLHttpRequest API, and will not work in
279 | # browsers. It is a stable node-xhr2 API.
280 | #
281 | # @param {Object} options one or more of the options below
282 | # @option options {?http.Agent} httpAgent the default value for the
283 | # nodejsHttpAgent property (the agent used for HTTP requests)
284 | # @option options {https.Agent} httpsAgent the default value for the
285 | # nodejsHttpsAgent property (the agent used for HTTPS requests)
286 | # @return {undefined} undefined
287 | # @see XMLHttpRequest.nodejsSet
288 | @nodejsSet: (options) ->
289 | # "this" will be set to XMLHttpRequest.prototype, so the instance nodejsSet
290 | # operates on default property values.
291 | XMLHttpRequest::nodejsSet options
292 |
293 | undefined
294 |
295 | # readyState value before XMLHttpRequest#open() is called
296 | UNSENT: 0
297 | # readyState value before XMLHttpRequest#open() is called
298 | @UNSENT: 0
299 |
300 | # readyState value after XMLHttpRequest#open() is called, and before
301 | # XMLHttpRequest#send() is called; XMLHttpRequest#setRequestHeader() can be
302 | # called in this state
303 | OPENED: 1
304 | # readyState value after XMLHttpRequest#open() is called, and before
305 | # XMLHttpRequest#send() is called; XMLHttpRequest#setRequestHeader() can be
306 | # called in this state
307 | @OPENED: 1
308 |
309 | # readyState value after redirects have been followed and the HTTP headers of
310 | # the final response have been received
311 | HEADERS_RECEIVED: 2
312 | # readyState value after redirects have been followed and the HTTP headers of
313 | # the final response have been received
314 | @HEADERS_RECEIVED: 2
315 |
316 | # readyState value when the response entity body is being received
317 | LOADING: 3
318 | # readyState value when the response entity body is being received
319 | @LOADING: 3
320 |
321 | # readyState value after the request has been completely processed
322 | DONE: 4
323 | # readyState value after the request has been completely processed
324 | @DONE: 4
325 |
326 | # @property {http.Agent} the agent option passed to HTTP requests
327 | #
328 | # NOTE: this is not in the XMLHttpRequest API, and will not work in browsers.
329 | # It is a stable node-xhr2 API that is useful for testing & going through
330 | # web-proxies.
331 | nodejsHttpAgent: http.globalAgent
332 |
333 | # @property {https.Agent} the agent option passed to HTTPS requests
334 | #
335 | # NOTE: this is not in the XMLHttpRequest API, and will not work in browsers.
336 | # It is a stable node-xhr2 API that is useful for testing & going through
337 | # web-proxies.
338 | nodejsHttpsAgent: https.globalAgent
339 |
340 | # @property {String} the base URL that relative URLs get resolved to
341 | #
342 | # NOTE: this is not in the XMLHttpRequest API, and will not work in browsers.
343 | # Its browser equivalent is the base URL of the document associated with the
344 | # Window object. It is a stable node-xhr2 API provided for libraries such as
345 | # Angular Universal.
346 | nodejsBaseUrl: null
347 |
348 | # HTTP methods that are disallowed in the XHR spec.
349 | #
350 | # @private
351 | # @see Step 6 in http://www.w3.org/TR/XMLHttpRequest/#the-open()-method
352 | _restrictedMethods:
353 | CONNECT: true
354 | TRACE: true
355 | TRACK: true
356 |
357 | # HTTP request headers that are disallowed in the XHR spec.
358 | #
359 | # @private
360 | # @see Step 5 in
361 | # http://www.w3.org/TR/XMLHttpRequest/#the-setrequestheader()-method
362 | _restrictedHeaders:
363 | 'accept-charset': true
364 | 'accept-encoding': true
365 | 'access-control-request-headers': true
366 | 'access-control-request-method': true
367 | connection: true
368 | 'content-length': true
369 | cookie: true
370 | cookie2: true
371 | date: true
372 | dnt: true
373 | expect: true
374 | host: true
375 | 'keep-alive': true
376 | origin: true
377 | referer: true
378 | te: true
379 | trailer: true
380 | 'transfer-encoding': true
381 | upgrade: true
382 | via: true
383 |
384 | # HTTP response headers that should not be exposed according to the XHR spec.
385 | #
386 | # @private
387 | # @see Step 3 in
388 | # http://www.w3.org/TR/XMLHttpRequest/#the-getresponseheader()-method
389 | _privateHeaders:
390 | 'set-cookie': true
391 | 'set-cookie2': true
392 |
393 | # The default value of the User-Agent header.
394 | _userAgent: "Mozilla/5.0 (#{os.type()} #{os.arch()}) " +
395 | "node.js/#{process.versions.node} v8/#{process.versions.v8}"
396 |
397 | # Sets the readyState property and fires the readystatechange event.
398 | #
399 | # @private
400 | # @param {Number} newReadyState the new value of readyState
401 | # @return {undefined} undefined
402 | _setReadyState: (newReadyState) ->
403 | @readyState = newReadyState
404 | event = new ProgressEvent 'readystatechange'
405 | @dispatchEvent event
406 | undefined
407 |
408 | # XMLHttpRequest#send() implementation for the file: protocol.
409 | #
410 | # @private
411 | _sendFile: ->
412 | unless @_url.method is 'GET'
413 | throw new NetworkError 'The file protocol only supports GET'
414 |
415 | throw new Error "Protocol file: not implemented"
416 |
417 | # XMLHttpRequest#send() implementation for the http: and https: protocols.
418 | #
419 | # @private
420 | # This method sets the instance variables and calls _sendHxxpRequest(), which
421 | # is responsible for building a node.js request and firing it off. The code
422 | # in _sendHxxpRequest() is separated off so it can be reused when handling
423 | # redirects.
424 | #
425 | # @see http://www.w3.org/TR/XMLHttpRequest/#infrastructure-for-the-send()-method
426 | _sendHttp: (data) ->
427 | if @_sync
428 | throw new Error "Synchronous XHR processing not implemented"
429 |
430 | if data? and (@_method is 'GET' or @_method is 'HEAD')
431 | console.warn "Discarding entity body for #{@_method} requests"
432 | data = null
433 | else
434 | # Send Content-Length: 0
435 | data or= ''
436 |
437 | # NOTE: this is called before finalizeHeaders so that the uploader can
438 | # figure out Content-Length and Content-Type.
439 | @upload._setData data
440 | @_finalizeHeaders()
441 |
442 | @_sendHxxpRequest()
443 | undefined
444 |
445 | # Sets up and fires off a HTTP/HTTPS request using the node.js API.
446 | #
447 | # @private
448 | # This method contains the bulk of the XMLHttpRequest#send() implementation,
449 | # and is also used to issue new HTTP requests when handling HTTP redirects.
450 | #
451 | # @see http://www.w3.org/TR/XMLHttpRequest/#infrastructure-for-the-send()-method
452 | _sendHxxpRequest: ->
453 | if @_url.protocol is 'http:'
454 | hxxp = http
455 | agent = @nodejsHttpAgent
456 | else
457 | hxxp = https
458 | agent = @nodejsHttpsAgent
459 |
460 | request = hxxp.request
461 | hostname: @_url.hostname, port: @_url.port, path: @_url.path,
462 | auth: @_url.auth, method: @_method, headers: @_headers, agent: agent
463 | @_request = request
464 | if @timeout
465 | request.setTimeout @timeout, => @_onHttpTimeout request
466 | request.on 'response', (response) => @_onHttpResponse request, response
467 | request.on 'error', (error) => @_onHttpRequestError request, error
468 | @upload._startUpload request
469 | if @_request is request # An http error might have already fired.
470 | @_dispatchProgress 'loadstart'
471 |
472 | undefined
473 |
474 | # Fills in the restricted HTTP headers with default values.
475 | #
476 | # This is called right before the HTTP request is sent off.
477 | #
478 | # @private
479 | # @return {undefined} undefined
480 | _finalizeHeaders: ->
481 | @_headers['Connection'] = 'keep-alive'
482 | @_headers['Host'] = @_url.host
483 | if @_anonymous
484 | @_headers['Referer'] = 'about:blank'
485 | @_headers['User-Agent'] ||= @_userAgent
486 | @upload._finalizeHeaders @_headers, @_loweredHeaders
487 | undefined
488 |
489 |
490 | # Called when the headers of an HTTP response have been received.
491 | #
492 | # @private
493 | # @param {http.ClientRequest} request the node.js ClientRequest instance that
494 | # produced this response
495 | # @param {http.ClientResponse} response the node.js ClientResponse instance
496 | # passed to
497 | _onHttpResponse: (request, response) ->
498 | return unless @_request is request
499 |
500 | # Transparent redirection handling.
501 | switch response.statusCode
502 | when 301, 302, 303, 307, 308
503 | @_url = @_parseUrl response.headers['location']
504 | @_method = 'GET'
505 | if 'content-type' of @_loweredHeaders
506 | delete @_headers[@_loweredHeaders['content-type']]
507 | delete @_loweredHeaders['content-type']
508 | # XMLHttpRequestUpload#_finalizeHeaders() sets Content-Type directly.
509 | if 'Content-Type' of @_headers
510 | delete @_headers['Content-Type']
511 | # Restricted headers can't be set by the user, no need to check
512 | # loweredHeaders.
513 | delete @_headers['Content-Length']
514 |
515 | @upload._reset()
516 | @_finalizeHeaders()
517 | @_sendHxxpRequest()
518 | return
519 |
520 | @_response = response
521 | @_response.on 'data', (data) => @_onHttpResponseData response, data
522 | @_response.on 'end', => @_onHttpResponseEnd response
523 | @_response.on 'close', => @_onHttpResponseClose response
524 |
525 | @responseURL = @_url.href.split('#')[0]
526 | @status = @_response.statusCode
527 | @statusText = http.STATUS_CODES[@status]
528 | @_parseResponseHeaders response
529 |
530 | if lengthString = @_responseHeaders['content-length']
531 | @_totalBytes = parseInt(lengthString)
532 | @_lengthComputable = true
533 | else
534 | @_lengthComputable = false
535 |
536 | @_setReadyState XMLHttpRequest.HEADERS_RECEIVED
537 |
538 | # Called when some data has been received on a HTTP connection.
539 | #
540 | # @private
541 | # @param {http.ClientResponse} response the node.js ClientResponse instance
542 | # that fired this event
543 | # @param {String, Buffer} data the data that has been received
544 | _onHttpResponseData: (response, data) ->
545 | return unless @_response is response
546 |
547 | @_responseParts.push data
548 | @_loadedBytes += data.length
549 |
550 | if @readyState isnt XMLHttpRequest.LOADING
551 | @_setReadyState XMLHttpRequest.LOADING
552 | @_dispatchProgress 'progress'
553 |
554 | # Called when the HTTP request finished processing.
555 | #
556 | # @private
557 | # @param {http.ClientResponse} response the node.js ClientResponse instance
558 | # that fired this event
559 | _onHttpResponseEnd: (response) ->
560 | return unless @_response is response
561 |
562 | @_parseResponse()
563 |
564 | @_request = null
565 | @_response = null
566 | @_setReadyState XMLHttpRequest.DONE
567 | @_dispatchProgress 'load'
568 | @_dispatchProgress 'loadend'
569 |
570 | # Called when the underlying HTTP connection was closed prematurely.
571 | #
572 | # If this method is called, it will be called after or instead of
573 | # onHttpResponseEnd.
574 | #
575 | # @private
576 | # @param {http.ClientResponse} response the node.js ClientResponse instance
577 | # that fired this event
578 | _onHttpResponseClose: (response) ->
579 | return unless @_response is response
580 |
581 | request = @_request
582 | @_setError()
583 | request.abort()
584 | @_setReadyState XMLHttpRequest.DONE
585 | @_dispatchProgress 'error'
586 | @_dispatchProgress 'loadend'
587 |
588 | # Called when the timeout set on the HTTP socket expires.
589 | #
590 | # @private
591 | # @param {http.ClientRequest} request the node.js ClientRequest instance that
592 | # fired this event
593 | _onHttpTimeout: (request) ->
594 | return unless @_request is request
595 |
596 | @_setError()
597 | request.abort()
598 | @_setReadyState XMLHttpRequest.DONE
599 | @_dispatchProgress 'timeout'
600 | @_dispatchProgress 'loadend'
601 |
602 | # Called when something wrong happens on the HTTP socket
603 | #
604 | # @private
605 | # @param {http.ClientRequest} request the node.js ClientRequest instance that
606 | # fired this event
607 | # @param {Error} error emitted exception
608 | _onHttpRequestError: (request, error) ->
609 | return unless @_request is request
610 |
611 | @_setError()
612 | request.abort()
613 | @_setReadyState XMLHttpRequest.DONE
614 | @_dispatchProgress 'error'
615 | @_dispatchProgress 'loadend'
616 |
617 | # Fires an XHR progress event.
618 | #
619 | # @private
620 | # @param {String} eventType one of the XHR progress event types, such as
621 | # 'load' and 'progress'
622 | _dispatchProgress: (eventType) ->
623 | event = new ProgressEvent eventType
624 | event.lengthComputable = @_lengthComputable
625 | event.loaded = @_loadedBytes
626 | event.total = @_totalBytes
627 | @dispatchEvent event
628 | undefined
629 |
630 | # Sets up the XHR to reflect the fact that an error has occurred.
631 | #
632 | # The possible errors are a network error, a timeout, or an abort.
633 | #
634 | # @private
635 | _setError: ->
636 | @_request = null
637 | @_response = null
638 | @_responseHeaders = null
639 | @_responseParts = null
640 | undefined
641 |
642 | # Parses a request URL string.
643 | #
644 | # @private
645 | # This method is a thin wrapper around url.parse() that normalizes HTTP
646 | # user/password credentials. It is used to parse the URL string passed to
647 | # XMLHttpRequest#open() and the URLs in the Location headers of HTTP redirect
648 | # responses.
649 | #
650 | # @param {String} urlString the URL to be parsed
651 | # @return {Object} parsed URL
652 | _parseUrl: (urlString) ->
653 | if @nodejsBaseUrl is null
654 | absoluteUrlString = urlString
655 | else
656 | absoluteUrlString = url.resolve @nodejsBaseUrl, urlString
657 |
658 | xhrUrl = url.parse absoluteUrlString, false, true
659 | xhrUrl.hash = null
660 | if xhrUrl.auth and (user? or password?)
661 | index = xhrUrl.auth.indexOf ':'
662 | if index is -1
663 | user = xhrUrl.auth unless user
664 | else
665 | user = xhrUrl.substring(0, index) unless user
666 | password = xhrUrl.substring(index + 1) unless password
667 | if user or password
668 | xhrUrl.auth = "#{user}:#{password}"
669 | xhrUrl
670 |
671 | # Reads the headers from a node.js ClientResponse instance.
672 | #
673 | # @private
674 | # @param {http.ClientResponse} response the response whose headers will be
675 | # imported into this XMLHttpRequest's state
676 | # @return {undefined} undefined
677 | # @see http://www.w3.org/TR/XMLHttpRequest/#the-getresponseheader()-method
678 | # @see http://www.w3.org/TR/XMLHttpRequest/#the-getallresponseheaders()-method
679 | _parseResponseHeaders: (response) ->
680 | @_responseHeaders = {}
681 | for name, value of response.headers
682 | loweredName = name.toLowerCase()
683 | continue if @_privateHeaders[loweredName]
684 | if @_mimeOverride isnt null and loweredName is 'content-type'
685 | value = @_mimeOverride
686 | @_responseHeaders[loweredName] = value
687 |
688 | if @_mimeOverride isnt null and !('content-type' of @_responseHeaders)
689 | @_responseHeaders['content-type'] = @_mimeOverride
690 | undefined
691 |
692 | # Sets the response and responseText properties when an XHR completes.
693 | #
694 | # @private
695 | # @return {undefined} undefined
696 | _parseResponse: ->
697 | if Buffer.concat
698 | buffer = Buffer.concat @_responseParts
699 | else
700 | # node 0.6
701 | buffer = @_concatBuffers @_responseParts
702 | @_responseParts = null
703 |
704 | switch @responseType
705 | when 'text'
706 | @_parseTextResponse buffer
707 | when 'json'
708 | @responseText = null
709 | try
710 | @response = JSON.parse buffer.toString('utf-8')
711 | catch jsonError
712 | @response = null
713 | when 'buffer'
714 | @responseText = null
715 | @response = buffer
716 | when 'arraybuffer'
717 | @responseText = null
718 | arrayBuffer = new ArrayBuffer buffer.length
719 | view = new Uint8Array arrayBuffer
720 | view[i] = buffer[i] for i in [0...buffer.length]
721 | @response = arrayBuffer
722 | else
723 | # TODO(pwnall): content-base detection
724 | @_parseTextResponse buffer
725 | undefined
726 |
727 | # Sets response and responseText for a 'text' response type.
728 | #
729 | # @private
730 | # @param {Buffer} buffer the node.js Buffer containing the binary response
731 | # @return {undefined} undefined
732 | _parseTextResponse: (buffer) ->
733 | try
734 | @responseText = buffer.toString @_parseResponseEncoding()
735 | catch e
736 | # Unknown encoding.
737 | @responseText = buffer.toString 'binary'
738 |
739 | @response = @responseText
740 | undefined
741 |
742 | # Figures out the string encoding of the XHR's response.
743 | #
744 | # This is called to determine the encoding when responseText is set.
745 | #
746 | # @private
747 | # @return {String} a string encoding, e.g. 'utf-8'
748 | _parseResponseEncoding: ->
749 | encoding = null
750 | if contentType = @_responseHeaders['content-type']
751 | if match = /\;\s*charset\=(.*)$/.exec contentType
752 | return match[1]
753 | 'utf-8'
754 |
755 | # Buffer.concat implementation for node 0.6.
756 | #
757 | # @private
758 | # @param {Array} buffers the buffers whose contents will be merged
759 | # @return {Buffer} same as Buffer.concat(buffers) in node 0.8 and above
760 | _concatBuffers: (buffers) ->
761 | if buffers.length is 0
762 | return Buffer.alloc 0
763 | if buffers.length is 1
764 | return buffers[0]
765 |
766 | length = 0
767 | length += buffer.length for buffer in buffers
768 | target = Buffer.alloc length
769 | length = 0
770 | for buffer in buffers
771 | buffer.copy target, length
772 | length += buffer.length
773 | target
774 |
775 | # XMLHttpRequest is the result of require('node-xhr2').
776 | module.exports = XMLHttpRequest
777 |
778 | # Make node-xhr2 work as a drop-in replacement for libraries that promote the
779 | # following usage pattern:
780 | # var XMLHttpRequest = require('xhr-library-name').XMLHttpRequest
781 | XMLHttpRequest.XMLHttpRequest = XMLHttpRequest
782 |
--------------------------------------------------------------------------------