All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jdrupes.httpcodec.protocols.websocket.WsDecoder Maven / Gradle / Ivy

There is a newer version: 3.1.0
Show newest version
/*
 * This file is part of the JDrupes non-blocking HTTP Codec
 * Copyright (C) 2016, 2018  Michael N. Lipp
 *
 * This program is free software; you can redistribute it and/or modify it 
 * under the terms of the GNU Lesser General Public License as published
 * by the Free Software Foundation; either version 3 of the License, or 
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but 
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 
 * License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License along 
 * with this program; if not, see .
 */

package org.jdrupes.httpcodec.protocols.websocket;

import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.CoderResult;
import java.util.Optional;

import org.jdrupes.httpcodec.Decoder;
import org.jdrupes.httpcodec.ProtocolException;
import org.jdrupes.httpcodec.util.ByteBufferUtils;
import org.jdrupes.httpcodec.util.OptimizedCharsetDecoder;

/**
 * The Websocket decoder.
 */
public class WsDecoder	implements Decoder {

	private static enum State { READING_HEADER, READING_LENGTH,
		READING_MASK, READING_PAYLOAD, READING_PING_DATA,
		READING_PONG_DATA, READING_CLOSE_DATA }
	
	private static enum Opcode { CONT_FRAME, TEXT_FRAME, BIN_FRAME,
		CON_CLOSE, PING, PONG;

		public static Opcode fromInt(int value) {
			switch (value) {
			case 0: return Opcode.CONT_FRAME;
			case 1: return Opcode.TEXT_FRAME;
			case 2: return Opcode.BIN_FRAME;
			case 8: return Opcode.CON_CLOSE;
			case 9: return Opcode.PING;
			case 10: return Opcode.PONG;
			}
			throw new IllegalArgumentException();
		}
	}
	
	private static Result.Factory resultFactory = new Result.Factory();
	
	private State state = State.READING_HEADER;
	private long bytesExpected = 2;
	private boolean dataMessageFinished = true;
	private int curHeaderHead = 0;
	private byte[] maskingKey = new byte[4];
	private int maskIndex;
	private long payloadLength = 0;
	private OptimizedCharsetDecoder charDecoder = null;
	private WsFrameHeader receivedHeader = null;
	private WsFrameHeader reportedHeader = null;
	private ByteBuffer controlData = null;
	private CharBuffer controlChars = null;
	
	/**
	 * Returns the result factory for this codec.
	 * 
	 * @return the factory
	 */
	protected Result.Factory resultFactory() {
		return resultFactory;
	}
	
	/* (non-Javadoc)
	 * @see org.jdrupes.httpcodec.Decoder#decoding()
	 */
	@Override
	public Class decoding() {
		return WsFrameHeader.class;
	}

	private void expectNextFrame() {
		state = State.READING_HEADER;
		bytesExpected = 2;
		curHeaderHead = 0;
		payloadLength = 0;
		if (dataMessageFinished && charDecoder != null) {
			charDecoder.reset();
		}		
	}
	
	/* (non-Javadoc)
	 * @see org.jdrupes.httpcodec.Decoder#getHeader()
	 */
	@Override
	public Optional header() {
		return Optional.ofNullable(receivedHeader);
	}

	private Result createResult(boolean overflow, boolean underflow, 
				WsFrameHeader response, boolean responseOnly) {
		if (receivedHeader != null && receivedHeader != reportedHeader) {
			reportedHeader = receivedHeader;
			return resultFactory().newResult(overflow, underflow, false, true, 
					response, responseOnly);
		}
		return resultFactory().newResult(overflow, underflow, false, false, 
				response, responseOnly);
	}

