/** * Copyright 2009 Google Inc. * * 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.waveprotocol.box.server.waveserver; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.util.concurrent.ListenableFuture; import com.google.inject.Inject; import com.google.inject.Singleton; import com.google.inject.name.Named; import com.google.protobuf.ByteString; import com.google.protobuf.InvalidProtocolBufferException; import org.waveprotocol.box.common.ExceptionalIterator; import org.waveprotocol.box.server.common.CoreWaveletOperationSerializer; import org.waveprotocol.box.server.frontend.CommittedWaveletSnapshot; import org.waveprotocol.box.server.persistence.PersistenceException; import org.waveprotocol.wave.crypto.SignatureException; import org.waveprotocol.wave.crypto.SignerInfo; import org.waveprotocol.wave.crypto.UnknownSignerException; import org.waveprotocol.wave.federation.FederationErrorProto.FederationError; import org.waveprotocol.wave.federation.FederationErrors; import org.waveprotocol.wave.federation.FederationException; import org.waveprotocol.wave.federation.FederationRemoteBridge; import org.waveprotocol.wave.federation.Proto.ProtocolAppliedWaveletDelta; import org.waveprotocol.wave.federation.Proto.ProtocolHashedVersion; import org.waveprotocol.wave.federation.Proto.ProtocolSignature; import org.waveprotocol.wave.federation.Proto.ProtocolSignedDelta; import org.waveprotocol.wave.federation.Proto.ProtocolSignerInfo; import org.waveprotocol.wave.federation.Proto.ProtocolWaveletDelta; import org.waveprotocol.wave.federation.WaveletFederationListener; import org.waveprotocol.wave.federation.WaveletFederationProvider; import org.waveprotocol.wave.model.id.WaveId; import org.waveprotocol.wave.model.id.WaveletId; import org.waveprotocol.wave.model.id.WaveletName; import org.waveprotocol.wave.model.operation.OperationException; import org.waveprotocol.wave.model.operation.wave.TransformedWaveletDelta; import org.waveprotocol.wave.model.version.HashedVersion; import org.waveprotocol.wave.model.wave.InvalidParticipantAddress; import org.waveprotocol.wave.model.wave.ParticipantId; import org.waveprotocol.wave.util.logging.Log; import java.util.Collection; import java.util.List; import java.util.Set; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicInteger; /** * The main class that services the FederationHost, FederationRemote and ClientFrontend. */ @Singleton public class WaveServerImpl implements WaveletProvider, WaveletFederationProvider, WaveletFederationListener.Factory { private static final Log LOG = Log.get(WaveServerImpl.class); private final Executor listenerExecutor; private final CertificateManager certificateManager; private final WaveletFederationProvider federationRemote; private final WaveMap waveMap; private boolean initialized = false; // // WaveletFederationListener.Factory implementation. // /** * Listener for notifications coming from the Federation Remote. For now we accept updates * for wavelets on any domain. */ @Override public WaveletFederationListener listenerForDomain(final String domain) { return new WaveletFederationListener() { @Override public void waveletDeltaUpdate(final WaveletName waveletName, List<ByteString> deltas, final WaveletUpdateCallback callback) { Preconditions.checkArgument(!deltas.isEmpty()); if (isLocalWavelet(waveletName)) { LOG.warning("Remote tried to update local wavelet " + waveletName); callback.onFailure(FederationErrors.badRequest("Received update to local wavelet")); return; } // Update wavelet container with the applied deltas final RemoteWaveletContainer remoteWavelet = getOrCreateRemoteWavelet(waveletName); // Update this remote wavelet with the immediately incoming delta, // providing a callback so that incoming historic deltas (as well as // this delta) can be provided to the wave bus. final ListenableFuture<Void> result = remoteWavelet.update(deltas, domain, federationRemote, certificateManager); result.addListener( new Runnable() { @Override public void run() { try { FutureUtil.getResultOrPropagateException(result, FederationException.class); callback.onSuccess(); } catch (FederationException e) { LOG.warning("Failed updating " + waveletName, e); callback.onFailure(e.getError()); } catch (InterruptedException e) { Thread.currentThread().interrupt(); LOG.severe("Interrupted updating " + waveletName, e); callback.onFailure(FederationErrors.internalServerError("Interrupted")); } } }, listenerExecutor); } @Override public void waveletCommitUpdate(WaveletName waveletName, ProtocolHashedVersion committedVersion, WaveletUpdateCallback callback) { Preconditions.checkNotNull(committedVersion); if (isLocalWavelet(waveletName)) { LOG.warning("Got commit update for local wavelet " + waveletName); callback.onFailure(FederationErrors.badRequest("Received comit update to local wavelet")); return; } RemoteWaveletContainer wavelet; try { wavelet = getRemoteWavelet(waveletName); } catch (WaveletStateException e) { LOG.warning("Failed to access wavelet " + waveletName + " for commit update", e); callback.onFailure(FederationErrors.internalServerError("Storage access failure")); return; } if (wavelet != null) { wavelet.commit(CoreWaveletOperationSerializer.deserialize(committedVersion)); } else { // TODO(soren): This should really be changed to create the wavelet if it doesn't // already exist and go get history up committedVersion. Moreover, when the // protocol is enhanced to deliver commit updates reliably, we will probably need // to only return success when we successfully retrieved history and persisted it all. LOG.info("Got commit update for missing wavelet " + waveletName); } callback.onSuccess(); } }; } // // WaveletFederationProvider implementation. // @Override public void submitRequest(WaveletName waveletName, ProtocolSignedDelta signedDelta, SubmitResultListener listener) { if (!isLocalWavelet(waveletName)) { LOG.warning("Remote tried to submit to non-local wavelet " + waveletName); listener.onFailure(FederationErrors.badRequest("Non-local wavelet update")); return; } ProtocolWaveletDelta delta; try { delta = ByteStringMessage.parseProtocolWaveletDelta(signedDelta.getDelta()).getMessage(); } catch (InvalidProtocolBufferException e) { LOG.warning("Submit request: Invalid delta protobuf. WaveletName: " + waveletName, e); listener.onFailure(FederationErrors.badRequest("Signed delta contains invalid delta")); return; } // Disallow creation of wavelets by remote users. if (delta.getHashedVersion().getVersion() == 0) { LOG.warning("Remote user tried to submit delta at version 0 - disallowed. " + signedDelta); listener.onFailure(FederationErrors.badRequest("Remote users may not create wavelets.")); return; } try { certificateManager.verifyDelta(signedDelta); submitDelta(waveletName, delta, signedDelta, listener); } catch (SignatureException e) { LOG.warning("Submit request: Delta failed verification. WaveletName: " + waveletName + " delta: " + signedDelta, e); listener.onFailure(FederationErrors.badRequest("Remote verification failed")); } catch (UnknownSignerException e) { LOG.warning("Submit request: unknown signer. WaveletName: " + waveletName + "delta: " + signedDelta, e); listener.onFailure(FederationErrors.internalServerError("Unknown signer")); } } @Override public void requestHistory(WaveletName waveletName, String domain, ProtocolHashedVersion startVersion, ProtocolHashedVersion endVersion, long lengthLimit, HistoryResponseListener listener) { LocalWaveletContainer wavelet = loadLocalWavelet(waveletName, listener); if (wavelet != null) { Collection<ByteStringMessage<ProtocolAppliedWaveletDelta>> deltaHistory; try { deltaHistory = wavelet.requestHistory( CoreWaveletOperationSerializer.deserialize(startVersion), CoreWaveletOperationSerializer.deserialize(endVersion)); } catch (WaveServerException e) { LOG.severe("Error retrieving wavelet history: " + waveletName + " " + startVersion + " - " + endVersion); // TODO(soren): choose a better error code (depending on e) listener.onFailure(FederationErrors.badRequest( "Server error while retrieving wavelet history.")); return; } // TODO(soren): enforce length limit ImmutableList.Builder<ByteString> deltaHistoryBytes = ImmutableList.builder(); for (ByteStringMessage<ProtocolAppliedWaveletDelta> d : deltaHistory) { deltaHistoryBytes.add(d.getByteString()); } // Now determine whether we received the entire requested wavelet history. LOG.info("Found deltaHistory between " + startVersion + " - " + endVersion + ", returning to requester domain " + domain + " -- " + deltaHistory); listener.onSuccess(deltaHistoryBytes.build(), endVersion, endVersion.getVersion()); } } @Override public void getDeltaSignerInfo(ByteString signerId, WaveletName waveletName, ProtocolHashedVersion deltaEndVersion, DeltaSignerInfoResponseListener listener) { LocalWaveletContainer wavelet = loadLocalWavelet(waveletName, listener); if (wavelet != null) { HashedVersion endVersion = CoreWaveletOperationSerializer.deserialize(deltaEndVersion); if (wavelet.isDeltaSigner(endVersion, signerId)) { ProtocolSignerInfo signerInfo = certificateManager.retrieveSignerInfo(signerId); if (signerInfo == null) { // Oh no! We are supposed to store it, and we already know they did sign this delta. LOG.severe("No stored signer info for valid getDeltaSignerInfo on " + waveletName); listener.onFailure(FederationErrors.badRequest("Unknown signer info")); } else { listener.onSuccess(signerInfo); } } else { LOG.info("getDeltaSignerInfo was not authrorised for wavelet " + waveletName + ", end version " + deltaEndVersion); listener.onFailure(FederationErrors.badRequest("Not authorised to get signer info")); } } } @Override public void postSignerInfo(String destinationDomain, ProtocolSignerInfo signerInfo, PostSignerInfoResponseListener listener) { try { certificateManager.storeSignerInfo(signerInfo); } catch (SignatureException e) { String error = "verification failure from domain " + signerInfo.getDomain(); LOG.warning("incoming postSignerInfo: " + error, e); listener.onFailure(FederationErrors.badRequest(error)); return; } listener.onSuccess(); } // // WaveletProvider implementation. // @Override public void initialize() throws WaveServerException { Preconditions.checkState(!initialized, "Wave server already initialized"); waveMap.loadAllWavelets(); initialized = true; } @Override public Collection<TransformedWaveletDelta> getHistory(WaveletName waveletName, HashedVersion startVersion, HashedVersion endVersion) throws WaveServerException { Preconditions.checkState(initialized, "Wave server not yet initialized"); WaveletContainer wavelet = getWavelet(waveletName); if (wavelet == null) { throw new AccessControlException( "Client request for history made for non-existent wavelet: " + waveletName); } return wavelet.requestTransformedHistory(startVersion, endVersion); } @Override public ExceptionalIterator<WaveId, WaveServerException> getWaveIds() { Preconditions.checkState(initialized, "Wave server not yet initialized"); return waveMap.getWaveIds(); } @Override public ImmutableSet<WaveletId> getWaveletIds(WaveId waveId) throws WaveServerException { Preconditions.checkState(initialized, "Wave server not yet initialized"); return waveMap.lookupWavelets(waveId); } @Override public CommittedWaveletSnapshot getSnapshot(WaveletName waveletName) throws WaveServerException { Preconditions.checkState(initialized, "Wave server not yet initialized"); WaveletContainer wavelet = getWavelet(waveletName); if (wavelet == null) { LOG.info("client requested snapshot for non-existent wavelet: " + waveletName); return null; } else { return wavelet.getSnapshot(); } } @Override public void submitRequest(WaveletName waveletName, ProtocolWaveletDelta delta, final SubmitRequestListener listener) { Preconditions.checkState(initialized, "Wave server not yet initialized"); if (delta.getOperationCount() == 0) { listener.onFailure("Empty delta at version " + delta.getHashedVersion().getVersion()); return; } // The serialised version of this delta happens now. This should be the only place, ever! ProtocolSignedDelta signedDelta = certificateManager.signDelta(ByteStringMessage.serializeMessage(delta)); submitDelta(waveletName, delta, signedDelta, new SubmitResultListener() { @Override public void onFailure(FederationError errorMessage) { listener.onFailure(errorMessage.getErrorMessage()); } @Override public void onSuccess(int operationsApplied, ProtocolHashedVersion hashedVersionAfterApplication, long applicationTimestamp) { listener.onSuccess(operationsApplied, CoreWaveletOperationSerializer.deserialize(hashedVersionAfterApplication), applicationTimestamp); } }); } @Override public boolean checkAccessPermission(WaveletName waveletName, ParticipantId participantId) throws WaveServerException { Preconditions.checkState(initialized, "Wave server not yet initialized"); WaveletContainer wavelet = getWavelet(waveletName); return wavelet != null && wavelet.checkAccessPermission(participantId); } /** * Constructor. * * @param listenerExecutor executes callback listeners * @param certificateManager provider of certificates; it also determines which * domains this wave server regards as local wavelets. * @param federationRemote federation remote interface * @param waveMap records the waves and wavelets in memory */ @Inject WaveServerImpl(@Named("listener_executor") Executor listenerExecutor, CertificateManager certificateManager, @FederationRemoteBridge WaveletFederationProvider federationRemote, WaveMap waveMap) { this.listenerExecutor = listenerExecutor; this.certificateManager = certificateManager; this.federationRemote = federationRemote; this.waveMap = waveMap; LOG.info("Wave Server configured to host local domains: " + certificateManager.getLocalDomains()); // Preemptively add our own signer info to the certificate manager SignerInfo signerInfo = certificateManager.getLocalSigner().getSignerInfo(); if (signerInfo != null) { try { certificateManager.storeSignerInfo(signerInfo.toProtoBuf()); } catch (SignatureException e) { LOG.severe("Failed to add our own signer info to the certificate store", e); } } } /** * Loads a local wavelet. If the request is invalid then the listener is * notified and null returned. * * @param waveletName wavelet to load * @param listener listener to notify on failure * @return the wavelet container, or null on failure */ private LocalWaveletContainer loadLocalWavelet(WaveletName waveletName, FederationListener listener) { // TODO(soren): once we support federated groups, expand support to request remote wavelets too. if (!isLocalWavelet(waveletName)) { LOG.warning("Attempt to get delta signer info for remote wavelet " + waveletName); listener.onFailure(FederationErrors.badRequest("Wavelet not hosted here.")); return null; } LocalWaveletContainer wavelet = null; try { wavelet = getLocalWavelet(waveletName); } catch (WaveletStateException e) { LOG.warning("Failed to access wavelet " + waveletName, e); listener.onFailure(FederationErrors.internalServerError("Storage access failure")); return null; } if (wavelet == null) { LOG.info("Non-existent wavelet " + waveletName); // TODO(soren): determine if it's ok to leak the fact that the wavelet doesn't exist, // or if we should hide this information and return the same error code as for wrong // version number or history hash listener.onFailure(FederationErrors.badRequest("Wavelet does not exist")); } return wavelet; } private boolean isLocalWavelet(WaveletName waveletName) { boolean isLocal = getLocalDomains().contains(waveletName.waveletId.getDomain()); LOG.fine("" + waveletName + " is " + (isLocal? "" : "not") + " local"); return isLocal; } private Set<String> getLocalDomains() { return certificateManager.getLocalDomains(); } /** * Returns a container for a remote wavelet. If it doesn't exist, it will be created. * This method is only called in response to a Federation Remote doing an update * or commit on this wavelet. * * @param waveletName name of wavelet * @return an existing or new instance. * @throws IllegalArgumentException if the name refers to a local wavelet. */ private RemoteWaveletContainer getOrCreateRemoteWavelet(WaveletName waveletName) { Preconditions.checkArgument(!isLocalWavelet(waveletName), "%s is not remote", waveletName); return waveMap.getOrCreateRemoteWavelet(waveletName); } /** * Returns a container for a local wavelet. If it doesn't exist, it will be created. * * @param waveletName name of wavelet * @return an existing or new instance. * @throws IllegalArgumentException if the name refers to a remote wavelet. */ private LocalWaveletContainer getOrCreateLocalWavelet(WaveletName waveletName) { Preconditions.checkArgument(isLocalWavelet(waveletName), "%s is not local", waveletName); return waveMap.getOrCreateLocalWavelet(waveletName); } private RemoteWaveletContainer getRemoteWavelet(WaveletName waveletName) throws WaveletStateException { Preconditions.checkArgument(!isLocalWavelet(waveletName), "%s is not remote", waveletName); return waveMap.getRemoteWavelet(waveletName); } private LocalWaveletContainer getLocalWavelet(WaveletName waveletName) throws WaveletStateException { Preconditions.checkArgument(isLocalWavelet(waveletName), "%s is not local", waveletName); return waveMap.getLocalWavelet(waveletName); } /** * Returns a generic wavelet container, when the caller doesn't need to validate whether * its a local or remote wavelet. * * @param waveletName name of wavelet. * @return an wavelet container or null if it doesn't exist. * @throw WaveServerException if storage lookup fails */ private WaveletContainer getWavelet(WaveletName waveletName) throws WaveServerException { return isLocalWavelet(waveletName) ? waveMap.getLocalWavelet(waveletName) : waveMap.getRemoteWavelet(waveletName); } /** * Callback interface for sending a list of certificates to a domain. */ private interface PostSignerInfoCallback { public void done(int successCount); } /** * Submit the delta to local or remote wavelets, return results via listener. * Also broadcast updates to federationHosts and clientFrontend. * * @param waveletName the wavelet to apply the delta to * @param delta the {@link ProtocolWaveletDelta} inside {@code signedDelta} * @param signedDelta the signed delta * @param resultListener callback * * TODO: For now the WaveletFederationProvider will have to ensure this is a * local wavelet. Once we support federated groups, that test should be * removed. */ private void submitDelta(final WaveletName waveletName, ProtocolWaveletDelta delta, final ProtocolSignedDelta signedDelta, final SubmitResultListener resultListener) { Preconditions.checkArgument(delta.getOperationCount() > 0, "empty delta"); if (isLocalWavelet(waveletName)) { LOG.info("Submit to " + waveletName + " by " + delta.getAuthor() + " @ " + delta.getHashedVersion().getVersion() + " with " + delta.getOperationCount() + " ops"); // TODO(arb): add v0 policer here. LocalWaveletContainer wavelet = getOrCreateLocalWavelet(waveletName); try { if (!wavelet.checkAccessPermission(ParticipantId.of(delta.getAuthor()))) { resultListener.onFailure(FederationErrors.badRequest( delta.getAuthor() + " is not a participant of " + waveletName)); return; } } catch (InvalidParticipantAddress e) { resultListener.onFailure(FederationErrors.badRequest( "Invalid author address: " + e.getMessage())); return; } catch (WaveServerException e) { resultListener.onFailure(FederationErrors.internalServerError(e.getMessage())); return; } try { WaveletDeltaRecord submitResult = wavelet.submitRequest(waveletName, signedDelta); TransformedWaveletDelta transformedDelta = submitResult.getTransformedDelta(); LOG.info("Submit result for " + waveletName + " by " + transformedDelta.getAuthor() + " applied " + transformedDelta.size() + " ops at v: " + transformedDelta.getAppliedAtVersion() + " t: " + transformedDelta.getApplicationTimestamp()); resultListener.onSuccess(transformedDelta.size(), CoreWaveletOperationSerializer.serialize(transformedDelta.getResultingVersion()), transformedDelta.getApplicationTimestamp()); } catch (OperationException e) { resultListener.onFailure(FederationErrors.badRequest(e.getMessage())); } catch (InvalidProtocolBufferException e) { resultListener.onFailure(FederationErrors.badRequest(e.getMessage())); } catch (InvalidHashException e) { resultListener.onFailure(FederationErrors.badRequest(e.getMessage())); } catch (PersistenceException e) { resultListener.onFailure(FederationErrors.internalServerError(e.getMessage())); } catch (WaveletStateException e) { resultListener.onFailure(FederationErrors.internalServerError(e.getMessage())); } } else { // For remote wavelets post required signatures to the authorative server then send delta postAllSignerInfo(signedDelta.getSignatureList(), waveletName.waveletId.getDomain(), new PostSignerInfoCallback() { @Override public void done(int successCount) { LOG.info("Remote: successfully sent " + successCount + " of " + signedDelta.getSignatureCount() + " certs to " + waveletName.waveletId.getDomain()); federationRemote.submitRequest(waveletName, signedDelta, resultListener); } }); } } /** * Post a list of certificates to a domain and run a callback when all are finished. The * callback will run whether or not all posts succeed. * * @param sigs list of signatures to post signer info for * @param domain to post signature to * @param callback to run when all signatures have been posted, successfully or unsuccessfully */ private void postAllSignerInfo(final List<ProtocolSignature> sigs, final String domain, final PostSignerInfoCallback callback) { // In the current implementation there should only be a single signer if (sigs.size() != 1) { LOG.warning(sigs.size() + " signatures to broadcast, expecting exactly 1"); } final AtomicInteger resultCount = new AtomicInteger(sigs.size()); final AtomicInteger successCount = new AtomicInteger(0); for (final ProtocolSignature sig : sigs) { final ProtocolSignerInfo psi = certificateManager.retrieveSignerInfo(sig.getSignerId()); if (psi == null) { LOG.warning("Couldn't find signer info for " + sig); if (resultCount.decrementAndGet() == 0) { LOG.info("Finished signature broadcast with " + successCount.get() + " successful, running callback"); callback.done(successCount.get()); } } else { federationRemote.postSignerInfo(domain, psi, new PostSignerInfoResponseListener() { @Override public void onFailure(FederationError error) { LOG.warning("Failed to post " + sig + " to " + domain + ": " + error); countDown(); } @Override public void onSuccess() { LOG.info("Successfully broadcasted " + sig + " to " + domain); successCount.incrementAndGet(); countDown(); } private void countDown() { if (resultCount.decrementAndGet() == 0) { LOG.info("Finished signature broadcast with " + successCount.get() + " successful, running callback"); callback.done(successCount.get()); } } }); } } } }