Issue-52: Refactoring and adding unit tests to cover the compression functionality

This commit is contained in:
Jason Erickson 2015-09-23 15:44:07 -07:00
parent 19f6fcaa51
commit 806844a89a
9 changed files with 275 additions and 61 deletions

View File

@ -16,15 +16,29 @@
package io.jsonwebtoken; package io.jsonwebtoken;
/** /**
* CompressionCodec * Defines how to compress and decompress byte arrays.
* *
* @since 0.59 * @since 0.5.2
*/ */
public interface CompressionCodec { public interface CompressionCodec {
/**
* The algorithm name that would appear in the JWT header.
* @return the algorithm name that would appear in the JWT header
*/
String getAlgorithmName(); String getAlgorithmName();
/**
* Takes a byte array and returns a compressed version.
* @param payload bytes to compress
* @return compressed bytes
*/
byte[] compress(byte[] payload); byte[] compress(byte[] payload);
/**
* Takes a compressed byte array and returns a decompressed version.
* @param compressed compressed bytes
* @return decompressed bytes
*/
byte[] decompress(byte[] compressed); byte[] decompress(byte[] compressed);
} }

View File

@ -16,12 +16,16 @@
package io.jsonwebtoken; package io.jsonwebtoken;
/** /**
* CompressionCodecResolver * Resolves "calg" header to an implementation of CompressionCodec.
* *
* @since 0.5.2 * @since 0.5.2
*/ */
public interface CompressionCodecResolver { public interface CompressionCodecResolver {
/**
* Examines the header and returns a CompressionCodec if it finds one that it recognizes.
* @param header of the JWT
* @return CompressionCodec matching the "calg" header, or null if there is no "calg" header.
*/
CompressionCodec resolveCompressionCodec(Header header); CompressionCodec resolveCompressionCodec(Header header);
} }

View File

@ -0,0 +1,79 @@
/*
* Copyright (C) 2015 jsonwebtoken.io
*
* Licensed 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 io.jsonwebtoken.impl.compression;
import io.jsonwebtoken.CompressionCodec;
import io.jsonwebtoken.CompressionException;
import io.jsonwebtoken.lang.Assert;
import java.io.IOException;
/**
* Base class that asserts arguments and wraps IOException with CompressionException.
*
* @since 0.5.2
*/
public abstract class BaseCompressionCodec implements CompressionCodec {
/**
* Implement this method to do the actual work of compressing the payload
* @param payload the bytes to compress
* @return the compressed bytes
* @throws IOException if the compression causes an IOException
*/
protected abstract byte[] doCompress(byte[] payload) throws IOException;
/**
* Asserts that payload is not null and calls doCompress
* @param payload bytes to compress
* @return compressed bytes
* @throws CompressionException if doCompress throws an IOException
*/
@Override
public final byte[] compress(byte[] payload) {
Assert.notNull(payload, "payload cannot be null.");
try {
return doCompress(payload);
} catch (IOException e) {
throw new CompressionException("Unable to compress payload.", e);
}
}
/**
* Asserts the compressed bytes is not null and calls doDecompress
* @param compressed compressed bytes
* @return decompressed bytes
* @throws CompressionException if doCompress throws an IOException
*/
@Override
public final byte[] decompress(byte[] compressed) {
Assert.notNull(compressed, "compressed bytes cannot be null.");
try {
return doDecompress(compressed);
} catch (IOException e) {
throw new CompressionException("Unable to decompress bytes.", e);
}
}
/**
* Implement this method to do the actual work of decompressing the compressed bytes.
* @param compressed compressed bytes
* @return decompressed bytes
* @throws IOException if the decompression runs into an IO problem
*/
protected abstract byte[] doDecompress(byte[] compressed) throws IOException;
}

View File

@ -22,13 +22,16 @@ import io.jsonwebtoken.CompressionCodec;
* *
* @since 0.5.2 * @since 0.5.2
*/ */
public abstract class CompressionCodecs { public interface CompressionCodecs {
private CompressionCodecs(){} /**
* Codec implementing the <a href="https://en.wikipedia.org/wiki/DEFLATE">deflate</a> compression algorithm
public static final CompressionCodec DEFLATE = new DeflateCompressionCodec(); */
CompressionCodec DEFLATE = new DeflateCompressionCodec();
public static final CompressionCodec GZIP = new GzipCompressionCodec();
/**
* Codec implementing the <a href="https://en.wikipedia.org/wiki/Gzip">gzip</a> compression algorithm
*/
CompressionCodec GZIP = new GzipCompressionCodec();
} }

View File

