// Copyright 2017 The Nomulus Authors. All Rights Reserved. // // 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 google.registry.rde; import static com.google.appengine.api.taskqueue.TaskOptions.Builder.withUrl; import static com.google.common.base.Verify.verify; import static com.google.common.net.MediaType.PLAIN_TEXT_UTF_8; import static com.jcraft.jsch.ChannelSftp.OVERWRITE; import static google.registry.model.common.Cursor.CursorType.RDE_UPLOAD_SFTP; import static google.registry.model.common.Cursor.getCursorTimeOrStartOfTime; import static google.registry.model.ofy.ObjectifyService.ofy; import static google.registry.model.rde.RdeMode.FULL; import static google.registry.request.Action.Method.POST; import static google.registry.util.DateTimeUtils.isBeforeOrAt; import static java.util.Arrays.asList; import com.google.appengine.api.taskqueue.Queue; import com.google.appengine.tools.cloudstorage.GcsFilename; import com.google.common.annotations.VisibleForTesting; import com.google.common.flogger.FluentLogger; import com.google.common.io.ByteStreams; import com.jcraft.jsch.JSch; import com.jcraft.jsch.JSchException; import com.jcraft.jsch.SftpProgressMonitor; import dagger.Lazy; import google.registry.config.RegistryConfig.Config; import google.registry.gcs.GcsUtils; import google.registry.keyring.api.KeyModule.Key; import google.registry.model.common.Cursor; import google.registry.model.common.Cursor.CursorType; import google.registry.model.rde.RdeNamingUtils; import google.registry.model.rde.RdeRevision; import google.registry.model.registry.Registry; import google.registry.rde.EscrowTaskRunner.EscrowTask; import google.registry.rde.JSchSshSession.JSchSshSessionFactory; import google.registry.request.Action; import google.registry.request.HttpException.NoContentException; import google.registry.request.Parameter; import google.registry.request.RequestParameters; import google.registry.request.Response; import google.registry.request.auth.Auth; import google.registry.util.Clock; import google.registry.util.Retrier; import google.registry.util.TaskQueueUtils; import google.registry.util.TeeOutputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URI; import javax.inject.Inject; import javax.inject.Named; import org.bouncycastle.openpgp.PGPKeyPair; import org.bouncycastle.openpgp.PGPPrivateKey; import org.bouncycastle.openpgp.PGPPublicKey; import org.joda.time.DateTime; import org.joda.time.Duration; /** * Action that securely uploads an RDE XML file from Cloud Storage to a trusted third party (such as * Iron Mountain) via SFTP. * *
This action is invoked by {@link RdeStagingAction} once it's created the files we need. The * date is calculated from {@link CursorType#RDE_UPLOAD}. * *
Once this action completes, it rolls the cursor forward a day and triggers
* {@link RdeReportAction}.
*/
@Action(
path = RdeUploadAction.PATH,
method = POST,
auth = Auth.AUTH_INTERNAL_ONLY
)
public final class RdeUploadAction implements Runnable, EscrowTask {
static final String PATH = "/_dr/task/rdeUpload";
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@Inject Clock clock;
@Inject GcsUtils gcsUtils;
@Inject EscrowTaskRunner runner;
// Using Lazy This is done by layering a bunch of {@link java.io.FilterOutputStream FilterOutputStreams}
* on top of each other in reverse order that turn XML bytes into a RyDE file while
* simultaneously uploading it to the SFTP endpoint, and then using {@link ByteStreams#copy} to
* blocking-copy bytes from the cloud storage {@code InputStream} to the RyDE/SFTP pipeline.
*
* In psuedoshell, the whole process looks like the following:
*
* {@code
* gcs read $xmlFile \ # Get GhostRyDE from cloud storage.
* | decrypt | decompress \ # Convert it to XML.
* | tar | file | compress | encrypt | sign /tmp/sig \ # Convert it to a RyDE file.
* | tee gs://bucket/$rydeFilename.ryde \ # Save a copy of the RyDE file to GCS.
* | sftp put $dstUrl/$rydeFilename.ryde \ # Upload to SFTP server.
* && sftp put $dstUrl/$rydeFilename.sig gs://bucket/$rydeFilename.sig # Save a copy of signature to GCS.
* }
*/
@VisibleForTesting
protected void upload(GcsFilename xmlFile, long xmlLength, DateTime watermark, String name)
throws Exception {
logger.atInfo().log("Uploading XML file '%s' to remote path '%s'.", xmlFile, uploadUrl);
try (InputStream gcsInput = gcsUtils.openInputStream(xmlFile);
InputStream ghostrydeDecoder = Ghostryde.decoder(gcsInput, stagingDecryptionKey)) {
try (JSchSshSession session = jschSshSessionFactory.create(lazyJsch.get(), uploadUrl);
JSchSftpChannel ftpChan = session.openSftpChannel()) {
ByteArrayOutputStream sigOut = new ByteArrayOutputStream();
String rydeFilename = name + ".ryde";
GcsFilename rydeGcsFilename = new GcsFilename(bucket, rydeFilename);
try (OutputStream ftpOutput =
ftpChan.get().put(rydeFilename, sftpProgressMonitor, OVERWRITE);
OutputStream gcsOutput = gcsUtils.openOutputStream(rydeGcsFilename);
TeeOutputStream teeOutput = new TeeOutputStream(asList(ftpOutput, gcsOutput));
RydeEncoder rydeEncoder =
new RydeEncoder.Builder()
.setRydeOutput(teeOutput, receiverKey)
.setSignatureOutput(sigOut, signingKey)
.setFileMetadata(name, xmlLength, watermark)
.build()) {
long bytesCopied = ByteStreams.copy(ghostrydeDecoder, rydeEncoder);
logger.atInfo().log("uploaded %,d bytes: %s", bytesCopied, rydeFilename);
}
String sigFilename = name + ".sig";
byte[] signature = sigOut.toByteArray();
gcsUtils.createFromBytes(new GcsFilename(bucket, sigFilename), signature);
ftpChan.get().put(new ByteArrayInputStream(signature), sigFilename);
logger.atInfo().log("uploaded %,d bytes: %s", signature.length, sigFilename);
}
}
}
/** Reads the contents of a file from Cloud Storage that contains nothing but an integer. */
private long readXmlLength(GcsFilename xmlLengthFilename) throws IOException {
try (InputStream input = gcsUtils.openInputStream(xmlLengthFilename)) {
return Ghostryde.readLength(input);
}
}
private void verifyFileExists(GcsFilename filename) {
verify(gcsUtils.existsAndNotEmpty(filename), "Missing file: %s", filename);
}
}