// Copyright (c) 2011 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. package org.chromium.sdk.internal.websocket; import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.SocketAddress; import java.nio.ByteBuffer; import java.nio.charset.Charset; import org.chromium.sdk.ConnectionLogger; import org.chromium.sdk.ConnectionLogger.StreamListener; import org.chromium.sdk.internal.transport.AbstractSocketWrapper; import org.chromium.sdk.internal.websocket.ManualLoggingSocketWrapper.LoggableInput; import org.chromium.sdk.internal.websocket.ManualLoggingSocketWrapper.LoggableOutput; /** * A wrapper around platform socket that handles logging and closing. It allows user to manually * control what goes to socket and what is logged. This makes sense when protocol * is not clear-text. */ public class ManualLoggingSocketWrapper extends AbstractSocketWrapper<LoggableInput, LoggableOutput> { public static final Charset UTF_8_CHARSET = Charset.forName("UTF-8"); public ManualLoggingSocketWrapper(SocketAddress endpoint, int connectionTimeoutMs, ConnectionLogger connectionLogger, WrapperFactory<LoggableInput, LoggableOutput> wrapperFactory) throws IOException { super(endpoint, connectionTimeoutMs, connectionLogger, wrapperFactory); } /** * Provides access to incoming bytes and possibly logs traffic. */ public static abstract class LoggableInput { public abstract int readByteOrEos() throws IOException; public abstract byte[] readBytes(int length) throws IOException; public abstract ByteBuffer readUpTo0x0D0A() throws IOException; public abstract void markSeparatorForLog(); } /** * Receives outgoing bytes and possibly logs traffic. Its methods allow to manually * control what goes into socket and what goes into log. */ public static abstract class LoggableOutput { public abstract void writeAsciiString(String string) throws IOException; public abstract void writeByte(byte b) throws IOException; public abstract void writeByteNoLogging(byte b) throws IOException; public abstract void writeByteToLog(byte b) throws IOException; public abstract void writeBytes(byte[] bytes) throws IOException; public abstract void writeBytesToLog(byte[] bytes); public abstract void writeBytesNoLogging(byte[] bytes) throws IOException; /** * Write a string to log with a small string that may somehow annotate that this * string is not a clear-text out-take. */ public abstract void writeToLog(String string, String annotation) throws IOException; public abstract void markSeparatorForLog(); } public static abstract class FactoryBase implements WrapperFactory<LoggableInput, LoggableOutput> { protected static final Charset CHARSET = AbstractWsConnection.LOGGER_CHARSET; @Override public LoggableInput wrapInputStream(InputStream inputStream) { final BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream); return new LoggableInput() { @Override public ByteBuffer readUpTo0x0D0A() throws IOException { ByteBuffer buffer = ByteBuffer.allocate(20); while (true) { byte b = expectByte(); if (b == (byte) 0x0D) { break; } if (!buffer.hasRemaining()) { buffer.flip(); ByteBuffer biggerBuffer = ByteBuffer.allocate(buffer.remaining() * 2); biggerBuffer.put(buffer); buffer = biggerBuffer; } buffer.put(b); } byte b2 = expectByte(); if (b2 != (byte) 0x0A) { throw new IOException("0x0A byte expected"); } buffer.flip(); return buffer; } @Override public int readByteOrEos() throws IOException { return bufferedInputStream.read(); } private byte expectByte() throws IOException { int next = bufferedInputStream.read(); if (next == -1) { throw new IOException("Unexpected EOS"); } return (byte) next; } @Override public byte[] readBytes(int length) throws IOException { byte[] result = new byte[length]; int offset = 0; while (length > 0) { int r = bufferedInputStream.read(result, offset, length); if (r == -1) { throw new IOException("Unexpected EOS"); } length -= r; offset += r; } return result; } @Override public void markSeparatorForLog() { } }; } @Override public LoggableOutput wrapOutputStream(final OutputStream outputStream) { return new LoggableOutput() { @Override public void writeAsciiString(String string) throws IOException { outputStream.write(string.getBytes(UTF_8_CHARSET)); } @Override public void writeByte(byte b) throws IOException { outputStream.write(b); } @Override public void writeBytes(byte[] bytes) throws IOException { outputStream.write(bytes); } @Override public void writeBytesToLog(byte[] bytes) { } @Override public void writeBytesNoLogging(byte[] bytes) throws IOException { outputStream.write(bytes); } @Override public void writeToLog(String string, String annotation) throws IOException { } @Override public void writeByteNoLogging(byte b) throws IOException { outputStream.write(b); } @Override public void writeByteToLog(byte b) throws IOException { } @Override public void markSeparatorForLog() { } }; } @Override public LoggableInput wrapInputStream(final LoggableInput originalInputWrapper, final StreamListener streamListener) { return new LoggableInput() { @Override public ByteBuffer readUpTo0x0D0A() throws IOException { ByteBuffer bytes = originalInputWrapper.readUpTo0x0D0A(); String logString = new String(bytes.array(), bytes.arrayOffset(), bytes.limit(), CHARSET) + "\r\n"; streamListener.addContent(logString); return bytes; } @Override public byte[] readBytes(int length) throws IOException { byte[] bytes = originalInputWrapper.readBytes(length); String logString = new String(bytes, CHARSET); streamListener.addContent(logString); return bytes; } @Override public int readByteOrEos() throws IOException { int res = originalInputWrapper.readByteOrEos(); if (res != -1) { StringBuilder builder = new StringBuilder(4); dumpByte((byte) res, builder); streamListener.addContent(builder); } return res; } @Override public void markSeparatorForLog() { streamListener.addSeparator(); } }; } protected static abstract class OutputWrapperBase extends LoggableOutput { private final LoggableOutput originalOutputWrapper; private final StreamListener streamListener; public OutputWrapperBase(LoggableOutput originalOutputWrapper, StreamListener streamListener) { this.originalOutputWrapper = originalOutputWrapper; this.streamListener = streamListener; } @Override public void writeAsciiString(String string) throws IOException { originalOutputWrapper.writeAsciiString(string); streamListener.addContent(string); } @Override public void writeByte(byte b) throws IOException { originalOutputWrapper.writeByte(b); dumpByte(b, getStreamListener()); } @Override public void writeBytes(byte[] bytes) throws IOException { originalOutputWrapper.writeBytes(bytes); StringBuilder builder = new StringBuilder(bytes.length * 4); for (byte b : bytes) { dumpByte(b, builder); } streamListener.addContent(builder); } @Override public void markSeparatorForLog() { streamListener.addSeparator(); } protected LoggableOutput getOriginalOutputWrapper() { return originalOutputWrapper; } protected StreamListener getStreamListener() { return streamListener; } } } /** * Creates loggable input/output that logs all traffic as a non-masked ASCII text or bytes. * Does not employ annotations. */ public static final FactoryBase PLAIN_ASCII = new FactoryBase() { @Override public LoggableOutput wrapOutputStream( LoggableOutput originalOutputWrapper, StreamListener streamListener) { return new OutputWrapperBase(originalOutputWrapper, streamListener) { @Override public void writeByteToLog(byte b) throws IOException { } @Override public void writeToLog(String string, String annotation) throws IOException { } @Override public void writeByteNoLogging(byte b) throws IOException { getOriginalOutputWrapper().writeByteNoLogging(b); dumpByte(b, getStreamListener()); } @Override public void writeBytesToLog(byte[] bytes) { } @Override public void writeBytesNoLogging(byte[] bytes) throws IOException { getOriginalOutputWrapper().writeBytesNoLogging(bytes); String str = new String(bytes, CHARSET); getStreamListener().addContent(str); } }; } }; /** * Creates loggable input/output that logs all traffic as an ASCII text or bytes, or * demasked annotated text. */ public static final FactoryBase ANNOTATED = new FactoryBase() { @Override public LoggableOutput wrapOutputStream( LoggableOutput originalOutputWrapper, StreamListener streamListener) { return new OutputWrapperBase(originalOutputWrapper, streamListener) { @Override public void writeByteToLog(byte b) throws IOException { dumpByte(b, getStreamListener()); } @Override public void writeToLog(String string, String annotation) throws IOException { getStreamListener().addContent(annotation + "<" + string + ">"); } @Override public void writeBytesToLog(byte[] bytes) { StringBuilder builder = new StringBuilder(bytes.length * 4); for (byte b : bytes) { dumpByte(b, builder); } getStreamListener().addContent(builder); } @Override public void writeByteNoLogging(byte b) throws IOException { getOriginalOutputWrapper().writeByteNoLogging(b); } @Override public void writeBytesNoLogging(byte[] bytes) throws IOException { getOriginalOutputWrapper().writeBytesNoLogging(bytes); } }; } }; private static void dumpByte(byte b, StringBuilder output) { AbstractWsConnection.dumpByte(b, output); } private static void dumpByte(byte b, StreamListener streamListener) { StringBuilder builder = new StringBuilder(4); dumpByte(b, builder); streamListener.addContent(builder); } }