emailjs-tcp-socket
Version:
This shim brings the W3C Raw Socket API to node.js and Chromium. Its purpose is to enable apps to use the same api in Firefox OS, Chrome OS, and on the server.
196 lines (161 loc) • 5.9 kB
JavaScript
import { propOr } from 'ramda'
export default class TCPSocket {
static open (host, port, options = {}) {
return new TCPSocket({ host, port, options })
}
constructor ({ host, port, options }) {
this.host = new Windows.Networking.HostName(host) // NB! HostName constructor will throw on invalid input
this.port = port
this.ssl = propOr(false, 'useSecureTransport')(options)
this.bufferedAmount = 0
this.readyState = 'connecting'
this.binaryType = propOr('arraybuffer', 'binaryType')(options)
if (this.binaryType !== 'arraybuffer') {
throw new Error('Only arraybuffers are supported!')
}
this._socket = new Windows.Networking.Sockets.StreamSocket()
this._socket.control.keepAlive = true
this._socket.control.noDelay = true
this._dataReader = null
this._dataWriter = null
// set to true if upgrading with STARTTLS
this._upgrading = false
// cache all client.send calls to this array if currently upgrading
this._upgradeCache = []
// initial socket type. default is 'plainSocket' (no encryption applied)
// 'tls12' supports the TLS 1.2, TLS 1.1 and TLS 1.0 protocols but no SSL
this._protectionLevel = Windows.Networking.Sockets.SocketProtectionLevel[this.ssl ? 'tls12' : 'plainSocket']
// Initiate connection to destination
this._socket
.connectAsync(this.host, this.port, this._protectionLevel)
.done(() => {
this._setStreamHandlers()
this._emit('open')
}, e => this._emit('error', e))
}
/**
* Initiate Reader and Writer interfaces for the socket
*/
_setStreamHandlers () {
this._dataReader = new Windows.Storage.Streams.DataReader(this._socket.inputStream)
this._dataReader.inputStreamOptions = Windows.Storage.Streams.InputStreamOptions.partial
// setup writer
this._dataWriter = new Windows.Storage.Streams.DataWriter(this._socket.outputStream)
// start byte reader loop
this._read()
}
/**
* Emit an error and close socket
*
* @param {Error} error Error object
*/
_errorHandler (error) {
// we ignore errors after close has been called, since all aborted operations
// will emit their error handlers
// this will also apply to starttls as a read call is aborted before upgrading the socket
if (this._upgrading || (this.readyState !== 'closing' && this.readyState !== 'closed')) {
this._emit('error', error)
this.close()
}
}
/**
* Read available bytes from the socket. This method is recursive once it ends, it restarts itthis
*/
_read () {
if (this._upgrading || (this.readyState !== 'open' && this.readyState !== 'connecting')) {
return // do nothing if socket not open
}
// Read up to 4096 bytes from the socket. This is not a fixed number (the mode was set
// with inputStreamOptions.partial property), so it might return with a smaller
// amount of bytes.
this._dataReader.loadAsync(4096).done(availableByteCount => {
if (!availableByteCount) {
// no bytes available for reading, restart the reading process
return setImmediate(this._read.bind(this))
}
// we need an Uint8Array that gets filled with the bytes from the buffer
var data = new Uint8Array(availableByteCount)
this._dataReader.readBytes(data) // data argument gets filled with the bytes
this._emit('data', data.buffer)
// restart reading process
return setImmediate(this._read.bind(this))
}, e => this._errorHandler(e))
}
//
// API
//
close () {
this.readyState = 'closing'
try {
this._socket.close()
} catch (E) {
this._emit('error', E)
}
setImmediate(this._emit.bind(this, 'close'))
}
send (data) {
if (this.readyState !== 'open') {
return
}
if (this._upgrading) {
this._upgradeCache.push(data)
return
}
// Write bytes to buffer
this._dataWriter.writeBytes(data)
// Emit buffer contents
this._dataWriter.storeAsync().done(() => this._emit('drain'), (e) => this._errorHandler(e))
}
upgradeToSecure () {
if (this.ssl || this._upgrading) return
this._upgrading = true
try {
// release current input stream. this is required to allow socket upgrade
// write stream is not released as all send calls are cached from this point onwards
// and not passed to socket until the socket is upgraded
this._dataReader.detachStream()
} catch (E) { }
// update protection level
this._protectionLevel = Windows.Networking.Sockets.SocketProtectionLevel.tls12
this._socket.upgradeToSslAsync(this._protectionLevel, this.host).done(
() => {
this._upgrading = false
this.ssl = true // secured connection from now on
this._dataReader = new Windows.Storage.Streams.DataReader(this._socket.inputStream)
this._dataReader.inputStreamOptions = Windows.Storage.Streams.InputStreamOptions.partial
this._read()
// emit all cached requests
while (this._upgradeCache.length) {
const data = this._upgradeCache.shift()
this.send(data)
}
},
(e) => {
this._upgrading = false
this._errorHandler(e)
}
)
}
_emit (type, data) {
const target = this
switch (type) {
case 'open':
this.readyState = 'open'
this.onopen && this.onopen({ target, type, data })
break
case 'error':
this.onerror && this.onerror({ target, type, data })
break
case 'data':
this.ondata && this.ondata({ target, type, data })
break
case 'drain':
this.ondrain && this.ondrain({ target, type, data })
break
case 'close':
this.readyState = 'closed'
this.onclose && this.onclose({ target, type, data })
break
}
}
}