001/*
002 * Syncany, www.syncany.org
003 * Copyright (C) 2011-2016 Philipp C. Heckel <philipp.heckel@gmail.com>
004 *
005 * This program is free software: you can redistribute it and/or modify
006 * it under the terms of the GNU General Public License as published by
007 * the Free Software Foundation, either version 3 of the License, or
008 * (at your option) any later version.
009 *
010 * This program is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
013 * GNU General Public License for more details.
014 *
015 * You should have received a copy of the GNU General Public License
016 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
017 */
018package org.syncany.operations.daemon.handlers;
019
020import java.io.IOException;
021import java.net.InetAddress;
022import java.net.UnknownHostException;
023import java.util.logging.Level;
024import java.util.logging.Logger;
025import java.util.regex.Pattern;
026
027import org.syncany.config.LocalEventBus;
028import org.syncany.operations.daemon.WebServer;
029import org.syncany.operations.daemon.WebServer.RequestFormatType;
030import org.syncany.operations.daemon.messages.BadRequestResponse;
031import org.syncany.operations.daemon.messages.api.JsonMessageFactory;
032import org.syncany.operations.daemon.messages.api.EventResponse;
033import org.syncany.operations.daemon.messages.api.Message;
034import org.syncany.operations.daemon.messages.api.Request;
035import org.syncany.operations.daemon.messages.api.XmlMessageFactory;
036
037import com.google.common.base.Joiner;
038
039import io.undertow.websockets.WebSocketConnectionCallback;
040import io.undertow.websockets.core.AbstractReceiveListener;
041import io.undertow.websockets.core.BufferedTextMessage;
042import io.undertow.websockets.core.StreamSourceFrameChannel;
043import io.undertow.websockets.core.WebSocketChannel;
044import io.undertow.websockets.spi.WebSocketHttpExchange;
045
046/**
047 * InternalWebSocketHandler handles the web socket requests
048 * sent to the daemon.
049 *
050 * @author Philipp C. Heckel (philipp.heckel@gmail.com)
051 */
052public class InternalWebSocketHandler implements WebSocketConnectionCallback {
053        private static final Logger logger = Logger.getLogger(InternalWebSocketHandler.class.getSimpleName());
054        private static final Pattern WEBSOCKET_ALLOWED_ORIGIN_HEADER = Pattern.compile("^(https?|wss?)://(localhost|127\\.\\d+\\.\\d+\\.\\d+):\\d+$");
055
056        private final WebServer daemonWebServer;
057        private final LocalEventBus eventBus;
058        private final String certificateCommonName;
059        private final RequestFormatType requestFormatType;
060
061        public InternalWebSocketHandler(WebServer daemonWebServer, String certificateCommonName, RequestFormatType requestFormatType) {
062                this.daemonWebServer = daemonWebServer;
063                this.eventBus = LocalEventBus.getInstance();
064                this.certificateCommonName = certificateCommonName;
065                this.requestFormatType = requestFormatType;
066
067                this.eventBus.register(this);
068        }
069
070        @Override
071        public void onConnect(WebSocketHttpExchange exchange, WebSocketChannel channel) {
072                logger.log(Level.INFO, "Connecting to websocket server.");
073
074                // Validate origin header (security!)
075                String originHeader = exchange.getRequestHeader("Origin");
076
077                if (!allowedOriginHeader(originHeader)) {
078                        logger.log(Level.INFO, channel.toString() + " disconnected due to invalid origin header: " + originHeader);
079                        exchange.close();
080                }
081                else {
082                        logger.log(Level.INFO, "Valid origin header, setting up connection.");
083
084                        channel.getReceiveSetter().set(new AbstractReceiveListener() {
085                                @Override
086                                protected void onFullTextMessage(WebSocketChannel clientChannel, BufferedTextMessage message) {
087                                        handleMessage(clientChannel, message.getData());
088                                }
089
090                                @Override
091                                protected void onError(WebSocketChannel webSocketChannel, Throwable error) {
092                                        logger.log(Level.INFO, "Server error : " + error.toString());
093                                }
094
095                                @Override
096                                protected void onClose(WebSocketChannel clientChannel, StreamSourceFrameChannel streamSourceChannel) throws IOException {
097                                        logger.log(Level.INFO, clientChannel.toString() + " disconnected");
098                                        daemonWebServer.removeClientChannel(clientChannel);
099                                }
100                        });
101
102                        logger.log(Level.INFO, "A new client (" + channel.hashCode() + ") connected using format " + requestFormatType);
103                        daemonWebServer.addClientChannel(channel, requestFormatType);
104
105                        channel.resumeReceives();
106                }
107        }
108
109        private boolean allowedOriginHeader(String originHeader) {
110                // Allow all non-browser clients (no "Origin:" header!)
111                if (originHeader == null) {
112                        return true;
113                }
114
115                // Allow localhost's hostname
116                try {
117                        if (originHeader.equals(InetAddress.getLocalHost().getHostName())) {
118                                return true;
119                        }
120                }
121                catch (UnknownHostException e) {
122                        logger.log(Level.FINE, "Could not get the localhost ip", e);
123                }
124
125                // Allow by whitelist
126                if (WEBSOCKET_ALLOWED_ORIGIN_HEADER.matcher(originHeader).matches()) {
127                        return true;
128                }
129
130                // Additional allowed origin header (certificate CN)
131                if (certificateCommonName != null && originHeader.startsWith("https://" + certificateCommonName + ":")) {
132                        return true;
133                }
134
135                // Otherwise, we fail
136                return false;
137        }
138
139        private void handleMessage(WebSocketChannel clientSocket, String messageStr) {
140                logger.log(Level.INFO, "Web socket message received: " + messageStr);
141
142                try {
143                        Message message;
144                        
145                        switch (requestFormatType) {
146                                case JSON:
147                                        message = JsonMessageFactory.toMessage(messageStr);
148                                        break;
149
150                                case XML:
151                                        message = XmlMessageFactory.toMessage(messageStr);
152                                        break;
153
154                                default:
155                                        throw new Exception("Unknown request format. Valid formats are " + Joiner.on(", ").join(RequestFormatType.values()));
156                        }
157
158                        if (message instanceof Request) {
159                                handleRequest(clientSocket, (Request) message);
160                        }
161                        else if (message instanceof EventResponse) {
162                                handleEventResponse(clientSocket, (EventResponse) message);
163                        }
164                        else {
165                                throw new Exception("Invalid message type received: " + message.getClass());
166                        }
167                }
168                catch (Exception e) {
169                        logger.log(Level.WARNING, "Invalid message received; cannot serialize to Request.", e);
170                        eventBus.post(new BadRequestResponse(-1, "Invalid message."));
171                }
172        }
173
174
175        private void handleRequest(WebSocketChannel clientSocket, Request request) {
176                daemonWebServer.putRequestFormatType(request.getId(), requestFormatType);
177                daemonWebServer.putCacheWebSocketRequest(request.getId(), clientSocket);
178                
179                eventBus.post(request);
180        }
181
182        private void handleEventResponse(WebSocketChannel clientSocket, EventResponse eventResponse) {
183                eventBus.post(eventResponse);
184        }
185}