@ -23,7 +23,8 @@ import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.Strings; import io.jsonwebtoken.lang.Strings;
/** /**
* DefaultCompressionCodecResolver * Default implementation of {@link CompressionCodecResolver}. This implementation will resolve DEF to
* {@link DeflateCompressionCodec} and GZIP to {@link GzipCompressionCodec}.
* *
* @since 0.5.2 * @since 0.5.2
*/ */
@ -31,22 +32,25 @@ public class DefaultCompressionCodecResolver implements CompressionCodecResolver
@Override @Override
public CompressionCodec resolveCompressionCodec(Header header) { public CompressionCodec resolveCompressionCodec(Header header) {
Assert.notNull(header, "header cannot be null."); String cmpAlg = getAlgorithmFromHeader(header);
String cmpAlg = header.getCompressionAlgorithm(); final boolean hasCompressionAlgorithm = Strings.hasText(cmpAlg);
if (!hasCompressionAlgorithm) {
if (!Strings.hasText(cmpAlg)) {
return null; return null;
} }
if (CompressionCodecs.DEFLATE.getAlgorithmName().equalsIgnoreCase(cmpAlg)) { if (CompressionCodecs.DEFLATE.getAlgorithmName().equalsIgnoreCase(cmpAlg)) {
return CompressionCodecs.DEFLATE; return CompressionCodecs.DEFLATE;
} }
if (CompressionCodecs.GZIP.getAlgorithmName().equalsIgnoreCase(cmpAlg)) { if (CompressionCodecs.GZIP.getAlgorithmName().equalsIgnoreCase(cmpAlg)) {
return CompressionCodecs.GZIP; return CompressionCodecs.GZIP;
} }
throw new CompressionException("Unsupported compression algorithm '" + cmpAlg + "'"); throw new CompressionException("Unsupported compression algorithm '" + cmpAlg + "'");
} }
protected final String getAlgorithmFromHeader(Header header) {
Assert.notNull(header, "header cannot be null.");
return header.getCompressionAlgorithm();
}
} }

View File

@ -15,9 +15,6 @@
*/ */
package io.jsonwebtoken.impl.compression; package io.jsonwebtoken.impl.compression;
import io.jsonwebtoken.CompressionCodec;
import io.jsonwebtoken.CompressionException;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.Objects; import io.jsonwebtoken.lang.Objects;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
@ -27,11 +24,10 @@ import java.util.zip.DeflaterOutputStream;
import java.util.zip.InflaterOutputStream; import java.util.zip.InflaterOutputStream;
/** /**
* DeflateCompressionCodec * Codec implementing the <a href="https://en.wikipedia.org/wiki/DEFLATE">deflate</a> compression algorithm
*
* @since 0.5.2 * @since 0.5.2
*/ */
public class DeflateCompressionCodec implements CompressionCodec { public class DeflateCompressionCodec extends BaseCompressionCodec {
private static final String DEFLATE = "DEF"; private static final String DEFLATE = "DEF";
@ -41,8 +37,7 @@ public class DeflateCompressionCodec implements CompressionCodec {
} }
@Override @Override
public byte[] compress(byte[] payload) { public byte[] doCompress(byte[] payload) throws IOException {
Assert.notNull(payload, "payload cannot be null.");
Deflater deflater = new Deflater(Deflater.BEST_COMPRESSION); Deflater deflater = new Deflater(Deflater.BEST_COMPRESSION);
@ -55,17 +50,13 @@ public class DeflateCompressionCodec implements CompressionCodec {
deflaterOutputStream.write(payload, 0, payload.length); deflaterOutputStream.write(payload, 0, payload.length);
deflaterOutputStream.flush(); deflaterOutputStream.flush();
return outputStream.toByteArray(); return outputStream.toByteArray();
} catch (IOException e) {
throw new CompressionException("Unable to compress payload.", e);
} finally { } finally {
Objects.nullSafeClose(outputStream, deflaterOutputStream); Objects.nullSafeClose(outputStream, deflaterOutputStream);
} }
} }
@Override @Override
public byte[] decompress(byte[] compressed) { public byte[] doDecompress(byte[] compressed) throws IOException {
Assert.notNull(compressed, "compressed cannot be null.");
InflaterOutputStream inflaterOutputStream = null; InflaterOutputStream inflaterOutputStream = null;
ByteArrayOutputStream decompressedOutputStream = null; ByteArrayOutputStream decompressedOutputStream = null;
@ -75,8 +66,6 @@ public class DeflateCompressionCodec implements CompressionCodec {
inflaterOutputStream.write(compressed); inflaterOutputStream.write(compressed);
inflaterOutputStream.flush(); inflaterOutputStream.flush();
return decompressedOutputStream.toByteArray(); return decompressedOutputStream.toByteArray();
} catch (IOException e) {
throw new CompressionException("Unable to decompress compressed payload.", e);
} finally { } finally {
Objects.nullSafeClose(decompressedOutputStream, inflaterOutputStream); Objects.nullSafeClose(decompressedOutputStream, inflaterOutputStream);
} }

View File

@ -16,8 +16,6 @@
package io.jsonwebtoken.impl.compression; package io.jsonwebtoken.impl.compression;
import io.jsonwebtoken.CompressionCodec; import io.jsonwebtoken.CompressionCodec;
import io.jsonwebtoken.CompressionException;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.Objects; import io.jsonwebtoken.lang.Objects;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
@ -27,11 +25,11 @@ import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream; import java.util.zip.GZIPOutputStream;
/** /**
* GzipCompressionCodec * Codec implementing the <a href="https://en.wikipedia.org/wiki/Gzip">gzip</a> compression algorithm
* *
* @since 0.5.2 * @since 0.5.2
*/ */
public class GzipCompressionCodec implements CompressionCodec { public class GzipCompressionCodec extends BaseCompressionCodec implements CompressionCodec {
private static final String GZIP = "GZIP"; private static final String GZIP = "GZIP";
@ -41,29 +39,7 @@ public class GzipCompressionCodec implements CompressionCodec {
} }
@Override @Override
public byte[] compress(byte[] payload) { protected byte[] doDecompress(byte[] compressed) throws IOException {
Assert.notNull(payload, "payload cannot be null.");
ByteArrayOutputStream outputStream = null;
GZIPOutputStream gzipOutputStream = null;
try {
outputStream = new ByteArrayOutputStream();
gzipOutputStream = new GZIPOutputStream(outputStream, true);
gzipOutputStream.write(payload, 0, payload.length);
gzipOutputStream.finish();
return outputStream.toByteArray();
} catch (IOException e) {
throw new CompressionException("Unable to compress payload.", e);
} finally {
Objects.nullSafeClose(outputStream, gzipOutputStream);
}
}
@Override
public byte[] decompress(byte[] compressed) {
Assert.notNull(compressed, "compressed cannot be null.");
byte[] buffer = new byte[512]; byte[] buffer = new byte[512];
ByteArrayOutputStream outputStream = null; ByteArrayOutputStream outputStream = null;
@ -79,10 +55,20 @@ public class GzipCompressionCodec implements CompressionCodec {
outputStream.write(buffer, 0, read); outputStream.write(buffer, 0, read);
} }
return outputStream.toByteArray(); return outputStream.toByteArray();
} catch (IOException e) {
throw new CompressionException("Unable to decompress compressed payload.", e);
} finally { } finally {
Objects.nullSafeClose(inputStream, gzipInputStream, outputStream); Objects.nullSafeClose(inputStream, gzipInputStream, outputStream);
} }
} }
protected byte[] doCompress(byte[] payload) throws IOException {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
GZIPOutputStream compressorOutputStream = new GZIPOutputStream(outputStream, true);
try {
compressorOutputStream.write(payload, 0, payload.length);
compressorOutputStream.finish();
return outputStream.toByteArray();
} finally {
Objects.nullSafeClose(compressorOutputStream, outputStream);
}
}
} }

