From 8e246331f6f71320cfcc8defdd04e756a75f71cf Mon Sep 17 00:00:00 2001 From: Skyler Grey Date: Fri, 18 Aug 2023 13:30:35 +0000 Subject: Add a FunctionBasedURPConnection and a websocket URP connector MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - FunctionBasedURPConnection is used to enable a client to open a URP connection to a fresh Kit instance in COOL. - This URP connector can be used with that and https://github.com/CollaboraOnline/online/pull/6992 to use a Java Uno Remote Protocol client over websockets - For interoperability with existing Collabora Online websockets a prefix (urp ) is added to each message sent and a similar prefix (urp: ) is expected on each message recieved. This allows sending over the same websocket as other data is being transmitted through. If you are writing a bridge to work with this, you will need to add/strip the prefixes accordingly - This commit uses Java WebSocket (https://github.com/TooTallNate/Java-WebSocket) to send data over websockets. Change-Id: I2bda3d0b988bef7883f9b6829eeb5b7ae8075f27 Signed-off-by: Skyler Grey Reviewed-on: https://gerrit.libreoffice.org/c/core/+/151171 Tested-by: Jenkins Reviewed-by: Caolán McNamara --- ridljar/Jar_libreoffice.mk | 8 + ridljar/com/sun/star/comp/helper/Bootstrap.java | 63 ++++ .../websocket/ConnectionDescriptor.java | 60 ++++ .../connections/websocket/WebsocketConnection.java | 335 +++++++++++++++++++++ .../connections/websocket/websocketConnector.java | 137 +++++++++ ridljar/source/libreoffice/module-info.java | 1 + ridljar/util/manifest | 3 + 7 files changed, 607 insertions(+) create mode 100644 ridljar/com/sun/star/lib/connections/websocket/ConnectionDescriptor.java create mode 100644 ridljar/com/sun/star/lib/connections/websocket/WebsocketConnection.java create mode 100644 ridljar/com/sun/star/lib/connections/websocket/websocketConnector.java (limited to 'ridljar') diff --git a/ridljar/Jar_libreoffice.mk b/ridljar/Jar_libreoffice.mk index d34ae3f5ebda..76a56eedc078 100644 --- a/ridljar/Jar_libreoffice.mk +++ b/ridljar/Jar_libreoffice.mk @@ -18,11 +18,16 @@ $(eval $(call gb_Jar_use_jars,libreoffice, \ unoloader \ )) +$(eval $(call gb_Jar_use_externals,libreoffice,\ + java_websocket \ +)) + $(eval $(call gb_Jar_set_packageroot,libreoffice,com)) $(eval $(call gb_Jar_set_manifest,libreoffice,$(SRCDIR)/ridljar/util/manifest)) $(eval $(call gb_Jar_add_manifest_classpath,libreoffice, \ + java_websocket.jar \ unoloader.jar \ $(if $(filter MACOSX,$(OS)),../../Frameworks/,../) \ )) @@ -63,6 +68,9 @@ $(eval $(call gb_Jar_add_sourcefiles,libreoffice,\ ridljar/com/sun/star/lib/connections/socket/SocketConnection \ ridljar/com/sun/star/lib/connections/socket/socketAcceptor \ ridljar/com/sun/star/lib/connections/socket/socketConnector \ + ridljar/com/sun/star/lib/connections/websocket/ConnectionDescriptor \ + ridljar/com/sun/star/lib/connections/websocket/WebsocketConnection \ + ridljar/com/sun/star/lib/connections/websocket/websocketConnector \ ridljar/com/sun/star/lib/uno/Proxy \ ridljar/com/sun/star/lib/uno/adapter/ByteArrayToXInputStreamAdapter \ ridljar/com/sun/star/lib/uno/adapter/InputStreamToXInputStreamAdapter \ diff --git a/ridljar/com/sun/star/comp/helper/Bootstrap.java b/ridljar/com/sun/star/comp/helper/Bootstrap.java index edf21d7d2adf..6b371f50324b 100644 --- a/ridljar/com/sun/star/comp/helper/Bootstrap.java +++ b/ridljar/com/sun/star/comp/helper/Bootstrap.java @@ -32,6 +32,7 @@ import com.sun.star.lib.util.NativeLibraryLoader; import com.sun.star.loader.XImplementationLoader; import com.sun.star.uno.UnoRuntime; import com.sun.star.uno.XComponentContext; +import com.sun.star.beans.XPropertySet; import java.io.BufferedReader; import java.io.File; @@ -397,6 +398,68 @@ public class Bootstrap { return xContext; } + /** + * Bootstraps the component context from a websocket location. + * + * @param url + * the ws:// or wss:// url of the websocket server + * + * @throws BootstrapException if things go awry. + * + * @return a bootstrapped component context. + * + * @since LibreOffice 24.2 + */ + public static final XComponentContext bootstrapWebsocketConnection( String url ) + throws BootstrapException { + + XComponentContext xContext = null; + + try { + // create default local component context + XComponentContext xLocalContext = + createInitialComponentContext( (Map) null ); + if ( xLocalContext == null ) + throw new BootstrapException( "no local component context!" ); + + // initial service manager + XMultiComponentFactory xLocalServiceManager = + xLocalContext.getServiceManager(); + if ( xLocalServiceManager == null ) + throw new BootstrapException( "no initial service manager!" ); + + // create a URL resolver + XUnoUrlResolver xUrlResolver = + UnoUrlResolver.create( xLocalContext ); + + // connection string + String sConnect = "uno:websocket" + + ",url=" + url + + ";urp;StarOffice.ComponentContext"; + + try { + // try to connect to office + Object xOfficeServiceManager = xUrlResolver.resolve(sConnect); + + xContext = UnoRuntime.queryInterface(XComponentContext.class, xOfficeServiceManager); + + if ( xContext == null ) + throw new BootstrapException( "no component context!" ); + } catch ( com.sun.star.connection.NoConnectException ex ) { + throw new BootstrapException(ex); + } + } catch ( BootstrapException e ) { + throw e; + } catch ( java.lang.RuntimeException e ) { + throw e; + } catch ( java.lang.Exception e ) { + throw new BootstrapException( e ); + } + + return xContext; + } + + private static final Random randomPipeName = new Random(); private static void pipe( diff --git a/ridljar/com/sun/star/lib/connections/websocket/ConnectionDescriptor.java b/ridljar/com/sun/star/lib/connections/websocket/ConnectionDescriptor.java new file mode 100644 index 000000000000..439a52551726 --- /dev/null +++ b/ridljar/com/sun/star/lib/connections/websocket/ConnectionDescriptor.java @@ -0,0 +1,60 @@ +/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +package com.sun.star.lib.connections.websocket; + +import java.util.Arrays; +import java.util.Iterator; + +/** + * Helper class for websocketConnector. + */ +final class ConnectionDescriptor { + public ConnectionDescriptor(String description) + throws com.sun.star.lang.IllegalArgumentException { + Iterator descriptionParts = Arrays.stream(description.split(",")).iterator(); + descriptionParts + .next(); // skip over the first part as it's the protocol not a real parameter + while (descriptionParts.hasNext()) + { + String parameter = descriptionParts.next(); + String[] pair = parameter.split("=", 2); + + if (pair.length != 2) + { + throw new com.sun.star.lang.IllegalArgumentException( + String.format("parameter %s lacks '='", parameter)); + } + + String key = pair[0]; + String value = pair[1]; + if (key.equalsIgnoreCase("url")) { + url = value; + } + } + } + + public String getURL() { + return url; + } + + private String url = null; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/ridljar/com/sun/star/lib/connections/websocket/WebsocketConnection.java b/ridljar/com/sun/star/lib/connections/websocket/WebsocketConnection.java new file mode 100644 index 000000000000..7f522df409d1 --- /dev/null +++ b/ridljar/com/sun/star/lib/connections/websocket/WebsocketConnection.java @@ -0,0 +1,335 @@ +/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ +package com.sun.star.lib.connections.websocket; + + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.net.ProtocolException; +import java.net.Socket; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; + +import javax.swing.text.html.HTMLDocument.Iterator; + +import com.sun.star.connection.XConnection; +import com.sun.star.connection.XConnectionBroadcaster; +import com.sun.star.io.XStreamListener; +import org.java_websocket.client.WebSocketClient; +import org.java_websocket.handshake.ServerHandshake; + +/** + * The WebsocketConnection implements the XConnection interface + * and is uses by the WebsocketConnector. + * + *

