/** * 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.collect.Lists; import com.google.common.collect.Maps; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; import com.google.protobuf.ByteString; import com.google.protobuf.InvalidProtocolBufferException; import org.jivesoftware.util.Base64; import org.waveprotocol.box.server.common.CoreWaveletOperationSerializer; import org.waveprotocol.box.server.waveserver.CertificateManager.SignerInfoPrefetchResultListener; import org.waveprotocol.wave.crypto.SignatureException; 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.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.WaveletFederationProvider; import org.waveprotocol.wave.federation.WaveletFederationProvider.HistoryResponseListener; import org.waveprotocol.wave.model.id.WaveletName; import org.waveprotocol.wave.model.operation.OperationException; import org.waveprotocol.wave.model.operation.wave.WaveletDelta; import org.waveprotocol.wave.model.version.HashedVersion; import org.waveprotocol.wave.util.logging.Log; import java.util.List; import java.util.Map; import java.util.NavigableMap; import java.util.concurrent.atomic.AtomicInteger; /** * Remote wavelets differ from local ones in that deltas are not submitted for OT, * rather they are updated when a remote wave service provider has applied and sent * a delta. */ class RemoteWaveletContainerImpl extends WaveletContainerImpl implements RemoteWaveletContainer { private static final Log LOG = Log.get(RemoteWaveletContainerImpl.class); /** * Stores all pending deltas for this wavelet, whose insertions would cause * discontiguous blocks of deltas. This must only be accessed under writeLock. */ private final NavigableMap<HashedVersion, ByteStringMessage<ProtocolAppliedWaveletDelta>> pendingDeltas = Maps.newTreeMap(); /** * Create a new RemoteWaveletContainerImpl. Just pass through to the parent * constructor. */ public RemoteWaveletContainerImpl(WaveletName waveletName, WaveletNotificationSubscriber notifiee, ListenableFuture<? extends WaveletState> waveletStateFuture) { // We pass here null for waveDomain because you have to be explicit // participant on remote wavelet to have access permission. // TODO (Yuri Z.): check if the assumption above is correct. super(waveletName, notifiee, waveletStateFuture, null); } @Override public ListenableFuture<Void> update(final List<ByteString> deltas, final String domain, final WaveletFederationProvider federationProvider, final CertificateManager certificateManager) { SettableFuture<Void> futureResult = SettableFuture.create(); internalUpdate(deltas, domain, federationProvider, certificateManager, futureResult); return futureResult; } @Override public void commit(HashedVersion version) { acquireWriteLock(); try { persist(version, ImmutableSet.<String>of()); } finally { releaseWriteLock(); } } private void internalUpdate(final List<ByteString> deltas, final String domain, final WaveletFederationProvider federationProvider, final CertificateManager certificateManager, final SettableFuture<Void> futureResult) { // Turn raw serialised ByteStrings in to a more useful representation final List<ByteStringMessage<ProtocolAppliedWaveletDelta>> appliedDeltas = Lists.newArrayList(); for (ByteString delta : deltas) { try { appliedDeltas.add(ByteStringMessage.parseProtocolAppliedWaveletDelta(delta)); } catch (InvalidProtocolBufferException e) { LOG.info("Invalid applied delta protobuf for incoming " + getWaveletName(), e); acquireWriteLock(); try { markStateCorrupted(); } finally { releaseWriteLock(); } futureResult.setException(new FederationException( FederationErrors.badRequest("Invalid applied delta protocol buffer"))); return; } } LOG.info("Got update: " + appliedDeltas); // Fetch any signer info that we don't already have and then run internalUpdate final AtomicInteger numSignerInfoPrefetched = new AtomicInteger(1); // extra 1 for sentinel final Runnable countDown = new Runnable() { @Override public void run() { if (numSignerInfoPrefetched.decrementAndGet() == 0) { internalUpdateAfterSignerInfoRetrieval( appliedDeltas, domain, federationProvider, certificateManager, futureResult); } } }; SignerInfoPrefetchResultListener prefetchListener = new SignerInfoPrefetchResultListener() { @Override public void onFailure(FederationError error) { LOG.warning("Signer info prefetch failed: " + error); countDown.run(); } @Override public void onSuccess(ProtocolSignerInfo signerInfo) { LOG.info("Signer info prefetch success for " + signerInfo.getDomain()); countDown.run(); } }; for (ByteStringMessage<ProtocolAppliedWaveletDelta> appliedDelta : appliedDeltas) { ProtocolSignedDelta toVerify = appliedDelta.getMessage().getSignedOriginalDelta(); HashedVersion deltaEndVersion; try { deltaEndVersion = AppliedDeltaUtil.calculateResultingHashedVersion(appliedDelta); } catch (InvalidProtocolBufferException e) { LOG.warning("Skipping illformed applied delta " + appliedDelta, e); continue; } for (ProtocolSignature sig : toVerify.getSignatureList()) { if (certificateManager.retrieveSignerInfo(sig.getSignerId()) == null) { LOG.info("Fetching signer info " + Base64.encodeBytes(sig.getSignerId().toByteArray())); numSignerInfoPrefetched.incrementAndGet(); certificateManager.prefetchDeltaSignerInfo(federationProvider, sig.getSignerId(), getWaveletName(), deltaEndVersion, prefetchListener); } } } // If we didn't fetch any signer info, run internalUpdate immediately countDown.run(); } private void internalUpdateAfterSignerInfoRetrieval( List<ByteStringMessage<ProtocolAppliedWaveletDelta>> appliedDeltas, final String domain, final WaveletFederationProvider federationProvider, final CertificateManager certificateManager, final SettableFuture<Void> futureResult) { LOG.info("Passed signer info check, now applying all " + appliedDeltas.size() + " deltas"); acquireWriteLock(); try { checkStateOk(); // TODO(soren): if CORRUPTED, throw away wavelet and start again HashedVersion expectedVersion = getCurrentVersion(); boolean haveRequestedHistory = false; // Verify signatures of all deltas for (ByteStringMessage<ProtocolAppliedWaveletDelta> appliedDelta : appliedDeltas) { try { certificateManager.verifyDelta(appliedDelta.getMessage().getSignedOriginalDelta()); } catch (SignatureException e) { LOG.warning("Verification failure for " + domain + " incoming " + getWaveletName(), e); throw new WaveServerException("Verification failure", e); } catch (UnknownSignerException e) { LOG.severe("Unknown signer for " + domain + " incoming " + getWaveletName() + ", this is BAD! We were supposed to have prefetched it!", e); throw new WaveServerException("Unknown signer", e); } } // Insert all available deltas into pendingDeltas. for (ByteStringMessage<ProtocolAppliedWaveletDelta> appliedDelta : appliedDeltas) { LOG.info("Delta incoming: " + appliedDelta); // Log any illformed signed original deltas. TODO: Check if this can be removed. try { ProtocolWaveletDelta actualDelta = ProtocolWaveletDelta.parseFrom( appliedDelta.getMessage().getSignedOriginalDelta().getDelta()); LOG.info("actual delta: " + actualDelta); } catch (InvalidProtocolBufferException e) { e.printStackTrace(); } HashedVersion appliedAt; try { appliedAt = AppliedDeltaUtil.getHashedVersionAppliedAt(appliedDelta); } catch (InvalidProtocolBufferException e) { markStateCorrupted(); throw new WaveServerException( "Authoritative server sent delta with badly formed original wavelet delta", e); } pendingDeltas.put(appliedAt, appliedDelta); } // Traverse pendingDeltas while we have any to process. ImmutableList.Builder<WaveletDeltaRecord> resultingDeltas = ImmutableList.builder(); while (pendingDeltas.size() > 0) { Map.Entry<HashedVersion, ByteStringMessage<ProtocolAppliedWaveletDelta>> first = pendingDeltas.firstEntry(); HashedVersion appliedAt = first.getKey(); ByteStringMessage<ProtocolAppliedWaveletDelta> appliedDelta = first.getValue(); // If we don't have the right version it implies there is a history we need, so set up a // callback to request it and fall out of this update if (appliedAt.getVersion() > expectedVersion.getVersion()) { LOG.info("Missing history from " + expectedVersion.getVersion() + "-" + appliedAt.getVersion() + ", requesting from upstream for " + getWaveletName()); if (federationProvider != null) { // TODO: only one request history should be pending at any one time? // We should derive a new one whenever the active one is finished, // based on the current state of pendingDeltas. federationProvider.requestHistory(getWaveletName(), domain, CoreWaveletOperationSerializer.serialize(expectedVersion), CoreWaveletOperationSerializer.serialize(appliedAt), -1, new HistoryResponseListener() { @Override public void onFailure(FederationError error) { LOG.severe("Callback failure: " + error); } @Override public void onSuccess(List<ByteString> deltaList, ProtocolHashedVersion lastCommittedVersion, long versionTruncatedAt) { LOG.info("Got response callback: " + getWaveletName() + ", lcv " + lastCommittedVersion + " deltaList length = " + deltaList.size()); // Try updating again with the new history internalUpdate(deltaList, domain, federationProvider, certificateManager, futureResult); } }); haveRequestedHistory = true; } else { LOG.severe("History request resulted in non-contiguous deltas!"); } break; } // This delta is at the correct (current) version - apply it. if (appliedAt.getVersion() == expectedVersion.getVersion()) { // Confirm that the applied at hash matches the expected hash. if (!appliedAt.equals(expectedVersion)) { markStateCorrupted(); throw new WaveServerException("Incoming delta applied at version " + appliedAt.getVersion() + " is not applied to the correct hash"); } LOG.info("Applying delta for version " + appliedAt.getVersion()); try { WaveletDeltaRecord applicationResult = transformAndApplyRemoteDelta(appliedDelta); long opsApplied = applicationResult.getResultingVersion().getVersion() - expectedVersion.getVersion(); if (opsApplied != appliedDelta.getMessage().getOperationsApplied()) { throw new OperationException("Operations applied here do not match the authoritative" + " server claim (got " + opsApplied + ", expected " + appliedDelta.getMessage().getOperationsApplied() + "."); } // Add transformed result to return list. resultingDeltas.add(applicationResult); LOG.fine("Applied delta: " + appliedDelta); } catch (OperationException e) { markStateCorrupted(); throw new WaveServerException("Couldn't apply authoritative delta", e); } catch (InvalidProtocolBufferException e) { markStateCorrupted(); throw new WaveServerException("Couldn't apply authoritative delta", e); } catch (InvalidHashException e) { markStateCorrupted(); throw new WaveServerException("Couldn't apply authoritative delta", e); } // TODO: does waveletData update? expectedVersion = getCurrentVersion(); } else { LOG.warning("Got delta from the past: " + appliedDelta); } pendingDeltas.remove(appliedAt); } if (!haveRequestedHistory) { notifyOfDeltas(resultingDeltas.build(), ImmutableSet.<String>of()); futureResult.set(null); } else if (!resultingDeltas.build().isEmpty()) { LOG.severe("History requested but non-empty result, non-contiguous deltas?"); } else { LOG.info("History requested, ignoring callback"); } } catch (WaveServerException e) { LOG.warning("Update failure", e); // TODO(soren): make everyone throw FederationException instead // of WaveServerException so we don't have to translate between them here futureResult.setException( new FederationException(FederationErrors.badRequest(e.getMessage()))); } finally { releaseWriteLock(); } } /** * Apply a serialised applied delta to a remote wavelet. This assumes the * caller has validated that the delta is at the correct version and can be * applied to the wavelet. Must be called with writelock held. * * @param appliedDelta that is to be applied to the wavelet in its serialised form * @return the transformed and applied delta. * @throws AccessControlException if the supplied Delta's historyHash does not * match the canonical history. * @throws WaveServerException if the delta transforms away. */ private WaveletDeltaRecord transformAndApplyRemoteDelta( ByteStringMessage<ProtocolAppliedWaveletDelta> appliedDelta) throws OperationException, AccessControlException, InvalidHashException, InvalidProtocolBufferException, WaveServerException { // The serialised hashed version should actually match the currentVersion at this point, since // the caller of transformAndApply delta will have made sure the applied deltas are ordered HashedVersion hashedVersion = AppliedDeltaUtil.getHashedVersionAppliedAt(appliedDelta); Preconditions.checkState(hashedVersion.equals(getCurrentVersion()), "Applied delta must apply to current version"); // Extract the serialised wavelet delta ByteStringMessage<ProtocolWaveletDelta> protocolDelta = ByteStringMessage.parseProtocolWaveletDelta( appliedDelta.getMessage().getSignedOriginalDelta().getDelta()); WaveletDelta delta = CoreWaveletOperationSerializer.deserialize(protocolDelta.getMessage()); // Transform operations against earlier deltas, if necessary WaveletDelta transformed = maybeTransformSubmittedDelta(delta); if (transformed.getTargetVersion().equals(delta.getTargetVersion())) { // No transformation took place. // As a sanity check, the hash from the applied delta should NOT be set (an optimisation, but // part of the protocol). if (appliedDelta.getMessage().hasHashedVersionAppliedAt()) { LOG.warning("Hashes are the same but applied delta has hashed_version_applied_at"); // TODO: re-enable this exception for version 0.3 of the spec // throw new InvalidHashException("Applied delta and its contained delta have same hash"); } } if (transformed.size() == 0) { // The host shouldn't be forwarding empty deltas! markStateCorrupted(); throw new WaveServerException("Couldn't apply authoritative delta, " + "it transformed away at version " + transformed.getTargetVersion().getVersion()); } if (!transformed.getTargetVersion().equals(hashedVersion)) { markStateCorrupted(); throw new WaveServerException("Couldn't apply authoritative delta, " + "it transformed to wrong version. Expected " + hashedVersion + ", actual " + transformed.getTargetVersion().getVersion()); } // Apply the delta to the local wavelet state. // This shouldn't fail since the delta is from the authoritative server, so if it fails // then the wavelet is corrupted (and the caller of this method will sort it out). return applyDelta(appliedDelta, transformed); } }