Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion jetty-core/jetty-server/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,12 @@
<bundle-symbolic-name>${project.groupId}.server</bundle-symbolic-name>
<spotbugs.onlyAnalyze>org.eclipse.jetty.server.*</spotbugs.onlyAnalyze>
</properties>

<dependencies>
<dependency>
<groupId>com.github.luben</groupId>
<artifactId>zstd-jni</artifactId>
<version>1.5.7-6</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-http</artifactId>
Expand All @@ -30,6 +34,10 @@
<artifactId>jetty-jmx</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.compression</groupId>
<artifactId>jetty-compression-brotli</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
Expand Down
6 changes: 4 additions & 2 deletions jetty-core/jetty-server/src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@
{
requires transitive org.eclipse.jetty.http;
requires transitive org.slf4j;

requires com.github.luben.zstd_jni;

// Only required if using JMX.
requires static org.eclipse.jetty.jmx;

requires org.eclipse.jetty.compression.brotli;

exports org.eclipse.jetty.server;
exports org.eclipse.jetty.server.handler;
exports org.eclipse.jetty.server.handler.gzip;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//

package org.eclipse.jetty.server;

import java.net.URI;
import java.net.URISyntaxException;
import java.nio.ByteBuffer;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.Base64.Encoder;

/**
* This class implements the Compression Dictionary as described in RFC 9842.
*
* @author arsenal
*/
public class CompressionDictionary
{
public static enum Type
{
RAW("raw"),
DCZ("dcz"),
DCB("dcb");

private String name;

Type(String name)
{
this.name = name;
}

public String getType()
{
return this.name;
}
}

private String match;
private StringBuilder matchDestList;
private String id;
private Type type;
private ByteBuffer dictionary;
private String dictDigestBase64;
private byte[] dictDigest;

/**
* Creates instance of Compression Directory
*
* @param match {@code String} value that provides the URL Pattern to use for request matching
* @param dictionary {@code ByteBuffer} content of compression directory
* @throws URISyntaxException
* @throws NoSuchAlgorithmException
*/
public CompressionDictionary(String match, ByteBuffer dictionary, Type type) throws URISyntaxException, NoSuchAlgorithmException
{
// URL is used for percent-encoded version of math
URI path = new URI(null, null, match, null);
this.match = path.toASCIIString();
this.matchDestList = new StringBuilder();
this.dictDigest = calcDictDigest(dictionary);
this.dictDigestBase64 = calcDictDigestBase64(this.dictDigest);
this.dictionary = dictionary;
this.type = type;
}

public synchronized byte[] getDictDigest()
{
return this.dictDigest;
}

public synchronized String getDictDigestBase64()
{
return this.dictDigestBase64;
}

/**
* Get a compression dictionary data
*
* @return compression dictionary {@code ByteBuffer}
*/
public synchronized ByteBuffer getDictionary()
{
return this.dictionary;
}

/**
* Add a match-dest value of a compression dictionary
*
* @param matchDest {@code String} The "match-dest" value of the "Use-As-Dictionary" response header
*/
public synchronized void addMatchDest(String matchDest)
{
if (matchDestList.length() == 0)
{
matchDestList.append('"' + matchDest + '"');
}
else
{
matchDestList.append(" \"" + matchDest + '"');
}
}

public synchronized boolean setId(String id)
{
if (id.length() > 1024)
throw new IllegalArgumentException();
this.id = id;

return true;
}

public synchronized void setType(Type type)
{
this.type = type;
}

/**
* Get a match value that provides the URL Pattern to use for request matching
*
* @return match {@code String} value
*/
public synchronized String getMatch()
{
return this.match;
}

public synchronized String getMatchDest()
{
return this.matchDestList.toString();
}

public synchronized String getId()
{
return this.id;
}

public synchronized String getType()
{
return this.type.getType();
}

/**
* Calculate SHA-256 digest of a compression dictionary in Base64 format
*
* @param dict A dictionary to calculate digest for
* @return {@code String} of dictionary digest
* @throws NoSuchAlgorithmException
*/
private String calcDictDigestBase64(byte[] sha256Digest)
{
Encoder base64Enc = Base64.getEncoder();
return base64Enc.encodeToString(sha256Digest);
}

/**
* Calculate SHA-256 digest of a compression dictionary
*
* @param dict A dictionary to calculate digest for
* @return {@code byte[]} of dictionary digest
* @throws NoSuchAlgorithmException
*/
private byte[] calcDictDigest(ByteBuffer dict) throws NoSuchAlgorithmException
{
return MessageDigest.getInstance("SHA-256").digest(dict.array());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//

package org.eclipse.jetty.server.handler;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.PathMatcher;
import java.nio.file.Paths;
import java.util.regex.PatternSyntaxException;

import com.github.luben.zstd.ZstdOutputStream;
import org.eclipse.jetty.compression.brotli.BrotliCompression;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.server.CompressionDictionary;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.util.Callback;

public class CompressionDictHandler extends Handler.Abstract
{
private CompressionDictionary dict;
private PathMatcher pattern;
private BrotliCompression bc = new BrotliCompression();
private byte[] testPage;
private static final byte[] ZSTD_MAGIC_NUMBER = {0x5e, 0x2a, 0x4d, 0x18, 0x20, 0x00, 0x00, 0x00};
private static final byte[] BR_MAGIC_NUMBER = {(byte)0xff, 0x44, 0x43, 0x42};

public CompressionDictHandler(CompressionDictionary dict, byte[] testPage) throws PatternSyntaxException, IOException
{
this.dict = dict;
this.testPage = testPage;
// It is the best matches URLPattern behavior;
FileSystem fs = FileSystems.getDefault();
this.pattern = fs.getPathMatcher("glob:**" + dict.getMatch());
}

@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
if (pattern.matches(Paths.get(request.getHttpURI().getPath())) &&
!request.getHeaders().contains("Available-Dictionary") &&
request.getMethod() == HttpMethod.GET.toString())
{
try (ByteArrayOutputStream buff = new ByteArrayOutputStream())
{
ByteBuffer compressedData = compressDataBrotli(dict.getDictionary().array());
String match = String.format("match=\"%s\"", dict.getMatch());
response.getHeaders().add("Use-As-Dictionary", match);
response.getHeaders().add("Content-Encoding", "br");
response.getHeaders().add("Content-type", "text/html");
response.getHeaders().add("Cache-Control", "public, max-age=31536000");
response.write(true, compressedData, callback);
callback.succeeded();
return true;
}
catch (Exception e)
{
e.printStackTrace();
Response.writeError(request, response, callback, 0);
return true;
}
}
else
{
try
{
if (pattern.matches(Paths.get(request.getHttpURI().getPath())) &&
dict.getDictDigestBase64().equals(getClientDictHash(request)))
{
String dictType = dict.getType();
switch (dictType)
{
case "dcz":
ByteBuffer compressedData = compressDataDcz(dict, testPage);
response.getHeaders().add("Content-Encoding", dictType);
response.getHeaders().add("Content-type", "text/html");
response.write(true, compressedData, callback);
break;
case "dcb":
compressDataBcz(dict, BR_MAGIC_NUMBER);
break;
default:
response.getHeaders().add("Content-Encoding", "br");
response.getHeaders().add("Content-type", "text/html");
response.write(true, ByteBuffer.wrap(testPage), callback);
break;
}
callback.succeeded();
return true;
}
}
catch (Exception e)
{
e.printStackTrace();
Response.writeError(request, response, callback, 0);
return true;
}
}
return false;
}

private String getClientDictHash(Request request)
{
return request.getHeaders().get("Available-Dictionary").replaceAll(":", "");
}

private ByteBuffer compressDataDcz(CompressionDictionary dictionary, byte[] data) throws IOException
{
try (ByteArrayOutputStream buff = new ByteArrayOutputStream();)
{
buff.write(ZSTD_MAGIC_NUMBER);
buff.write(dict.getDictDigest());
ZstdOutputStream compressor = new ZstdOutputStream(buff);
compressor.setDict(dictionary.getDictionary().array());
compressor.write(data);
compressor.close();
return ByteBuffer.wrap(buff.toByteArray());
}
}

// TODO
private ByteBuffer compressDataBcz(CompressionDictionary dictionary, byte[] data)
{
return null;
}

private ByteBuffer compressDataBrotli(byte[] data) throws IOException
{
try (ByteArrayOutputStream buff = new ByteArrayOutputStream())
{
OutputStream compressor = bc.newEncoderOutputStream(buff);
compressor.write(data);
compressor.close();
return ByteBuffer.wrap(buff.toByteArray());
}
}
}