/* * This Socket plugin only fulfills http:/https:/ws:/wss: requests by intercepting them * and sending as either XMLHttpRequest or Fetch or WebSocket. * To make connections to servers without CORS, it uses a CORS proxy. * * When a WebSocket connection is created in the Smalltalk image a low level socket is * assumed to be provided by this plugin. Since low level sockets are not supported * in the browser a WebSocket is used here. This does however require the WebSocket * protocol (applied by the Smalltalk image) to be 'reversed' or 'faked' here in the * plugin. * The WebSocket handshake protocol is faked within the plugin and a regular WebSocket * connection is set up with the other party resulting in a real handshake. * When a (WebSocket) message is sent from the Smalltalk runtime it will be packed * inside a frame (fragment). This Socket plugin will extract the message from the * frame and send it using the WebSocket object (which will put it into a frame * again). A bit of unnecessary byte and bit fiddling unfortunately. * See the following site for an explanation of the WebSocket protocol: * https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers * * DNS requests are done through DNS over HTTPS (DoH). Quad9 (IP 9.9.9.9) is used * as server because it seems to take privacy of users serious. Other servers can * be found at https://en.wikipedia.org/wiki/Public_recursive_name_server */ function SocketPlugin() { "use strict"; return { getModuleName: function() { return 'SocketPlugin (http-only)'; }, interpreterProxy: null, primHandler: null, handleCounter: 0, needProxy: new Set(), // DNS Lookup // Cache elements: key is name, value is { address: 1.2.3.4, validUntil: Date.now() + 30000 } status: 0, // Resolver_Uninitialized, lookupCache: { localhost: { address: [ 127, 0, 0, 1], validUntil: Number.MAX_SAFE_INTEGER } }, lastLookup: null, lookupSemaIdx: 0, // Constants TCP_Socket_Type: 0, Resolver_Uninitialized: 0, Resolver_Ready: 1, Resolver_Busy: 2, Resolver_Error: 3, Socket_InvalidSocket: -1, Socket_Unconnected: 0, Socket_WaitingForConnection: 1, Socket_Connected: 2, Socket_OtherEndClosed: 3, Socket_ThisEndClosed: 4, setInterpreter: function(anInterpreter) { this.interpreterProxy = anInterpreter; this.primHandler = this.interpreterProxy.vm.primHandler; return true; }, _signalSemaphore: function(semaIndex) { if (semaIndex <= 0) return; this.primHandler.signalSemaphoreWithIndex(semaIndex); }, _signalLookupSemaphore: function() { this._signalSemaphore(this.lookupSemaIdx); }, _getAddressFromLookupCache: function(name, skipExpirationCheck) { if (name) { // Check for valid dotted decimal name first var dottedDecimalsMatch = name.match(/^\d+\.\d+\.\d+\.\d+$/); if (dottedDecimalsMatch) { var result = name.split(".").map(function(d) { return +d; }); if (result.every(function(d) { return d <= 255; })) { return new Uint8Array(result); } } // Lookup in cache var cacheEntry = this.lookupCache[name]; if (cacheEntry && (skipExpirationCheck || cacheEntry.validUntil >= Date.now())) { return new Uint8Array(cacheEntry.address); } } return null; }, _addAddressFromResponseToLookupCache: function(response) { // Check for valid response if (!response || response.Status !== 0 || !response.Question || !response.Answer) { return; } // Clean up all response elements by removing trailing dots in names var removeTrailingDot = function(element, field) { if (element[field] && element[field].replace) { element[field] = element[field].replace(/\.$/, ""); } }; var originalQuestion = response.Question[0]; removeTrailingDot(originalQuestion, "name"); response.Answer.forEach(function(answer) { removeTrailingDot(answer, "name"); removeTrailingDot(answer, "data"); }); // Get address by traversing alias chain var lookup = originalQuestion.name; var address = null; var ttl = 24 * 60 * 60; // One day as safe default var hasAddress = response.Answer.some(function(answer) { if (answer.name === lookup) { // Time To Live can be set on alias and address, keep shortest period if (answer.TTL) { ttl = Math.min(ttl, answer.TTL); } if (answer.type === 1) { // Retrieve IP address as array with 4 numeric values address = answer.data.split(".").map(function(numberString) { return +numberString; }); return true; } else if (answer.type === 5) { // Lookup name points to alias, follow alias from here on lookup = answer.data; } } return false; }); // Store address found if (hasAddress) { this.lookupCache[originalQuestion.name] = { address: address, validUntil: Date.now() + (ttl * 1000) }; } }, _compareAddresses: function(address1, address2) { return address1.every(function(addressPart, index) { return address2[index] === addressPart; }); }, _reverseLookupNameForAddress: function(address) { // Currently public API's for IP to hostname are not standardized yet (like DoH). // Assume most lookup's will be for reversing earlier name to address lookups. // Therefor use the lookup cache and otherwise create a dotted decimals name. var thisHandle = this; var result = null; Object.keys(this.lookupCache).some(function(name) { if (thisHandle._compareAddresses(address, thisHandle.lookupCache[name].address)) { result = name; return true; } return false; }); return result || address.join("."); }, // A socket handle emulates socket behavior _newSocketHandle: function(sendBufSize, connSemaIdx, readSemaIdx, writeSemaIdx) { var plugin = this; return { hostAddress: null, host: null, port: null, connSemaIndex: connSemaIdx, readSemaIndex: readSemaIdx, writeSemaIndex: writeSemaIdx, webSocket: null, sendBuffer: null, sendTimeout: null, response: null, responseReadUntil: 0, responseReceived: false, status: plugin.Socket_Unconnected, _signalConnSemaphore: function() { plugin._signalSemaphore(this.connSemaIndex); }, _signalReadSemaphore: function() { plugin._signalSemaphore(this.readSemaIndex); }, _signalWriteSemaphore: function() { plugin._signalSemaphore(this.writeSemaIndex); }, _otherEndClosed: function() { this.status = plugin.Socket_OtherEndClosed; this.webSocket = null; this._signalConnSemaphore(); }, _hostAndPort: function() { return this.host + ':' + this.port; }, _requestNeedsProxy: function() { return plugin.needProxy.has(this._hostAndPort()); }, _getURL: function(targetURL, isRetry) { var url = ''; if (isRetry || this._requestNeedsProxy()) { var proxy = typeof SqueakJS === "object" && SqueakJS.options.proxy; url = proxy || Squeak.defaultCORSProxy; } if (this.port !== 443) { url += 'http://' + this._hostAndPort() + targetURL; } else { url += 'https://' + this.host + targetURL; } return url; }, _performRequest: function() { // Assume a send is requested through WebSocket if connection is present if (this.webSocket) { this._performWebSocketSend(); return; } var request = new TextDecoder("utf-8").decode(this.sendBuffer); // Remove request from send buffer var endOfRequestIndex = this.sendBuffer.findIndex(function(element, index, array) { // Check for presence of "\r\n\r\n" denoting the end of the request (do simplistic but fast check) return array[index] === "\r" && array[index + 2] === "\r" && array[index + 1] === "\n" && array[index + 3] === "\n"; }); if (endOfRequestIndex >= 0) { this.sendBuffer = this.sendBuffer.subarray(endOfRequestIndex + 4); } else { this.sendBuffer = null; } // Extract header fields var headerLines = request.split('\r\n\r\n')[0].split('\n'); // Split header lines and parse first line var firstHeaderLineItems = headerLines[0].split(' '); var httpMethod = firstHeaderLineItems[0]; if (httpMethod !== 'GET' && httpMethod !== 'PUT' && httpMethod !== 'POST') { this._otherEndClosed(); return -1; } var targetURL = firstHeaderLineItems[1]; // Extract possible data to send var seenUpgrade = false; var seenWebSocket = false; var data = null; for (var i = 1; i < headerLines.length; i++) { var line = headerLines[i]; if (line.match(/Content-Length:/i)) { var contentLength = parseInt(line.substr(16)); var end = this.sendBuffer.byteLength; data = this.sendBuffer.subarray(end - contentLength, end); } else if (line.match(/Host:/i)) { var hostAndPort = line.substr(6).trim(); var host = hostAndPort.split(':')[0]; var port = parseInt(hostAndPort.split(':')[1]) || this.port; if (this.host !== host) { console.warn('Host for ' + this.hostAddress + ' was ' + this.host + ' but from HTTP request now ' + host); this.host = host; } if (this.port !== port) { console.warn('Port for ' + this.hostAddress + ' was ' + this.port + ' but from HTTP request now ' + port); this.port = port; } } if (line.match(/Connection: Upgrade/i)) { seenUpgrade = true; } else if (line.match(/Upgrade: WebSocket/i)) { seenWebSocket = true; } } if (httpMethod === "GET" && seenUpgrade && seenWebSocket) { this._performWebSocketRequest(targetURL, httpMethod, data, headerLines); } else if (self.fetch) { this._performFetchAPIRequest(targetURL, httpMethod, data, headerLines); } else { this._performXMLHTTPRequest(targetURL, httpMethod, data, headerLines); } }, _performFetchAPIRequest: function(targetURL, httpMethod, data, requestLines) { var thisHandle = this; var headers = {}; for (var i = 1; i < requestLines.length; i++) { var lineItems = requestLines[i].split(':'); if (lineItems.length === 2) { headers[lineItems[0]] = lineItems[1].trim(); } } if (typeof SqueakJS === "object" && SqueakJS.options.ajax) { headers["X-Requested-With"] = "XMLHttpRequest"; } var init = { method: httpMethod, headers: headers, body: data, mode: 'cors' }; fetch(this._getURL(targetURL), init) .then(thisHandle._handleFetchAPIResponse.bind(thisHandle)) .catch(function (e) { var url = thisHandle._getURL(targetURL, true); console.warn('Retrying with CORS proxy: ' + url); fetch(url, init) .then(function(res) { console.log('Success: ' + url); thisHandle._handleFetchAPIResponse(res); plugin.needProxy.add(thisHandle._hostAndPort()); }) .catch(function (e) { // KLUDGE! This is just a workaround for a broken // proxy server - we should remove it when // crossorigin.me is fixed console.warn('Fetch API failed, retrying with XMLHttpRequest'); thisHandle._performXMLHTTPRequest(targetURL, httpMethod, data, requestLines); }); }); }, _handleFetchAPIResponse: function(res) { if (this.response === null) { var header = ['HTTP/1.0 ', res.status, ' ', res.statusText, '\r\n']; res.headers.forEach(function(value, key, array) { header = header.concat([key, ': ', value, '\r\n']); }); header.push('\r\n'); this.response = [new TextEncoder('utf-8').encode(header.join(''))]; } this._readIncremental(res.body.getReader()); }, _readIncremental: function(reader) { var thisHandle = this; return reader.read().then(function (result) { if (result.done) { thisHandle.responseReceived = true; return; } thisHandle.response.push(result.value); thisHandle._signalReadSemaphore(); return thisHandle._readIncremental(reader); }); }, _performXMLHTTPRequest: function(targetURL, httpMethod, data, requestLines){ var thisHandle = this; var contentType; for (var i = 1; i < requestLines.length; i++) { var line = requestLines[i]; if (line.match(/Content-Type:/i)) { contentType = encodeURIComponent(line.substr(14)); break; } } var httpRequest = new XMLHttpRequest(); httpRequest.open(httpMethod, this._getURL(targetURL)); if (contentType !== undefined) { httpRequest.setRequestHeader('Content-type', contentType); } if (typeof SqueakJS === "object" && SqueakJS.options.ajax) { httpRequest.setRequestHeader("X-Requested-With", "XMLHttpRequest"); } httpRequest.responseType = "arraybuffer"; httpRequest.onload = function (oEvent) { thisHandle._handleXMLHTTPResponse(this); }; httpRequest.onerror = function(e) { var url = thisHandle._getURL(targetURL, true); console.warn('Retrying with CORS proxy: ' + url); var retry = new XMLHttpRequest(); retry.open(httpMethod, url); retry.responseType = httpRequest.responseType; if (typeof SqueakJS === "object" && SqueakJS.options.ajaxx) { retry.setRequestHeader("X-Requested-With", "XMLHttpRequest"); } retry.onload = function(oEvent) { console.log('Success: ' + url); thisHandle._handleXMLHTTPResponse(this); plugin.needProxy.add(thisHandle._hostAndPort()); }; retry.onerror = function() { thisHandle._otherEndClosed(); console.error("Failed to download:\n" + url); }; retry.send(data); }; httpRequest.send(data); }, _handleXMLHTTPResponse: function(response) { this.responseReceived = true; var content = response.response; if (!content) { this._otherEndClosed(); return; } // Recreate header var header = new TextEncoder('utf-8').encode( 'HTTP/1.0 ' + response.status + ' ' + response.statusText + '\r\n' + response.getAllResponseHeaders() + '\r\n'); // Concat header and response var res = new Uint8Array(header.byteLength + content.byteLength); res.set(header, 0); res.set(new Uint8Array(content), header.byteLength); this.response = [res]; this._signalReadSemaphore(); }, _performWebSocketRequest: function(targetURL, httpMethod, data, requestLines){ var url = this._getURL(targetURL); // Extract WebSocket key and subprotocol var webSocketSubProtocol; var webSocketKey; for (var i = 1; i < requestLines.length; i++) { var requestLine = requestLines[i].split(":"); if (requestLine[0] === "Sec-WebSocket-Protocol") { webSocketSubProtocol = requestLine[1].trim(); if (webSocketKey) { break; // Only break if both webSocketSubProtocol and webSocketKey are found } } else if (requestLine[0] === "Sec-WebSocket-Key") { webSocketKey = requestLine[1].trim(); if (webSocketSubProtocol) { break; // Only break if both webSocketSubProtocol and webSocketKey are found } } } // Keep track of WebSocket for future send and receive operations this.webSocket = new WebSocket(url.replace(/^http/, "ws"), webSocketSubProtocol); var thisHandle = this; this.webSocket.onopen = function() { if (thisHandle.status !== plugin.Socket_Connected) { thisHandle.status = plugin.Socket_Connected; thisHandle._signalConnSemaphore(); thisHandle._signalWriteSemaphore(); // Immediately ready to write } // Send the (fake) handshake back to the caller var acceptKey = new Uint8Array(sha1.array(webSocketKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")); var acceptKeyString = Squeak.bytesAsString(acceptKey); thisHandle._performWebSocketReceive( "HTTP/1.1 101 Switching Protocols\r\n" + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + "Sec-WebSocket-Accept: " + btoa(acceptKeyString) + "\r\n\r\n", true ); }; this.webSocket.onmessage = function(event) { thisHandle._performWebSocketReceive(event.data); }; this.webSocket.onerror = function(e) { thisHandle._otherEndClosed(); console.error("Error in WebSocket:", e); }; this.webSocket.onclose = function() { thisHandle._otherEndClosed(); }; }, _performWebSocketReceive: function(message, skipFramePacking) { // Process received message var dataIsBinary = !message.substr; if (!dataIsBinary) { message = new TextEncoder("utf-8").encode(message); } if (!skipFramePacking) { // Create WebSocket frame from message for Smalltalk runtime var frameLength = 1 + 1 + message.length + 4; // 1 byte for initial header bits & opcode, 1 byte for length and 4 bytes for mask var payloadLengthByte; if (message.byteLength < 126) { payloadLengthByte = message.length; } else if (message.byteLength < 0xffff) { frameLength += 2; // 2 additional bytes for payload length payloadLengthByte = 126; } else { frameLength += 8; // 8 additional bytes for payload length payloadLengthByte = 127; } var frame = new Uint8Array(frameLength); frame[0] = dataIsBinary ? 0x82 : 0x81; // Final bit 0x80 set and opcode 0x01 for text and 0x02 for binary frame[1] = 0x80 | payloadLengthByte; // Mask bit 0x80 and payload length byte var nextByteIndex; if (payloadLengthByte === 126) { frame[2] = message.length >>> 8; frame[3] = message.length & 0xff; nextByteIndex = 4; } else if (payloadLengthByte === 127) { frame[2] = message.length >>> 56; frame[3] = (message.length >>> 48) & 0xff; frame[4] = (message.length >>> 40) & 0xff; frame[5] = (message.length >>> 32) & 0xff; frame[6] = (message.length >>> 24) & 0xff; frame[7] = (message.length >>> 16) & 0xff; frame[8] = (message.length >>> 8) & 0xff; frame[9] = message.length & 0xff; nextByteIndex = 10; } else { nextByteIndex = 2; } // Add 'empty' mask (requiring no transformation) // Otherwise a (random) mask and the following line should be added: // var payload = message.map(function(b, index) { return b ^ maskKey[index & 0x03]; }); var maskKey = new Uint8Array(4); frame.set(maskKey, nextByteIndex); nextByteIndex += 4; var payload = message; frame.set(payload, nextByteIndex); // Make sure the frame is set as the response message = frame; } // Store received message in response buffer if (!this.response || !this.response.length) { this.response = [ message ]; } else { this.response.push(message); } this.responseReceived = true; this._signalReadSemaphore(); }, _performWebSocketSend: function() { // Decode sendBuffer which is a WebSocket frame (from Smalltalk runtime) // Read frame header fields var firstByte = this.sendBuffer[0]; var finalBit = firstByte >>> 7; var opcode = firstByte & 0x0f; var dataIsBinary; if (opcode === 0x00) { // Continuation frame console.error("No support for WebSocket frame continuation yet!"); return true; } else if (opcode === 0x01) { // Text frame dataIsBinary = false; } else if (opcode === 0x02) { // Binary frame dataIsBinary = true; } else if (opcode === 0x08) { // Close connection this.webSocket.close(); this.webSocket = null; return; } else if (opcode === 0x09 || opcode === 0x0a) { // Ping/pong frame (ignoring it, is handled by WebSocket implementation itself) return; } else { console.error("Unsupported WebSocket frame opcode " + opcode); return; } var secondByte = this.sendBuffer[1]; var maskBit = secondByte >>> 7; var payloadLength = secondByte & 0x7f; var nextByteIndex; if (payloadLength === 126) { payloadLength = (this.sendBuffer[2] << 8) | this.sendBuffer[3]; nextByteIndex = 4; } else if (payloadLength === 127) { payloadLength = (this.sendBuffer[2] << 56) | (this.sendBuffer[3] << 48) | (this.sendBuffer[4] << 40) | (this.sendBuffer[5] << 32) | (this.sendBuffer[6] << 24) | (this.sendBuffer[7] << 16) | (this.sendBuffer[8] << 8) | this.sendBuffer[9] ; nextByteIndex = 10; } else { nextByteIndex = 2; } var maskKey; if (maskBit) { maskKey = this.sendBuffer.subarray(nextByteIndex, nextByteIndex + 4); nextByteIndex += 4; } // Read (remaining) payload var payloadData = this.sendBuffer.subarray(nextByteIndex, nextByteIndex + payloadLength); nextByteIndex += payloadLength; // Unmask the payload if (maskBit) { payloadData = payloadData.map(function(b, index) { return b ^ maskKey[index & 0x03]; }); } // Extract data from payload var data; if (dataIsBinary) { data = payloadData; } else { data = Squeak.bytesAsString(payloadData); } // Remove frame from send buffer this.sendBuffer = this.sendBuffer.subarray(nextByteIndex); this.webSocket.send(data); // Send remaining frames if (this.sendBuffer.byteLength > 0) { this._performWebSocketSend(); } }, connect: function(hostAddress, port) { this.hostAddress = hostAddress; this.host = plugin._reverseLookupNameForAddress(hostAddress); this.port = port; this.status = plugin.Socket_Connected; this._signalConnSemaphore(); this._signalWriteSemaphore(); // Immediately ready to write }, close: function() { if (this.status == plugin.Socket_Connected || this.status == plugin.Socket_OtherEndClosed || this.status == plugin.Socket_WaitingForConnection) { if (this.webSocket) { this.webSocket.close(); this.webSocket = null; } this.status = plugin.Socket_Unconnected; this._signalConnSemaphore(); } }, destroy: function() { this.status = plugin.Socket_InvalidSocket; }, dataAvailable: function() { if (this.status == plugin.Socket_InvalidSocket) return false; if (this.status == plugin.Socket_Connected) { if (this.webSocket) { return this.response && this.response.length > 0; } else { if (this.response && this.response.length > 0) { this._signalReadSemaphore(); return true; } if (this.responseSentCompletly) { // Signal older Socket implementations that they reached the end this.status = plugin.Socket_OtherEndClosed; this._signalConnSemaphore(); } } } return false; }, recv: function(count) { if (this.response === null) return []; var data = this.response[0] || new Uint8Array(0); if (data.length > count) { var rest = data.subarray(count); if (rest) { this.response[0] = rest; } else { this.response.shift(); } data = data.subarray(0, count); } else { this.response.shift(); } if (this.responseReceived && this.response.length === 0 && !this.webSocket) { this.responseSentCompletly = true; } return data; }, send: function(data, start, end) { if (this.sendTimeout !== null) { self.clearTimeout(this.sendTimeout); } this.lastSend = Date.now(); var newBytes = data.bytes.subarray(start, end); if (this.sendBuffer === null) { // Make copy of buffer otherwise the stream buffer will overwrite it on next call (inside Smalltalk image) this.sendBuffer = newBytes.slice(); } else { var newLength = this.sendBuffer.byteLength + newBytes.byteLength; var newBuffer = new Uint8Array(newLength); newBuffer.set(this.sendBuffer, 0); newBuffer.set(newBytes, this.sendBuffer.byteLength); this.sendBuffer = newBuffer; } // Give image some time to send more data before performing requests this.sendTimeout = self.setTimeout(this._performRequest.bind(this), 50); return newBytes.byteLength; } }; }, primitiveHasSocketAccess: function(argCount) { this.interpreterProxy.popthenPush(argCount + 1, this.interpreterProxy.trueObject()); return true; }, primitiveInitializeNetwork: function(argCount) { if (argCount !== 1) return false; this.lookupSemaIdx = this.interpreterProxy.stackIntegerValue(0); this.status = this.Resolver_Ready; this.interpreterProxy.pop(argCount); // Answer self return true; }, primitiveResolverNameLookupResult: function(argCount) { if (argCount !== 0) return false; // Validate that lastLookup is in fact a name (and not an address) if (!this.lastLookup || !this.lastLookup.substr) { this.interpreterProxy.popthenPush(argCount + 1, this.interpreterProxy.nilObject()); return true; } // Retrieve result from cache var address = this._getAddressFromLookupCache(this.lastLookup, true); this.interpreterProxy.popthenPush(argCount + 1, address ? this.primHandler.makeStByteArray(address) : this.interpreterProxy.nilObject() ); return true; }, primitiveResolverStartNameLookup: function(argCount) { if (argCount !== 1) return false; // Start new lookup, ignoring if one is in progress var lookup = this.lastLookup = this.interpreterProxy.stackValue(0).bytesAsString(); // Perform lookup in local cache var result = this._getAddressFromLookupCache(lookup, false); if (result) { this.status = this.Resolver_Ready; this._signalLookupSemaphore(); } else { // Perform DNS request var dnsQueryURL = "https://9.9.9.9:5053/dns-query?name=" + encodeURIComponent(this.lastLookup) + "&type=A"; var queryStarted = false; if (self.fetch) { var thisHandle = this; var init = { method: "GET", mode: "cors", credentials: "omit", cache: "no-store", // do not use the browser cache for DNS requests (a separate cache is kept) referrer: "no-referrer", referrerPolicy: "no-referrer", }; self.fetch(dnsQueryURL, init) .then(function(response) { return response.json(); }) .then(function(response) { thisHandle._addAddressFromResponseToLookupCache(response); }) .catch(function(error) { console.error("Name lookup failed", error); }) .then(function() { // If no other lookup is started, signal the receiver (ie resolver) is ready if (lookup === thisHandle.lastLookup) { thisHandle.status = thisHandle.Resolver_Ready; thisHandle._signalLookupSemaphore(); } }) ; queryStarted = true; } else { var thisHandle = this; var lookupReady = function() { // If no other lookup is started, signal the receiver (ie resolver) is ready if (lookup === thisHandle.lastLookup) { thisHandle.status = thisHandle.Resolver_Ready; thisHandle._signalLookupSemaphore(); } }; var httpRequest = new XMLHttpRequest(); httpRequest.open("GET", dnsQueryURL, true); httpRequest.timeout = 2000; // milliseconds httpRequest.responseType = "json"; httpRequest.onload = function(oEvent) { thisHandle._addAddressFromResponseToLookupCache(this.response); lookupReady(); }; httpRequest.onerror = function() { console.error("Name lookup failed", httpRequest.statusText); lookupReady(); }; httpRequest.send(); queryStarted = true; } // Mark the receiver (ie resolver) is busy if (queryStarted) { this.status = this.Resolver_Busy; this._signalLookupSemaphore(); } } this.interpreterProxy.popthenPush(argCount + 1, this.interpreterProxy.nilObject()); return true; }, primitiveResolverAddressLookupResult: function(argCount) { if (argCount !== 0) return false; // Validate that lastLookup is in fact an address (and not a name) if (!this.lastLookup || !this.lastLookup.every) { this.interpreterProxy.popthenPush(argCount + 1, this.interpreterProxy.nilObject()); return true; } // Retrieve result from cache var name = this._reverseLookupNameForAddress(this.lastLookup); var result = this.primHandler.makeStString(name); this.interpreterProxy.popthenPush(argCount + 1, result); return true; }, primitiveResolverStartAddressLookup: function(argCount) { if (argCount !== 1) return false; // Start new lookup, ignoring if one is in progress this.lastLookup = this.interpreterProxy.stackBytes(0); this.interpreterProxy.popthenPush(argCount + 1, this.interpreterProxy.nilObject()); // Immediately signal the lookup is ready (since all lookups are done internally) this.status = this.Resolver_Ready; this._signalLookupSemaphore(); return true; }, primitiveResolverStatus: function(argCount) { if (argCount !== 0) return false; this.interpreterProxy.popthenPush(argCount + 1, this.status); return true; }, primitiveResolverAbortLookup: function(argCount) { if (argCount !== 0) return false; // Unable to abort send request (although future browsers might support AbortController), // just cancel the handling of the request by resetting the lastLookup value this.lastLookup = null; this.status = this.Resolver_Ready; this._signalLookupSemaphore(); this.interpreterProxy.popthenPush(argCount + 1, this.interpreterProxy.nilObject()); return true; }, primitiveSocketRemoteAddress: function(argCount) { if (argCount !== 1) return false; var handle = this.interpreterProxy.stackObjectValue(0).handle; if (handle === undefined) return false; this.interpreterProxy.popthenPush(argCount + 1, handle.hostAddress ? this.primHandler.makeStByteArray(handle.hostAddress) : this.interpreterProxy.nilObject() ); return true; }, primitiveSocketRemotePort: function(argCount) { if (argCount !== 1) return false; var handle = this.interpreterProxy.stackObjectValue(0).handle; if (handle === undefined) return false; this.interpreterProxy.popthenPush(argCount + 1, handle.port); return true; }, primitiveSocketConnectionStatus: function(argCount) { if (argCount !== 1) return false; var handle = this.interpreterProxy.stackObjectValue(0).handle; if (handle === undefined) return false; var status = handle.status; if (status === undefined) status = this.Socket_InvalidSocket; this.interpreterProxy.popthenPush(argCount + 1, status); return true; }, primitiveSocketConnectToPort: function(argCount) { if (argCount !== 3) return false; var handle = this.interpreterProxy.stackObjectValue(2).handle; if (handle === undefined) return false; var hostAddress = this.interpreterProxy.stackBytes(1); var port = this.interpreterProxy.stackIntegerValue(0); handle.connect(hostAddress, port); this.interpreterProxy.popthenPush(argCount + 1, this.interpreterProxy.nilObject()); return true; }, primitiveSocketCloseConnection: function(argCount) { if (argCount !== 1) return false; var handle = this.interpreterProxy.stackObjectValue(0).handle; if (handle === undefined) return false; handle.close(); this.interpreterProxy.popthenPush(argCount + 1, this.interpreterProxy.nilObject()); return true; }, primitiveSocketCreate3Semaphores: function(argCount) { if (argCount !== 7) return false; var writeSemaIndex = this.interpreterProxy.stackIntegerValue(0); var readSemaIndex = this.interpreterProxy.stackIntegerValue(1); var semaIndex = this.interpreterProxy.stackIntegerValue(2); var sendBufSize = this.interpreterProxy.stackIntegerValue(3); var socketType = this.interpreterProxy.stackIntegerValue(5); if (socketType !== this.TCP_Socket_Type) return false; var name = '{SqueakJS Socket #' + (++this.handleCounter) + '}'; var sqHandle = this.primHandler.makeStString(name); sqHandle.handle = this._newSocketHandle(sendBufSize, semaIndex, readSemaIndex, writeSemaIndex); this.interpreterProxy.popthenPush(argCount + 1, sqHandle); return true; }, primitiveSocketDestroy: function(argCount) { if (argCount !== 1) return false; var handle = this.interpreterProxy.stackObjectValue(0).handle; if (handle === undefined) return false; handle.destroy(); this.interpreterProxy.popthenPush(argCount + 1, handle.status); return true; }, primitiveSocketReceiveDataAvailable: function(argCount) { if (argCount !== 1) return false; var handle = this.interpreterProxy.stackObjectValue(0).handle; if (handle === undefined) return false; var ret = this.interpreterProxy.falseObject(); if (handle.dataAvailable()) { ret = this.interpreterProxy.trueObject(); } this.interpreterProxy.popthenPush(argCount + 1, ret); return true; }, primitiveSocketReceiveDataBufCount: function(argCount) { if (argCount !== 4) return false; var handle = this.interpreterProxy.stackObjectValue(3).handle; if (handle === undefined) return false; var target = this.interpreterProxy.stackObjectValue(2); var start = this.interpreterProxy.stackIntegerValue(1) - 1; var count = this.interpreterProxy.stackIntegerValue(0); if ((start + count) > target.bytes.length) return false; var bytes = handle.recv(count); target.bytes.set(bytes, start); this.interpreterProxy.popthenPush(argCount + 1, bytes.length); return true; }, primitiveSocketSendDataBufCount: function(argCount) { if (argCount !== 4) return false; var handle = this.interpreterProxy.stackObjectValue(3).handle; if (handle === undefined) return false; var data = this.interpreterProxy.stackObjectValue(2); var start = this.interpreterProxy.stackIntegerValue(1) - 1; if (start < 0 ) return false; var count = this.interpreterProxy.stackIntegerValue(0); var end = start + count; if (end > data.length) return false; var res = handle.send(data, start, end); this.interpreterProxy.popthenPush(argCount + 1, res); return true; }, primitiveSocketSendDone: function(argCount) { if (argCount !== 1) return false; this.interpreterProxy.popthenPush(argCount + 1, this.interpreterProxy.trueObject()); return true; }, primitiveSocketListenWithOrWithoutBacklog: function(argCount) { if (argCount < 2) return false; this.interpreterProxy.popthenPush(argCount + 1, this.interpreterProxy.nilObject()); return true; }, }; } function registerSocketPlugin() { if (typeof Squeak === "object" && Squeak.registerExternalModule) { Squeak.registerExternalModule('SocketPlugin', SocketPlugin()); } else self.setTimeout(registerSocketPlugin, 100); }; registerSocketPlugin();