/*******************************************************************************
*
* Copyright (c) 2004-2010, Oracle Corporation.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
*
*
*
*
*******************************************************************************/
package hudson.console;
import hudson.model.Hudson;
import hudson.remoting.ObjectInputStreamEx;
import hudson.util.IOException2;
import hudson.util.IOUtils;
import hudson.util.Secret;
import hudson.util.TimeUnit2;
import org.apache.commons.io.output.ByteArrayOutputStream;
import org.kohsuke.stapler.Stapler;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.framework.io.ByteBuffer;
import org.kohsuke.stapler.framework.io.LargeText;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.Writer;
import java.nio.charset.Charset;
import java.security.GeneralSecurityException;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
import static java.lang.Math.abs;
import org.apache.commons.codec.binary.Base64;
/**
* Extension to {@link LargeText} that handles annotations by
* {@link ConsoleAnnotator}.
*
* <p> In addition to run each line through
* {@link ConsoleAnnotationOutputStream} for adding markup, this class persists
* {@link ConsoleAnnotator} into a byte sequence and send it to the client as an
* HTTP header. The client JavaScript sends it back next time it fetches the
* following output.
*
* <p> The serialized {@link ConsoleAnnotator} is encrypted to avoid malicious
* clients from instantiating arbitrary {@link ConsoleAnnotator}s.
*
* @param <T> Context type.
* @author Kohsuke Kawaguchi
* @since 1.349
*/
public class AnnotatedLargeText<T> extends LargeText {
/**
* Can be null.
*/
private T context;
public AnnotatedLargeText(File file, Charset charset, boolean completed, T context) {
super(file, charset, completed);
this.context = context;
}
public AnnotatedLargeText(ByteBuffer memory, Charset charset, boolean completed, T context) {
super(memory, charset, completed);
this.context = context;
}
public void doProgressiveHtml(StaplerRequest req, StaplerResponse rsp) throws IOException {
req.setAttribute("html", true);
doProgressText(req, rsp);
}
/**
* Aliasing what I think was a wrong name in {@link LargeText}
*/
public void doProgressiveText(StaplerRequest req, StaplerResponse rsp) throws IOException {
doProgressText(req, rsp);
}
/**
* For reusing code between text/html and text/plain, we run them both
* through the same code path and use this request attribute to
* differentiate.
*/
private boolean isHtml() {
return Stapler.getCurrentRequest().getAttribute("html") != null;
}
@Override
protected void setContentType(StaplerResponse rsp) {
rsp.setContentType(isHtml() ? "text/html;charset=UTF-8" : "text/plain;charset=UTF-8");
}
private ConsoleAnnotator createAnnotator(StaplerRequest req) throws IOException {
ObjectInputStream ois = null;
try {
String base64 = req != null ? req.getHeader("X-ConsoleAnnotator") : null;
if (base64 != null) {
Cipher sym = Secret.getCipher("AES");
sym.init(Cipher.DECRYPT_MODE, Hudson.getInstance().getSecretKeyAsAES128());
ois = new ObjectInputStreamEx(new GZIPInputStream(
new CipherInputStream(new ByteArrayInputStream(Base64.decodeBase64(base64)), sym)),
Hudson.getInstance().pluginManager.uberClassLoader);
long timestamp = ois.readLong();
if (TimeUnit2.HOURS.toMillis(1) > abs(System.currentTimeMillis() - timestamp)) // don't deserialize something too old to prevent a replay attack
{
return (ConsoleAnnotator) ois.readObject();
}
}
} catch (GeneralSecurityException e) {
throw new IOException2(e);
} catch (ClassNotFoundException e) {
throw new IOException2(e);
} finally {
IOUtils.closeQuietly(ois);
}
// start from scratch
return ConsoleAnnotator.initial(context == null ? null : context.getClass());
}
@Override
public long writeLogTo(long start, Writer w) throws IOException {
if (isHtml()) {
return writeHtmlTo(start, w);
} else {
return super.writeLogTo(start, w);
}
}
@Override
public long writeLogTo(long start, OutputStream out) throws IOException {
return super.writeLogTo(start, new PlainTextConsoleOutputStream(out));
}
public long writeHtmlTo(long start, Writer w) throws IOException {
ConsoleAnnotationOutputStream caw = new ConsoleAnnotationOutputStream(
w, createAnnotator(Stapler.getCurrentRequest()), context, charset);
long r = super.writeLogTo(start, caw);
ObjectOutputStream oos = null;
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Cipher sym = Secret.getCipher("AES");
sym.init(Cipher.ENCRYPT_MODE, Hudson.getInstance().getSecretKeyAsAES128());
try {
oos = new ObjectOutputStream(new GZIPOutputStream(new CipherOutputStream(baos, sym)));
oos.writeLong(System.currentTimeMillis()); // send timestamp to prevent a replay attack
oos.writeObject(caw.getConsoleAnnotator());
} finally {
IOUtils.closeQuietly(oos);
}
StaplerResponse rsp = Stapler.getCurrentResponse();
if (rsp != null) {
rsp.setHeader("X-ConsoleAnnotator", new String(Base64.encodeBase64(baos.toByteArray())));
}
} catch (GeneralSecurityException e) {
throw new IOException2(e);
}
return r;
}
}