net.js 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  1. 'use strict'
  2. const url = require('url')
  3. const { EventEmitter } = require('events')
  4. const { Readable } = require('stream')
  5. const { app } = require('electron')
  6. const { Session } = process.atomBinding('session')
  7. const { net, Net } = process.atomBinding('net')
  8. const { URLRequest } = net
  9. // Net is an EventEmitter.
  10. Object.setPrototypeOf(Net.prototype, EventEmitter.prototype)
  11. EventEmitter.call(net)
  12. Object.setPrototypeOf(URLRequest.prototype, EventEmitter.prototype)
  13. const kSupportedProtocols = new Set(['http:', 'https:'])
  14. class IncomingMessage extends Readable {
  15. constructor (urlRequest) {
  16. super()
  17. this.urlRequest = urlRequest
  18. this.shouldPush = false
  19. this.data = []
  20. this.urlRequest.on('data', (event, chunk) => {
  21. this._storeInternalData(chunk)
  22. this._pushInternalData()
  23. })
  24. this.urlRequest.on('end', () => {
  25. this._storeInternalData(null)
  26. this._pushInternalData()
  27. })
  28. }
  29. get statusCode () {
  30. return this.urlRequest.statusCode
  31. }
  32. get statusMessage () {
  33. return this.urlRequest.statusMessage
  34. }
  35. get headers () {
  36. return this.urlRequest.rawResponseHeaders
  37. }
  38. get httpVersion () {
  39. return `${this.httpVersionMajor}.${this.httpVersionMinor}`
  40. }
  41. get httpVersionMajor () {
  42. return this.urlRequest.httpVersionMajor
  43. }
  44. get httpVersionMinor () {
  45. return this.urlRequest.httpVersionMinor
  46. }
  47. get rawTrailers () {
  48. throw new Error('HTTP trailers are not supported.')
  49. }
  50. get trailers () {
  51. throw new Error('HTTP trailers are not supported.')
  52. }
  53. _storeInternalData (chunk) {
  54. this.data.push(chunk)
  55. }
  56. _pushInternalData () {
  57. while (this.shouldPush && this.data.length > 0) {
  58. const chunk = this.data.shift()
  59. this.shouldPush = this.push(chunk)
  60. }
  61. }
  62. _read () {
  63. this.shouldPush = true
  64. this._pushInternalData()
  65. }
  66. }
  67. URLRequest.prototype._emitRequestEvent = function (isAsync, ...rest) {
  68. if (isAsync) {
  69. process.nextTick(() => {
  70. this.clientRequest.emit(...rest)
  71. })
  72. } else {
  73. this.clientRequest.emit(...rest)
  74. }
  75. }
  76. URLRequest.prototype._emitResponseEvent = function (isAsync, ...rest) {
  77. if (isAsync) {
  78. process.nextTick(() => {
  79. this._response.emit(...rest)
  80. })
  81. } else {
  82. this._response.emit(...rest)
  83. }
  84. }
  85. class ClientRequest extends EventEmitter {
  86. constructor (options, callback) {
  87. super()
  88. if (!app.isReady()) {
  89. throw new Error('net module can only be used after app is ready')
  90. }
  91. if (typeof options === 'string') {
  92. options = url.parse(options)
  93. } else {
  94. options = Object.assign({}, options)
  95. }
  96. const method = (options.method || 'GET').toUpperCase()
  97. let urlStr = options.url
  98. if (!urlStr) {
  99. const urlObj = {}
  100. const protocol = options.protocol || 'http:'
  101. if (!kSupportedProtocols.has(protocol)) {
  102. throw new Error('Protocol "' + protocol + '" not supported. ')
  103. }
  104. urlObj.protocol = protocol
  105. if (options.host) {
  106. urlObj.host = options.host
  107. } else {
  108. if (options.hostname) {
  109. urlObj.hostname = options.hostname
  110. } else {
  111. urlObj.hostname = 'localhost'
  112. }
  113. if (options.port) {
  114. urlObj.port = options.port
  115. }
  116. }
  117. if (options.path && / /.test(options.path)) {
  118. // The actual regex is more like /[^A-Za-z0-9\-._~!$&'()*+,;=/:@]/
  119. // with an additional rule for ignoring percentage-escaped characters
  120. // but that's a) hard to capture in a regular expression that performs
  121. // well, and b) possibly too restrictive for real-world usage. That's
  122. // why it only scans for spaces because those are guaranteed to create
  123. // an invalid request.
  124. throw new TypeError('Request path contains unescaped characters.')
  125. }
  126. const pathObj = url.parse(options.path || '/')
  127. urlObj.pathname = pathObj.pathname
  128. urlObj.search = pathObj.search
  129. urlObj.hash = pathObj.hash
  130. urlStr = url.format(urlObj)
  131. }
  132. const redirectPolicy = options.redirect || 'follow'
  133. if (!['follow', 'error', 'manual'].includes(redirectPolicy)) {
  134. throw new Error('redirect mode should be one of follow, error or manual')
  135. }
  136. const urlRequestOptions = {
  137. method: method,
  138. url: urlStr,
  139. redirect: redirectPolicy
  140. }
  141. if (options.session) {
  142. if (options.session instanceof Session) {
  143. urlRequestOptions.session = options.session
  144. } else {
  145. throw new TypeError('`session` should be an instance of the Session class.')
  146. }
  147. } else if (options.partition) {
  148. if (typeof options.partition === 'string') {
  149. urlRequestOptions.partition = options.partition
  150. } else {
  151. throw new TypeError('`partition` should be an a string.')
  152. }
  153. }
  154. const urlRequest = new URLRequest(urlRequestOptions)
  155. // Set back and forward links.
  156. this.urlRequest = urlRequest
  157. urlRequest.clientRequest = this
  158. // This is a copy of the extra headers structure held by the native
  159. // net::URLRequest. The main reason is to keep the getHeader API synchronous
  160. // after the request starts.
  161. this.extraHeaders = {}
  162. if (options.headers) {
  163. for (const key in options.headers) {
  164. this.setHeader(key, options.headers[key])
  165. }
  166. }
  167. // Set when the request uses chunked encoding. Can be switched
  168. // to true only once and never set back to false.
  169. this.chunkedEncodingEnabled = false
  170. urlRequest.on('response', () => {
  171. const response = new IncomingMessage(urlRequest)
  172. urlRequest._response = response
  173. this.emit('response', response)
  174. })
  175. urlRequest.on('login', (event, authInfo, callback) => {
  176. this.emit('login', authInfo, (username, password) => {
  177. // If null or undefined username/password, force to empty string.
  178. if (username === null || username === undefined) {
  179. username = ''
  180. }
  181. if (typeof username !== 'string') {
  182. throw new Error('username must be a string')
  183. }
  184. if (password === null || password === undefined) {
  185. password = ''
  186. }
  187. if (typeof password !== 'string') {
  188. throw new Error('password must be a string')
  189. }
  190. callback(username, password)
  191. })
  192. })
  193. if (callback) {
  194. this.once('response', callback)
  195. }
  196. }
  197. get chunkedEncoding () {
  198. return this.chunkedEncodingEnabled
  199. }
  200. set chunkedEncoding (value) {
  201. if (!this.urlRequest.notStarted) {
  202. throw new Error('Can\'t set the transfer encoding, headers have been sent.')
  203. }
  204. this.chunkedEncodingEnabled = value
  205. }
  206. setHeader (name, value) {
  207. if (typeof name !== 'string') {
  208. throw new TypeError('`name` should be a string in setHeader(name, value).')
  209. }
  210. if (value == null) {
  211. throw new Error('`value` required in setHeader("' + name + '", value).')
  212. }
  213. if (!this.urlRequest.notStarted) {
  214. throw new Error('Can\'t set headers after they are sent.')
  215. }
  216. const key = name.toLowerCase()
  217. this.extraHeaders[key] = value
  218. this.urlRequest.setExtraHeader(name, value.toString())
  219. }
  220. getHeader (name) {
  221. if (name == null) {
  222. throw new Error('`name` is required for getHeader(name).')
  223. }
  224. if (!this.extraHeaders) {
  225. return
  226. }
  227. const key = name.toLowerCase()
  228. return this.extraHeaders[key]
  229. }
  230. removeHeader (name) {
  231. if (name == null) {
  232. throw new Error('`name` is required for removeHeader(name).')
  233. }
  234. if (!this.urlRequest.notStarted) {
  235. throw new Error('Can\'t remove headers after they are sent.')
  236. }
  237. const key = name.toLowerCase()
  238. delete this.extraHeaders[key]
  239. this.urlRequest.removeExtraHeader(name)
  240. }
  241. _write (chunk, encoding, callback, isLast) {
  242. const chunkIsString = typeof chunk === 'string'
  243. const chunkIsBuffer = chunk instanceof Buffer
  244. if (!chunkIsString && !chunkIsBuffer) {
  245. throw new TypeError('First argument must be a string or Buffer.')
  246. }
  247. if (chunkIsString) {
  248. // We convert all strings into binary buffers.
  249. chunk = Buffer.from(chunk, encoding)
  250. }
  251. // Since writing to the network is asynchronous, we conservatively
  252. // assume that request headers are written after delivering the first
  253. // buffer to the network IO thread.
  254. if (this.urlRequest.notStarted) {
  255. this.urlRequest.setChunkedUpload(this.chunkedEncoding)
  256. }
  257. // Headers are assumed to be sent on first call to _writeBuffer,
  258. // i.e. after the first call to write or end.
  259. const result = this.urlRequest.write(chunk, isLast)
  260. // The write callback is fired asynchronously to mimic Node.js.
  261. if (callback) {
  262. process.nextTick(callback)
  263. }
  264. return result
  265. }
  266. write (data, encoding, callback) {
  267. if (this.urlRequest.finished) {
  268. const error = new Error('Write after end.')
  269. process.nextTick(writeAfterEndNT, this, error, callback)
  270. return true
  271. }
  272. return this._write(data, encoding, callback, false)
  273. }
  274. end (data, encoding, callback) {
  275. if (this.urlRequest.finished) {
  276. return false
  277. }
  278. if (typeof data === 'function') {
  279. callback = data
  280. encoding = null
  281. data = null
  282. } else if (typeof encoding === 'function') {
  283. callback = encoding
  284. encoding = null
  285. }
  286. data = data || ''
  287. return this._write(data, encoding, callback, true)
  288. }
  289. followRedirect () {
  290. this.urlRequest.followRedirect()
  291. }
  292. abort () {
  293. this.urlRequest.cancel()
  294. }
  295. getUploadProgress () {
  296. return this.urlRequest.getUploadProgress()
  297. }
  298. }
  299. function writeAfterEndNT (self, error, callback) {
  300. self.emit('error', error)
  301. if (callback) callback(error)
  302. }
  303. Net.prototype.request = function (options, callback) {
  304. return new ClientRequest(options, callback)
  305. }
  306. net.ClientRequest = ClientRequest
  307. module.exports = net