	private Result createResult(boolean overflow, boolean underflow) {
		return createResult(overflow, underflow, null, false);
	}

	
	/* (non-Javadoc)
	 * @see RequestDecoder#decode(java.nio.ByteBuffer, java.nio.Buffer, boolean)
	 */
	@Override
	public Decoder.Result decode(ByteBuffer in, Buffer out, 
			boolean endOfInput) throws ProtocolException {
		Decoder.Result result = null;
		while (in.hasRemaining()) {
			switch (state) {
			case READING_HEADER:
				curHeaderHead = (curHeaderHead << 8) | (in.get() & 0xFF);
				if (--bytesExpected == 0) {
					payloadLength = curHeaderHead & 0x7f;
					if (payloadLength == 126) {
						payloadLength = 0;
						bytesExpected = 2;
						state = State.READING_LENGTH;
						continue; // shortcut, no need to check result
					}
					if (payloadLength == 127) {
						payloadLength = 0;
						bytesExpected = 8;
						state = State.READING_LENGTH;
						continue; // shortcut, no need to check result
					}
					if (isDataMasked()) {
						bytesExpected = 4;
						state = State.READING_MASK;
						continue; // shortcut, no need to check result
					}
					result = headerComplete();
					break;
				}
				break;
				
			case READING_LENGTH:
				payloadLength = (payloadLength << 8) | (in.get() & 0xff);
				if (--bytesExpected > 0) {
					continue; // shortcut, no need to check result
				}
				if (isDataMasked()) {
					bytesExpected = 4;
					state = State.READING_MASK;
					continue; // shortcut, no need to check result
				}
				result = headerComplete();
				break;
				
			case READING_MASK:
				maskingKey[4 - (int)bytesExpected] = in.get();
				if (--bytesExpected > 0) {
					continue; // shortcut, no need to check result
				}
				maskIndex = 0;
				result = headerComplete();
				break;
				
			case READING_PAYLOAD:
				if (out == null) {
					return createResult(true, false);
				}
				int initiallyAvailable = in.remaining();
				CoderResult decRes = copyData(out, in,
				        bytesExpected > Integer.MAX_VALUE
			                ? Integer.MAX_VALUE : (int) bytesExpected, 
			            endOfInput);
				bytesExpected -= (initiallyAvailable - in.remaining());
				if (bytesExpected == 0) {
					expectNextFrame();
					if (dataMessageFinished) {
						result = createResult(false, false);
					}
					break;
				}
				return createResult(
				        (in.hasRemaining() && !out.hasRemaining())
				                || (decRes != null && decRes.isOverflow()),
				        !in.hasRemaining()
				                || (decRes != null && decRes.isUnderflow()));

			case READING_PING_DATA:
			case READING_PONG_DATA:
				initiallyAvailable = in.remaining();
				copyData(controlData, in, (int) bytesExpected, endOfInput);
				bytesExpected -= (initiallyAvailable - in.remaining());
				if (bytesExpected == 0) {
					controlData.flip();
					if (state == State.READING_PING_DATA) {
						receivedHeader = new WsPingFrame(controlData);
						result = createResult(false, !dataMessageFinished, 
							new WsPongFrame(controlData.duplicate()), true);
						expectNextFrame();
					} else {
						receivedHeader = new WsPongFrame(controlData);
						result = createResult(false, !dataMessageFinished);
						expectNextFrame();
					}
					controlData = null;
					return result;
				}
				return createResult(false, true);
				
			case READING_CLOSE_DATA:
				if (controlData.position() < 2) {
					controlData.put(in.get());
					bytesExpected -= 1;
					if (bytesExpected == 0) {
						// Close frame with status code only
						expectNextFrame();
						return createCloseResult();
					}
					continue;
				}
				if (charDecoder == null) {
					charDecoder = new OptimizedCharsetDecoder(
					        Charset.forName("UTF-8").newDecoder());
				}
				initiallyAvailable = in.remaining();
				copyData(controlChars, in, (int) bytesExpected, endOfInput);
				bytesExpected -= (initiallyAvailable - in.remaining());
				if (bytesExpected == 0) {
					expectNextFrame();
					return createCloseResult();
				}
				return createResult(false, true);
			}
			if (result != null) {
				return result;
			}
		}
		return createResult(false, bytesExpected > 0);
	}

	private Decoder.Result headerComplete() {
		receivedHeader = null;
		reportedHeader = null;
		boolean finalFrame = isFinalFrame();
		if ((curHeaderHead >> 8 & 0x8) == 0) {
			// Not a control frame, update from FIN bit
			dataMessageFinished = finalFrame;
		}
		bytesExpected = payloadLength;
		Opcode opcode = Opcode.fromInt(curHeaderHead >> 8 & 0xf);
		switch (opcode) {
		case CONT_FRAME:
			if (bytesExpected == 0) {
				// kind of ridiculous
				expectNextFrame();
				return createResult(false, !finalFrame);
			}
			state = State.READING_PAYLOAD;
			return null;
		case TEXT_FRAME:
			if (charDecoder == null) {
				charDecoder = new OptimizedCharsetDecoder(
				        Charset.forName("UTF-8").newDecoder());
			}
			break;
		case PING:
			if (bytesExpected == 0) {
				expectNextFrame();
				receivedHeader = new WsPingFrame(null);
				return createResult(false, !dataMessageFinished, 
						new WsPongFrame(null), true);
			}
			controlData = ByteBuffer.allocate((int)bytesExpected);
			state = State.READING_PING_DATA;
			return null;
		case PONG:
			if (bytesExpected == 0) {
				expectNextFrame();
				receivedHeader = new WsPongFrame(null);
				return createResult(false, !dataMessageFinished);
			}
			controlData = ByteBuffer.allocate((int)bytesExpected);
			state = State.READING_PONG_DATA;
			return null;
		case CON_CLOSE:
			if (bytesExpected == 0) {
				receivedHeader = new WsCloseFrame(null, null);
				expectNextFrame();
				return resultFactory().newResult(false, false, true, 
						true, new WsCloseResponse(null), false);
			}
			controlData = ByteBuffer.allocate(2);
			// upper limit (reached if each byte becomes a char)
			controlChars = CharBuffer.allocate((int)bytesExpected);
			state = State.READING_CLOSE_DATA;
			return null;
		default:
			break;
		}
		receivedHeader = new WsMessageHeader(opcode == Opcode.TEXT_FRAME,
				bytesExpected > 0);
		if (bytesExpected == 0) {
			expectNextFrame();
			return createResult(false, false);
		}
		state = State.READING_PAYLOAD;
		return null;
	}
	