View File

@ -20,9 +20,12 @@ import io.jsonwebtoken.impl.DefaultHeader
import io.jsonwebtoken.impl.DefaultJwsHeader import io.jsonwebtoken.impl.DefaultJwsHeader
import io.jsonwebtoken.impl.TextCodec import io.jsonwebtoken.impl.TextCodec
import io.jsonwebtoken.impl.compression.CompressionCodecs import io.jsonwebtoken.impl.compression.CompressionCodecs
import io.jsonwebtoken.impl.compression.DefaultCompressionCodecResolver
import io.jsonwebtoken.impl.compression.GzipCompressionCodec
import io.jsonwebtoken.impl.crypto.EllipticCurveProvider import io.jsonwebtoken.impl.crypto.EllipticCurveProvider
import io.jsonwebtoken.impl.crypto.MacProvider import io.jsonwebtoken.impl.crypto.MacProvider
import io.jsonwebtoken.impl.crypto.RsaProvider import io.jsonwebtoken.impl.crypto.RsaProvider
import io.jsonwebtoken.lang.Strings
import org.junit.Test import org.junit.Test
import javax.crypto.Mac import javax.crypto.Mac
@ -109,6 +112,7 @@ class JwtsTest {
def token = Jwts.parser().parse(jwt); def token = Jwts.parser().parse(jwt);
//noinspection GrEqualsBetweenInconvertibleTypes
assert token.body == claims assert token.body == claims
} }
@ -331,6 +335,27 @@ class JwtsTest {
assertNull claims.getId() assertNull claims.getId()
} }
@Test
void testUncompressedJwt() {
byte[] key = MacProvider.generateKey().getEncoded()
String id = UUID.randomUUID().toString()
String compact = Jwts.builder().setId(id).setAudience("an audience").signWith(SignatureAlgorithm.HS256, key)
.claim("state", "hello this is an amazing jwt").compact()
def jws = Jwts.parser().setSigningKey(key).parseClaimsJws(compact)
Claims claims = jws.body
assertNull jws.header.getCompressionAlgorithm()
assertEquals id, claims.getId()
assertEquals "an audience", claims.getAudience()
assertEquals "hello this is an amazing jwt", claims.state
}
@Test @Test
void testCompressedJwtWithDeflate() { void testCompressedJwtWithDeflate() {
@ -373,6 +398,58 @@ class JwtsTest {
assertEquals "hello this is an amazing jwt", claims.state assertEquals "hello this is an amazing jwt", claims.state
} }
@Test
void testCompressedWithCustomResolver() {
byte[] key = MacProvider.generateKey().getEncoded()
String id = UUID.randomUUID().toString()
String compact = Jwts.builder().setId(id).setAudience("an audience").signWith(SignatureAlgorithm.HS256, key)
.claim("state", "hello this is an amazing jwt").compressWith(new GzipCompressionCodec() {
@Override
String getAlgorithmName() {
return "CUSTOM"
}
}).compact()
def jws = Jwts.parser().setSigningKey(key).setCompressionCodecResolver(new DefaultCompressionCodecResolver() {
@Override
CompressionCodec resolveCompressionCodec(Header header) {
String algorithm = getAlgorithmFromHeader(header);
if ("CUSTOM".equals(algorithm)) {
return CompressionCodecs.GZIP
} else {
return null
}
}
}).parseClaimsJws(compact)
Claims claims = jws.body
assertEquals "CUSTOM", jws.header.getCompressionAlgorithm()
assertEquals id, claims.getId()
assertEquals "an audience", claims.getAudience()
assertEquals "hello this is an amazing jwt", claims.state
}
@Test(expected = CompressionException.class)
void testCompressedJwtWithUnrecognizedHeader() {
byte[] key = MacProvider.generateKey().getEncoded()
String id = UUID.randomUUID().toString()
String compact = Jwts.builder().setId(id).setAudience("an audience").signWith(SignatureAlgorithm.HS256, key)
.claim("state", "hello this is an amazing jwt").compressWith(new GzipCompressionCodec() {
@Override
String getAlgorithmName() {
return "CUSTOM"
}
}).compact()
Jwts.parser().setSigningKey(key).parseClaimsJws(compact)
}
@Test @Test
void testCompressStringPayloadWithDeflate() { void testCompressStringPayloadWithDeflate() {
@ -643,6 +720,7 @@ class JwtsTest {
def token = Jwts.parser().setSigningKey(key).parse(jwt); def token = Jwts.parser().setSigningKey(key).parse(jwt);
assert [alg: alg.name()] == token.header assert [alg: alg.name()] == token.header
//noinspection GrEqualsBetweenInconvertibleTypes
assert token.body == claims assert token.body == claims
} }
@ -657,6 +735,7 @@ class JwtsTest {
def token = Jwts.parser().setSigningKey(key).parse(jwt) def token = Jwts.parser().setSigningKey(key).parse(jwt)
assert token.header == [alg: alg.name()] assert token.header == [alg: alg.name()]
//noinspection GrEqualsBetweenInconvertibleTypes
assert token.body == claims assert token.body == claims
} }
@ -678,6 +757,7 @@ class JwtsTest {
def token = Jwts.parser().setSigningKey(key).parse(jwt); def token = Jwts.parser().setSigningKey(key).parse(jwt);
assert token.header == [alg: alg.name()] assert token.header == [alg: alg.name()]
//noinspection GrEqualsBetweenInconvertibleTypes
assert token.body == claims assert token.body == claims
} }
} }

View File

@ -0,0 +1,55 @@
/*
* Copyright (C) 2015 jsonwebtoken.io
*
* Licensed 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 io.jsonwebtoken.impl.compression
import io.jsonwebtoken.CompressionCodec
import io.jsonwebtoken.CompressionException
import org.junit.Test
/**
* @since 0.5.2
*/
class BaseCompressionCodecTest {
static class ExceptionThrowingCodec extends BaseCompressionCodec {
@Override
protected byte[] doCompress(byte[] payload) throws IOException {
throw new IOException("Test Exception")
}
@Override
String getAlgorithmName() {
return "Test"
}
@Override
protected byte[] doDecompress(byte[] payload) throws IOException {
throw new IOException("Test Decompress Exception");
}
}
@Test(expected = CompressionException.class)
void testCompressWithException() {
CompressionCodec codecUT = new ExceptionThrowingCodec();
codecUT.compress(new byte[0]);
}
@Test(expected = CompressionException.class)
void testDecompressWithException() {
CompressionCodec codecUT = new ExceptionThrowingCodec();
codecUT.decompress(new byte[0]);
}
}