diff --git a/plugins/SocketPlugin.js b/plugins/SocketPlugin.js new file mode 100644 index 00000000..b756061c --- /dev/null +++ b/plugins/SocketPlugin.js @@ -0,0 +1,345 @@ +function SocketPlugin() { + + return { + getModuleName() { return 'SocketPlugin with support for web requests'; }, + interpreterProxy: null, + primHandler: null, + + handleCounter: 0, + + // DNS Lookup + LastLookup: null, + + // 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(anInterpreter) { + this.interpreterProxy = anInterpreter; + this.primHandler = this.interpreterProxy.vm.primHandler; + return true; + }, + + // A socket handle emulates socket behavior + _newSocketHandle(sendBufSize, connSemaIndex, readSemaIndex, writeSemaIndex) { + var that = this; + return { + host: null, + port: null, + + connSemaIndex: connSemaIndex, + readSemaIndex: readSemaIndex, + writeSemaIndex: writeSemaIndex, + + sendBuffer: new Uint8Array(sendBufSize), + sendBufferIndex: 0, + sendTimeout: null, + + response: [], + responseIndex: 0, + responseLength: null, + responseReceived: false, + + status: that.Socket_Unconnected, + + _signalSemaphore(semaIndex) { + if (semaIndex <= 0) return; + that.primHandler.signalSemaphoreWithIndex(semaIndex); + }, + _signalConnSemaphore() { this._signalSemaphore(this.connSemaIndex); }, + _signalReadSemaphore() { this._signalSemaphore(this.readSemaIndex); }, + _signalWriteSemaphore() { this._signalSemaphore(this.writeSemaIndex); }, + + _performRequest() { + var request = new TextDecoder("utf-8").decode(this.sendBuffer); + var requestLines = request.split('\n'); + // Split header lines and parse first line + var firstLineItems = request.split('\n')[0].split(' '); + var httpMethod = firstLineItems[0]; + if (httpMethod !== 'GET' && httpMethod !== 'PUT' && + httpMethod !== 'POST') { + this.status = that.Socket_OtherEndClosed; + this._signalConnSemaphore(); + return -1; + } + var targetURL = firstLineItems[1]; + + var url; + if (this.port !== 443) { + url = 'http://' + this.host + ':' + this.port + targetURL; + } else { + url = 'https://' + this.host + targetURL; + } + + var contentType, contentLength; + for (var i = 1; i < requestLines.length; i++) { + var line = requestLines[i]; + if (line.indexOf('Content-Type: ') === 0) { + contentType = encodeURIComponent(line.substr(14)); + } else if (line.indexOf('Content-Length: ') === 0) { + contentLength = parseInt(line.substr(16)); + } + } + + // Extract possible data to send + var data = null; + if (contentLength !== undefined) { + data = this.sendBuffer.slice(this.sendBufferIndex - contentLength, this.sendBufferIndex); + } + + var httpRequest = new XMLHttpRequest(); + httpRequest.open(httpMethod, url); + if (contentType !== undefined) { + httpRequest.setRequestHeader('Content-type', contentType); + } + httpRequest.responseType = "arraybuffer"; + + var thisHandle = this; + httpRequest.onload = function (oEvent) { + var content = this.response; + if (content) { + // Fake header + var header = new TextEncoder('utf-8').encode( + 'HTTP/1.0 ' + this.status + ' OK\r\n' + + 'Server: SqueakJS SocketPlugin Proxy\r\n' + + 'Content-Length: ' + content.byteLength + '\r\n\r\n'); + + // Merge fake header with content + thisHandle.response = new Uint8Array(header.byteLength + content.byteLength); + thisHandle.response.set(header, 0); + thisHandle.response.set(new Uint8Array(content), header.byteLength); + thisHandle.responseLength = thisHandle.response.byteLength; + + that.primHandler.signalSemaphoreWithIndex(thisHandle.readSemaIndex); + } else { + thisHandle.status = that.Socket_OtherEndClosed; + } + }; + + httpRequest.onerror = function(e) { + console.warn('Retrying with CORS proxy: ' + url); + var proxy = 'https://cors-anywhere.herokuapp.com/'; //'https://crossorigin.me/', + retry = new XMLHttpRequest(); + var proxyURL = proxy + (this.port === 443 ? url : 'http://' + thisHandle.host + targetURL); + retry.open(httpMethod, proxyURL); + retry.responseType = httpRequest.responseType; + retry.onload = httpRequest.onload; + retry.onerror = function() { + thisHandle.status = that.Socket_OtherEndClosed; + alert("Failed to download:\n" + url); + }; + retry.send(data); + }; + + httpRequest.send(data); + }, + + connect(host, port) { + this.host = host; + this.port = port; + this.status = that.Socket_Connected; + this._signalConnSemaphore(); + this._signalWriteSemaphore(); // Immediately ready to write + }, + + close() { + if (this.status == that.Socket_Connected || + this.status == that.Socket_OtherEndClosed || + this.status == that.Socket_WaitingForConnection) { + this.status = that.Socket_Unconnected; + this._signalConnSemaphore(); + } + }, + + destroy() { + this.status = that.Socket_InvalidSocket; + }, + + dataAvailable() { + if (this.status == that.Socket_InvalidSocket) return false; + if (this.status == that.Socket_Connected) { + if (!this.responseReceived) { + this._signalReadSemaphore(); + return true; + } else { + this.status = that.Socket_OtherEndClosed; + this._signalConnSemaphore(); + } + } + return false; + }, + + recv(start, count) { + if (this.responseLength !== null && start >= this.responseLength) { + this.responseReceived = true; // Everything has been read + } + this.responseIndex = start + count; + return this.response.slice(start, start + count); + }, + + send(data, start, end) { + if (this.sendTimeout !== null) { + window.clearTimeout(this.sendTimeout); + } + this.lastSend = Date.now(); + newBytes = data.bytes.slice(start, end); + this.sendBuffer.set(newBytes, this.sendBufferIndex); + this.sendBufferIndex += newBytes.byteLength; + // Give image some time to send more data before performing requests + this.sendTimeout = setTimeout(this._performRequest.bind(this), 50); + return newBytes.byteLength; + } + }; + }, + + primitiveHasSocketAccess(argCount) { + this.interpreterProxy.popthenPush(1, this.interpreterProxy.trueObject()); + return true; + }, + + primitiveInitializeNetwork(argCount) { + this.interpreterProxy.pop(1); + return true; + }, + + primitiveResolverNameLookupResult(argCount) { + if (argCount !== 0) return false; + var inet; + if (this.LastLookup !== null) { + inet = this.primHandler.makeStString(this.LastLookup); + this.LastLookup = null; + } else { + inet = this.interpreterProxy.nilObject(); + } + this.interpreterProxy.popthenPush(1, inet); + return true; + }, + + primitiveResolverStartNameLookup(argCount) { + if (argCount !== 1) return false; + this.LastLookup = this.interpreterProxy.stackValue(0).bytesAsString(); + this.interpreterProxy.popthenPush(1, this.interpreterProxy.nilObject()); + return true; + }, + + primitiveResolverStatus(argCount) { + this.interpreterProxy.popthenPush(1, this.Resolver_Ready); + return true; + }, + + primitiveSocketConnectionStatus(argCount) { + if (argCount !== 1) return false; + var handle = this.interpreterProxy.stackObjectValue(0).handle; + var status = handle.status; + if (status === undefined) status = this.Socket_InvalidSocket; + this.interpreterProxy.popthenPush(1, status); + return true; + }, + + primitiveSocketConnectToPort(argCount) { + if (argCount !== 3) return false; + var handle = this.interpreterProxy.stackObjectValue(2).handle; + if (handle === undefined) return false; + var host = this.interpreterProxy.stackObjectValue(1).bytesAsString(); + var port = this.interpreterProxy.stackIntegerValue(0); + handle.connect(host, port); + this.interpreterProxy.popthenPush(argCount, + this.interpreterProxy.nilObject()); + return true; + }, + + primitiveSocketCloseConnection(argCount) { + if (argCount !== 1) return false; + var handle = this.interpreterProxy.stackObjectValue(0).handle; + if (handle === undefined) return false; + handle.close(); + this.interpreterProxy.popthenPush(1, this.interpreterProxy.nilObject()); + return true; + }, + + primitiveSocketCreate3Semaphores(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 = '{socket handle #' + (++this.handleCounter) + '}'; + var sqHandle = this.primHandler.makeStString(name); + sqHandle.handle = this._newSocketHandle(sendBufSize, semaIndex, + readSemaIndex, writeSemaIndex); + this.interpreterProxy.popthenPush(argCount, sqHandle); + return true; + }, + + primitiveSocketDestroy(argCount) { + if (argCount !== 1) return false; + var handle = this.interpreterProxy.stackObjectValue(0).handle; + if (handle === undefined) return false; + handle.destroy(); + this.interpreterProxy.popthenPush(1, handle.status); + return true; + }, + + primitiveSocketReceiveDataAvailable(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(1, ret); + return true; + }, + + primitiveSocketReceiveDataBufCount(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); + var bytes = handle.recv(start, count); + target.bytes.set(bytes, start); + this.interpreterProxy.popthenPush(argCount, bytes.length); + return true; + }, + + primitiveSocketSendDataBufCount(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; + + res = handle.send(data, start, end); + this.interpreterProxy.popthenPush(1, res); + return true; + }, + + primitiveSocketSendDone(argCount) { + if (argCount !== 1) return false; + this.interpreterProxy.popthenPush(1, this.interpreterProxy.trueObject()); + return true; + }, + }; +} + +window.addEventListener('load', function() { + Squeak.registerExternalModule('SocketPlugin', SocketPlugin()); +}); diff --git a/plugins/SqueakSSL.js b/plugins/SqueakSSL.js new file mode 100644 index 00000000..61eec595 --- /dev/null +++ b/plugins/SqueakSSL.js @@ -0,0 +1,115 @@ +function SqueakSSL() { + + return { + getModuleName() { return 'Dummy SqueakSSL'; }, + interpreterProxy: null, + primHandler: null, + + handleCounter: 0, + + // Return codes from the core SSL functions + SQSSL_OK: 0, + SQSSL_NEED_MORE_DATA: -1, + SQSSL_INVALID_STATE: -2, + SQSSL_BUFFER_TOO_SMALL: -3, + SQSSL_INPUT_TOO_LARGE: -4, + SQSSL_GENERIC_ERROR: -5, + SQSSL_OUT_OF_MEMORY: -6, + // SqueakSSL getInt/setInt property IDs + SQSSL_PROP_VERSION: 0, + SQSSL_PROP_LOGLEVEL: 1, + SQSSL_PROP_SSLSTATE: 2, + SQSSL_PROP_CERTSTATE: 3, + // SqueakSSL getString/setString property IDs + SQSSL_PROP_PEERNAME: 0, + SQSSL_PROP_CERTNAME: 1, + SQSSL_PROP_SERVERNAME: 2, + + setInterpreter(anInterpreter) { + this.interpreterProxy = anInterpreter; + this.primHandler = this.interpreterProxy.vm.primHandler; + return true; + }, + + primitiveCreate(argCount) { + var name = '{ssl handle #' + (++this.handleCounter) + '}'; + var sqHandle = this.primHandler.makeStString(name); + sqHandle.handle = true; + this.interpreterProxy.popthenPush(argCount, sqHandle); + return true; + }, + + primitiveConnect(argCount) { + if (argCount !== 5) return false; + this.interpreterProxy.popthenPush(argCount, 0); + return true; + }, + + primitiveDestroy(argCount) { + if (argCount !== 1) return false; + this.interpreterProxy.popthenPush(1, 1); // Non-zero if successful + return true; + }, + + primitiveGetIntProperty(argCount) { + if (argCount !== 2) return false; + var handle = this.interpreterProxy.stackObjectValue(1).handle; + if (handle === undefined) return false; + var propID = this.interpreterProxy.stackIntegerValue(0); + + var res; + if (propID === this.SQSSL_PROP_CERTSTATE) { + res = this.SQSSL_OK; // Always valid + } else { + res = 0; + } + this.interpreterProxy.popthenPush(argCount, res); + return true; + }, + + primitiveGetStringProperty(argCount) { + if (argCount !== 2) return false; + var handle = this.interpreterProxy.stackObjectValue(1).handle; + if (handle === undefined) return false; + var propID = this.interpreterProxy.stackIntegerValue(0); + + if (propID === this.SQSSL_PROP_PEERNAME) { + res = this.primHandler.makeStString('*'); // Match all + } else { + res = this.interpreterProxy.nilObject(); + } + this.interpreterProxy.popthenPush(argCount, res); + return true; + }, + + primitiveEncrypt(argCount) { + if (argCount !== 5) return false; + var handle = this.interpreterProxy.stackObjectValue(4).handle; + if (handle === undefined) return false; + var srcBuf = this.interpreterProxy.stackObjectValue(3); + var start = this.interpreterProxy.stackIntegerValue(2) - 1; + var length = this.interpreterProxy.stackIntegerValue(1); + var dstBuf = this.interpreterProxy.stackObjectValue(0); + dstBuf.bytes = srcBuf.bytes; // Just copy all there is + this.interpreterProxy.popthenPush(argCount, length); + return true; + }, + + primitiveDecrypt(argCount) { + if (argCount !== 5) return false; + var handle = this.interpreterProxy.stackObjectValue(4).handle; + if (handle === undefined) return false; + var srcBuf = this.interpreterProxy.stackObjectValue(3); + var start = this.interpreterProxy.stackIntegerValue(2) - 1; + var length = this.interpreterProxy.stackIntegerValue(1); + var dstBuf = this.interpreterProxy.stackObjectValue(0); + dstBuf.bytes = srcBuf.bytes; // Just copy all there is + this.interpreterProxy.popthenPush(argCount, length); + return true; + } + }; +} + +window.addEventListener('load', function() { + Squeak.registerExternalModule('SqueakSSL', SqueakSSL()); +}); diff --git a/squeak.js b/squeak.js index 68715290..b148c891 100644 --- a/squeak.js +++ b/squeak.js @@ -109,6 +109,8 @@ Function.prototype.subclass = function(classPath /* + more args */ ) { "plugins/Matrix2x3Plugin.js", "plugins/MiscPrimitivePlugin.js", "plugins/ScratchPlugin.js", + "plugins/SocketPlugin.js", + "plugins/SqueakSSL.js", "plugins/SoundGenerationPlugin.js", "plugins/StarSqueakPlugin.js", "plugins/ZipPlugin.js",