	private Decoder.Result createCloseResult() {
		controlData.flip();
		int status = 0;
		while (controlData.hasRemaining()) {
			status = (status << 8) | (controlData.get() & 0xff);
		}
		controlData = null;
		controlChars.flip();
		receivedHeader = new WsCloseFrame(status, controlChars);
		controlChars = null;
		return resultFactory().newResult(false, false, false, 
				true, new WsCloseResponse(status), false);
	}

	private boolean isFinalFrame() {
		return (curHeaderHead & 0x8000) != 0;
	}
	
	private boolean isDataMasked() {
		return (curHeaderHead & 0x80) != 0;
	}
	
	private CoderResult copyData(
			Buffer out, ByteBuffer in, int limit, boolean endOfInput) {
		if (out instanceof ByteBuffer) {
			if (!isDataMasked()) {
				ByteBufferUtils.putAsMuchAsPossible((ByteBuffer) out, in, limit);
				return null;
			}
			while (limit > 0 && in.hasRemaining() && out.hasRemaining()) {
				((ByteBuffer) out).put(
						(byte)(in.get() ^ maskingKey[maskIndex]));
				maskIndex = (maskIndex + 1) % 4;
				limit -= 1;
			}
			return null;
		} 
		if (out instanceof CharBuffer) {
			if (isDataMasked()) {
				ByteBuffer unmasked = ByteBuffer.allocate(1);
				CoderResult res = null;
				while (limit > 0 && in.hasRemaining() && out.hasRemaining()) {
					unmasked.put((byte)(in.get() ^ maskingKey[maskIndex]));
					maskIndex = (maskIndex + 1) % 4;
					limit -= 1;
					unmasked.flip();
					res = charDecoder.decode(unmasked, (CharBuffer)out, 
							!in.hasRemaining() && endOfInput);
					unmasked.clear();
				}
				return res;
			}
			int oldLimit = in.limit();
			try {
				if (in.remaining() > limit) {
					in.limit(in.position() + limit);
				}
				return charDecoder.decode(in, (CharBuffer)out, endOfInput);
			} finally {
				in.limit(oldLimit);
			}
		} else {
			throw new IllegalArgumentException(
			        "Only Byte- or CharBuffer are allowed.");
		}
	}

	/**
	 * Results from {@link WsDecoder} add no additional
	 * information to {@link org.jdrupes.httpcodec.Decoder.Result}. This
	 * class just provides a factory for creating concrete results.
	 * 
	 * The class is declared abstract to promote the usage of the factory
	 * method.
	 */
	public abstract static class Result
		extends Decoder.Result {

		protected Result(boolean overflow, boolean underflow,
		        boolean closeConnection, boolean headerCompleted,
		        WsFrameHeader response, boolean responseOnly) {
			super(overflow, underflow, closeConnection, headerCompleted, response,
			        responseOnly);
		}

		protected static class Factory 
			extends Decoder.Result.Factory {
			
			/**
			 * Create a new result.
			 * 
			 * @param overflow
			 *            {@code true} if the data didn't fit in the out buffer
			 * @param underflow
			 *            {@code true} if more data is expected
			 * @param closeConnection
			 *            {@code true} if the connection should be closed
			 * @param headerCompleted
			 *            {@code true} if the header has completely been decoded
			 * @param response
			 *            a response to send due to an error
			 * @param responseOnly
			 *            if the result includes a response this flag indicates
			 *            that no further processing besides sending the
			 *            response is required
			 * @return the result
			 */
			public Result newResult(boolean overflow, boolean underflow, 
					boolean closeConnection, boolean headerCompleted, 
					WsFrameHeader response, boolean responseOnly) {
				return new Result(overflow, underflow, closeConnection,
						headerCompleted, response, responseOnly) {
				};
			}
		}
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy