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; 019 020import static io.undertow.Handlers.path; 021import static io.undertow.Handlers.websocket; 022 023import javax.net.ssl.SSLContext; 024 025import java.io.File; 026import java.security.KeyPair; 027import java.security.KeyStore; 028import java.security.cert.Certificate; 029import java.security.cert.X509Certificate; 030import java.util.ArrayList; 031import java.util.Collections; 032import java.util.List; 033import java.util.Map; 034import java.util.concurrent.TimeUnit; 035import java.util.logging.Level; 036import java.util.logging.Logger; 037 038import org.bouncycastle.asn1.x500.RDN; 039import org.bouncycastle.asn1.x500.X500Name; 040import org.bouncycastle.asn1.x500.style.BCStyle; 041import org.bouncycastle.asn1.x500.style.IETFUtils; 042import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder; 043import org.syncany.config.LocalEventBus; 044import org.syncany.config.UserConfig; 045import org.syncany.config.to.DaemonConfigTO; 046import org.syncany.config.to.UserTO; 047import org.syncany.config.to.WebServerTO; 048import org.syncany.crypto.CipherParams; 049import org.syncany.crypto.CipherUtil; 050import org.syncany.operations.daemon.handlers.InternalRestHandler; 051import org.syncany.operations.daemon.handlers.InternalWebInterfaceHandler; 052import org.syncany.operations.daemon.handlers.InternalWebSocketHandler; 053import org.syncany.operations.daemon.messages.GetFileFolderResponse; 054import org.syncany.operations.daemon.messages.GetFileFolderResponseInternal; 055import org.syncany.operations.daemon.messages.api.ExternalEvent; 056import org.syncany.operations.daemon.messages.api.JsonMessageFactory; 057import org.syncany.operations.daemon.messages.api.Message; 058import org.syncany.operations.daemon.messages.api.Response; 059import org.syncany.operations.daemon.messages.api.XmlMessageFactory; 060import org.syncany.plugins.web.WebInterfacePlugin; 061 062import com.google.common.cache.Cache; 063import com.google.common.cache.CacheBuilder; 064import com.google.common.collect.Maps; 065import com.google.common.eventbus.Subscribe; 066 067import io.undertow.Undertow; 068import io.undertow.security.api.AuthenticationMechanism; 069import io.undertow.security.api.AuthenticationMode; 070import io.undertow.security.handlers.AuthenticationCallHandler; 071import io.undertow.security.handlers.AuthenticationConstraintHandler; 072import io.undertow.security.handlers.AuthenticationMechanismsHandler; 073import io.undertow.security.handlers.SecurityInitialHandler; 074import io.undertow.security.idm.IdentityManager; 075import io.undertow.security.impl.BasicAuthenticationMechanism; 076import io.undertow.server.HttpHandler; 077import io.undertow.server.HttpServerExchange; 078import io.undertow.websockets.core.WebSocketChannel; 079import io.undertow.websockets.core.WebSockets; 080 081/** 082 * The web server provides a HTTP/REST and WebSocket API to thin clients, 083 * as well as a mechanism to run a web interface by implementing a 084 * {@link WebInterfacePlugin}. 085 * 086 * @author Philipp C. Heckel (philipp.heckel@gmail.com) 087 */ 088public class WebServer { 089 public static final String API_ENDPOINT_WS_XML = "/api/ws/xml"; 090 public static final String API_ENDPOINT_WS_JSON = "/api/ws/json"; 091 public static final String API_ENDPOINT_REST_XML = "/api/rs/xml"; 092 public static final String API_ENDPOINT_REST_JSON = "/api/rs/json"; 093 094 public enum RequestFormatType { 095 XML, JSON 096 } 097 098 private static final Logger logger = Logger.getLogger(WebServer.class.getSimpleName()); 099 private static final RequestFormatType DEFAULT_RESPONSE_FORMAT = RequestFormatType.XML; 100 101 private Undertow webServer; 102 private LocalEventBus eventBus; 103 104 private Cache<Integer, WebSocketChannel> requestIdWebSocketCache; 105 private Cache<Integer, HttpServerExchange> requestIdRestSocketCache; 106 private Cache<Integer, RequestFormatType> requestIdRestFormatCache; 107 private Cache<String, File> fileTokenTempFileCache; 108 109 private Map<WebSocketChannel, RequestFormatType> webSocketChannelRequestFormatMap; 110 111 public WebServer(DaemonConfigTO daemonConfig) throws Exception { 112 this.webSocketChannelRequestFormatMap = Maps.newConcurrentMap(); 113 114 initCaches(); 115 initEventBus(); 116 initServer(daemonConfig); 117 } 118 119 public void start() throws ServiceAlreadyStartedException { 120 webServer.start(); 121 } 122 123 public void stop() { 124 try { 125 logger.log(Level.INFO, "Shutting down websocket server."); 126 webServer.stop(); 127 } 128 catch (Exception e) { 129 logger.log(Level.SEVERE, "Could not stop websocket server.", e); 130 } 131 } 132 133 private void initCaches() { 134 requestIdWebSocketCache = CacheBuilder.newBuilder().maximumSize(10000) 135 .concurrencyLevel(2).expireAfterAccess(1, TimeUnit.MINUTES).build(); 136 137 requestIdRestSocketCache = CacheBuilder.newBuilder().maximumSize(10000) 138 .concurrencyLevel(2).expireAfterAccess(1, TimeUnit.MINUTES).build(); 139 140 fileTokenTempFileCache = CacheBuilder.newBuilder().maximumSize(10000) 141 .concurrencyLevel(2).expireAfterAccess(1, TimeUnit.MINUTES).build(); 142 143 requestIdRestFormatCache = CacheBuilder.newBuilder().maximumSize(10000) 144 .concurrencyLevel(2).expireAfterAccess(1, TimeUnit.MINUTES).build(); 145 } 146 147 private void initEventBus() { 148 eventBus = LocalEventBus.getInstance(); 149 eventBus.register(this); 150 } 151 152 private void initServer(DaemonConfigTO daemonConfigTO) throws Exception { 153 WebServerTO webServerConfig = daemonConfigTO.getWebServer(); 154 155 // Bind address and port 156 String bindAddress = webServerConfig.getBindAddress(); 157 int bindPort = webServerConfig.getBindPort(); 158 159 // Users (incl. CLI user!) 160 List<UserTO> users = readWebServerUsers(daemonConfigTO); 161 IdentityManager identityManager = new MapIdentityManager(users); 162 163 // (Re-)generate keypair/certificate (if requested) 164 boolean certificateAutoGenerate = webServerConfig.isCertificateAutoGenerate(); 165 String certificateCommonName = webServerConfig.getCertificateCommonName(); 166 167 if (certificateAutoGenerate && certificateCommonNameChanged(certificateCommonName)) { 168 generateNewKeyPairAndCertificate(certificateCommonName); 169 } 170 171 // Set up the handlers for WebSocket, REST and the web interface 172 HttpHandler pathHttpHandler = path() 173 .addPrefixPath(API_ENDPOINT_WS_XML, websocket(new InternalWebSocketHandler(this, certificateCommonName, RequestFormatType.XML))) 174 .addPrefixPath(API_ENDPOINT_WS_JSON, websocket(new InternalWebSocketHandler(this, certificateCommonName, RequestFormatType.JSON))) 175 .addPrefixPath(API_ENDPOINT_REST_XML, new InternalRestHandler(this, RequestFormatType.XML)) 176 .addPrefixPath(API_ENDPOINT_REST_JSON, new InternalRestHandler(this, RequestFormatType.JSON)) 177 .addPrefixPath("/", new InternalWebInterfaceHandler()); 178 179 // Add some security spices 180 HttpHandler securityPathHttpHandler = addSecurity(pathHttpHandler, identityManager); 181 SSLContext sslContext = UserConfig.createUserSSLContext(); 182 183 // And go for it! 184 webServer = Undertow 185 .builder() 186 .addHttpsListener(bindPort, bindAddress, sslContext) 187 .setHandler(securityPathHttpHandler) 188 .build(); 189 190 logger.log(Level.INFO, "Initialized web server."); 191 } 192 193 private List<UserTO> readWebServerUsers(DaemonConfigTO daemonConfigTO) { 194 List<UserTO> users = daemonConfigTO.getUsers(); 195 196 if (users == null) { 197 users = new ArrayList<UserTO>(); 198 } 199 200 // Add CLI credentials 201 if (daemonConfigTO.getPortTO() != null) { 202 users.add(daemonConfigTO.getPortTO().getUser()); 203 } 204 205 return users; 206 } 207 208 private boolean certificateCommonNameChanged(String certificateCommonName) { 209 try { 210 KeyStore userKeyStore = UserConfig.getUserKeyStore(); 211 X509Certificate currentCertificate = (X509Certificate) userKeyStore.getCertificate(CipherParams.CERTIFICATE_IDENTIFIER); 212 213 if (currentCertificate != null) { 214 X500Name currentCertificateSubject = new JcaX509CertificateHolder(currentCertificate).getSubject(); 215 RDN currentCertificateSubjectCN = currentCertificateSubject.getRDNs(BCStyle.CN)[0]; 216 217 String currentCertificateSubjectCnStr = IETFUtils.valueToString(currentCertificateSubjectCN.getFirst().getValue()); 218 219 if (!certificateCommonName.equals(currentCertificateSubjectCnStr)) { 220 logger.log(Level.INFO, "- Certificate regeneration necessary: Cert common name in daemon config changed from " + currentCertificateSubjectCnStr + " to " + certificateCommonName + "."); 221 return true; 222 } 223 } 224 else { 225 logger.log(Level.INFO, "- Certificate regeneration necessary, because no certificate found in key store."); 226 return true; 227 } 228 229 return false; 230 } 231 catch (Exception e) { 232 throw new RuntimeException("Cannot (re-)generate server certificate for hostname: " + certificateCommonName, e); 233 } 234 } 235 236 public static void generateNewKeyPairAndCertificate(String certificateCommonName) { 237 try { 238 logger.log(Level.INFO, "(Re-)generating keypair and certificate for hostname " + certificateCommonName + " ..."); 239 240 // Generate key pair and certificate 241 KeyPair keyPair = CipherUtil.generateRsaKeyPair(); 242 X509Certificate certificate = CipherUtil.generateSelfSignedCertificate(certificateCommonName, keyPair); 243 244 // Add key and certificate to key store 245 UserConfig.getUserKeyStore().setKeyEntry(CipherParams.CERTIFICATE_IDENTIFIER, keyPair.getPrivate(), new char[0], new Certificate[]{certificate}); 246 UserConfig.storeUserKeyStore(); 247 248 // Add certificate to trust store (for CLI->API connection) 249 UserConfig.getUserTrustStore().setCertificateEntry(CipherParams.CERTIFICATE_IDENTIFIER, certificate); 250 UserConfig.storeTrustStore(); 251 } 252 catch (Exception e) { 253 throw new RuntimeException("Unable to read key store or generate self-signed certificate.", e); 254 } 255 } 256 257 private static HttpHandler addSecurity(final HttpHandler toWrap, IdentityManager identityManager) { 258 List<AuthenticationMechanism> mechanisms = 259 Collections.<AuthenticationMechanism>singletonList(new BasicAuthenticationMechanism("Syncany")); 260 261 HttpHandler handler = toWrap; 262 263 handler = new AuthenticationCallHandler(handler); 264 handler = new AuthenticationConstraintHandler(handler); 265 handler = new AuthenticationMechanismsHandler(handler, mechanisms); 266 handler = new SecurityInitialHandler(AuthenticationMode.PRO_ACTIVE, identityManager, handler); 267 268 return handler; 269 } 270 271 @Subscribe 272 public void onGetFileResponseInternal(GetFileFolderResponseInternal fileResponseInternal) { 273 File tempFile = fileResponseInternal.getTempFile(); 274 GetFileFolderResponse fileResponse = fileResponseInternal.getFileResponse(); 275 276 fileTokenTempFileCache.asMap().put(fileResponse.getTempToken(), tempFile); 277 eventBus.post(fileResponse); 278 } 279 280 @Subscribe 281 public void onEvent(ExternalEvent event) { 282 try { 283 sendBroadcast(event); 284 } 285 catch (Exception e) { 286 logger.log(Level.SEVERE, "Cannot send event.", e); 287 } 288 } 289 290 @Subscribe 291 public void onResponse(Response response) { 292 try { 293 // Send to one or many receivers 294 boolean responseWithoutRequest = response.getRequestId() == null || response.getRequestId() <= 0; 295 296 if (responseWithoutRequest) { 297 sendBroadcast(response); 298 } 299 else { 300 HttpServerExchange responseToHttpServerExchange = requestIdRestSocketCache.asMap().get(response.getRequestId()); 301 WebSocketChannel responseToWebSocketChannel = requestIdWebSocketCache.asMap().get(response.getRequestId()); 302 303 if (responseToHttpServerExchange != null) { 304 sendTo(responseToHttpServerExchange, response); 305 } 306 else if (responseToWebSocketChannel != null) { 307 sendTo(responseToWebSocketChannel, response); 308 } 309 else { 310 logger.log(Level.WARNING, "Cannot send message, because request ID in response is unknown or timed out." + response); 311 } 312 } 313 } 314 catch (Exception e) { 315 logger.log(Level.SEVERE, "Cannot send response.", e); 316 } 317 } 318 319 private void sendBroadcast(Message message) throws Exception { 320 logger.log(Level.INFO, "Sending broadcast message to " + webSocketChannelRequestFormatMap.size() + " websocket client(s)"); 321 322 synchronized (webSocketChannelRequestFormatMap) { 323 for (WebSocketChannel clientChannel : webSocketChannelRequestFormatMap.keySet()) { 324 sendTo(clientChannel, message); 325 } 326 } 327 } 328 329 private void sendTo(WebSocketChannel clientChannel, Message message) throws Exception { 330 String messageStr = createMessageStr(clientChannel, message); 331 332 logger.log(Level.INFO, "Sending message to " + clientChannel + ": " + messageStr); 333 WebSockets.sendText(messageStr, clientChannel, null); 334 } 335 336 private void sendTo(HttpServerExchange serverExchange, Response response) throws Exception { 337 String responseStr = createMessageStr(response); 338 339 logger.log(Level.INFO, "Sending message to " + serverExchange.getHostAndPort() + ": " + responseStr); 340 341 serverExchange.getResponseSender().send(responseStr); 342 serverExchange.endExchange(); 343 } 344 345 private String createMessageStr(WebSocketChannel channel, Message message) throws Exception { 346 RequestFormatType requestFormatType = webSocketChannelRequestFormatMap.get(channel); 347 return createMessageStr(message, requestFormatType); 348 } 349 350 private String createMessageStr(Response response) throws Exception { 351 RequestFormatType requestFormatType = requestIdRestFormatCache.getIfPresent(response.getRequestId()); 352 return createMessageStr(response, requestFormatType); 353 } 354 355 private String createMessageStr(Message message, RequestFormatType outputFormat) throws Exception { 356 if (outputFormat == null) { 357 outputFormat = DEFAULT_RESPONSE_FORMAT; 358 } 359 360 switch (outputFormat) { 361 case JSON: 362 return JsonMessageFactory.toJson(message); 363 364 case XML: 365 default: 366 return XmlMessageFactory.toXml(message); 367 } 368 } 369 370 // Client channel access methods 371 372 public void addClientChannel(WebSocketChannel clientChannel, RequestFormatType format) { 373 webSocketChannelRequestFormatMap.put(clientChannel, format); 374 } 375 376 public void removeClientChannel(WebSocketChannel clientChannel) { 377 webSocketChannelRequestFormatMap.remove(clientChannel); 378 } 379 380 // Cache access methods 381 382 public void putCacheRestRequest(int id, HttpServerExchange exchange) { 383 synchronized (requestIdRestSocketCache) { 384 requestIdRestSocketCache.put(id, exchange); 385 } 386 } 387 388 public void putCacheWebSocketRequest(int id, WebSocketChannel clientSocket) { 389 synchronized (requestIdWebSocketCache) { 390 requestIdWebSocketCache.put(id, clientSocket); 391 } 392 } 393 394 public void putRequestFormatType(int id, RequestFormatType requestFormatType) { 395 synchronized (requestIdRestFormatCache) { 396 requestIdRestFormatCache.put(id, requestFormatType); 397 } 398 } 399 400 public File getFileTokenTempFileFromCache(String fileToken) { 401 return fileTokenTempFileCache.asMap().get(fileToken); 402 } 403}