diff --git a/capacitor/capacitor.config.js b/capacitor/capacitor.config.js index 6ea64fb..a1e4760 100644 --- a/capacitor/capacitor.config.js +++ b/capacitor/capacitor.config.js @@ -6,6 +6,9 @@ const config = { plugins: { SplashScreen: { launchShowDuration: 0 + }, + CapacitorHttp: { + enabled: false } }, // remove server section before making production build @@ -13,7 +16,7 @@ const config = { // for android only, below settings will work out of the box // for iOS or both, change the url to http://your-device-ip // To discover your workstation IP, just run ifconfig - url: 'http://10.0.2.2:5001', + url: 'http://localhost:5001/app.html', cleartext: true } } diff --git a/capacitor/package.json b/capacitor/package.json index 7658861..d8eeff6 100644 --- a/capacitor/package.json +++ b/capacitor/package.json @@ -1,26 +1,43 @@ { + "name": "capacitor", + "private": true, "scripts": { "build:app": "vite build", "build:android": "run-s build:app cap-run:android", "build:ios": "run-s build:app cap-run:ios", - "cap-run:android": "cap sync android && cap open android", + "cap-run:android": "cap run android --target=Pixel_XL_API_33 --external --public-host=10.5.0.2", "cap-run:ios": "cap sync ios && cap open ios", "dev:ios": "run-p dev:start cap-run:ios", - "dev:android": "run-p dev:start cap-run:android", "dev:preview": "vite preview", - "dev:start": "run-p dev:vite", - "dev:vite": "vite --host --port 5001" + "dev:start": "run-p dev:webpack cap-run:android", + "dev:webpack": "webpack serve", + "dev:localhost-bind": "adb reverse tcp:5001 tcp:5001" }, "devDependencies": { - "cordova-res": "^0.15.4" + "assert": "^2.1.0", + "buffer": "^6.0.3", + "chrome-dgram": "^3.0.6", + "chrome-net": "^3.3.4", + "cordova-res": "^0.15.4", + "crypto-browserify": "^3.12.0", + "hybrid-chunk-store": "^1.2.2", + "path-esm": "^1.0.0", + "querystring": "^0.2.1", + "stream-browserify": "^3.0.0", + "stream-http": "^3.2.0", + "timers-browserify": "^2.0.12", + "util": "^0.12.5", + "webpack-cli": "^5.1.4" }, "dependencies": { "@capacitor/android": "^5.5.1", "@capacitor/cli": "^5.5.1", "@capacitor/core": "^5.5.1", "@capacitor/ios": "^5.5.1", + "@superfrogbe/cordova-plugin-chrome-apps-sockets-udp": "github:superfrogbe/cordova-plugin-chrome-apps-sockets-udp", + "common": "workspace:*", "cordova-plugin-chrome-apps-common": "^1.0.7", "cordova-plugin-chrome-apps-sockets-tcp": "github:KoenLav/cordova-plugin-chrome-apps-sockets-tcp", - "cordova-plugin-chrome-apps-sockets-udp": "^1.3.0" + "webpack-merge": "^5.10.0" } } diff --git a/capacitor/src/App.svelte b/capacitor/src/App.svelte deleted file mode 100644 index 01b86dc..0000000 --- a/capacitor/src/App.svelte +++ /dev/null @@ -1 +0,0 @@ -

uwu

diff --git a/capacitor/src/chrome-dgram.js b/capacitor/src/chrome-dgram.js new file mode 100644 index 0000000..bddd101 --- /dev/null +++ b/capacitor/src/chrome-dgram.js @@ -0,0 +1,497 @@ +/*! chrome-dgram. MIT License. Feross Aboukhadijeh */ +/* global chrome */ + +/** + * UDP / Datagram Sockets + * ====================== + * + * Datagram sockets are available through require('chrome-dgram'). + */ + +exports.Socket = Socket + +const EventEmitter = require('events').EventEmitter +const inherits = require('inherits') +const series = require('run-series') + +const BIND_STATE_UNBOUND = 0 +const BIND_STATE_BINDING = 1 +const BIND_STATE_BOUND = 2 + +// Track open sockets to route incoming data (via onReceive) to the right handlers. +const sockets = {} + +// Thorough check for Chrome App since both Edge and Chrome implement dummy chrome object +if ( + typeof chrome === 'object' && + typeof chrome.sockets === 'object' && + typeof chrome.sockets.udp === 'object' +) { + chrome.sockets.udp.onReceive.addListener(onReceive) + chrome.sockets.udp.onReceiveError.addListener(onReceiveError) +} + +function onReceive (info) { + if (info.socketId in sockets) { + sockets[info.socketId]._onReceive(info) + } else { + console.error('Unknown socket id: ' + info.socketId) + } +} + +function onReceiveError (info) { + if (info.socketId in sockets) { + sockets[info.socketId]._onReceiveError(info.resultCode) + } else { + console.error('Unknown socket id: ' + info.socketId) + } +} + +/** + * dgram.createSocket(type, [callback]) + * + * Creates a datagram Socket of the specified types. Valid types are `udp4` + * and `udp6`. + * + * Takes an optional callback which is added as a listener for message events. + * + * Call socket.bind if you want to receive datagrams. socket.bind() will bind + * to the "all interfaces" address on a random port (it does the right thing + * for both udp4 and udp6 sockets). You can then retrieve the address and port + * with socket.address().address and socket.address().port. + * + * @param {string} type Either 'udp4' or 'udp6' + * @param {function} listener Attached as a listener to message events. + * Optional + * @return {Socket} Socket object + */ +exports.createSocket = function (type, listener) { + return new Socket(type, listener) +} + +inherits(Socket, EventEmitter) + +/** + * Class: dgram.Socket + * + * The dgram Socket class encapsulates the datagram functionality. It should + * be created via `dgram.createSocket(type, [callback])`. + * + * Event: 'message' + * - msg Buffer object. The message + * - rinfo Object. Remote address information + * Emitted when a new datagram is available on a socket. msg is a Buffer and + * rinfo is an object with the sender's address information and the number + * of bytes in the datagram. + * + * Event: 'listening' + * Emitted when a socket starts listening for datagrams. This happens as soon + * as UDP sockets are created. + * + * Event: 'close' + * Emitted when a socket is closed with close(). No new message events will + * be emitted on this socket. + * + * Event: 'error' + * - exception Error object + * Emitted when an error occurs. + */ +function Socket (options, listener) { + const self = this + EventEmitter.call(self) + if (typeof options === 'string') options = { type: options } + if (options.type !== 'udp4') throw new Error('Bad socket type specified. Valid types are: udp4') + + if (typeof listener === 'function') self.on('message', listener) + + self._destroyed = false + self._bindState = BIND_STATE_UNBOUND + self._bindTasks = [] +} + +/** + * socket.bind(port, [address], [callback]) + * + * For UDP sockets, listen for datagrams on a named port and optional address. + * If address is not specified, the OS will try to listen on all addresses. + * After binding is done, a "listening" event is emitted and the callback(if + * specified) is called. Specifying both a "listening" event listener and + * callback is not harmful but not very useful. + * + * A bound datagram socket keeps the node process running to receive + * datagrams. + * + * If binding fails, an "error" event is generated. In rare case (e.g. binding + * a closed socket), an Error may be thrown by this method. + * + * @param {number} port + * @param {string} address Optional + * @param {function} callback Function with no parameters, Optional. Callback + * when binding is done. + */ +Socket.prototype.bind = function (port, address, callback) { + const self = this + if (typeof address === 'function') { + callback = address + address = undefined + } + + if (!address) address = '0.0.0.0' + + if (!port) port = 0 + + if (self._bindState !== BIND_STATE_UNBOUND) throw new Error('Socket is already bound') + + self._bindState = BIND_STATE_BINDING + + if (typeof callback === 'function') self.once('listening', callback) + + chrome.sockets.udp.create(function (createInfo) { + self.id = createInfo.socketId + + sockets[self.id] = self + + const bindFns = self._bindTasks.map(function (t) { return t.fn }) + + series(bindFns, function (err) { + if (err) return self.emit('error', err) + chrome.sockets.udp.bind(self.id, address, port, function (result) { + if (result < 0) { + self.emit('error', new Error('Socket ' + self.id + ' failed to bind. ' + + chrome.runtime.lastError.message)) + return + } + chrome.sockets.udp.getInfo(self.id, function (socketInfo) { + if (!socketInfo.localPort || !socketInfo.localAddress) { + self.emit('error', new Error('Cannot get local port/address for Socket ' + self.id)) + return + } + + self._port = socketInfo.localPort + self._address = socketInfo.localAddress + + self._bindState = BIND_STATE_BOUND + self.emit('listening') + + self._bindTasks.map(function (t) { + t.callback() + }) + }) + }) + }) + }) +} + +/** + * Internal function to receive new messages and emit `message` events. + */ +Socket.prototype._onReceive = function (info) { + const self = this + + const buf = Buffer.from(new Uint8Array(info.data)) + const rinfo = { + address: info.remoteAddress, + family: 'IPv4', + port: info.remotePort, + size: buf.length + } + self.emit('message', buf, rinfo) +} + +Socket.prototype._onReceiveError = function (resultCode) { + const self = this + self.emit('error', new Error('Socket ' + self.id + ' receive error ' + resultCode)) +} + +/** + * socket.send(buf, offset, length, port, address, [callback]) + * + * For UDP sockets, the destination port and IP address must be + * specified. A string may be supplied for the address parameter, and it will + * be resolved with DNS. An optional callback may be specified to detect any + * DNS errors and when buf may be re-used. Note that DNS lookups will delay + * the time that a send takes place, at least until the next tick. The only + * way to know for sure that a send has taken place is to use the callback. + * + * If the socket has not been previously bound with a call to bind, it's + * assigned a random port number and bound to the "all interfaces" address + * (0.0.0.0 for udp4 sockets, ::0 for udp6 sockets). + * + * @param {Buffer|Arrayish|string} buf Message to be sent + * @param {number} offset Offset in the buffer where the message starts. Optional. + * @param {number} length Number of bytes in the message. Optional. + * @param {number} port destination port + * @param {string} address destination IP + * @param {function} callback Callback when message is done being delivered. + * Optional. + * + * Valid combinations: + * send(buffer, offset, length, port, address, callback) + * send(buffer, offset, length, port, address) + * send(buffer, offset, length, port) + * send(bufferOrList, port, address, callback) + * send(bufferOrList, port, address) + * send(bufferOrList, port) + * + */ +Socket.prototype.send = function (buffer, offset, length, port, address, callback) { + const self = this + + let list + + if (address || (port && typeof port !== 'function')) { + buffer = sliceBuffer(buffer, offset, length) + } else { + callback = port + port = offset + address = length + } + + if (!Array.isArray(buffer)) { + if (typeof buffer === 'string') { + list = [Buffer.from(buffer)] + } else if (!(buffer instanceof Uint8Array)) { + throw new TypeError('First argument must be a buffer or a string') + } else { + list = [Buffer.from(buffer)] + } + } else if (!(list = fixBufferList(buffer))) { + throw new TypeError('Buffer list arguments must be buffers or strings') + } + + port = port >>> 0 + if (port === 0 || port > 65535) { + throw new RangeError('Port should be > 0 and < 65536') + } + + // Normalize callback so it's always a function + if (typeof callback !== 'function') { + callback = function () {} + } + + if (self._bindState === BIND_STATE_UNBOUND) self.bind(0) + + // If the socket hasn't been bound yet, push the outbound packet onto the + // send queue and send after binding is complete. + if (self._bindState !== BIND_STATE_BOUND) { + // If the send queue hasn't been initialized yet, do it, and install an + // event handler that flishes the send queue after binding is done. + if (!self._sendQueue) { + self._sendQueue = [] + self.once('listening', function () { + // Flush the send queue. + for (let i = 0; i < self._sendQueue.length; i++) { + self.send.apply(self, self._sendQueue[i]) + } + self._sendQueue = undefined + }) + } + self._sendQueue.push([buffer, offset, length, port, address, callback]) + return + } + + const ab = Buffer.concat(list).buffer + + chrome.sockets.udp.send(self.id, ab, address, port, function (sendInfo) { + if (sendInfo.resultCode < 0) { + const err = new Error('Socket ' + self.id + ' send error ' + sendInfo.resultCode) + callback(err) + self.emit('error', err) + } else { + callback(null) + } + }) +} + +function sliceBuffer (buffer, offset, length) { + buffer = Buffer.from(buffer) + + offset = offset >>> 0 + length = length >>> 0 + + // assuming buffer is browser implementation (`buffer` package on npm) + let buf = buffer.buffer + if (buffer.byteOffset || buffer.byteLength !== buf.byteLength) { + buf = buf.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength) + } + if (offset || length !== buffer.length) { + buf = buf.slice(offset, length) + } + + return Buffer.from(buf) +} + +function fixBufferList (list) { + const newlist = new Array(list.length) + + for (let i = 0, l = list.length; i < l; i++) { + const buf = list[i] + if (typeof buf === 'string') { + newlist[i] = Buffer.from(buf) + } else if (!(buf instanceof Uint8Array)) { + return null + } else { + newlist[i] = Buffer.from(buf) + } + } + + return newlist +} + +/** + * Close the underlying socket and stop listening for data on it. + */ +Socket.prototype.close = function () { + const self = this + if (self._destroyed) return + + delete sockets[self.id] + chrome.sockets.udp.close(self.id) + self._destroyed = true + + self.emit('close') +} + +/** + * Returns an object containing the address information for a socket. For UDP + * sockets, this object will contain address, family and port. + * + * @return {Object} information + */ +Socket.prototype.address = function () { + const self = this + return { + address: self._address, + port: self._port, + family: 'IPv4' + } +} + +Socket.prototype.setBroadcast = function (flag) { + // No chrome.sockets equivalent +} + +Socket.prototype.setTTL = function (ttl) { + // No chrome.sockets equivalent +} + +// NOTE: Multicast code is untested. Pull requests accepted for bug fixes and to +// add tests! + +/** + * Sets the IP_MULTICAST_TTL socket option. TTL stands for "Time to Live," but + * in this context it specifies the number of IP hops that a packet is allowed + * to go through, specifically for multicast traffic. Each router or gateway + * that forwards a packet decrements the TTL. If the TTL is decremented to 0 + * by a router, it will not be forwarded. + * + * The argument to setMulticastTTL() is a number of hops between 0 and 255. + * The default on most systems is 1. + * + * NOTE: The Chrome version of this function is async, whereas the node + * version is sync. Keep this in mind. + * + * @param {number} ttl + * @param {function} callback CHROME-SPECIFIC: Called when the configuration + * operation is done. + */ +Socket.prototype.setMulticastTTL = function (ttl, callback) { + const self = this + if (!callback) callback = function () {} + if (self._bindState === BIND_STATE_BOUND) { + setMulticastTTL(callback) + } else { + self._bindTasks.push({ + fn: setMulticastTTL, + callback + }) + } + + function setMulticastTTL (callback) { + chrome.sockets.udp.setMulticastTimeToLive(self.id, ttl, callback) + } +} + +/** + * Sets or clears the IP_MULTICAST_LOOP socket option. When this option is + * set, multicast packets will also be received on the local interface. + * + * NOTE: The Chrome version of this function is async, whereas the node + * version is sync. Keep this in mind. + * + * @param {boolean} flag + * @param {function} callback CHROME-SPECIFIC: Called when the configuration + * operation is done. + */ +Socket.prototype.setMulticastLoopback = function (flag, callback) { + const self = this + if (!callback) callback = function () {} + if (self._bindState === BIND_STATE_BOUND) { + setMulticastLoopback(callback) + } else { + self._bindTasks.push({ + fn: setMulticastLoopback, + callback + }) + } + + function setMulticastLoopback (callback) { + chrome.sockets.udp.setMulticastLoopbackMode(self.id, flag, callback) + } +} + +/** + * Tells the kernel to join a multicast group with IP_ADD_MEMBERSHIP socket + * option. + * + * If multicastInterface is not specified, the OS will try to add membership + * to all valid interfaces. + * + * NOTE: The Chrome version of this function is async, whereas the node + * version is sync. Keep this in mind. + * + * @param {string} multicastAddress + * @param {string} [multicastInterface] Optional + * @param {function} callback CHROME-SPECIFIC: Called when the configuration + * operation is done. + */ +Socket.prototype.addMembership = function (multicastAddress, + multicastInterface, + callback) { + const self = this + if (!callback) callback = function () {} + chrome.sockets.udp.joinGroup(self.id, multicastAddress, callback) +} + +/** + * Opposite of addMembership - tells the kernel to leave a multicast group + * with IP_DROP_MEMBERSHIP socket option. This is automatically called by the + * kernel when the socket is closed or process terminates, so most apps will + * never need to call this. + * + * NOTE: The Chrome version of this function is async, whereas the node + * version is sync. Keep this in mind. + * + * If multicastInterface is not specified, the OS will try to drop membership + * to all valid interfaces. + * + * @param {[type]} multicastAddress + * @param {[type]} multicastInterface Optional + * @param {function} callback CHROME-SPECIFIC: Called when the configuration + * operation is done. + */ +Socket.prototype.dropMembership = function (multicastAddress, + multicastInterface, + callback) { + const self = this + if (!callback) callback = function () {} + chrome.sockets.udp.leaveGroup(self.id, multicastAddress, callback) +} + +Socket.prototype.unref = function () { + // No chrome.sockets equivalent +} + +Socket.prototype.ref = function () { + // No chrome.sockets equivalent +} diff --git a/capacitor/src/chrome-net.js b/capacitor/src/chrome-net.js new file mode 100644 index 0000000..ac77cef --- /dev/null +++ b/capacitor/src/chrome-net.js @@ -0,0 +1,1205 @@ +/*! chrome-net. MIT License. Feross Aboukhadijeh */ +/* global chrome */ +'use strict' + +/** + * net + * === + * + * The net module provides you with an asynchronous network wrapper. It + * contains methods for creating both servers and clients (called streams). + * You can include this module with require('chrome-net') + */ + +const EventEmitter = require('events') +const inherits = require('inherits') +const stream = require('stream') +const deprecate = require('util').deprecate +const timers = require('timers') +const Buffer = require('buffer').Buffer + +// Track open servers and sockets to route incoming sockets (via onAccept and onReceive) +// to the right handlers. +const servers = {} +const sockets = {} + +// Thorough check for Chrome App since both Edge and Chrome implement dummy chrome object +if ( + typeof chrome === 'object' && + typeof chrome.sockets === 'object' && + typeof chrome.sockets.tcp === 'object' +) { + chrome.sockets.tcp.onReceive.addListener(onReceive) + chrome.sockets.tcp.onReceiveError.addListener(onReceiveError) +} + +function onAccept (info) { + if (info.socketId in servers) { + servers[info.socketId]._onAccept(info.clientSocketId) + } else { + console.error('Unknown server socket id: ' + info.socketId) + } +} + +function onAcceptError (info) { + if (info.socketId in servers) { + servers[info.socketId]._onAcceptError(info.resultCode) + } else { + console.error('Unknown server socket id: ' + info.socketId) + } +} + +function onReceive (info) { + if (info.socketId in sockets) { + sockets[info.socketId]._onReceive(info.data) + } else { + console.error('Unknown socket id: ' + info.socketId) + } +} + +function onReceiveError (info) { + if (info.socketId in sockets) { + sockets[info.socketId]._onReceiveError(info.resultCode) + } else { + if (info.resultCode === -100) return // net::ERR_CONNECTION_CLOSED + console.error('Unknown socket id: ' + info.socketId) + } +} + +/** + * Creates a new TCP server. The connectionListener argument is automatically + * set as a listener for the 'connection' event. + * + * @param {Object} options + * @param {function} connectionListener + * @return {Server} + */ +exports.createServer = function (options, connectionListener) { + return new Server(options, connectionListener) +} + +/** + * net.connect(options, [connectionListener]) + * net.createConnection(options, [connectionListener]) + * + * Constructs a new socket object and opens the socket to the given location. + * When the socket is established, the 'connect' event will be emitted. + * + * For TCP sockets, options argument should be an object which specifies: + * + * port: Port the client should connect to (Required). + * host: Host the client should connect to. Defaults to 'localhost'. + * localAddress: Local interface to bind to for network connections. + * + * =============================================================== + * + * net.connect(port, [host], [connectListener]) + * net.createConnection(port, [host], [connectListener]) + * + * Creates a TCP connection to port on host. If host is omitted, + * 'localhost' will be assumed. The connectListener parameter will be + * added as an listener for the 'connect' event. + * + * @param {Object} options + * @param {function} listener + * @return {Socket} + */ +exports.connect = exports.createConnection = function () { + const argsLen = arguments.length + let args = new Array(argsLen) + for (let i = 0; i < argsLen; i++) args[i] = arguments[i] + args = normalizeConnectArgs(args) + const s = new Socket(args[0]) + return Socket.prototype.connect.apply(s, args) +} + +inherits(Server, EventEmitter) + +/** + * Class: net.Server + * ================= + * + * This class is used to create a TCP server. + * + * Event: 'listening' + * Emitted when the server has been bound after calling server.listen. + * + * Event: 'connection' + * - Socket object The connection object + * Emitted when a new connection is made. socket is an instance of net.Socket. + * + * Event: 'close' + * Emitted when the server closes. Note that if connections exist, this event + * is not emitted until all connections are ended. + * + * Event: 'error' + * - Error Object + * Emitted when an error occurs. The 'close' event will be called directly + * following this event. See example in discussion of server.listen. + */ +function Server (options, connectionListener) { + if (!(this instanceof Server)) return new Server(options, connectionListener) + EventEmitter.call(this) + + if (typeof options === 'function') { + connectionListener = options + options = {} + this.on('connection', connectionListener) + } else if (options == null || typeof options === 'object') { + options = options || {} + + if (typeof connectionListener === 'function') { + this.on('connection', connectionListener) + } + } else { + throw new TypeError('options must be an object') + } + + this._connections = 0 + + Object.defineProperty(this, 'connections', { + get: deprecate(() => this._connections, + 'Server.connections property is deprecated. ' + + 'Use Server.getConnections method instead.'), + set: deprecate((val) => (this._connections = val), + 'Server.connections property is deprecated.'), + configurable: true, + enumerable: false + }) + + this.id = null // a number > 0 + this.connecting = false + + this.allowHalfOpen = options.allowHalfOpen || false + this.pauseOnConnect = !!options.pauseOnConnect + this._address = null + + this._host = null + this._port = null + this._backlog = null +} +exports.Server = Server + +Server.prototype._usingSlaves = false // not used + +/** + * server.listen(port, [host], [backlog], [callback]) + * + * Begin accepting connections on the specified port and host. If the host is + * omitted, the server will accept connections directed to any IPv4 address + * (INADDR_ANY). A port value of zero will assign a random port. + * + * Backlog is the maximum length of the queue of pending connections. The + * actual length will be determined by your OS through sysctl settings such as + * tcp_max_syn_backlog and somaxconn on linux. The default value of this + * parameter is 511 (not 512). + * + * This function is asynchronous. When the server has been bound, 'listening' + * event will be emitted. The last parameter callback will be added as an + * listener for the 'listening' event. + * + * @return {Socket} + */ +Server.prototype.listen = function (/* variable arguments... */) { + const lastArg = arguments[arguments.length - 1] + if (typeof lastArg === 'function') { + this.once('listening', lastArg) + } + + let port = toNumber(arguments[0]) + + let address + + // The third optional argument is the backlog size. + // When the ip is omitted it can be the second argument. + let backlog = toNumber(arguments[1]) || toNumber(arguments[2]) || undefined + + if (arguments[0] !== null && typeof arguments[0] === 'object') { + const h = arguments[0] + + if (h._handle || h.handle) { + throw new Error('handle is not supported in Chrome Apps.') + } + if (typeof h.fd === 'number' && h.fd >= 0) { + throw new Error('fd is not supported in Chrome Apps.') + } + + // The first argument is a configuration object + if (h.backlog) { + backlog = h.backlog + } + + if (typeof h.port === 'number' || typeof h.port === 'string' || + (typeof h.port === 'undefined' && 'port' in h)) { + // Undefined is interpreted as zero (random port) for consistency + // with net.connect(). + address = h.host || null + port = h.port + } else if (h.path && isPipeName(h.path)) { + throw new Error('Pipes are not supported in Chrome Apps.') + } else { + throw new Error('Invalid listen argument: ' + h) + } + } else if (isPipeName(arguments[0])) { + // UNIX socket or Windows pipe. + throw new Error('Pipes are not supported in Chrome Apps.') + } else if (arguments[1] === undefined || + typeof arguments[1] === 'function' || + typeof arguments[1] === 'number') { + // The first argument is the port, no IP given. + address = null + } else { + // The first argument is the port, the second an IP. + address = arguments[1] + } + + // now do something with port, address, backlog + + if (this.id) { + this.close() + } + + // If port is invalid or undefined, bind to a random port. + assertPort(port) + this._port = port | 0 + + this._host = address + + const isAny6 = !this._host + if (isAny6) { + this._host = '::' + } + + this._backlog = typeof backlog === 'number' ? backlog : undefined + + this.connecting = true + + // chrome.sockets.tcpServer.create((createInfo) => { + // if (!this.connecting || this.id) { + // ignoreLastError() + // chrome.sockets.tcpServer.close(createInfo.socketId) + // return + // } + // if (chrome.runtime.lastError) { + // this.emit('error', new Error(chrome.runtime.lastError.message)) + // return + // } + + // const socketId = this.id = createInfo.socketId + // servers[this.id] = this + + // const listen = () => chrome.sockets.tcpServer.listen(this.id, this._host, + // this._port, this._backlog, + // (result) => { + // // callback may be after close + // if (this.id !== socketId) { + // ignoreLastError() + // return + // } + // if (result !== 0 && isAny6) { + // ignoreLastError() + // this._host = '0.0.0.0' // try IPv4 + // isAny6 = false + // return listen() + // } + + // this._onListen(result) + // }) + // listen() + // }) + this._address = {} + this.emit('listening') + + return this +} + +Server.prototype._onListen = function (result) { + this.connecting = false + + if (result === 0) { + const idBefore = this.id + chrome.sockets.tcpServer.getInfo(this.id, (info) => { + if (this.id !== idBefore) { + ignoreLastError() + return + } + if (chrome.runtime.lastError) { + this._onListen(-2) // net::ERR_FAILED + return + } + + this._address = { + port: info.localPort, + family: info.localAddress && + info.localAddress.indexOf(':') !== -1 + ? 'IPv6' + : 'IPv4', + address: info.localAddress + } + this.emit('listening') + }) + } else { + this.emit('error', exceptionWithHostPort(result, 'listen', this._host, this._port)) + if (this.id) { + chrome.sockets.tcpServer.close(this.id) + delete servers[this.id] + this.id = null + } + } +} + +Server.prototype._onAccept = function (clientSocketId) { + // Set the `maxConnections` property to reject connections when the server's + // connection count gets high. + if (this.maxConnections && this._connections >= this.maxConnections) { + chrome.sockets.tcp.close(clientSocketId) + console.warn('Rejected connection - hit `maxConnections` limit') + return + } + + this._connections += 1 + + const acceptedSocket = new Socket({ + server: this, + id: clientSocketId, + allowHalfOpen: this.allowHalfOpen, + pauseOnCreate: this.pauseOnConnect + }) + acceptedSocket.on('connect', () => this.emit('connection', acceptedSocket)) +} + +Server.prototype._onAcceptError = function (resultCode) { + this.emit('error', errnoException(resultCode, 'accept')) + this.close() +} + +/** + * Stops the server from accepting new connections and keeps existing + * connections. This function is asynchronous, the server is finally closed + * when all connections are ended and the server emits a 'close' event. + * Optionally, you can pass a callback to listen for the 'close' event. + * @param {function} callback + */ +Server.prototype.close = function (callback) { + if (typeof callback === 'function') { + if (!this.id) { + this.once('close', () => callback(new Error('Not running'))) + } else { + this.once('close', callback) + } + } + + if (this.id) { + chrome.sockets.tcpServer.close(this.id) + delete servers[this.id] + this.id = null + } + this._address = null + this.connecting = false + + this._emitCloseIfDrained() + + return this +} + +Server.prototype._emitCloseIfDrained = function () { + if (this.id || this.connecting || this._connections) { + return + } + + process.nextTick(emitCloseNT, this) +} + +function emitCloseNT (self) { + if (self.id || self.connecting || self._connections) { + return + } + self.emit('close') +} + +Object.defineProperty(Server.prototype, 'listening', { + get: function () { + return !!this._address + }, + configurable: true, + enumerable: true +}) + +/** + * Returns the bound address, the address family name and port of the socket + * as reported by the operating system. Returns an object with three + * properties, e.g. { port: 12346, family: 'IPv4', address: '127.0.0.1' } + * + * @return {Object} information + */ +Server.prototype.address = function () { + return this._address +} + +Server.prototype.unref = +Server.prototype.ref = function () { + // No chrome.socket equivalent + return this +} + +/** + * Asynchronously get the number of concurrent connections on the server. + * Works when sockets were sent to forks. + * + * Callback should take two arguments err and count. + * + * @param {function} callback + */ +Server.prototype.getConnections = function (callback) { + process.nextTick(callback, null, this._connections) +} + +inherits(Socket, stream.Duplex) + +/** + * Class: net.Socket + * ================= + * + * This object is an abstraction of a TCP or UNIX socket. net.Socket instances + * implement a duplex Stream interface. They can be created by the user and + * used as a client (with connect()) or they can be created by Node and passed + * to the user through the 'connection' event of a server. + * + * Construct a new socket object. + * + * options is an object with the following defaults: + * + * { fd: null // NO CHROME EQUIVALENT + * type: null + * allowHalfOpen: false // NO CHROME EQUIVALENT + * } + * + * `type` can only be 'tcp4' (for now). + * + * Event: 'connect' + * Emitted when a socket connection is successfully established. See + * connect(). + * + * Event: 'data' + * - Buffer object + * Emitted when data is received. The argument data will be a Buffer or + * String. Encoding of data is set by socket.setEncoding(). (See the Readable + * Stream section for more information.) + * + * Note that the data will be lost if there is no listener when a Socket + * emits a 'data' event. + * + * Event: 'end' + * Emitted when the other end of the socket sends a FIN packet. + * + * By default (allowHalfOpen == false) the socket will destroy its file + * descriptor once it has written out its pending write queue. However, + * by setting allowHalfOpen == true the socket will not automatically + * end() its side allowing the user to write arbitrary amounts of data, + * with the caveat that the user is required to end() their side now. + * + * Event: 'timeout' + * Emitted if the socket times out from inactivity. This is only to notify + * that the socket has been idle. The user must manually close the connection. + * + * See also: socket.setTimeout() + * + * Event: 'drain' + * Emitted when the write buffer becomes empty. Can be used to throttle + * uploads. + * + * See also: the return values of socket.write() + * + * Event: 'error' + * - Error object + * Emitted when an error occurs. The 'close' event will be called directly + * following this event. + * + * Event: 'close' + * - had_error Boolean true if the socket had a transmission error + * Emitted once the socket is fully closed. The argument had_error is a + * boolean which says if the socket was closed due to a transmission error. + */ +function Socket (options) { + if (!(this instanceof Socket)) return new Socket(options) + + if (typeof options === 'number') { + options = { fd: options } // Legacy interface. + } else if (options === undefined) { + options = {} + } + + if (options.handle) { + throw new Error('handle is not supported in Chrome Apps.') + } else if (options.fd !== undefined) { + throw new Error('fd is not supported in Chrome Apps.') + } + + options.decodeStrings = true + options.objectMode = false + stream.Duplex.call(this, options) + + this.destroyed = false + this._hadError = false // Used by _http_client.js + this.id = null // a number > 0 + this._parent = null + this._host = null + this._port = null + this._pendingData = null + + this.ondata = null + this.onend = null + + this._init() + this._reset() + + // default to *not* allowing half open sockets + // Note: this is not possible in Chrome Apps, see https://crbug.com/124952 + this.allowHalfOpen = options.allowHalfOpen || false + + // shut down the socket when we're finished with it. + this.on('finish', this.destroy) + + if (options.server) { + this.server = this._server = options.server + this.id = options.id + sockets[this.id] = this + + if (options.pauseOnCreate) { + // stop the handle from reading and pause the stream + // (Already paused in Chrome version) + this._readableState.flowing = false + } + + // For incoming sockets (from server), it's already connected. + this.connecting = true + this.writable = true + this._onConnect() + } +} +exports.Socket = Socket + +// called when creating new Socket, or when re-using a closed Socket +Socket.prototype._init = function () { + // The amount of received bytes. + this.bytesRead = 0 + + this._bytesDispatched = 0 + + // Reserve properties + this.server = null + this._server = null +} + +// called when creating new Socket, or when closing a Socket +Socket.prototype._reset = function () { + this.remoteAddress = this.remotePort = + this.localAddress = this.localPort = null + this.remoteFamily = 'IPv4' + this.readable = this.writable = false + this.connecting = false +} + +/** + * socket.connect(port, [host], [connectListener]) + * socket.connect(options, [connectListener]) + * + * Opens the connection for a given socket. If port and host are given, then + * the socket will be opened as a TCP socket, if host is omitted, localhost + * will be assumed. If a path is given, the socket will be opened as a unix + * socket to that path. + * + * Normally this method is not needed, as net.createConnection opens the + * socket. Use this only if you are implementing a custom Socket. + * + * This function is asynchronous. When the 'connect' event is emitted the + * socket is established. If there is a problem connecting, the 'connect' + * event will not be emitted, the 'error' event will be emitted with the + * exception. + * + * The connectListener parameter will be added as an listener for the + * 'connect' event. + * + * @param {Object} options + * @param {function} cb + * @return {Socket} this socket (for chaining) + */ +Socket.prototype.connect = function () { + const argsLen = arguments.length + let args = new Array(argsLen) + for (let i = 0; i < argsLen; i++) args[i] = arguments[i] + args = normalizeConnectArgs(args) + const options = args[0] + const cb = args[1] + + if (options.path) { + throw new Error('Pipes are not supported in Chrome Apps.') + } + + if (this.id) { + // already connected, destroy and connect again + this.destroy() + } + + if (this.destroyed) { + this._readableState.reading = false + this._readableState.ended = false + this._readableState.endEmitted = false + this._writableState.ended = false + this._writableState.ending = false + this._writableState.finished = false + this._writableState.errorEmitted = false + this._writableState.length = 0 + this.destroyed = false + } + + this.connecting = true + this.writable = true + + this._host = options.host || 'localhost' + this._port = options.port + + if (typeof this._port !== 'undefined') { + if (typeof this._port !== 'number' && typeof this._port !== 'string') { + throw new TypeError('"port" option should be a number or string: ' + this._port) + } + if (!isLegalPort(this._port)) { + throw new RangeError('"port" option should be >= 0 and < 65536: ' + this._port) + } + } + this._port |= 0 + + this._init() + + this._unrefTimer() + + if (typeof cb === 'function') { + this.once('connect', cb) + } + + chrome.sockets.tcp.create((createInfo) => { + if (!this.connecting || this.id) { + ignoreLastError() + chrome.sockets.tcp.close(createInfo.socketId) + return + } + if (chrome.runtime.lastError) { + this.destroy(new Error(chrome.runtime.lastError.message)) + return + } + + this.id = createInfo.socketId + sockets[this.id] = this + + chrome.sockets.tcp.setPaused(this.id, true) + + chrome.sockets.tcp.connect(this.id, this._host, this._port, (result) => { + // callback may come after call to destroy + if (this.id !== createInfo.socketId) { + ignoreLastError() + return + } + if (result !== 0) { + this.destroy(exceptionWithHostPort(result, 'connect', this._host, this._port)) + return + } + + this._unrefTimer() + this._onConnect() + }) + }) + + return this +} + +Socket.prototype._onConnect = function () { + const idBefore = this.id + chrome.sockets.tcp.getInfo(this.id, (result) => { + if (this.id !== idBefore) { + ignoreLastError() + return + } + if (chrome.runtime.lastError) { + this.destroy(new Error(chrome.runtime.lastError.message)) + return + } + + this.remoteAddress = result.peerAddress + this.remoteFamily = result.peerAddress && + result.peerAddress.indexOf(':') !== -1 + ? 'IPv6' + : 'IPv4' + this.remotePort = result.peerPort + this.localAddress = result.localAddress + this.localPort = result.localPort + + this.connecting = false + this.readable = true + + this.emit('connect') + // start the first read, or get an immediate EOF. + // this doesn't actually consume any bytes, because len=0 + if (!this.isPaused()) this.read(0) + }) +} + +/** + * The number of characters currently buffered to be written. + * @type {number} + */ +Object.defineProperty(Socket.prototype, 'bufferSize', { + get: function () { + if (this.id) { + let bytes = this._writableState.length + if (this._pendingData) bytes += this._pendingData.length + return bytes + } + } +}) + +Socket.prototype.end = function (data, encoding) { + stream.Duplex.prototype.end.call(this, data, encoding) + this.writable = false +} + +Socket.prototype._write = function (chunk, encoding, callback) { + if (!callback) callback = () => {} + + if (this.connecting) { + this._pendingData = chunk + this.once('connect', () => this._write(chunk, encoding, callback)) + return + } + this._pendingData = null + + if (!this.id) { + callback(new Error('This socket is closed')) + return + } + + // assuming buffer is browser implementation (`buffer` package on npm) + let buffer = chunk.buffer + if (chunk.byteLength !== buffer.byteLength) { + buffer = buffer.slice(chunk.byteOffset, chunk.byteOffset + chunk.byteLength) + } + + const idBefore = this.id + chrome.sockets.tcp.send(this.id, buffer, (sendInfo) => { + if (this.id !== idBefore) { + ignoreLastError() + return + } + + if (sendInfo.resultCode < 0) { + this._destroy(exceptionWithHostPort(sendInfo.resultCode, 'write', this.remoteAddress, this.remotePort), callback) + } else { + this._unrefTimer() + callback(null) + } + }) + + this._bytesDispatched += chunk.length +} + +Socket.prototype._read = function (bufferSize) { + if (this.connecting || !this.id) { + this.once('connect', () => this._read(bufferSize)) + return + } + + chrome.sockets.tcp.setPaused(this.id, false) + + const idBefore = this.id + chrome.sockets.tcp.getInfo(this.id, (result) => { + if (this.id !== idBefore) { + ignoreLastError() + return + } + if (chrome.runtime.lastError || !result.connected) { + this._onReceiveError(-15) // workaround for https://crbug.com/518161 + } + }) +} + +Socket.prototype._onReceive = function (data) { + const buffer = Buffer.from(data) + const offset = this.bytesRead + + this.bytesRead += buffer.length + this._unrefTimer() + + if (this.ondata) { + console.error('socket.ondata = func is non-standard, use socket.on(\'data\', func)') + this.ondata(buffer, offset, this.bytesRead) + } + if (!this.push(buffer)) { // if returns false, then apply backpressure + chrome.sockets.tcp.setPaused(this.id, true) + } +} + +Socket.prototype._onReceiveError = function (resultCode) { + if (resultCode === -100) { // net::ERR_CONNECTION_CLOSED + if (this.onend) { + console.error('socket.onend = func is non-standard, use socket.on(\'end\', func)') + this.once('end', this.onend) + } + this.push(null) + this.destroy() + } else if (resultCode < 0) { + this.destroy(errnoException(resultCode, 'read')) + } +} + +function protoGetter (name, callback) { + Object.defineProperty(Socket.prototype, name, { + configurable: false, + enumerable: true, + get: callback + }) +} + +/** + * The amount of bytes sent. + * @return {number} + */ +protoGetter('bytesWritten', function bytesWritten () { + if (this.id) return this._bytesDispatched + this.bufferSize +}) + +Socket.prototype.destroy = function (exception) { + this._destroy(exception) +} + +Socket.prototype._destroy = function (exception, cb) { + const fireErrorCallbacks = () => { + if (cb) cb(exception) + if (exception && !this._writableState.errorEmitted) { + process.nextTick(emitErrorNT, this, exception) + this._writableState.errorEmitted = true + } + } + + if (this.destroyed) { + // already destroyed, fire error callbacks + fireErrorCallbacks() + return + } + + if (this._server) { + this._server._connections -= 1 + if (this._server._emitCloseIfDrained) this._server._emitCloseIfDrained() + } + + this._reset() + + for (let s = this; s !== null; s = s._parent) timers.unenroll(s) // eslint-disable-line node/no-deprecated-api + + this.destroyed = true + + // If _destroy() has been called before chrome.sockets.tcp.create() + // callback, we don't have an id. Therefore we don't need to close + // or disconnect + if (this.id) { + delete sockets[this.id] + chrome.sockets.tcp.close(this.id, () => { + if (this.destroyed) { + this.emit('close', !!exception) + } + }) + this.id = null + } + + fireErrorCallbacks() +} + +Socket.prototype.destroySoon = function () { + if (this.writable) this.end() + + if (this._writableState.finished) this.destroy() +} + +/** + * Sets the socket to timeout after timeout milliseconds of inactivity on the socket. + * By default net.Socket do not have a timeout. When an idle timeout is triggered the + * socket will receive a 'timeout' event but the connection will not be severed. The + * user must manually end() or destroy() the socket. + * + * If timeout is 0, then the existing idle timeout is disabled. + * + * The optional callback parameter will be added as a one time listener for the 'timeout' event. + * + * @param {number} timeout + * @param {function} callback + */ +Socket.prototype.setTimeout = function (timeout, callback) { + if (timeout === 0) { + timers.unenroll(this) // eslint-disable-line node/no-deprecated-api + if (callback) { + this.removeListener('timeout', callback) + } + } else { + timers.enroll(this, timeout) // eslint-disable-line node/no-deprecated-api + timers._unrefActive(this) + if (callback) { + this.once('timeout', callback) + } + } + + return this +} + +Socket.prototype._onTimeout = function () { + this.emit('timeout') +} + +Socket.prototype._unrefTimer = function unrefTimer () { + for (let s = this; s !== null; s = s._parent) { + timers._unrefActive(s) + } +} + +/** + * Disables the Nagle algorithm. By default TCP connections use the Nagle + * algorithm, they buffer data before sending it off. Setting true for noDelay + * will immediately fire off data each time socket.write() is called. noDelay + * defaults to true. + * + * NOTE: The Chrome version of this function is async, whereas the node + * version is sync. Keep this in mind. + * + * @param {boolean} [noDelay] Optional + * @param {function} callback CHROME-SPECIFIC: Called when the configuration + * operation is done. + */ +Socket.prototype.setNoDelay = function (noDelay, callback) { + if (!this.id) { + this.once('connect', () => this.setNoDelay(noDelay, callback)) + return this + } + + // backwards compatibility: assume true when `noDelay` is omitted + noDelay = noDelay === undefined ? true : !!noDelay + chrome.sockets.tcp.setNoDelay(this.id, noDelay, chromeCallbackWrap(callback)) + + return this +} + +/** + * Enable/disable keep-alive functionality, and optionally set the initial + * delay before the first keepalive probe is sent on an idle socket. enable + * defaults to false. + * + * Set initialDelay (in milliseconds) to set the delay between the last data + * packet received and the first keepalive probe. Setting 0 for initialDelay + * will leave the value unchanged from the default (or previous) setting. + * Defaults to 0. + * + * NOTE: The Chrome version of this function is async, whereas the node + * version is sync. Keep this in mind. + * + * @param {boolean} [enable] Optional + * @param {number} [initialDelay] + * @param {function} callback CHROME-SPECIFIC: Called when the configuration + * operation is done. + */ +Socket.prototype.setKeepAlive = function (enable, initialDelay, callback) { + if (!this.id) { + this.once('connect', () => this.setKeepAlive(enable, initialDelay, callback)) + return this + } + + chrome.sockets.tcp.setKeepAlive(this.id, !!enable, ~~(initialDelay / 1000), + chromeCallbackWrap(callback)) + + return this +} + +/** + * Returns the bound address, the address family name and port of the socket + * as reported by the operating system. Returns an object with three + * properties, e.g. { port: 12346, family: 'IPv4', address: '127.0.0.1' } + * + * @return {Object} information + */ +Socket.prototype.address = function () { + return { + address: this.localAddress, + port: this.localPort, + family: this.localAddress && + this.localAddress.indexOf(':') !== -1 + ? 'IPv6' + : 'IPv4' + } +} + +Object.defineProperty(Socket.prototype, '_connecting', { + get: function () { + return this.connecting + } +}) + +Object.defineProperty(Socket.prototype, 'readyState', { + get: function () { + if (this.connecting) { + return 'opening' + } else if (this.readable && this.writable) { + return 'open' + } else { + return 'closed' + } + } +}) + +Socket.prototype.unref = +Socket.prototype.ref = function () { + // No chrome.socket equivalent + return this +} + +// +// EXPORTED HELPERS +// + +// Source: https://developers.google.com/web/fundamentals/input/form/provide-real-time-validation#use-these-attributes-to-validate-input +const IPv4Regex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/ +const IPv6Regex = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]).){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]).){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/ + +exports.isIPv4 = IPv4Regex.test.bind(IPv4Regex) +exports.isIPv6 = IPv6Regex.test.bind(IPv6Regex) + +exports.isIP = function (ip) { + return exports.isIPv4(ip) ? 4 : exports.isIPv6(ip) ? 6 : 0 +} + +// +// HELPERS +// + +/** + * Returns an array [options] or [options, cb] + * It is the same as the argument of Socket.prototype.connect(). + */ +function normalizeConnectArgs (args) { + let options = {} + + if (args[0] !== null && typeof args[0] === 'object') { + // connect(options, [cb]) + options = args[0] + } else if (isPipeName(args[0])) { + // connect(path, [cb]) + throw new Error('Pipes are not supported in Chrome Apps.') + } else { + // connect(port, [host], [cb]) + options.port = args[0] + if (typeof args[1] === 'string') { + options.host = args[1] + } + } + + const cb = args[args.length - 1] + return typeof cb === 'function' ? [options, cb] : [options] +} + +function toNumber (x) { + return (x = Number(x)) >= 0 ? x : false +} + +function isPipeName (s) { + return typeof s === 'string' && toNumber(s) === false +} + +// Check that the port number is not NaN when coerced to a number, +// is an integer and that it falls within the legal range of port numbers. +function isLegalPort (port) { + if ((typeof port !== 'number' && typeof port !== 'string') || + (typeof port === 'string' && port.trim().length === 0)) { + return false + } + return +port === (+port >>> 0) && port <= 0xFFFF +} + +function assertPort (port) { + if (typeof port !== 'undefined' && !isLegalPort(port)) { + throw new RangeError('"port" argument must be >= 0 and < 65536') + } +} + +// Call the getter function to prevent "Unchecked runtime.lastError" errors +function ignoreLastError () { + void chrome.runtime.lastError // eslint-disable-line no-void +} + +function chromeCallbackWrap (callback) { + return () => { + let error + if (chrome.runtime.lastError) { + console.error(chrome.runtime.lastError.message) + error = new Error(chrome.runtime.lastError.message) + } + if (callback) callback(error) + } +} + +function emitErrorNT (self, err) { + self.emit('error', err) +} + +// Full list of possible error codes: https://code.google.com/p/chrome-browser/source/browse/trunk/src/net/base/net_error_list.h +// TODO: Try to reproduce errors in both node & Chrome Apps and extend this list +// (what conditions lead to EPIPE?) +const errorChromeToUv = { + '-10': 'EACCES', + '-22': 'EACCES', + '-138': 'EACCES', + '-147': 'EADDRINUSE', + '-108': 'EADDRNOTAVAIL', + '-103': 'ECONNABORTED', + '-102': 'ECONNREFUSED', + '-101': 'ECONNRESET', + '-16': 'EEXIST', + '-8': 'EFBIG', + '-109': 'EHOSTUNREACH', + '-4': 'EINVAL', + '-23': 'EISCONN', + '-6': 'ENOENT', + '-13': 'ENOMEM', + '-106': 'ENONET', + '-18': 'ENOSPC', + '-11': 'ENOSYS', + '-15': 'ENOTCONN', + '-105': 'ENOTFOUND', + '-118': 'ETIMEDOUT', + '-100': 'EOF' +} +function errnoException (err, syscall, details) { + const uvCode = errorChromeToUv[err] || 'UNKNOWN' + let message = syscall + ' ' + err + ' ' + details + if (chrome.runtime.lastError) { + message += ' ' + chrome.runtime.lastError.message + } + message += ' (mapped uv code: ' + uvCode + ')' + const e = new Error(message) + e.code = e.errno = uvCode + // TODO: expose chrome error code; what property name? + e.syscall = syscall + return e +} + +function exceptionWithHostPort (err, syscall, address, port, additional) { + let details + if (port && port > 0) { + details = address + ':' + port + } else { + details = address + } + + if (additional) { + details += ' - Local (' + additional + ')' + } + const ex = errnoException(err, syscall, details) + ex.address = address + if (port) { + ex.port = port + } + return ex +} diff --git a/capacitor/src/ipc.js b/capacitor/src/ipc.js new file mode 100644 index 0000000..370de00 --- /dev/null +++ b/capacitor/src/ipc.js @@ -0,0 +1,40 @@ +import EventEmitter from 'events' + +const ipcRendererUI = new EventEmitter() + +export default { + emit: (event, data) => { + ipcRendererUI.emit(event, data) + }, + on: (event, callback) => { + ipcRendererUI.on(event, (event, ...args) => callback(...args)) + }, + once: (event, callback) => { + ipcRendererUI.once(event, (event, ...args) => callback(...args)) + }, + off: event => { + ipcRendererUI.removeAllListeners(event) + } +} + +ipcRendererUI.on('portRequest', async () => { + const { port1, port2 } = new MessageChannel() + window.port = { + onmessage: cb => { + port2.onmessage = ({ type, data }) => cb({ type, data }) + }, + postMessage: (a, b) => { + port2.postMessage(a, b) + } + } + await window.controller + ipcRendererWebTorrent.emit('port', { ports: [port1] }) + ipcRendererUI.emit('port', { ports: [port2] }) +}) + +export const ipcRendererWebTorrent = new EventEmitter() + +window.version = { + arch: 'uwu', + platform: 'nyaa' +} diff --git a/capacitor/src/main.js b/capacitor/src/main.js deleted file mode 100644 index d8200ac..0000000 --- a/capacitor/src/main.js +++ /dev/null @@ -1,7 +0,0 @@ -import App from './App.svelte' - -const app = new App({ - target: document.getElementById('app') -}) - -export default app diff --git a/capacitor/src/webtorrent.js b/capacitor/src/webtorrent.js new file mode 100644 index 0000000..d70829e --- /dev/null +++ b/capacitor/src/webtorrent.js @@ -0,0 +1,21 @@ +import TorrentClient from 'common/modules/webtorrent.js' +import { ipcRendererWebTorrent } from './ipc.js' + +globalThis.chrome.runtime = { lastError: false, id: 'something' } + +const controller = (async () => { + const reg = await navigator.serviceWorker.register('./sw.js', { scope: './' }) + + const worker = reg.active || reg.waiting || reg.installing + return new Promise(resolve => { + function checkState (worker) { + return worker.state === 'activated' && resolve(reg) + } + if (!checkState(worker)) { + worker.addEventListener('statechange', ({ target }) => checkState(target)) + } + }) +})() +window.controller = controller + +window.client = new TorrentClient(ipcRendererWebTorrent, () => ({ bsize: Infinity, bavail: Infinity }), 'browser', controller) diff --git a/capacitor/webpack.config.cjs b/capacitor/webpack.config.cjs index e52ba04..847858f 100644 --- a/capacitor/webpack.config.cjs +++ b/capacitor/webpack.config.cjs @@ -1,54 +1,57 @@ -import webpack from 'webpack' -import TerserPlugin from 'terser-webpack-plugin' -import info from 'webtorrent/package.json' assert { type: 'json' } +const webpack = require('webpack') +const commonConfig = require('common/webpack.config.cjs') +const { merge } = require('webpack-merge') +const { join, resolve } = require('path') /** @type {import('webpack').Configuration} */ -export default { - entry: './index.js', - devtool: 'source-map', - resolve: { - aliasFields: ['chromeapp'], - alias: { - ...info.chromeapp, - path: 'path-esm', - stream: 'stream-browserify', - timers: 'timers-browserify', - crypto: 'crypto-browserify', - buffer: 'buffer', - querystring: 'querystring', - zlib: '/polyfills/inflate-sync-web.js' - } - }, - output: { - chunkFormat: 'module', - filename: 'webtorrent.chromeapp.js', - library: { - type: 'module' - } - }, - mode: 'production', - target: 'web', - experiments: { - outputModule: true - }, +const capacitorConfig = { + entry: [join(__dirname, 'src', 'webtorrent.js')], plugins: [ new webpack.ProvidePlugin({ - process: '/polyfills/process-fast.js', + process: 'webtorrent/polyfills/process-fast.js', Buffer: ['buffer', 'Buffer'] }), new webpack.DefinePlugin({ global: 'globalThis' }) ], - optimization: { - minimize: true, - minimizer: [new TerserPlugin({ - terserOptions: { - format: { - comments: false - } - }, - extractComments: false - })] + devServer: { + devMiddleware: { + writeToDisk: true + }, + hot: true, + client: { + overlay: { errors: true, warnings: false, runtimeErrors: false } + }, + port: 5001 } } +const alias = { + '@/modules/ipc.js': join(__dirname, 'src', 'ipc.js'), + 'webtorrent/lib/utp.cjs': false, + '@silentbot1/nat-api': false, + fs: false, + http: 'stream-http', + https: 'stream-http', + 'load-ip-set': false, + net: join(__dirname, 'src', 'chrome-net.js'), + dgram: join(__dirname, 'src', 'chrome-dgram.js'), + util: 'util', + assert: 'assert', + os: false, + ws: false, + ut_pex: 'ut_pex', + 'bittorrent-dht': false, + path: 'path-esm', + 'fs-chunk-store': 'hybrid-chunk-store', + stream: 'stream-browserify', + timers: 'timers-browserify', + crypto: 'crypto-browserify', + buffer: 'buffer', + 'bittorrent-tracker': 'bittorrent-tracker', + querystring: 'querystring', + zlib: 'webtorrent/polyfills/inflate-sync-web.js', + 'bittorrent-tracker/lib/client/http-tracker.js': resolve('../node_modules/bittorrent-tracker/lib/client/http-tracker.js') +} + +module.exports = merge(commonConfig(__dirname, alias, 'chromeapp'), capacitorConfig) diff --git a/common/App.svelte b/common/App.svelte index 83f9767..c6bb5d1 100644 --- a/common/App.svelte +++ b/common/App.svelte @@ -2,6 +2,7 @@ import { setContext } from 'svelte' import { writable } from 'simple-store-svelte' import { alRequest } from '@/modules/anilist.js' + import IPC from '@/modules/ipc.js' export const page = writable('home') export const view = writable(null) @@ -9,8 +10,8 @@ view.set(null) view.set((await alRequest({ method: 'SearchIDSingle', id: anime })).data.Media) } - window.IPC.on('open-anime', handleAnime) - window.IPC.on('schedule', () => { + IPC.on('open-anime', handleAnime) + IPC.on('schedule', () => { page.set('schedule') }) diff --git a/common/components/Menubar.svelte b/common/components/Menubar.svelte index 685a71b..27173f3 100644 --- a/common/components/Menubar.svelte +++ b/common/components/Menubar.svelte @@ -1,6 +1,7 @@ diff --git a/common/views/WatchTogether/Lobby.svelte b/common/views/WatchTogether/Lobby.svelte index 407a4a8..1ac9bf5 100644 --- a/common/views/WatchTogether/Lobby.svelte +++ b/common/views/WatchTogether/Lobby.svelte @@ -1,5 +1,6 @@