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}