
org.structr.websocket.StructrWebSocket Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of structr-ui Show documentation
Show all versions of structr-ui Show documentation
Structr is an open source framework based on the popular Neo4j graph database.
The newest version!
/**
* Copyright (C) 2010-2016 Structr GmbH
*
* This file is part of Structr .
*
* Structr is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* Structr 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Structr. If not, see .
*/
package org.structr.websocket;
import com.google.gson.Gson;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.http.HttpServletRequest;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.WebSocketListener;
import org.structr.common.AccessMode;
import org.structr.common.SecurityContext;
import org.structr.common.error.FrameworkException;
import org.structr.core.GraphObject;
import org.structr.core.Services;
import org.structr.core.app.App;
import org.structr.core.app.StructrApp;
import org.structr.core.auth.Authenticator;
import org.structr.core.entity.Principal;
import org.structr.core.graph.Tx;
import org.structr.dynamic.File;
import org.structr.rest.auth.AuthHelper;
import org.structr.rest.auth.SessionHelper;
import org.structr.web.entity.FileBase;
import org.structr.web.entity.User;
import org.structr.websocket.command.AbstractCommand;
import org.structr.websocket.command.FileUploadHandler;
import org.structr.websocket.command.LoginCommand;
import org.structr.websocket.message.MessageBuilder;
import org.structr.websocket.message.WebSocketMessage;
//~--- classes ----------------------------------------------------------------
/**
*
*
*
*/
public class StructrWebSocket implements WebSocketListener {
private static final Logger logger = Logger.getLogger(StructrWebSocket.class.getName());
private static final Map commandSet = new LinkedHashMap<>();
//~--- fields ---------------------------------------------------------
private Session session = null;
private Gson gson = null;
private HttpServletRequest request = null;
private SecurityContext securityContext = null;
private WebsocketController syncController = null;
private Map uploads = null;
private Authenticator authenticator = null;
private String pagePath = null;
//~--- constructors ---------------------------------------------------
public StructrWebSocket() {}
public StructrWebSocket(final WebsocketController syncController, final Gson gson, final Authenticator authenticator) {
this.uploads = new LinkedHashMap<>();
this.syncController = syncController;
this.gson = gson;
this.authenticator = authenticator;
}
//~--- methods --------------------------------------------------------
public void setRequest(final HttpServletRequest request) {
this.request = request;
}
@Override
public void onWebSocketConnect(final Session session) {
logger.log(Level.FINE, "New connection with protocol {0}", session.getProtocolVersion());
this.session = session;
syncController.registerClient(this);
pagePath = request.getQueryString();
}
@Override
public void onWebSocketClose(final int closeCode, final String message) {
logger.log(Level.FINE, "Connection closed with closeCode {0} and message {1}", new Object[]{closeCode, message});
final App app = StructrApp.getInstance(securityContext);
try (final Tx tx = app.tx()) {
this.session = null;
syncController.unregisterClient(this);
// flush and close open uploads
for (FileUploadHandler upload : uploads.values()) {
upload.finish();
}
tx.success();
uploads.clear();
} catch (FrameworkException fex) {
logger.log(Level.SEVERE, "Error while closing connection", fex);
}
}
@Override
public void onWebSocketText(final String data) {
if (data == null) {
logger.log(Level.WARNING, "Empty text message received.");
return;
}
logger.log(Level.FINE, "############################################################ RECEIVED \n{0}", data.substring(0, Math.min(data.length(), 1000)));
// parse web socket data from JSON
final WebSocketMessage webSocketData = gson.fromJson(data, WebSocketMessage.class);
final App app = StructrApp.getInstance(securityContext);
final String command = webSocketData.getCommand();
final Class type = commandSet.get(command);
final String sessionIdFromMessage = webSocketData.getSessionId();
if (type != null) {
try (final Tx tx = app.tx()) {
if (sessionIdFromMessage != null) {
// try to authenticated this connection by sessionId
authenticate(sessionIdFromMessage);
}
// we only permit LOGIN commands if authentication based on sessionId was not successful
if (!isAuthenticated() && !type.equals(LoginCommand.class)) {
// send 401 Authentication Required
send(MessageBuilder.status().code(401).message("").build(), true);
return;
}
tx.success();
} catch (FrameworkException t) {
logger.log(Level.WARNING, "Unable to parse message.", t);
}
// process message
try {
AbstractCommand abstractCommand = (AbstractCommand) type.newInstance();
abstractCommand.setWebSocket(this);
abstractCommand.setSession(session);
abstractCommand.setCallback(webSocketData.getCallback());
// The below blocks allow a websocket command to manage its own
// transactions in case of bulk processing commands etc.
if (abstractCommand.requiresEnclosingTransaction()) {
try (final Tx tx = app.tx()) {
// store authenticated-Flag in webSocketData
// so the command can access it
webSocketData.setSessionValid(isAuthenticated());
abstractCommand.processMessage(webSocketData);
// commit transaction
tx.success();
}
} else {
try (final Tx tx = app.tx()) {
// store authenticated-Flag in webSocketData
// so the command can access it
webSocketData.setSessionValid(isAuthenticated());
// commit transaction
tx.success();
}
// process message without transaction context!
abstractCommand.processMessage(webSocketData);
}
} catch (FrameworkException | InstantiationException | IllegalAccessException t) {
t.printStackTrace(System.out);
// Clear result in case of rollback
//webSocketData.clear();
try (final Tx tx = app.tx()) {
// send 400 Bad Request
if (t instanceof FrameworkException) {
send(MessageBuilder.status().message(t.toString()).jsonErrorObject(((FrameworkException) t).toJSON()).build(), true);
} else {
send(MessageBuilder.status().code(400).message(t.toString()).build(), true);
}
// commit transaction
tx.success();
} catch (FrameworkException fex) {
logger.log(Level.WARNING, "", fex);
}
return;
}
} else {
logger.log(Level.WARNING, "Unknown command {0}", command);
// send 400 Bad Request
send(MessageBuilder.status().code(400).message("Unknown command").build(), true);
return;
}
}
public void send(final WebSocketMessage message, final boolean clearSessionId) {
boolean isAuthenticated = false;
try (final Tx tx = StructrApp.getInstance(securityContext).tx()) {
isAuthenticated = isAuthenticated();
tx.success();
} catch (FrameworkException t) {
logger.log(Level.WARNING, "", t);
}
// return session status to client
message.setSessionValid(isAuthenticated);
// whether to clear the token (all command except LOGIN (for now) should absolutely do this!)
if (clearSessionId) {
message.setSessionId(null);
}
if ("LOGIN".equals(message.getCommand()) && !isAuthenticated) {
message.setMessage("User has no backend access.");
message.setCode(403);
//logger.log(Level.WARNING, "NOT sending message to unauthenticated client.");
}
try (final Tx tx = StructrApp.getInstance(securityContext).tx()) {
final String msg = gson.toJson(message, WebSocketMessage.class);
logger.log(Level.FINE, "################### Private message: {0}", message.getCommand());
logger.log(Level.FINEST, "############################################################ SENDING \n{0}", msg);
// Clear custom view here. This is necessary because the security context is reused for all websocket frames.
if (securityContext != null) {
securityContext.clearCustomView();
}
session.getRemote().sendString(msg);
tx.success();
} catch (Throwable t) {
// ignore
logger.log(Level.FINE, "Unable to send websocket message to remote client");
}
}
// ----- file handling -----
public void createFileUploadHandler(FileBase file) {
final String uuid = file.getProperty(GraphObject.id);
uploads.put(uuid, new FileUploadHandler(file));
}
public void removeFileUploadHandler(final String uuid) {
uploads.remove(uuid);
}
private FileUploadHandler handleExistingFile(final String uuid) {
FileUploadHandler newHandler = null;
try {
File file = (File) StructrApp.getInstance(securityContext).getNodeById(uuid);
if (file != null) {
newHandler = new FileUploadHandler(file);
//uploads.put(uuid, newHandler);
}
} catch (FrameworkException ex) {
logger.log(Level.WARNING, "File not found with id " + uuid, ex);
}
return newHandler;
}
public void handleFileChunk(final String uuid, final int sequenceNumber, final int chunkSize, final byte[] data, final int chunks) throws IOException {
FileUploadHandler upload = uploads.get(uuid);
if (upload == null) {
upload = handleExistingFile(uuid);
}
if (upload != null) {
upload.handleChunk(sequenceNumber, chunkSize, data, chunks);
}
}
private void authenticate(final String sessionId) {
final Principal user = AuthHelper.getPrincipalForSessionId(sessionId);
if (user != null) {
try {
final boolean sessionValid = ! SessionHelper.isSessionTimedOut(SessionHelper.getSessionBySessionId(sessionId));
if (sessionValid) {
this.setAuthenticated(sessionId, user);
} else {
logger.log(Level.WARNING, "Session {0} timed out - last accessed by {1}", new Object[]{sessionId, user});
SessionHelper.clearSession(sessionId);
SessionHelper.invalidateSession(SessionHelper.getSessionBySessionId(sessionId));
AuthHelper.sendLogoutNotification(user);
}
} catch (FrameworkException ex) {
logger.log(Level.WARNING, "FXE", ex);
}
}
}
public static void addCommand(final Class command) {
try {
final AbstractCommand msg = (AbstractCommand) command.newInstance();
commandSet.put(msg.getCommand(), command);
} catch (Throwable t) {
logger.log(Level.SEVERE, "Unable to add command {0}", command.getName());
}
}
public Session getSession() {
return session;
}
public HttpServletRequest getRequest() {
return request;
}
public Principal getCurrentUser() {
return (securityContext == null ? null : securityContext.getUser(false));
}
public SecurityContext getSecurityContext() {
return securityContext;
}
public String getPagePath() {
return pagePath;
}
public boolean isAuthenticated() {
final Principal user = getCurrentUser();
return (user != null && (isPriviledgedUser(user) || isFrontendWebsocketAccessEnabled()));
}
public boolean isPriviledgedUser(Principal user) {
return (user != null && (user.getProperty(Principal.isAdmin) || user.getProperty(User.backendUser)));
}
public boolean isFrontendWebsocketAccessEnabled() {
return Boolean.parseBoolean(StructrApp.getConfigurationValue(Services.WEBSOCKET_FRONTEND_ACCESS, "false"));
}
public Authenticator getAuthenticator() {
return authenticator;
}
//~--- set methods ----------------------------------------------------
public void setAuthenticated(final String sessionId, final Principal user) {
this.securityContext = SecurityContext.getInstance(user, AccessMode.Backend);
}
@Override
public void onWebSocketBinary(final byte[] bytes, int i, int i1) {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public void onWebSocketError(final Throwable t) {
logger.log(Level.FINE, "Error in StructrWebSocket occured", t);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy