/* * Syncany, www.syncany.org * Copyright (C) 2011-2015 Philipp C. Heckel <philipp.heckel@gmail.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.syncany.operations.gui; import io.undertow.websockets.WebSocketExtension; import io.undertow.websockets.client.WebSocketClient; import io.undertow.websockets.client.WebSocketClientNegotiation; import io.undertow.websockets.core.AbstractReceiveListener; import io.undertow.websockets.core.BufferedTextMessage; import io.undertow.websockets.core.WebSocketCallback; import io.undertow.websockets.core.WebSocketChannel; import io.undertow.websockets.core.WebSocketVersion; import io.undertow.websockets.core.WebSockets; import java.io.File; import java.io.IOException; import java.net.URI; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.LinkedList; import java.util.Map; import java.util.Queue; import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; import java.util.logging.Logger; import javax.net.ssl.SSLContext; import org.apache.commons.codec.binary.Base64; import org.syncany.config.DaemonConfigHelper; import org.syncany.config.GuiEventBus; import org.syncany.config.UserConfig; import org.syncany.config.to.DaemonConfigTO; import org.syncany.config.to.UserTO; import org.syncany.operations.daemon.WebServer; import org.syncany.operations.daemon.messages.ListWatchesManagementRequest; import org.syncany.operations.daemon.messages.api.ExternalEventResponse; import org.syncany.operations.daemon.messages.api.Message; import org.syncany.operations.daemon.messages.api.Request; import org.syncany.operations.daemon.messages.api.XmlMessageFactory; import org.syncany.util.StringUtil; import org.xnio.BufferAllocator; import org.xnio.ByteBufferSlicePool; import org.xnio.OptionMap; import org.xnio.Options; import org.xnio.Pool; import org.xnio.Xnio; import org.xnio.XnioWorker; import org.xnio.ssl.JsseXnioSsl; import org.xnio.ssl.XnioSsl; import com.google.common.eventbus.Subscribe; /** * @author Vincent Wiencek <vwiencek@gmail.com> * @author Philipp C. Heckel <philipp.heckel@gmail.com> */ public class GuiWebSocketClient { private static final Logger logger = Logger.getLogger(GuiWebSocketClient.class.getSimpleName()); private final static String PROTOCOL = "wss://"; private final static String ENDPOINT = WebServer.API_ENDPOINT_WS_XML; private GuiEventBus eventBus; private WebSocketChannel webSocketChannel; private Thread clientThread; private AtomicBoolean clientThreadRunning; private Queue<Message> failedOutgoingMessages; public GuiWebSocketClient() { this.eventBus = GuiEventBus.getInstance(); this.eventBus.register(this); this.failedOutgoingMessages = new LinkedList<>(); initClientThread(); } private void initClientThread() { clientThread = new Thread(new Runnable() { @Override public void run() { while (clientThreadRunning.get()) { try { connectAndWait(); } catch (InterruptedException e) { logger.log(Level.INFO, "Web socket interrupted. EXITING websocket client thread.", e); clientThreadRunning.set(false); } catch (Exception e) { logger.log(Level.WARNING, "Web socket connect failure. Waiting, then retrying ...", e); try { Thread.sleep(5000); } catch (InterruptedException e1) { logger.log(Level.INFO, "Web socket interrupted. EXITING websocket client thread.", e1); clientThreadRunning.set(false); } } } } }, "GuiWsClient"); } public void start() { clientThreadRunning = new AtomicBoolean(true); clientThread.start(); } public void stop() { clientThreadRunning.set(false); clientThread.interrupt(); } public void connectAndWait() throws Exception { logger.log(Level.INFO, "Trying to connect to websocket server ..."); DaemonConfigTO daemonConfig = loadDaemonConfig(); UserTO firstDaemonUser = loadFirstDaemonUser(daemonConfig); connect(daemonConfig, firstDaemonUser); sendFailedOutgoingMessages(); sendListWatchesRequest(); while (clientThreadRunning.get()) { Thread.sleep(500); } } private void sendFailedOutgoingMessages() { while (failedOutgoingMessages.size() > 0) { postMessage(failedOutgoingMessages.poll(), false); } } private void sendListWatchesRequest() { onRequest(new ListWatchesManagementRequest()); } private DaemonConfigTO loadDaemonConfig() throws Exception { File daemonConfigFile = new File(UserConfig.getUserConfigDir(), UserConfig.DAEMON_FILE); if (!daemonConfigFile.exists()) { throw new Exception("Daemon configuration does not exist at " + daemonConfigFile); } return DaemonConfigTO.load(daemonConfigFile); } private UserTO loadFirstDaemonUser(DaemonConfigTO daemonConfig) throws Exception { UserTO firstDaemonUser = DaemonConfigHelper.getFirstDaemonUser(daemonConfig); if (firstDaemonUser == null) { throw new Exception("Daemon configuration does not contain any users."); } return firstDaemonUser; } private void connect(final DaemonConfigTO daemonConfig, final UserTO daemonUser) throws Exception { logger.log(Level.INFO, "Starting GUI websocket client with user " + daemonUser.getUsername() + " at " + daemonConfig.getWebServer().getBindAddress() + " ..."); SSLContext sslContext = UserConfig.createUserSSLContext(); Xnio xnio = Xnio.getInstance(this.getClass().getClassLoader()); Pool<ByteBuffer> buffer = new ByteBufferSlicePool(BufferAllocator.BYTE_BUFFER_ALLOCATOR, 1024, 1024); OptionMap workerOptions = OptionMap.builder() .set(Options.WORKER_IO_THREADS, 2) .set(Options.WORKER_TASK_CORE_THREADS, 30) .set(Options.WORKER_TASK_MAX_THREADS, 30) .set(Options.SSL_PROTOCOL, sslContext.getProtocol()) .set(Options.SSL_PROVIDER, sslContext.getProvider().getName()) .set(Options.TCP_NODELAY, true) .set(Options.CORK, true) .getMap(); XnioWorker worker = xnio.createWorker(workerOptions); XnioSsl xnioSsl = new JsseXnioSsl(xnio, OptionMap.create(Options.USE_DIRECT_BUFFERS, true), sslContext); URI uri = new URI(PROTOCOL + daemonConfig.getWebServer().getBindAddress() + ":" + daemonConfig.getWebServer().getBindPort() + ENDPOINT); WebSocketClientNegotiation clientNegotiation = new WebSocketClientNegotiation(new ArrayList<String>(), new ArrayList<WebSocketExtension>()) { @Override public void beforeRequest(Map<String, String> headers) { String basicAuthPlainUserPass = daemonUser.getUsername() + ":" + daemonUser.getPassword(); String basicAuthEncodedUserPass = Base64.encodeBase64String(StringUtil.toBytesUTF8(basicAuthPlainUserPass)); headers.put("Authorization", "Basic " + basicAuthEncodedUserPass); } }; webSocketChannel = WebSocketClient.connect(worker, xnioSsl, buffer, workerOptions, uri, WebSocketVersion.V13, clientNegotiation).get(); webSocketChannel.getReceiveSetter().set(new AbstractReceiveListener() { @Override protected void onFullTextMessage(WebSocketChannel channel, BufferedTextMessage textMessage) throws IOException { String messageStr = textMessage.getData(); Message message; try { logger.log(Level.FINEST, "GUI received message: " + messageStr); message = XmlMessageFactory.toMessage(messageStr); eventBus.post(message); } catch (Exception e) { logger.log(Level.WARNING, "Unable to parse message: " + e); } } @Override protected void onError(WebSocketChannel channel, Throwable error) { logger.log(Level.WARNING, "Error: " + error.getMessage()); waitAndReconnect(); } }); webSocketChannel.resumeReceives(); } protected void waitAndReconnect() { try { logger.log(Level.WARNING, "Web socket cannot connect. Waiting, then retrying ..."); Thread.sleep(2000); connectAndWait(); } catch (Exception e) { logger.log(Level.WARNING, "Unable to reconnect to daemon", e); } } @Subscribe public void onRequest(Request request) { postMessage(request, true); } @Subscribe public void onEventResponse(ExternalEventResponse eventResponse) { postMessage(eventResponse, true); } private void postMessage(final Message message, final boolean retryOnFailure) { // Parse message String messageStr = null; try { messageStr = XmlMessageFactory.toXml(message); } catch (Exception e) { logger.log(Level.WARNING, "Unable to transform message to XML. Throwing message away!", e); return; } // Send to websocket if (webSocketChannel != null) { logger.log(Level.FINEST, "Sending WS message to daemon: " + messageStr); WebSockets.sendText(messageStr, webSocketChannel, new WebSocketCallback<Void>() { @Override public void onError(WebSocketChannel channel, Void context, Throwable throwable) { logger.log(Level.SEVERE, "WS Error", throwable); if (retryOnFailure) { failedOutgoingMessages.add(message); } } @Override public void complete(WebSocketChannel channel, Void context) { // Nothing. } }); } else { logger.log(Level.WARNING, "Failed to send WS message to daemon. Not (yet) connected; " + messageStr); if (retryOnFailure) { failedOutgoingMessages.add(message); } } } }