scratch0-5 / plugins /SocketPlugin.js
soiz1's picture
Upload folder using huggingface_hub
8f3f8db verified
/*
* 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();