package com.github.dockerjava.netty.handler; import java.util.Arrays; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import com.github.dockerjava.api.async.ResultCallback; import com.github.dockerjava.api.model.Frame; import com.github.dockerjava.api.model.StreamType; /** * Handler that decodes a docker-raw-stream as described here: * * https://docs.docker.com/engine/reference/api/docker_remote_api_v1.21/#attach-to-a-container * * It drives the {@link ResultCallback#onNext(Object)} method of the passed {@link ResultCallback}. * * @author Marcus Linke */ public class FramedResponseStreamHandler extends SimpleChannelInboundHandler<ByteBuf> { private static final int HEADER_SIZE = 8; private final ByteBuf rawBuffer = Unpooled.buffer(1000); private byte[] header = new byte[HEADER_SIZE]; private int headerCnt = 0; private byte[] payload = new byte[0]; private int payloadCnt = 0; private ResultCallback<Frame> resultCallback; private StreamType streamType = null; public FramedResponseStreamHandler(ResultCallback<Frame> resultCallback) { this.resultCallback = resultCallback; } @Override protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception { rawBuffer.writeBytes(msg, 0, msg.readableBytes()); Frame frame = null; do { frame = decode(); if (frame != null) { resultCallback.onNext(frame); } } while (frame != null); } private int read(byte[] buf, int offset, int length) { length = Math.min(rawBuffer.readableBytes(), length); rawBuffer.readBytes(buf, offset, length); rawBuffer.discardReadBytes(); return length; } private Frame decode() { if (headerCnt < HEADER_SIZE) { int headerCount = read(header, headerCnt, HEADER_SIZE - headerCnt); if (headerCount == 0) { return null; } headerCnt += headerCount; streamType = streamType(header[0]); if (streamType.equals(StreamType.RAW)) { return new Frame(streamType, Arrays.copyOf(header, headerCount)); } if (headerCnt < HEADER_SIZE) { return null; } } if (streamType.equals(StreamType.RAW)) { if (payloadCnt == 0) { payload = new byte[rawBuffer.readableBytes()]; } int count = read(payload, payloadCnt, rawBuffer.readableBytes()); if (count == 0) { return null; } payloadCnt = 0; return new Frame(StreamType.RAW, payload); } else { int payloadSize = ((header[4] & 0xff) << 24) + ((header[5] & 0xff) << 16) + ((header[6] & 0xff) << 8) + (header[7] & 0xff); if (payloadCnt == 0) { payload = new byte[payloadSize]; } int count = read(payload, payloadCnt, payloadSize - payloadCnt); if (count == 0) { return null; } payloadCnt += count; if (payloadCnt < payloadSize) { return null; } headerCnt = 0; payloadCnt = 0; return new Frame(streamType, payload); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { resultCallback.onError(cause); ctx.close(); } private static StreamType streamType(byte streamType) { switch (streamType) { case 0: return StreamType.STDIN; case 1: return StreamType.STDOUT; case 2: return StreamType.STDERR; default: return StreamType.RAW; } } }