/**
* Copyright (C) 2010-2017 Structr GmbH
*
* This file is part of Structr <http://structr.org>.
*
* 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 <http://www.gnu.org/licenses/>.
*/
package org.structr.net;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.security.KeyPair;
import java.util.Base64;
import java.util.Base64.Decoder;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.structr.api.config.Settings;
import org.structr.api.service.Command;
import org.structr.api.service.RunnableService;
import org.structr.api.service.StructrServices;
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.app.App;
import org.structr.core.app.StructrApp;
import org.structr.core.entity.Principal;
import org.structr.core.graph.NodeAttribute;
import org.structr.core.graph.NodeServiceCommand;
import org.structr.core.graph.Tx;
import org.structr.core.property.PropertyKey;
import org.structr.net.common.KeyHelper;
import org.structr.net.data.RemoteTransaction;
import org.structr.net.data.time.PseudoTime;
import org.structr.net.peer.Peer;
import org.structr.net.repository.DefaultRepository;
import org.structr.net.repository.ExternalChangeListener;
import org.structr.net.repository.ObjectListener;
import org.structr.net.repository.RepositoryObject;
import org.structr.schema.ConfigurationProvider;
/**
*
*/
public class PeerToPeerService implements RunnableService, ExternalChangeListener {
public static final String PEER_UUID_KEY = "peer.config.uuid";
public static final String INITIAL_PEER_KEY = "peer.config.initial.peer";
public static final String BIND_ADDRESS_KEY = "peer.config.bind.address";
public static final String VERBOSE_KEY = "peer.config.verbose";
public static final String PUBLIC_KEY_CONFIG_KEY = "peer.key.public.file";
public static final String PRIVATE_KEY_CONFIG_KEY = "peer.key.private.file";
private static final Logger logger = LoggerFactory.getLogger(PeerToPeerService.class.getName());
private DefaultRepository repository = null;
private boolean initialized = false;
private Peer peer = null;
@Override
public void startService() {
if (initialized) {
peer.start();
logger.info("Service started");
}
}
@Override
public void stopService() {
if (initialized) {
peer.stop();
}
}
@Override
public boolean runOnStartup() {
return true;
}
@Override
public boolean isRunning() {
return initialized && peer.isRunning();
}
@Override
public void injectArguments(final Command command) {
}
@Override
public void initialize(final StructrServices services) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
logger.info("Initializing..");
try {
final KeyPair keyPair = getOrCreateKeyPair();
if (keyPair != null) {
final String initialPeer = Settings.getStringSetting(INITIAL_PEER_KEY, "255.255.255.255").getValue();
final String bindAddress = Settings.getStringSetting(BIND_ADDRESS_KEY, "0.0.0.0").getValue();
final String peerId = Settings.getStringSetting(PEER_UUID_KEY, StructrApp.getInstance().getInstanceId()).getValue();
final boolean verbose = Settings.getBooleanSetting(VERBOSE_KEY).getValue();
logger.info("{}: {}", new Object[] { BIND_ADDRESS_KEY, bindAddress });
logger.info("{}: {}", new Object[] { INITIAL_PEER_KEY, initialPeer });
logger.info("{}: {}", new Object[] { PRIVATE_KEY_CONFIG_KEY, Settings.getStringSetting(PRIVATE_KEY_CONFIG_KEY).getValue() });
logger.info("{}: {}", new Object[] { PUBLIC_KEY_CONFIG_KEY, Settings.getStringSetting(PUBLIC_KEY_CONFIG_KEY).getValue() });
logger.info("{}: {}", new Object[] { PEER_UUID_KEY, peerId });
logger.info("{}: {}", new Object[] { VERBOSE_KEY, verbose });
this.repository = new DefaultRepository(peerId);
this.peer = new Peer(getOrCreateKeyPair(), repository, bindAddress, initialPeer);
this.peer.initializeServer();
this.peer.setVerbose(verbose);
this.repository.addExternalChangeListener(this);
this.repository.setPeer(peer);
initialized = true;
}
} catch (Throwable t) {
logger.warn("Unable to initialize PeerToPeerService", t);
}
}
@Override
public void shutdown() {
if (initialized) {
peer.stop();
}
}
@Override
public void initialized() {
}
@Override
public String getName() {
return PeerToPeerService.class.getSimpleName();
}
@Override
public boolean isVital() {
return false;
}
// ----- repository connection -----
public void create(final SharedNodeInterface sharedNode) {
final String uuid = sharedNode.getUuid();
final String type = sharedNode.getType();
final String user = sharedNode.getUserId();
final PseudoTime time = sharedNode.getCreationPseudoTime();
logger.info("Shared node with UUID {} CREATED in Structr at {}", new Object[] { sharedNode.getUuid(), time.toString() } );
repository.create(uuid, type, peer.getUuid(), user, time, sharedNode.getData());
}
public void delete(final String uuid) {
final PseudoTime time = peer.getPseudoTemporalEnvironment().current();
logger.info("Shared node with UUID {} DELETED in Structr at {}", new Object[] { uuid, time } );
repository.delete(uuid, time);
}
public void update(final SharedNodeInterface sharedNode) {
final RepositoryObject obj = repository.getObject(sharedNode.getUuid());
if (obj != null) {
final PseudoTime time = sharedNode.getLastModificationPseudoTime();
final Map<String, Object> data = sharedNode.getData();
final String type = sharedNode.getType();
final String user = sharedNode.getUserId();
logger.info("Shared node with UUID {} UPDATED in Structr at {}", new Object[] { sharedNode.getUuid(), time.toString() } );
repository.update(obj, type, peer.getUuid(), user, time, data);
}
}
public void setProperty(final String uuid, final String key, final Object value) throws FrameworkException {
logger.info("Attempting to modify shared node with UUID {}: {} = {} in Structr", new Object[] { uuid, key, value });
final RepositoryObject sharedObject = repository.getObject(uuid);
if (sharedObject != null) {
try (final RemoteTransaction tx = peer.beginTx(sharedObject)) {
tx.setProperty(sharedObject, key, value);
tx.commit();
} catch (Exception ex) {
System.out.println("timeout");
throw new FrameworkException(500, ex.getMessage());
}
} else {
System.out.println("No such object " + uuid);
}
}
public void addObjectToRepository(final SharedNodeInterface node) {
final String objectId = node.getProperty(GraphObject.id);
final String objectType = node.getProperty(GraphObject.type);
final PseudoTime modified = node.getLastModificationPseudoTime();
final PseudoTime created = node.getCreationPseudoTime();
final String transactionId = NodeServiceCommand.getNextUuid();
final RepositoryObject obj = repository.add(objectId, objectType, peer.getUuid(), node.getUserId(), created);
//logger.info("External object with UUID {} ADDED at {}, created at {}", new Object[] { node.getUuid(), modified, created });
for (final Entry<String, Object> entry : node.getData().entrySet()) {
obj.setProperty(modified, transactionId, entry.getKey(), entry.getValue());
}
repository.complete(transactionId);
// add update listener
obj.addListener(new ObjectUpdater(objectId));
}
public PseudoTime getTime() {
return peer.getPseudoTemporalEnvironment().next();
}
// ----- interface UpdateListener -----
@Override
public void onQuery() {
final App app = StructrApp.getInstance();
try (final Tx tx = app.tx()) {
final Map<String, SharedNodeInterface> nodes = new LinkedHashMap<>();
for (final SharedNodeInterface node : app.nodeQuery(SharedNodeInterface.class).getAsList()) {
nodes.put(node.getUuid(), node);
}
if (nodes.size() != repository.objectCount()) {
logger.info("Rebuilding list of shared objects: {} vs. {}", new Object[] { nodes.size(), repository.objectCount() } );
repository.clear();
for (final SharedNodeInterface node : nodes.values()) {
addObjectToRepository(node);
}
}
tx.success();
} catch (FrameworkException fex) {
logger.warn("", fex);
}
}
@Override
public void onCreate(final RepositoryObject object, final Map<String, Object> data) {
logger.info("New object with UUID {} created in repository", object.getUuid());
final ConfigurationProvider config = StructrApp.getConfiguration();
final Class type = config.getNodeEntityClass(object.getType());
if (type != null) {
final App app = StructrApp.getInstance(getUserContext(object));
try (final Tx tx = app.tx()) {
// create new node
final SharedNodeInterface newNode = app.create(type,
new NodeAttribute<>(GraphObject.id, object.getUuid()),
new NodeAttribute<>(SharedNodeInterface.lastModifiedPseudoTime, object.getLastModificationTime().toString()),
new NodeAttribute<>(SharedNodeInterface.createdPseudoTime, object.getCreationTime().toString())
);
// store data
for (final Entry<String, Object> entry : data.entrySet()) {
final PropertyKey key = config.getPropertyKeyForJSONName(type, entry.getKey(), false);
if (key != null) {
newNode.setProperty(app, key, entry.getValue());
}
}
// add listener to store future updates
object.addListener(new ObjectUpdater(object.getUuid()));
tx.success();
} catch (FrameworkException fex) {
logger.warn("", fex);
}
} else {
System.out.println("Type " + object.getType() + " not found, NOT creating object with ID " + object.getUuid());
}
}
@Override
public void onDelete(final RepositoryObject object) {
final App app = StructrApp.getInstance(getUserContext(object));
try (final Tx tx = app.tx()) {
final SharedNodeInterface node = app.get(SharedNodeInterface.class, object.getUuid());
if (node != null) {
app.delete(node);
}
tx.success();
} catch (FrameworkException fex) {
logger.warn("", fex);
}
}
@Override
public void onModify(final RepositoryObject object, final Map<String, Object> data) {
}
@Override
public void onAdd() {
}
@Override
public void onRemove() {
}
// ----- private methods -----
private SecurityContext getUserContext(final RepositoryObject obj) {
final String userId = obj.getUserId();
Principal principal = null;
if (userId != null) {
final App app = StructrApp.getInstance();
try (final Tx tx = app.tx()) {
// TODO: change this, userId is currently the name of a user,
// WITHOUT ANY AUTHENTICATION.
principal = app.nodeQuery(Principal.class).and(Principal.name, userId).getFirst();
tx.success();
} catch (FrameworkException fex) {
logger.warn("", fex);
}
} else {
System.out.println("Object without userId!");
}
// return user
if (principal != null) {
return SecurityContext.getInstance(principal, AccessMode.Backend);
}
return SecurityContext.getSuperUserInstance();
}
private KeyPair getOrCreateKeyPair() {
final String privateKeyFileName = Settings.getStringSetting(PRIVATE_KEY_CONFIG_KEY).getValue();
final String publicKeyFileName = Settings.getStringSetting(PUBLIC_KEY_CONFIG_KEY).getValue();
if (privateKeyFileName == null) {
logger.warn("No private key file name set for PeerToPeerService, aborting. Please set a value for {} in structr.conf.", PRIVATE_KEY_CONFIG_KEY);
return null;
}
if (publicKeyFileName == null) {
logger.warn("No public key file name set for PeerToPeerService, aborting. Please set value for {} in structr.conf.", PUBLIC_KEY_CONFIG_KEY);
return null;
}
try {
final File privateKeyFile = new File(privateKeyFileName);
final File publicKeyFile = new File(publicKeyFileName);
if (!privateKeyFile.exists()) {
logger.warn("Private key file {} not found, aborting.", privateKeyFileName);
return null;
}
if (!publicKeyFile.exists()) {
logger.warn("Public key file {} not found, aborting.", publicKeyFileName);
return null;
}
final String privkeyBase64 = getKey(Files.readAllLines(privateKeyFile.toPath()));
final String pubkeyBase64 = getKey(Files.readAllLines(publicKeyFile.toPath()));
if (privkeyBase64 == null || privkeyBase64.isEmpty()) {
logger.warn("No private key found in file {}, aborting", privateKeyFileName);
return null;
}
if (pubkeyBase64 == null || pubkeyBase64.isEmpty()) {
logger.warn("No public key found in file {}, aborting", publicKeyFileName);
return null;
}
final Decoder decoder = Base64.getDecoder();
return KeyHelper.fromBytes("RSA", decoder.decode(privkeyBase64), decoder.decode(pubkeyBase64));
} catch (IOException ex) {
logger.error("", ex);
}
return null;
}
private String getKey(final List<String> lines) {
// return the first non-empty line that does not begin with a comment char
for (final String line : lines) {
if (line.contains("#")) {
final String cleanedLine = line.substring(0, line.indexOf("#"));
if (!cleanedLine.isEmpty()) {
return cleanedLine;
}
} else if (!line.isEmpty()) {
return line;
}
}
return null;
}
// ----- nested classes -----
private class ObjectUpdater implements ObjectListener {
private String uuid = null;
public ObjectUpdater(final String uuid) {
this.uuid = uuid;
}
@Override
public void onPropertyChange(final RepositoryObject obj, final String key, final Object value) {
final ConfigurationProvider config = StructrApp.getConfiguration();
final App app = StructrApp.getInstance(getUserContext(obj));
try (final Tx tx = app.tx()) {
final SharedNodeInterface node = app.get(SharedNodeInterface.class, uuid);
if (node != null) {
// store data
final PropertyKey propertyKey = config.getPropertyKeyForJSONName(node.getClass(), key, false);
if (propertyKey != null) {
node.setProperty(app, propertyKey, value);
}
// update last modified date
node.setProperty(app, SharedNodeInterface.lastModifiedPseudoTime, obj.getLastModificationTime().toString());
tx.success();
}
} catch (FrameworkException fex) {
logger.warn("", fex);
}
}
}
}