/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.eclipse.jetty.websocket;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.zip.DataFormatException;
import java.util.zip.Deflater;
import java.util.zip.Inflater;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.websocket.WebSocket.FrameConnection;
import org.eclipse.jetty.websocket.WebSocketParser.FrameHandler;
import android.annotation.TargetApi;
import android.os.Build;
import android.text.TextUtils;
/**
* PerMessageDeflateExtension
*
* Copyright (c) 2015 KNOWLEDGECODE
*/
public class PerMessageDeflateExtension implements Extension {
private static final Logger __log = Log.getLogger(PerMessageDeflateExtension.class.getName());
private static final byte[] TAIL_BYTES = new byte[] { 0x00, 0x00, (byte) 0xff, (byte) 0xff };
private static final int TAIL_LENGTH = TAIL_BYTES.length;
private static final int INITIAL_CAPACITY = 16384;
private static final String EXTENSION = "permessage-deflate";
private static final String CLIENT_NO_CONTEXT_TAKEOVER = "client_no_context_takeover";
private static final String SERVER_NO_CONTEXT_TAKEOVER = "server_no_context_takeover";
private static final String SERVER_MAX_WINDOW_BITS = "server_max_window_bits";
private static class Zlib {
protected Deflater _deflater;
protected Inflater _inflater;
protected byte[] _buffer;
protected int _capcacity;
protected boolean _deflaterReset;
protected boolean _inflaterReset;
public Zlib(boolean deflaterReset, boolean inflaterReset) {
_deflater = new Deflater(Deflater.DEFAULT_COMPRESSION, true);
_inflater = new Inflater(true);
_buffer = new byte[INITIAL_CAPACITY];
_capcacity = _buffer.length;
_deflaterReset = deflaterReset;
_inflaterReset = inflaterReset;
}
public static byte[] appendTailBytes(byte[] array, int offset, int length) {
byte[] buffer = Arrays.copyOfRange(array, offset, offset + length + TAIL_LENGTH);
System.arraycopy(TAIL_BYTES, 0, buffer, length, TAIL_LENGTH);
return buffer;
}
public byte[] compress(byte[] b, int offset, int length) {
int len;
int position = 0;
_deflater.reset();
_deflater.setInput(b, offset, length);
_deflater.finish();
while ((len = _deflater.deflate(_buffer, position, _capcacity - position)) > 0) {
if ((position += len) == _capcacity) {
_buffer = Arrays.copyOf(_buffer, _capcacity <<= 1);
}
}
return Arrays.copyOf(_buffer, position);
}
public byte[] decompress(byte[] b) throws DataFormatException {
return decompress(b, 0, b.length);
}
public byte[] decompress(byte[] b, int offset, int length) throws DataFormatException {
int len;
int position = 0;
if (_inflaterReset) {
_inflater.reset();
}
_inflater.setInput(b, offset, length);
while ((len = _inflater.inflate(_buffer, position, _capcacity - position)) > 0) {
if ((position += len) == _capcacity) {
_buffer = Arrays.copyOf(_buffer, _capcacity <<= 1);
}
}
return Arrays.copyOf(_buffer, position);
}
public boolean isCompressible() {
return _deflaterReset;
}
public void end() {
_deflater.end();
_inflater.end();
}
}
@TargetApi(Build.VERSION_CODES.KITKAT)
private static class NewZlib extends Zlib {
public NewZlib(boolean deflaterReset, boolean inflaterReset) {
super(deflaterReset, inflaterReset);
}
@Override
public byte[] compress(byte[] b, int offset, int length) {
int len;
int position = 0;
if (_deflaterReset) {
_deflater.reset();
}
_deflater.setInput(b, offset, length);
while ((len = _deflater.deflate(_buffer, position, _capcacity - position, Deflater.SYNC_FLUSH)) > 0) {
if ((position += len) == _capcacity) {
_buffer = Arrays.copyOf(_buffer, _capcacity <<= 1);
}
}
return Arrays.copyOf(_buffer, position - TAIL_LENGTH);
}
@Override
public boolean isCompressible() {
return true;
}
}
private FrameConnection _connection;
private FrameHandler _inbound;
private WebSocketGenerator _outbound;
private Zlib _zlib;
private String _parameters;
private boolean _compressed;
public PerMessageDeflateExtension() {
if (Build.VERSION.SDK_INT < 19) {
_parameters = TextUtils.join("; ", new String[]{ EXTENSION, CLIENT_NO_CONTEXT_TAKEOVER });
} else {
_parameters = EXTENSION;
}
_compressed = false;
}
@Override
public void onFrame(byte flags, byte opcode, byte[] array, int offset, int length) {
switch (opcode) {
case WebSocketConnectionRFC6455.OP_TEXT:
case WebSocketConnectionRFC6455.OP_BINARY:
_compressed = ((flags & 0x07) == 0x04);
case WebSocketConnectionRFC6455.OP_CONTINUATION:
if (_compressed) {
try {
byte[] buffer;
if ((flags &= 0x08) > 0) {
buffer = _zlib.decompress(Zlib.appendTailBytes(array, offset, length));
} else {
buffer = _zlib.decompress(array, offset, length);
}
_inbound.onFrame(flags, opcode, buffer, 0, buffer.length);
} catch (DataFormatException e) {
__log.warn(e);
_connection.close(WebSocketConnectionRFC6455.CLOSE_BAD_DATA, "Bad data");
}
return;
}
}
_inbound.onFrame(flags, opcode, array, offset, length);
}
@Override
public void close(int code, String message) {
_zlib.end();
}
@Override
public int flush() throws IOException {
return 0;
}
@Override
public boolean isBufferEmpty() {
return _outbound.isBufferEmpty();
}
@Override
public void addFrame(byte flags, byte opcode, byte[] content, int offset, int length) throws IOException {
if (opcode == WebSocketConnectionRFC6455.OP_TEXT || opcode == WebSocketConnectionRFC6455.OP_BINARY) {
if (_zlib.isCompressible()) {
byte[] compressed = _zlib.compress(content, offset, length);
_outbound.addFrame((byte) (flags | 0x04), opcode, compressed, 0, compressed.length);
return;
}
}
_outbound.addFrame(flags, opcode, content, offset, length);
}
@Override
public String getName() {
return EXTENSION;
}
@Override
public String getParameterizedName() {
return _parameters;
}
@Override
public boolean init(Map<String, String> parameters) {
boolean extension = false;
boolean client_no_context_takeover = false;
boolean server_no_context_takeover = false;
int server_max_window_bits = 0;
_parameters = "";
for (String key : parameters.keySet()) {
String value = parameters.get(key);
if (EXTENSION.equals(key) && TextUtils.isEmpty(value) && !extension) {
extension = true;
} else if (CLIENT_NO_CONTEXT_TAKEOVER.equals(key) && TextUtils.isEmpty(value) && !client_no_context_takeover) {
client_no_context_takeover = true;
} else if (SERVER_NO_CONTEXT_TAKEOVER.equals(key) && TextUtils.isEmpty(value) && !server_no_context_takeover) {
server_no_context_takeover = true;
} else if (SERVER_MAX_WINDOW_BITS.equals(key) && server_max_window_bits == 0) {
if (TextUtils.isEmpty(value)) {
__log.warn("Unexpected parameter: {}", key);
return false;
}
if (!value.matches("[1-9]\\d?")) {
__log.warn("Unexpected parameter: {}={}", key, value);
return false;
}
int v = Integer.parseInt(value);
if (v < 8 || v > 15) {
__log.warn("Unexpected parameter: {}={}", key, value);
return false;
}
server_max_window_bits = v;
} else if (!TextUtils.isEmpty(value)) {
__log.warn("Unexpected parameter: {}={}", key, value);
return false;
} else {
__log.warn("Unexpected parameter: {}", key);
return false;
}
}
if (extension) {
List<String> p = new ArrayList<String>();
p.add(EXTENSION);
if (client_no_context_takeover) {
p.add(CLIENT_NO_CONTEXT_TAKEOVER);
}
if (server_no_context_takeover) {
p.add(SERVER_NO_CONTEXT_TAKEOVER);
}
if (server_max_window_bits > 0) {
p.add(String.format("%s=%d", SERVER_MAX_WINDOW_BITS, server_max_window_bits));
}
_parameters = TextUtils.join("; ", p);
if (Build.VERSION.SDK_INT < 19) {
_zlib = new Zlib(client_no_context_takeover, server_no_context_takeover);
} else {
_zlib = new NewZlib(client_no_context_takeover, server_no_context_takeover);
}
}
return extension;
}
@Override
public void bind(FrameConnection connection, FrameHandler inbound, WebSocketGenerator outbound) {
_connection = connection;
_inbound = inbound;
_outbound = outbound;
}
}