/**
* JBoss, Home of Professional Open Source
* Copyright Red Hat, Inc., and individual contributors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jboss.aerogear.simplepush.server;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import org.jboss.aerogear.crypto.RandomUtils;
import org.jboss.aerogear.simplepush.protocol.Ack;
import org.jboss.aerogear.simplepush.protocol.AckMessage;
import org.jboss.aerogear.simplepush.protocol.HelloMessage;
import org.jboss.aerogear.simplepush.protocol.HelloResponse;
import org.jboss.aerogear.simplepush.protocol.RegisterMessage;
import org.jboss.aerogear.simplepush.protocol.RegisterResponse;
import org.jboss.aerogear.simplepush.protocol.Status;
import org.jboss.aerogear.simplepush.protocol.UnregisterMessage;
import org.jboss.aerogear.simplepush.protocol.UnregisterResponse;
import org.jboss.aerogear.simplepush.protocol.impl.HelloResponseImpl;
import org.jboss.aerogear.simplepush.protocol.impl.RegisterResponseImpl;
import org.jboss.aerogear.simplepush.protocol.impl.StatusImpl;
import org.jboss.aerogear.simplepush.protocol.impl.UnregisterResponseImpl;
import org.jboss.aerogear.simplepush.protocol.impl.AckImpl;
import org.jboss.aerogear.simplepush.server.datastore.ChannelNotFoundException;
import org.jboss.aerogear.simplepush.server.datastore.DataStore;
import org.jboss.aerogear.simplepush.util.CryptoUtil;
import org.jboss.aerogear.simplepush.util.VersionExtractor;
/**
* Concrete implementation of {@link SimplePushServer} that uses a {@link DataStore} to
* store information about UserAgents and channels.
*/
public class DefaultSimplePushServer implements SimplePushServer {
private final DataStore store;
private final SimplePushServerConfig config;
private final byte[] privateKey;
/**
* Sole constructor.
*
* @param store the {@link DataStore} that this server should use.
* @param config the {@link SimplePushServerConfig} for this server.
*/
public DefaultSimplePushServer(final DataStore store, final SimplePushServerConfig config, final byte[] privateKey) {
this.store = store;
this.config = config;
this.privateKey = privateKey;
}
@Override
public HelloResponse handleHandshake(final HelloMessage handshake) {
final Set<String> oldChannels = store.getChannelIds(handshake.getUAID());
for (String channelId : handshake.getChannelIds()) {
if (!oldChannels.contains(channelId)) {
store.saveChannel(new DefaultChannel(handshake.getUAID(),
channelId,
generateEndpointToken(handshake.getUAID(), channelId)));
} else {
oldChannels.remove(channelId);
}
}
store.removeChannels(oldChannels);
return new HelloResponseImpl(handshake.getUAID());
}
private String generateEndpointToken(final String uaid, final String channelId) {
return CryptoUtil.endpointToken(uaid, channelId, privateKey);
}
@Override
public RegisterResponse handleRegister(final RegisterMessage register, final String uaid) {
final String channelId = register.getChannelId();
final String endpointToken = generateEndpointToken(uaid, channelId);
final boolean saved = store.saveChannel(new DefaultChannel(uaid, channelId, endpointToken));
final Status status = saved ? new StatusImpl(200, "OK") : new StatusImpl(409, "Conflict: channeld [" + channelId + " is already in use");
return new RegisterResponseImpl(channelId, status, makeEndpointUrl(endpointToken));
}
@Override
public Notification handleNotification(final String endpointToken, final String body) throws ChannelNotFoundException {
final Long version = Long.valueOf(VersionExtractor.extractVersion(body));
final String channelId = store.updateVersion(endpointToken, version);
if (channelId == null) {
throw new ChannelNotFoundException("Could not find channel for endpoint [" + endpointToken + "]", null);
}
final Ack ack = new AckImpl(channelId, version);
final String uaid = store.saveUnacknowledged(channelId, ack.getVersion());
return new Notification(uaid, ack);
}
@Override
public UnregisterResponse handleUnregister(final UnregisterMessage unregister, final String uaid) {
final String channelId = unregister.getChannelId();
try {
removeChannel(channelId, uaid);
return new UnregisterResponseImpl(channelId, new StatusImpl(200, "OK"));
} catch (final Exception e) {
return new UnregisterResponseImpl(channelId, new StatusImpl(500, "Could not remove the channel"));
}
}
@Override
public Set<Ack> handleAcknowledgement(final AckMessage ackMessage, final String uaid) {
return store.removeAcknowledged(uaid, ackMessage.getAcks());
}
@Override
public Set<Ack> getUnacknowledged(final String uaid) {
return store.getUnacknowledged(uaid);
}
public String getUAID(final String channelId) throws ChannelNotFoundException {
return getChannel(channelId).getUAID();
}
public Channel getChannel(final String channelId) throws ChannelNotFoundException {
return store.getChannel(channelId);
}
public boolean hasChannel(final String uaid, final String channelId) {
try {
final Channel channel = store.getChannel(channelId);
return channel.getUAID().equals(uaid);
} catch (final ChannelNotFoundException e) {
return false;
}
}
public boolean removeChannel(final String channnelId, final String uaid) {
try {
final Channel channel = store.getChannel(channnelId);
if (channel.getUAID().equals(uaid)) {
store.removeChannels(new HashSet<String>(Arrays.asList(channnelId)));
return true;
}
} catch (final ChannelNotFoundException ignored) {
}
return false;
}
private String makeEndpointUrl(final String endpointToken) {
return config.endpointUrl() + "/" + endpointToken;
}
@Override
public void removeAllChannels(final String uaid) {
store.removeChannels(uaid);
}
@Override
public SimplePushServerConfig config() {
return config;
}
public static byte[] generateAndStorePrivateKey(final DataStore store, final SimplePushServerConfig config) {
byte[] keySalt = store.getPrivateKeySalt();
if (keySalt.length == 0) {
keySalt = RandomUtils.randomBytes();
store.savePrivateKeySalt(keySalt);
}
return CryptoUtil.secretKey(config.password(), keySalt);
}
}