This class is not part of the provided api.

+ * + * @see com.sun.star.lib.connections.socket.socketAcceptor + * @see com.sun.star.lib.connections.socket.socketConnector + * @see com.sun.star.connection.XConnection + */ +public class WebsocketConnection extends WebSocketClient implements XConnection, XConnectionBroadcaster { + /** + * When set to true, enables various debugging output. + */ + public static final boolean DEBUG = false; + static final byte[] outgoingPrefix = { 'u', 'r', 'p', ' ' }; + + protected String _description; + protected InputStream _inputStream; + protected OutputStream _outputStream; + + protected InputStream _outputStreamReader; + protected OutputStream _inputStreamWriter; + + protected ArrayList _listeners; + + /** + * Constructs a new WebsocketConnection. + * + * @param description the description of the connection. + * @param desc the websocket ConnectionDescriptor containing information such as the websocket URL + */ + public WebsocketConnection(String description, ConnectionDescriptor desc) throws IOException, URISyntaxException, InterruptedException { + super(new URI(desc.getURL())); + + if (DEBUG) System.err.println("##### " + getClass().getName() + " - instantiated " + description + " " + desc); + + _description = description; + + PipedOutputStream inputStreamWriter = new PipedOutputStream(); + PipedInputStream inputPipe = new PipedInputStream(inputStreamWriter); + PipedOutputStream outputPipe = new PipedOutputStream(); + PipedInputStream outputStreamReader = new PipedInputStream(outputPipe); + + + _inputStream = new BufferedInputStream(inputPipe); + _inputStreamWriter = inputStreamWriter; + _outputStream = new BufferedOutputStream(outputPipe); + _outputStreamReader = outputStreamReader; + + _listeners = new ArrayList(); + + connectBlocking(); + } + + public void addStreamListener(XStreamListener aListener ) + throws com.sun.star.uno.RuntimeException { + _listeners.add(aListener); + } + + public void removeStreamListener(XStreamListener aListener ) + throws com.sun.star.uno.RuntimeException { + _listeners.remove(aListener); + } + + private void notifyListeners_open() { + for (XStreamListener xStreamListener : _listeners) { + xStreamListener.started(); + } + } + + private void notifyListeners_close() { + for (XStreamListener xStreamListener : _listeners) { + xStreamListener.closed(); + } + } + + private void notifyListeners_error(com.sun.star.uno.Exception exception) { + for (XStreamListener xStreamListener : _listeners) { + xStreamListener.error(exception); + } + } + + /** + * Read the required number of bytes. + * + * @param bytes the outparameter, where the bytes have to be placed. + * @param nBytesToRead the number of bytes to read. + * @return the number of bytes read. + * + * @see com.sun.star.connection.XConnection#read + */ + public int read(/*OUT*/byte[][] bytes, int nBytesToRead) + throws com.sun.star.io.IOException, com.sun.star.uno.RuntimeException { + + String errMessage = null; + + int read_bytes = 0; + bytes[0] = new byte[nBytesToRead]; + + try { + _inputStreamWriter.flush(); + + int count ; + + do { + count = _inputStream.read(bytes[0], read_bytes, nBytesToRead - read_bytes); + if(count == -1) + errMessage = "EOF reached - " + getDescription(); + + read_bytes += count; + } + while(read_bytes >= 0 && read_bytes < nBytesToRead && count >= 0); + } catch(IOException ioException) { + if(DEBUG) { + System.err.println("##### " + getClass().getName() + ".read - exception occurred:" + ioException); + ioException.printStackTrace(); + } + + errMessage = ioException.toString(); + } + + if(errMessage != null) { + com.sun.star.io.IOException unoIOException = new com.sun.star.io.IOException(errMessage); + notifyListeners_error(unoIOException); + + throw unoIOException; + } + + if (DEBUG) System.err.println(String.format("##### %s - read %s bytes of %s requested", getClass().getName(), Integer.toString(read_bytes), Integer.toString(nBytesToRead))); + + return read_bytes; + } + + /** + * Write bytes. + * + * @param aData the bytes to write. + * @see com.sun.star.connection.XConnection#write + */ + public void write(byte aData[]) throws com.sun.star.io.IOException, + com.sun.star.uno.RuntimeException { + try { + _outputStream.write(aData); + } catch(IOException ioException) { + com.sun.star.io.IOException unoIOException = new com.sun.star.io.IOException(ioException); + notifyListeners_error(unoIOException); + + throw unoIOException; + } + + if (DEBUG) System.err.println(String.format("##### %s - wrote %s bytes", getClass().getName(), Integer.toString(aData.length))); + } + + /** + * Sends the data over the websocket to whatever is on the other side. + * + * **NOTE**: unlike with genuine streams, without flushing the data is + * never sent + * + * @see com.sun.star.connection.XConnection#flush + */ + public void flush() throws com.sun.star.io.IOException, + com.sun.star.uno.RuntimeException { + try { + _outputStream.flush(); + + Integer available = _outputStreamReader.available(); + + byte[] outputBytes = new byte[available + outgoingPrefix.length]; + System.arraycopy(outgoingPrefix, 0, outputBytes, 0, outgoingPrefix.length); + + _outputStreamReader.read(outputBytes, outgoingPrefix.length, available); + + send(outputBytes); + } catch(IOException ioException) { + com.sun.star.io.IOException unoIOException = new com.sun.star.io.IOException(ioException); + notifyListeners_error(unoIOException); + + throw unoIOException; + } + + if (DEBUG) + System.err.println(String.format("##### %s - flushed", getClass().getName())); + } + + /** + * Closes the connection. + * + * @see com.sun.star.connection.XConnection#close + */ + public void close() throws com.sun.star.uno.RuntimeException { + if (DEBUG) System.err.println("##### " + getClass().getName() + " - socket closed"); + } + + /** + * Gives a description of the connection. + * + * @return the description. + * @see com.sun.star.connection.XConnection#getDescription + */ + public String getDescription() throws com.sun.star.uno.RuntimeException { + return _description; + } + + @Override + public void onOpen(ServerHandshake handshakedata) { + notifyListeners_open(); + } + + @Override + public void onClose(int code, String reason, boolean remote) { + notifyListeners_close(); + } + + @Override + public void onMessage(String message) { + String[] messageParts = message.split(": ", 2); + if (messageParts.length != 2) + { + notifyListeners_error(new com.sun.star.uno.Exception(new ProtocolException(String.format("Recieved URP/WS message (%s) without a type specifier. Messages must be proceeded by 'urp: '", message)))); + return; + } + + String messageType = messageParts[0]; + + if (!messageType.equals("urp")) + { + if (DEBUG) System.err.println(String.format("##### %s - received %s message but that is not URP", getClass().getName(), messageType)); + return; + } + + byte[] messageBytes = messageParts[1].getBytes(); + + try { + _inputStreamWriter.write(messageBytes); + } catch (IOException e) { + notifyListeners_error(new com.sun.star.uno.Exception(e)); + return; + } + + if (DEBUG) System.err.println(String.format("##### %s - recieved %s chars", getClass().getName(), Integer.toString(messageBytes.length))); + } + + @Override + public void onMessage(ByteBuffer message) { + byte[] prefixedMessageBytes = message.array(); + + String messageType = ""; + boolean hasType = false; + int i; + for (i = 0; i < prefixedMessageBytes.length - 1; i++) { + if (prefixedMessageBytes[i] == ':' && (prefixedMessageBytes[i+1] == ' ' || prefixedMessageBytes[i+1] == '\n')) { + hasType = true; + break; // The type ends with ": ", so if we find this sequence we found the end of our type + } + messageType += (char)prefixedMessageBytes[i]; + } + + if(!hasType) { + notifyListeners_error(new com.sun.star.uno.Exception(new ProtocolException(String.format("Recieved URP/WS message (%s) without a type specifier. Binary messages must be proceeded by 'urp: ' or 'urp:\\n'", message)))); + return; + } + + int messageStartIndex = i + 2; + + if (!messageType.equals("urp")) { + if (DEBUG) System.err.println(String.format("##### %s - recieved %s binary message but that is not URP", getClass().getName(), messageType)); + return; + } + + byte[] messageBytes = Arrays.copyOfRange(prefixedMessageBytes, messageStartIndex, prefixedMessageBytes.length); + + try { + _inputStreamWriter.write(messageBytes); + } catch (IOException e) { + notifyListeners_error(new com.sun.star.uno.Exception(e)); + return; + } + + if (DEBUG) System.err.println(String.format("##### %s - recieved %s bytes", getClass().getName(), Integer.toString(prefixedMessageBytes.length))); + } + + @Override + public void onError(Exception ex) { + notifyListeners_error(new com.sun.star.uno.Exception(ex)); + } + +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/ridljar/com/sun/star/lib/connections/websocket/websocketConnector.java b/ridljar/com/sun/star/lib/connections/websocket/websocketConnector.java new file mode 100644 index 000000000000..a40bb0093c4d --- /dev/null +++ b/ridljar/com/sun/star/lib/connections/websocket/websocketConnector.java @@ -0,0 +1,137 @@ +/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +package com.sun.star.lib.connections.websocket; + +import com.sun.star.comp.loader.FactoryHelper; +import com.sun.star.connection.ConnectionSetupException; +import com.sun.star.connection.NoConnectException; +import com.sun.star.connection.XConnection; +import com.sun.star.connection.XConnector; +import com.sun.star.lang.XMultiServiceFactory; +import com.sun.star.lang.XSingleServiceFactory; +import com.sun.star.registry.XRegistryKey; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; +import java.net.URISyntaxException; +import java.net.UnknownHostException; + +/** + * A component that implements the XConnector interface. + * + *

The websocketConnector is a specialized component that uses + * websockets for communication. The websocketConnector is generally + * used by the com.sun.star.connection.Connector service.

+ * + * @see com.sun.star.connection.XAcceptor + * @see com.sun.star.connection.XConnection + * @see com.sun.star.connection.XConnector + * @see com.sun.star.comp.loader.JavaLoader + */ +public final class websocketConnector implements XConnector { + /** + * The name of the service. + * + *

The JavaLoader accesses this through reflection.

+ * + * @see com.sun.star.comp.loader.JavaLoader + */ + public static final String __serviceName + = "com.sun.star.connection.websocketConnector"; + + /** + * Returns a factory for creating the service. + * + *

This method is called by the JavaLoader.

+ * + * @param implName the name of the implementation for which a service is + * requested. + * @param multiFactory the service manager to be used (if needed). + * @param regKey the registry key. + * @return an XSingleServiceFactory for creating the component. + * + * @see com.sun.star.comp.loader.JavaLoader + */ + public static XSingleServiceFactory __getServiceFactory( + String implName, XMultiServiceFactory multiFactory, XRegistryKey regKey) + { + return implName.equals(websocketConnector.class.getName()) + ? FactoryHelper.getServiceFactory(websocketConnector.class, + __serviceName, multiFactory, + regKey) + : null; + } + + /** + * Connects via the described websocket to a waiting server. + * + *

The connection description has the following format: + * type*(key=value), + * where type should be websocket + * (ignoring case). Supported keys (ignoring case) currently are

+ *
+ *
url + *
The URL the websocket server is listening on, starting with + * either ws:// or wss:// + *
+ * + * @param connectionDescription the description of the connection. + * @return an XConnection to the server. + * + * @see com.sun.star.connection.XAcceptor + * @see com.sun.star.connection.XConnection + */ + public synchronized XConnection connect(String connectionDescription) + throws NoConnectException, ConnectionSetupException + { + if (connected) + throw new ConnectionSetupException("Already connected to the socket"); + + ConnectionDescriptor desc; + try { + desc = new ConnectionDescriptor(connectionDescription); + } catch (com.sun.star.lang.IllegalArgumentException e) { + throw new ConnectionSetupException(e); + } + + WebsocketConnection websocket = null; + try { + websocket = new WebsocketConnection(connectionDescription, desc); + connected = websocket.isOpen(); + } catch (IOException e) { + throw new ConnectionSetupException(e); + } catch (URISyntaxException e) { + throw new ConnectionSetupException(e); + } catch (InterruptedException e) { + throw new ConnectionSetupException(e); + } + + if (websocket == null || !connected) + throw new ConnectionSetupException("Could not connect to the server. Is it up?"); + + return websocket; + } + + private boolean connected = false; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/ridljar/source/libreoffice/module-info.java b/ridljar/source/libreoffice/module-info.java index 8d24c7ccb13c..f913597600db 100644 --- a/ridljar/source/libreoffice/module-info.java +++ b/ridljar/source/libreoffice/module-info.java @@ -64,6 +64,7 @@ module org.libreoffice.uno exports com.sun.star.ldap; exports com.sun.star.lib.connections.pipe; exports com.sun.star.lib.connections.socket; + exports com.sun.star.lib.connections.websocket; exports com.sun.star.lib.uno; exports com.sun.star.lib.uno.adapter; exports com.sun.star.lib.uno.bridges.java_remote; diff --git a/ridljar/util/manifest b/ridljar/util/manifest index bb1209f90a22..44eb59ff784b 100644 --- a/ridljar/util/manifest +++ b/ridljar/util/manifest @@ -24,6 +24,9 @@ Sealed: true Name: com/sun/star/lib/connections/socket/ Sealed: true +Name: com/sun/star/lib/connections/websocket/ +Sealed: true + Name: com/sun/star/lib/uno/ Sealed: true -- cgit