clean up util classes, add cookie signing helper class

This commit is contained in:
Jörg Prante 2019-08-06 16:33:18 +02:00
parent 3d944e417e
commit 816266e2b2
30 changed files with 396 additions and 608 deletions

View file

@ -1,323 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE module PUBLIC
"-//Puppy Crawl//DTD Check Configuration 1.3//EN"
"http://www.puppycrawl.com/dtds/configuration_1_3.dtd">
<!-- This is a checkstyle configuration file. For descriptions of
what the following rules do, please see the checkstyle configuration
page at http://checkstyle.sourceforge.net/config.html -->
<module name="Checker">
<module name="FileTabCharacter">
<!-- Checks that there are no tab characters in the file.
-->
</module>
<module name="NewlineAtEndOfFile">
<property name="lineSeparator" value="lf"/>
</module>
<module name="RegexpSingleline">
<!-- Checks that FIXME is not used in comments. TODO is preferred.
-->
<property name="format" value="((//.*)|(\*.*))FIXME" />
<property name="message" value='TODO is preferred to FIXME. e.g. "TODO(johndoe): Refactor when v2 is released."' />
</module>
<module name="RegexpSingleline">
<!-- Checks that TODOs are named. (Actually, just that they are followed
by an open paren.)
-->
<property name="format" value="((//.*)|(\*.*))TODO[^(]" />
<property name="message" value='All TODOs should be named. e.g. "TODO(johndoe): Refactor when v2 is released."' />
</module>
<module name="JavadocPackage">
<!-- Checks that each Java package has a Javadoc file used for commenting.
Only allows a package-info.java, not package.html. -->
</module>
<!-- All Java AST specific tests live under TreeWalker module. -->
<module name="TreeWalker">
<!--
IMPORT CHECKS
-->
<module name="RedundantImport">
<!-- Checks for redundant import statements. -->
<property name="severity" value="error"/>
</module>
<module name="ImportOrder">
<!-- Checks for out of order import statements. -->
<property name="severity" value="warning"/>
<property name="groups" value="com,io,junit,net,org,javax,java"/>
<!-- This ensures that static imports go last. -->
<property name="option" value="under"/>
<property name="tokens" value="IMPORT, STATIC_IMPORT"/>
</module>
<!--
JAVADOC CHECKS
-->
<!-- Checks for Javadoc comments. -->
<!-- See http://checkstyle.sf.net/config_javadoc.html -->
<module name="JavadocMethod">
<property name="scope" value="protected"/>
<property name="severity" value="warning"/>
<property name="allowMissingJavadoc" value="true"/>
<property name="allowMissingParamTags" value="true"/>
<property name="allowMissingReturnTag" value="true"/>
<property name="allowMissingThrowsTags" value="true"/>
<property name="allowThrowsTagsForSubclasses" value="true"/>
<property name="allowUndeclaredRTE" value="true"/>
</module>
<module name="JavadocType">
<property name="scope" value="protected"/>
<property name="severity" value="error"/>
</module>
<module name="JavadocStyle">
<property name="severity" value="warning"/>
</module>
<!--
NAMING CHECKS
-->
<!-- Item 38 - Adhere to generally accepted naming conventions -->
<module name="PackageName">
<!-- Validates identifiers for package names against the
supplied expression. -->
<!-- Here the default checkstyle rule restricts package name parts to
seven characters, this is not in line with common practice at Google.
-->
<property name="format" value="^[a-z]+(\.[a-z][a-z0-9]{1,})*$"/>
<property name="severity" value="warning"/>
</module>
<module name="TypeNameCheck">
<!-- Validates static, final fields against the
expression "^[A-Z][a-zA-Z0-9]*$". -->
<metadata name="altname" value="TypeName"/>
<property name="severity" value="warning"/>
</module>
<module name="ConstantNameCheck">
<!-- Validates non-private, static, final fields against the supplied
public/package final fields "^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$". -->
<metadata name="altname" value="ConstantName"/>
<property name="applyToPublic" value="true"/>
<property name="applyToProtected" value="true"/>
<property name="applyToPackage" value="true"/>
<property name="applyToPrivate" value="false"/>
<property name="format" value="^([A-Z][A-Z0-9]*(_[A-Z0-9]+)*|FLAG_.*)$"/>
<message key="name.invalidPattern"
value="Variable ''{0}'' should be in ALL_CAPS (if it is a constant) or be private (otherwise)."/>
<property name="severity" value="warning"/>
</module>
<module name="StaticVariableNameCheck">
<!-- Validates static, non-final fields against the supplied
expression "^[a-z][a-zA-Z0-9]*_?$". -->
<metadata name="altname" value="StaticVariableName"/>
<property name="applyToPublic" value="true"/>
<property name="applyToProtected" value="true"/>
<property name="applyToPackage" value="true"/>
<property name="applyToPrivate" value="true"/>
<property name="format" value="^[a-z][a-zA-Z0-9]*_?$"/>
<property name="severity" value="warning"/>
</module>
<module name="MemberNameCheck">
<!-- Validates non-static members against the supplied expression. -->
<metadata name="altname" value="MemberName"/>
<property name="applyToPublic" value="true"/>
<property name="applyToProtected" value="true"/>
<property name="applyToPackage" value="true"/>
<property name="applyToPrivate" value="true"/>
<property name="format" value="^[a-z][a-zA-Z0-9]*$"/>
<property name="severity" value="warning"/>
</module>
<module name="MethodNameCheck">
<!-- Validates identifiers for method names. -->
<metadata name="altname" value="MethodName"/>
<property name="format" value="^[a-z][a-zA-Z0-9]*(_[a-zA-Z0-9]+)*$"/>
<property name="severity" value="warning"/>
</module>
<module name="ParameterName">
<!-- Validates identifiers for method parameters against the
expression "^[a-z][a-zA-Z0-9]*$". -->
<property name="severity" value="warning"/>
</module>
<module name="LocalFinalVariableName">
<!-- Validates identifiers for local final variables against the
expression "^[a-z][a-zA-Z0-9]*$". -->
<property name="severity" value="warning"/>
</module>
<module name="LocalVariableName">
<!-- Validates identifiers for local variables against the
expression "^[a-z][a-zA-Z0-9]*$". -->
<property name="severity" value="warning"/>
</module>
<!--
LENGTH and CODING CHECKS
-->
<module name="LineLength">
<!-- Checks if a line is too long. -->
<property name="max" value="${com.puppycrawl.tools.checkstyle.checks.sizes.LineLength.max}" default="128"/>
<property name="severity" value="error"/>
<!--
The default ignore pattern exempts the following elements:
- import statements
- long URLs inside comments
-->
<property name="ignorePattern"
value="${com.puppycrawl.tools.checkstyle.checks.sizes.LineLength.ignorePattern}"
default="^(package .*;\s*)|(import .*;\s*)|( *(\*|//).*https?://.*)$"/>
</module>
<module name="LeftCurly">
<!-- Checks for placement of the left curly brace ('{'). -->
<property name="severity" value="warning"/>
</module>
<module name="RightCurly">
<!-- Checks right curlies on CATCH, ELSE, and TRY blocks are on
the same line. e.g., the following example is fine:
<pre>
if {
...
} else
</pre>
-->
<!-- This next example is not fine:
<pre>
if {
...
}
else
</pre>
-->
<property name="option" value="same"/>
<property name="severity" value="warning"/>
</module>
<!-- Checks for braces around if and else blocks -->
<module name="NeedBraces">
<property name="severity" value="warning"/>
<property name="tokens" value="LITERAL_IF, LITERAL_ELSE, LITERAL_FOR, LITERAL_WHILE, LITERAL_DO"/>
</module>
<module name="UpperEll">
<!-- Checks that long constants are defined with an upper ell.-->
<property name="severity" value="error"/>
</module>
<module name="FallThrough">
<!-- Warn about falling through to the next case statement. Similar to
javac -Xlint:fallthrough, but the check is suppressed if a single-line comment
on the last non-blank line preceding the fallen-into case contains 'fall through' (or
some other variants which we don't publicized to promote consistency).
-->
<property name="reliefPattern"
value="fall through|Fall through|fallthru|Fallthru|falls through|Falls through|fallthrough|Fallthrough|No break|NO break|no break|continue on"/>
<property name="severity" value="error"/>
</module>
<!--
MODIFIERS CHECKS
-->
<module name="ModifierOrder">
<!-- Warn if modifier order is inconsistent with JLS3 8.1.1, 8.3.1, and
8.4.3. The prescribed order is:
public, protected, private, abstract, static, final, transient, volatile,
synchronized, native, strictfp
-->
</module>
<!--
WHITESPACE CHECKS
-->
<module name="WhitespaceAround">
<!-- Checks that various tokens are surrounded by whitespace.
This includes most binary operators and keywords followed
by regular or curly braces.
-->
<property name="tokens" value="ASSIGN, BAND, BAND_ASSIGN, BOR,
BOR_ASSIGN, BSR, BSR_ASSIGN, BXOR, BXOR_ASSIGN, COLON, DIV, DIV_ASSIGN,
EQUAL, GE, GT, LAND, LE, LITERAL_CATCH, LITERAL_DO, LITERAL_ELSE,
LITERAL_FINALLY, LITERAL_FOR, LITERAL_IF, LITERAL_RETURN,
LITERAL_SYNCHRONIZED, LITERAL_TRY, LITERAL_WHILE, LOR, LT, MINUS,
MINUS_ASSIGN, MOD, MOD_ASSIGN, NOT_EQUAL, PLUS, PLUS_ASSIGN, QUESTION,
SL, SL_ASSIGN, SR_ASSIGN, STAR, STAR_ASSIGN"/>
<property name="severity" value="error"/>
</module>
<module name="WhitespaceAfter">
<!-- Checks that commas, semicolons and typecasts are followed by
whitespace.
-->
<property name="tokens" value="COMMA, SEMI, TYPECAST"/>
</module>
<module name="NoWhitespaceAfter">
<!-- Checks that there is no whitespace after various unary operators.
Linebreaks are allowed.
-->
<property name="tokens" value="BNOT, DEC, DOT, INC, LNOT, UNARY_MINUS,
UNARY_PLUS"/>
<property name="allowLineBreaks" value="true"/>
<property name="severity" value="error"/>
</module>
<module name="NoWhitespaceBefore">
<!-- Checks that there is no whitespace before various unary operators.
Linebreaks are allowed.
-->
<property name="tokens" value="SEMI, DOT, POST_DEC, POST_INC"/>
<property name="allowLineBreaks" value="true"/>
<property name="severity" value="error"/>
</module>
<module name="ParenPad">
<!-- Checks that there is no whitespace before close parens or after
open parens.
-->
<property name="severity" value="warning"/>
</module>
</module>
</module>

View file

@ -1,6 +1,6 @@
group = org.xbib
name = netty-http
version = 4.1.36.7
version = 4.1.36.8
# main packages
netty.version = 4.1.36.Final
@ -18,14 +18,13 @@ reactivestreams.version = 1.0.2
# rest
xbib-guice.version = 4.0.4
# xmlrpc-client
commons-httpclient.version = 3.1
# test packages
junit.version = 5.4.2
junit4.version = 4.12
conscrypt.version = 2.0.0
jackson.version = 2.8.11.1
jackson.version = 2.9.9
# doc
asciidoclet.version = 1.5.4
org.gradle.warning.mode = all

View file

@ -1,6 +1,6 @@
#Mon Apr 22 17:45:04 CEST 2019
#Tue Aug 06 15:30:36 CEST 2019
distributionUrl=https\://services.gradle.org/distributions/gradle-5.3.1-all.zip
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.3-all.zip
zipStoreBase=GRADLE_USER_HOME

View file

@ -34,7 +34,7 @@ import org.xbib.netty.http.client.transport.HttpTransport;
import org.xbib.netty.http.client.transport.Transport;
import org.xbib.netty.http.common.HttpAddress;
import org.xbib.netty.http.common.NetworkUtils;
import org.xbib.netty.http.common.SecurityUtil;
import org.xbib.netty.http.common.security.SecurityUtil;
import javax.net.ssl.SNIHostName;
import javax.net.ssl.SNIServerName;

View file

@ -10,7 +10,7 @@ import io.netty.handler.ssl.CipherSuiteFilter;
import io.netty.handler.ssl.SslProvider;
import org.xbib.netty.http.client.retry.BackOff;
import org.xbib.netty.http.common.HttpAddress;
import org.xbib.netty.http.common.SecurityUtil;
import org.xbib.netty.http.common.security.SecurityUtil;
import javax.net.ssl.TrustManagerFactory;
import java.io.InputStream;

View file

@ -4,7 +4,8 @@ import org.xbib.netty.http.common.cookie.Cookie;
import org.xbib.netty.http.common.cookie.CookieDecoder;
import org.xbib.netty.http.common.cookie.CookieHeaderNames;
import org.xbib.netty.http.common.cookie.DefaultCookie;
import org.xbib.netty.http.common.util.DateTimeUtils;
import org.xbib.netty.http.common.cookie.SameSite;
import org.xbib.netty.http.common.util.DateTimeUtil;
import java.time.Instant;
import java.util.Locale;
@ -42,7 +43,7 @@ public final class ClientCookieDecoder extends CookieDecoder {
public Cookie decode(String header) {
final int headerLen = Objects.requireNonNull(header, "header").length();
if (headerLen == 0) {
return null;
throw new IllegalArgumentException("header length is 0");
}
CookieBuilder cookieBuilder = null;
int i = 0;
@ -116,7 +117,10 @@ public final class ClientCookieDecoder extends CookieDecoder {
}
}
}
return cookieBuilder != null ? cookieBuilder.cookie() : null;
if (cookieBuilder == null) {
throw new IllegalArgumentException("no cookie found");
}
return cookieBuilder.cookie();
}
private static class CookieBuilder {
@ -139,7 +143,7 @@ public final class ClientCookieDecoder extends CookieDecoder {
private boolean httpOnly;
private Cookie.SameSite sameSite = Cookie.SameSite.STRICT;
private SameSite sameSite = SameSite.STRICT;
CookieBuilder(DefaultCookie cookie, String header) {
this.cookie = cookie;
@ -208,7 +212,7 @@ public final class ClientCookieDecoder extends CookieDecoder {
if (maxAge != Long.MIN_VALUE) {
return maxAge;
} else if (isValueDefined(expiresStart, expiresEnd)) {
Instant expiresDate = DateTimeUtils.parseDate(header, expiresStart, expiresEnd);
Instant expiresDate = DateTimeUtil.parseDate(header, expiresStart, expiresEnd);
if (expiresDate != null) {
Instant now = Instant.now();
long maxAgeMillis = expiresDate.toEpochMilli() - now.toEpochMilli();
@ -233,7 +237,7 @@ public final class ClientCookieDecoder extends CookieDecoder {
} else if (header.regionMatches(true, nameStart, CookieHeaderNames.SAMESITE, 0, 8)) {
String string = computeValue(valueStart, valueEnd);
if (string != null) {
setSameSite(Cookie.SameSite.valueOf(string.toUpperCase(Locale.ROOT)));
setSameSite(SameSite.valueOf(string.toUpperCase(Locale.ROOT)));
}
}
}
@ -246,7 +250,7 @@ public final class ClientCookieDecoder extends CookieDecoder {
return isValueDefined(valueStart, valueEnd) ? header.substring(valueStart, valueEnd) : null;
}
private void setSameSite(Cookie.SameSite value) {
private void setSameSite(SameSite value) {
sameSite = value;
}
}

View file

@ -3,7 +3,8 @@ package org.xbib.netty.http.client.test.cookie;
import org.junit.jupiter.api.Test;
import org.xbib.netty.http.client.cookie.ClientCookieDecoder;
import org.xbib.netty.http.common.cookie.Cookie;
import org.xbib.netty.http.common.util.DateTimeUtils;
import org.xbib.netty.http.common.cookie.SameSite;
import org.xbib.netty.http.common.util.DateTimeUtil;
import java.time.ZoneId;
import java.time.ZonedDateTime;
@ -24,7 +25,7 @@ class ClientCookieDecoderTest {
void testDecodingSingleCookieV0() {
long millis = System.currentTimeMillis() + 50000;
String cookieString = "myCookie=myValue;expires=" +
DateTimeUtils.formatMillis(millis) +
DateTimeUtil.formatMillis(millis) +
";path=/apathsomewhere;domain=.adomainsomewhere;secure;";
Cookie cookie = ClientCookieDecoder.STRICT.decode(cookieString);
assertNotNull(cookie);
@ -55,8 +56,8 @@ class ClientCookieDecoderTest {
String cookieString = "myCookie=myValue;max-age=50;path=/apathsomewhere;domain=.adomainsomewhere" +
";secure;comment=this is a comment;version=1;";
Cookie cookie = ClientCookieDecoder.STRICT.decode(cookieString);
assertEquals("myValue", cookie.value());
assertNotNull(cookie);
assertEquals("myValue", cookie.value());
assertEquals(".adomainsomewhere", cookie.domain());
assertEquals(50, cookie.maxAge());
assertEquals("/apathsomewhere", cookie.path());
@ -134,6 +135,7 @@ class ClientCookieDecoderTest {
"__utmz=48461872.1258140131.1.1.utmcsr=overstock.com|utmccn=(referral)|" +
"utmcmd=referral|utmcct=/Home-Garden/Furniture/Clearance,/clearance,/32/dept.html";
Cookie cookie = ClientCookieDecoder.STRICT.decode(source);
assertNotNull(cookie);
assertEquals("ARPT", cookie.name());
assertEquals("LWUKQPSWRTUN04CKKJI", cookie.value());
}
@ -144,6 +146,7 @@ class ClientCookieDecoderTest {
long expectedMaxAge = ((zonedDateTime.toEpochSecond() * 1000L) - System.currentTimeMillis()) / 1000;
String source = "Format=EU; expires=Fri, 31-Dec-2100 23:59:59 GMT; path=/";
Cookie cookie = ClientCookieDecoder.STRICT.decode(source);
assertNotNull(cookie);
assertTrue(Math.abs(expectedMaxAge - cookie.maxAge()) < 2);
}
@ -159,6 +162,7 @@ class ClientCookieDecoderTest {
void testDecodingWeirdNames1() {
String src = "path=; expires=Mon, 01-Jan-1990 00:00:00 GMT; path=/; domain=.www.google.com";
Cookie cookie = ClientCookieDecoder.STRICT.decode(src);
assertNotNull(cookie);
assertEquals("path", cookie.name());
assertEquals("", cookie.value());
assertEquals("/", cookie.path());
@ -168,15 +172,14 @@ class ClientCookieDecoderTest {
void testDecodingWeirdNames2() {
String src = "HTTPOnly=";
Cookie cookie = ClientCookieDecoder.STRICT.decode(src);
assertNotNull(cookie);
assertEquals("HTTPOnly", cookie.name());
assertEquals("", cookie.value());
}
@Test
void testDecodingValuesWithCommasAndEqualsFails() {
String src = "A=v=1&lg=en-US,it-IT,it&intl=it&np=1;T=z=E";
Cookie cookie = ClientCookieDecoder.STRICT.decode(src);
assertNull(cookie);
assertNull(ClientCookieDecoder.STRICT.decode( "A=v=1&lg=en-US,it-IT,it&intl=it&np=1;T=z=E"));
}
@Test
@ -226,6 +229,7 @@ class ClientCookieDecoderTest {
"%=KqtH_$?mi____'=KqtH_$?mx____'=KqtH_$D7]____#=J_#p_$D@T____#=J_#p_$V<g____" +
"'=KqtH";
Cookie cookie = ClientCookieDecoder.STRICT.decode("bh=\"" + longValue + "\";");
assertNotNull(cookie);
assertEquals("bh", cookie.name());
assertEquals(longValue, cookie.value());
}
@ -234,6 +238,7 @@ class ClientCookieDecoderTest {
void testIgnoreEmptyDomain() {
String emptyDomain = "sessionid=OTY4ZDllNTgtYjU3OC00MWRjLTkzMWMtNGUwNzk4MTY0MTUw;Domain=;Path=/";
Cookie cookie = ClientCookieDecoder.STRICT.decode(emptyDomain);
assertNotNull(cookie);
assertNull(cookie.domain());
}
@ -241,6 +246,7 @@ class ClientCookieDecoderTest {
void testIgnoreEmptyPath() {
String emptyPath = "sessionid=OTY4ZDllNTgtYjU3OC00MWRjLTkzMWMtNGUwNzk4MTY0MTUw;Domain=;Path=";
Cookie cookie = ClientCookieDecoder.STRICT.decode(emptyPath);
assertNotNull(cookie);
assertNull(cookie.path());
}
@ -248,20 +254,23 @@ class ClientCookieDecoderTest {
void testSameSiteStrict() {
String sameSite = "sessionid=OTY4ZDllNTgtYjU3OC00MWRjLTkzMWMtNGUwNzk4MTY0MTUw;SameSite=Strict";
Cookie cookie = ClientCookieDecoder.STRICT.decode(sameSite);
assertEquals(Cookie.SameSite.STRICT, cookie.sameSite());
assertNotNull(cookie);
assertEquals(SameSite.STRICT, cookie.sameSite());
}
@Test
void testSameSiteLax() {
String sameSite = "sessionid=OTY4ZDllNTgtYjU3OC00MWRjLTkzMWMtNGUwNzk4MTY0MTUw;SameSite=Lax";
Cookie cookie = ClientCookieDecoder.STRICT.decode(sameSite);
assertEquals(Cookie.SameSite.LAX, cookie.sameSite());
assertNotNull(cookie);
assertEquals(SameSite.LAX, cookie.sameSite());
}
@Test
void testEmptySameSite() {
String sameSite = "sessionid=OTY4ZDllNTgtYjU3OC00MWRjLTkzMWMtNGUwNzk4MTY0MTUw;SameSite=";
Cookie cookie = ClientCookieDecoder.STRICT.decode(sameSite);
assertEquals(Cookie.SameSite.STRICT, cookie.sameSite());
assertNotNull(cookie);
assertEquals(SameSite.STRICT, cookie.sameSite());
}
}

View file

@ -38,8 +38,7 @@ class ClientCookieEncoderTest {
@Test
void testRejectCookieValueWithSemicolon() {
assertThrows(IllegalArgumentException.class, () -> {
ClientCookieEncoder.STRICT.encode(new DefaultCookie("myCookie", "foo;bar"));
});
assertThrows(IllegalArgumentException.class, () ->
ClientCookieEncoder.STRICT.encode(new DefaultCookie("myCookie", "foo;bar")));
}
}

View file

@ -147,6 +147,4 @@ public interface Cookie extends Comparable<Cookie> {
* @param sameSite the same site value
*/
void setSameSite(SameSite sameSite);
enum SameSite { STRICT, LAX }
}

View file

@ -0,0 +1,169 @@
package org.xbib.netty.http.common.cookie;
import org.xbib.net.PercentDecoder;
import org.xbib.net.PercentEncoder;
import org.xbib.net.PercentEncoders;
import org.xbib.netty.http.common.security.Codec;
import org.xbib.netty.http.common.security.CryptUtil;
import org.xbib.netty.http.common.security.HMac;
import java.nio.charset.Charset;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.MalformedInputException;
import java.nio.charset.StandardCharsets;
import java.nio.charset.UnmappableCharacterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SignatureException;
public class CookieSigner {
private String signature;
private final String publicValue;
private final String privateValue;
private String cookieValue;
/**
* Construct cookie for signing.
*
* @param charset the character set
* @param hmac the HMAC code
* @param codec the codec for the private value
* @param privateValue the private value
* @param publicValue the public value
* @param secret the secret
* @throws MalformedInputException if signing cookie fails
* @throws UnmappableCharacterException if signing cookie fails
* @throws NoSuchAlgorithmException if signing cookie fails
* @throws InvalidKeyException if signing cookie fails
*/
private CookieSigner(Charset charset, HMac hmac, Codec codec, String privateValue, String publicValue, String secret)
throws MalformedInputException, UnmappableCharacterException, NoSuchAlgorithmException, InvalidKeyException {
PercentEncoder percentEncoder = PercentEncoders.getCookieEncoder(charset);
this.privateValue = privateValue;
this.publicValue = publicValue;
this.signature = CryptUtil.hmac(charset, hmac, codec, privateValue, secret);
this.cookieValue = percentEncoder.encode(String.join(":", publicValue, privateValue, signature));
}
/**
* Parse signed cookie value.
*
* @param charset the character set
* @param hmac the HMAC code
* @param codec the codec for the private value
* @param rawValue the raw value for parsing
* @param secret the secret
* @throws MalformedInputException if parsing failed
* @throws UnmappableCharacterException if parsing failed
* @throws NoSuchAlgorithmException if parsing failed
* @throws InvalidKeyException if parsing failed
* @throws SignatureException if signature is invalid
*/
private CookieSigner(Charset charset, HMac hmac, Codec codec, String rawValue, String secret)
throws MalformedInputException, UnmappableCharacterException, NoSuchAlgorithmException, InvalidKeyException,
SignatureException {
PercentDecoder persentDecoder = new PercentDecoder(charset.newDecoder()
.onMalformedInput(CodingErrorAction.REPORT)
.onUnmappableCharacter(CodingErrorAction.REPORT)
);
String[] s = persentDecoder.decode(rawValue).split(":", 3);
if (s.length != 3) {
throw new IllegalStateException("unable to find three colon-separated components in cookie value");
}
this.signature = CryptUtil.hmac(charset, hmac, codec, s[1], secret);
if (!s[2].equals(signature)) {
throw new SignatureException("HMAC signature does not match");
}
this.publicValue = s[0];
this.privateValue = s[1];
this.cookieValue = rawValue;
}
public String getPublicValue() {
return publicValue;
}
public String getPrivateValue() {
return privateValue;
}
public String getCookieValue() {
return cookieValue;
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
private Charset charset;
private HMac hmac;
private Codec codec;
private String privateValue;
private String publicValue;
private String secret;
private String rawValue;
public Builder() {
this.charset = StandardCharsets.UTF_8;
this.hmac = HMac.HMAC_SHA1;
this.codec = Codec.BASE64;
}
public Builder withCharset(Charset charset) {
this.charset = charset;
return this;
}
public Builder withHMac(HMac hmac) {
this.hmac = hmac;
return this;
}
public Builder withCodec(Codec codec) {
this.codec = codec;
return this;
}
public Builder withPrivateValue(String privateValue) {
this.privateValue = privateValue;
return this;
}
public Builder withPublicValue(String publicValue) {
this.publicValue = publicValue;
return this;
}
public Builder withSecret(String secret) {
this.secret = secret;
return this;
}
public Builder withRawValue(String rawValue) {
this.rawValue = rawValue;
return this;
}
public CookieSigner build() {
try {
return rawValue != null ?
new CookieSigner(charset, hmac, codec, rawValue, secret) :
new CookieSigner(charset, hmac, codec, privateValue, publicValue, secret);
} catch (Exception e) {
throw new IllegalArgumentException(e);
}
}
}
}

View file

@ -25,6 +25,10 @@ public class DefaultCookie implements Cookie {
private SameSite sameSite;
public DefaultCookie(String name, CookieSigner cookieSigner) {
this(name, cookieSigner.getCookieValue());
}
/**
* Creates a new cookie with the specified name and value.
* @param name name
@ -38,10 +42,6 @@ public class DefaultCookie implements Cookie {
setValue(value);
}
public DefaultCookie(String name, Payload payload) {
this(name, payload.toString());
}
@Override
public String name() {
return name;

View file

@ -1,78 +0,0 @@
package org.xbib.netty.http.common.cookie;
import org.xbib.net.PercentDecoder;
import org.xbib.net.PercentEncoder;
import org.xbib.net.PercentEncoders;
import org.xbib.netty.http.common.util.Codec;
import org.xbib.netty.http.common.util.CryptUtils;
import org.xbib.netty.http.common.util.HMac;
import java.nio.charset.MalformedInputException;
import java.nio.charset.StandardCharsets;
import java.nio.charset.UnmappableCharacterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SignatureException;
public class Payload {
private static final PercentEncoder PERCENT_ENCODER = PercentEncoders.getCookieEncoder(StandardCharsets.UTF_8);
private static final PercentDecoder PERCENT_DECODER = new PercentDecoder(StandardCharsets.UTF_8.newDecoder());
private final Codec codec;
private final HMac hmac;
private final String publicValue;
private final String privateValue;
private final String secret;
public Payload(Codec codec, HMac hmac, String publicValue, String privateValue, String secret) {
this.codec = codec;
this.hmac = hmac;
this.publicValue = publicValue;
this.privateValue = privateValue;
this.secret = secret;
}
public Payload(Codec codec, HMac hmac, String rawValue, String secret)
throws MalformedInputException, UnmappableCharacterException, InvalidKeyException,
NoSuchAlgorithmException, SignatureException {
this.codec = codec;
this.hmac = hmac;
String[] s = PERCENT_DECODER.decode(rawValue).split(":", 3);
if (s.length != 3) {
throw new IllegalStateException();
}
this.publicValue = s[0];
this.privateValue = s[1];
this.secret = secret;
if (!s[2].equals(CryptUtils.hmac(codec, privateValue, secret, hmac))) {
throw new SignatureException("HMAC signature does not match");
}
}
public String getPublicValue() {
return publicValue;
}
public String getPrivateValue() {
return privateValue;
}
public String getSecret() {
return secret;
}
public String toString() {
try {
return PERCENT_ENCODER.encode(String.join(":", publicValue, privateValue,
CryptUtils.hmac(codec, privateValue, secret, hmac)));
} catch (NoSuchAlgorithmException | InvalidKeyException | MalformedInputException | UnmappableCharacterException e) {
throw new IllegalArgumentException(e);
}
}
}

View file

@ -0,0 +1,5 @@
package org.xbib.netty.http.common.cookie;
public enum SameSite {
STRICT, LAX
}

View file

@ -1,4 +1,4 @@
package org.xbib.netty.http.common.util;
package org.xbib.netty.http.common.security;
public enum Algo {
MD5("MD5", "md5"),

View file

@ -0,0 +1,5 @@
package org.xbib.netty.http.common.security;
public enum Codec {
BASE64, HEX
}

View file

@ -1,7 +1,8 @@
package org.xbib.netty.http.common.util;
package org.xbib.netty.http.common.security;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
@ -15,10 +16,13 @@ import java.util.Base64;
* A utility class for invoking encryption methods and returning password strings,
* using {@link java.security.MessageDigest} and {@link javax.crypto.Mac}.
*/
public class CryptUtils {
public class CryptUtil {
private static final Random random = new SecureRandom();
private CryptUtil() {
}
public static String randomHex(int length) {
byte[] b = new byte[length];
random.nextBytes(b);
@ -53,32 +57,32 @@ public class CryptUtils {
return digest(Codec.BASE64, plainText.getBytes(StandardCharsets.UTF_8), salt, Algo.SSHA512.algo, Algo.SSHA512.prefix);
}
public static String hmacSHA1(String plainText, String secret) throws NoSuchAlgorithmException, InvalidKeyException {
return hmac(Codec.BASE64, plainText.getBytes(StandardCharsets.UTF_8), secret.getBytes(StandardCharsets.UTF_8), HMac.HMAC_SHA1);
public static String hmacSHA1(Charset charset, String plainText, String secret) throws NoSuchAlgorithmException, InvalidKeyException {
return hmac(HMac.HMAC_SHA1, Codec.BASE64, plainText.getBytes(charset), secret.getBytes(charset));
}
public static String hmacSHA1(byte[] plainText, String secret) throws InvalidKeyException, NoSuchAlgorithmException {
return hmac(Codec.BASE64, plainText, secret.getBytes(StandardCharsets.UTF_8), HMac.HMAC_SHA1);
public static String hmacSHA1(Charset charset, byte[] plainText, String secret) throws InvalidKeyException, NoSuchAlgorithmException {
return hmac(HMac.HMAC_SHA1, Codec.BASE64, plainText, secret.getBytes(charset));
}
public static String hmacSHA1(byte[] plainText, byte[] secret) throws InvalidKeyException, NoSuchAlgorithmException {
return hmac(Codec.BASE64, plainText, secret, HMac.HMAC_SHA1);
return hmac(HMac.HMAC_SHA1, Codec.BASE64, plainText, secret);
}
public static String hmacSHA256(String plainText, String secret) throws NoSuchAlgorithmException, InvalidKeyException {
return hmac(Codec.BASE64, plainText.getBytes(StandardCharsets.UTF_8), secret.getBytes(StandardCharsets.UTF_8), HMac.HMAC_SHA256);
public static String hmacSHA256(Charset charset, String plainText, String secret) throws NoSuchAlgorithmException, InvalidKeyException {
return hmac(HMac.HMAC_SHA256, Codec.BASE64, plainText.getBytes(charset), secret.getBytes(charset));
}
public static String hmacSHA256(byte[] plainText, String secret) throws InvalidKeyException, NoSuchAlgorithmException {
return hmac(Codec.BASE64, plainText, secret.getBytes(StandardCharsets.UTF_8), HMac.HMAC_SHA256);
public static String hmacSHA256(Charset charset, byte[] plainText, String secret) throws InvalidKeyException, NoSuchAlgorithmException {
return hmac(HMac.HMAC_SHA256, Codec.BASE64, plainText, secret.getBytes(charset));
}
public static String hmacSHA256(byte[] plainText, byte[] secret) throws InvalidKeyException, NoSuchAlgorithmException {
return hmac(Codec.BASE64, plainText, secret, HMac.HMAC_SHA256);
return hmac(HMac.HMAC_SHA256, Codec.BASE64, plainText, secret);
}
public static String hmac(Codec codec, String plainText, String secret, HMac hmac) throws InvalidKeyException, NoSuchAlgorithmException {
return hmac(codec, plainText.getBytes(StandardCharsets.UTF_8), secret.getBytes(StandardCharsets.UTF_8), hmac);
public static String hmac(Charset charset, HMac hmac, Codec codec, String plainText, String secret) throws InvalidKeyException, NoSuchAlgorithmException {
return hmac(hmac, codec, plainText.getBytes(charset), secret.getBytes(charset));
}
public static String digest(Codec codec, byte[] plainText, byte[] salt, String algo, String prefix) throws NoSuchAlgorithmException {
@ -98,11 +102,11 @@ public class CryptUtils {
codec == Codec.HEX ? encodeHex(bytes) : null);
}
public static String hmac(Codec codec, byte[] plainText, byte[] secret, HMac hmac) throws NoSuchAlgorithmException, InvalidKeyException {
public static String hmac(HMac hmac, Codec codec, byte[] plainText, byte[] secret) throws NoSuchAlgorithmException, InvalidKeyException {
Objects.requireNonNull(plainText);
Objects.requireNonNull(secret);
Mac mac = Mac.getInstance(hmac.algo);
SecretKeySpec secretKeySpec = new SecretKeySpec(secret, hmac.algo);
Mac mac = Mac.getInstance(hmac.getAlgo());
SecretKeySpec secretKeySpec = new SecretKeySpec(secret, hmac.getAlgo());
mac.init(secretKeySpec);
return codec == Codec.BASE64 ? Base64.getEncoder().encodeToString(mac.doFinal(plainText)) :
codec == Codec.HEX ? encodeHex(mac.doFinal(plainText)) : null;

View file

@ -1,4 +1,4 @@
package org.xbib.netty.http.common.util;
package org.xbib.netty.http.common.security;
public enum HMac {
HMAC_SHA1("HMacSHA1"),
@ -9,4 +9,8 @@ public enum HMac {
HMac(String algo) {
this.algo = algo;
}
public String getAlgo() {
return algo;
}
}

View file

@ -1,4 +1,4 @@
package org.xbib.netty.http.common;
package org.xbib.netty.http.common.security;
import io.netty.handler.codec.http2.Http2SecurityUtil;
import io.netty.handler.ssl.CipherSuiteFilter;
@ -23,6 +23,9 @@ public class SecurityUtil {
}
}
private SecurityUtil() {
}
public interface Defaults {
List<String> OPENSSL_CIPHERS = Http2SecurityUtil.CIPHERS;

View file

@ -1,5 +0,0 @@
package org.xbib.netty.http.common.util;
public enum Codec {
BASE64, HEX
}

View file

@ -8,7 +8,7 @@ import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Locale;
public class DateTimeUtils {
public class DateTimeUtil {
private static final ZoneId ZONE_UTC = ZoneId.of("UTC");
@ -18,6 +18,9 @@ public class DateTimeUtils {
private static final String ASCIITIME_PATTERN = "EEE MMM d HH:mm:ss yyyyy";
private DateTimeUtil() {
}
public static String formatInstant(Instant instant) {
return DateTimeFormatter.RFC_1123_DATE_TIME.format(ZonedDateTime.ofInstant(instant, ZoneOffset.UTC));
}

View file

@ -0,0 +1,85 @@
package org.xbib.netty.http.common.test.cookie;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.xbib.netty.http.common.cookie.DefaultCookie;
import org.xbib.netty.http.common.cookie.CookieSigner;
import org.xbib.netty.http.common.security.HMac;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import static org.junit.jupiter.api.Assertions.assertEquals;
class CookieSignerTest {
@Test
void testEncodeSignedCookie() {
String id = "dummy";
CookieSigner cookieSigner = CookieSigner.builder().withHMac(HMac.HMAC_SHA256)
.withPublicValue(id)
.withPrivateValue(Base64.getEncoder().encodeToString("Hello".getBytes(StandardCharsets.UTF_8)))
.withSecret("d9fd3a19707be6025b4f5a98c320745960a1b95144f040afcda2a4997dbae0cb")
.build();
DefaultCookie cookie = new DefaultCookie("SESS", cookieSigner);
assertEquals("dummy", cookieSigner.getPublicValue());
assertEquals("dummy%3ASGVsbG8%3D%3AqEdbImuwfh7r%2BmOaShC3IjXhkdiiF3Y1RgSZ%2FFAZrQ4%3D", cookie.value());
}
@Test
void testDecodeSignedCookie() {
String rawValue = "dummy%3ASGVsbG8%3D%3AqEdbImuwfh7r%2BmOaShC3IjXhkdiiF3Y1RgSZ%2FFAZrQ4%3D";
CookieSigner cookieSigner = CookieSigner.builder()
.withHMac(HMac.HMAC_SHA256)
.withRawValue(rawValue)
.withSecret("d9fd3a19707be6025b4f5a98c320745960a1b95144f040afcda2a4997dbae0cb")
.build();
DefaultCookie cookie = new DefaultCookie("SESS", cookieSigner);
assertEquals("dummy%3ASGVsbG8%3D%3AqEdbImuwfh7r%2BmOaShC3IjXhkdiiF3Y1RgSZ%2FFAZrQ4%3D", cookie.value());
assertEquals("dummy", cookieSigner.getPublicValue());
assertEquals("Hello", new String(Base64.getDecoder().decode(cookieSigner.getPrivateValue()),
StandardCharsets.UTF_8));
}
@Test
void testDecodeInvalidTuple() {
Assertions.assertThrows(IllegalArgumentException.class, () -> {
String rawValue = "dummy%3JSGVsbG8%3D%3AqEdbImuwfh7r%2BmOaShC3IjXhkdiiF3Y1RgSZ%2FFAZrQ4%3D";
CookieSigner.builder()
.withHMac(HMac.HMAC_SHA256)
.withRawValue(rawValue)
.withSecret("d9fd3a19707be6025b4f5a98c320745960a1b95144f040afcda2a4997dbae0cb")
.build();
});
}
@Test
void testDecodeISOGivesMalformedInputException() {
Assertions.assertThrows(IllegalArgumentException.class, () -> {
String rawValue = "dummy%FCSGVsbG8%3D%3AqEdbImuwfh7r%2BmOaShC3IjXhkdiiF3Y1RgSZ%2FFAZrQ4%3D";
CookieSigner.builder()
.withHMac(HMac.HMAC_SHA256)
.withRawValue(rawValue)
.withSecret("d9fd3a19707be6025b4f5a98c320745960a1b95144f040afcda2a4997dbae0cb")
.build();
});
}
@Test
void testDecodeISO() {
String rawValue = "d%FCmmy%3ASGVsbG8%3D%3AqEdbImuwfh7r%2BmOaShC3IjXhkdiiF3Y1RgSZ%2FFAZrQ4%3D";
CookieSigner cookieSigner = CookieSigner.builder()
.withCharset(StandardCharsets.ISO_8859_1)
.withHMac(HMac.HMAC_SHA256)
.withRawValue(rawValue)
.withSecret("d9fd3a19707be6025b4f5a98c320745960a1b95144f040afcda2a4997dbae0cb")
.build();
DefaultCookie cookie = new DefaultCookie("SESS", cookieSigner);
assertEquals("d%FCmmy%3ASGVsbG8%3D%3AqEdbImuwfh7r%2BmOaShC3IjXhkdiiF3Y1RgSZ%2FFAZrQ4%3D", cookie.value());
assertEquals("dümmy", cookieSigner.getPublicValue());
assertEquals("Hello", new String(Base64.getDecoder().decode(cookieSigner.getPrivateValue()),
StandardCharsets.UTF_8));
}
}

View file

@ -1,72 +0,0 @@
package org.xbib.netty.http.common.test.cookie;
import org.junit.jupiter.api.Test;
import org.xbib.netty.http.common.cookie.Cookie;
import org.xbib.netty.http.common.cookie.DefaultCookie;
import org.xbib.netty.http.common.cookie.Payload;
import org.xbib.netty.http.common.util.Codec;
import org.xbib.netty.http.common.util.HMac;
import java.nio.charset.MalformedInputException;
import java.nio.charset.StandardCharsets;
import java.nio.charset.UnmappableCharacterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SignatureException;
import java.util.Base64;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class SignedCookieTest {
@Test
void testEncodeDefaultCookie() {
Base64Codec codec = new Base64Codec();
String cookieName = "SESS";
String domain = ".hbz-nrw.de";
String path = "/";
String id = "dummy";
Payload payload = new Payload(Codec.BASE64, HMac.HMAC_SHA256,
id, new String(codec.encode("Hello"), StandardCharsets.UTF_8),
"d9fd3a19707be6025b4f5a98c320745960a1b95144f040afcda2a4997dbae0cb");
DefaultCookie cookie = new DefaultCookie(cookieName, payload);
cookie.setDomain(domain);
cookie.setPath(path);
cookie.setMaxAge(3600);
cookie.setHttpOnly(true);
cookie.setSecure(true);
cookie.setSameSite(Cookie.SameSite.LAX);
assertEquals("dummy%3ASGVsbG8%3D%3AqEdbImuwfh7r%2BmOaShC3IjXhkdiiF3Y1RgSZ%2FFAZrQ4%3D", cookie.value());
assertEquals(domain, cookie.domain());
assertEquals(path, cookie.path());
assertEquals(3600, cookie.maxAge());
assertEquals(cookieName, cookie.name());
assertTrue(cookie.isHttpOnly());
assertTrue(cookie.isSecure());
assertEquals(Cookie.SameSite.LAX, cookie.sameSite());
}
@Test
void testCookieValue()
throws MalformedInputException, UnmappableCharacterException, NoSuchAlgorithmException, InvalidKeyException, SignatureException {
Base64Codec codec = new Base64Codec();
String rawCookieValue = "dummy%3ASGVsbG8%3D%3AqEdbImuwfh7r%2BmOaShC3IjXhkdiiF3Y1RgSZ%2FFAZrQ4%3D";
Payload payload = new Payload(Codec.BASE64, HMac.HMAC_SHA256,
rawCookieValue, "d9fd3a19707be6025b4f5a98c320745960a1b95144f040afcda2a4997dbae0cb");
DefaultCookie cookie = new DefaultCookie("SESS", payload);
assertEquals("dummy", payload.getPublicValue());
assertEquals("Hello", codec.decode(payload.getPrivateValue().getBytes(StandardCharsets.UTF_8)));
}
class Base64Codec {
byte[] encode(String payload) {
return Base64.getEncoder().encode(payload.getBytes(StandardCharsets.UTF_8));
}
String decode(byte[] bytes) {
return new String(Base64.getDecoder().decode(bytes), StandardCharsets.UTF_8);
}
}
}

View file

@ -1,33 +1,33 @@
package org.xbib.netty.http.common.test;
package org.xbib.netty.http.common.test.security;
import org.junit.jupiter.api.Test;
import org.xbib.netty.http.common.util.CryptUtils;
import org.xbib.netty.http.common.security.CryptUtil;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import static org.junit.jupiter.api.Assertions.assertEquals;
class CryptUtilsTest {
class CryptUtilTest {
@Test
void testRfc2307() throws NoSuchAlgorithmException {
assertEquals("{md5}ixqZU8RhEpaoJ6v4xHgE1w==",
CryptUtils.md5("Hello"));
CryptUtil.md5("Hello"));
assertEquals("{sha}9/+ei3uy4Jtwk1pdeF4MxdnQq/A=",
CryptUtils.sha("Hello"));
CryptUtil.sha("Hello"));
assertEquals("{sha256}GF+NsyJx/iX1Yab8k4suJkMG7DBO2lGAB9F2SCY4GWk=",
CryptUtils.sha256("Hello"));
CryptUtil.sha256("Hello"));
assertEquals("{sha512}NhX4DJ0pPtdAJof5SyLVjlKbjMeRb4+sf933+9WvTPd309eVp6AKFr9+fz+5Vh7puq5IDan+ehh2nnGIawPzFQ==",
CryptUtils.sha512("Hello"));
CryptUtil.sha512("Hello"));
}
@Test
void testHmac() throws InvalidKeyException, NoSuchAlgorithmException {
assertEquals("Wgxn2SLeDKU+MGJQ5oWMH20sSUM=",
CryptUtils.hmacSHA1("hello", "world"));
CryptUtil.hmacSHA1(StandardCharsets.ISO_8859_1, "hello", "world"));
assertEquals("PPp27xSTfBwOpRn4/AV6gPzQSnQg+Oi80KdWfCcuAHs=",
CryptUtils.hmacSHA256("hello", "world"));
CryptUtil.hmacSHA256(StandardCharsets.ISO_8859_1, "hello", "world"));
}
}

View file

@ -8,7 +8,7 @@ import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.SslProvider;
import org.xbib.netty.http.common.HttpAddress;
import org.xbib.netty.http.common.SecurityUtil;
import org.xbib.netty.http.common.security.SecurityUtil;
import org.xbib.netty.http.server.endpoint.HttpEndpoint;
import org.xbib.netty.http.server.endpoint.HttpEndpointResolver;
import org.xbib.netty.http.server.endpoint.service.Service;
@ -147,7 +147,7 @@ public class Domain {
this.serverName = serverName;
this.aliases = new LinkedHashSet<>();
this.httpEndpointResolvers = new ArrayList<>();
this.trustManagerFactory = SecurityUtil.Defaults.DEFAULT_TRUST_MANAGER_FACTORY; // InsecureTrustManagerFactory.INSTANCE;
this.trustManagerFactory = SecurityUtil.Defaults.DEFAULT_TRUST_MANAGER_FACTORY;
this.sslProvider = SecurityUtil.Defaults.DEFAULT_SSL_PROVIDER;
this.ciphers = SecurityUtil.Defaults.DEFAULT_CIPHERS;
this.cipherSuiteFilter = SecurityUtil.Defaults.DEFAULT_CIPHER_SUITE_FILTER;

View file

@ -22,7 +22,7 @@ import org.xbib.netty.http.common.HttpAddress;
import org.xbib.netty.http.common.NetworkUtils;
import org.xbib.netty.http.server.handler.http.HttpChannelInitializer;
import org.xbib.netty.http.server.handler.http2.Http2ChannelInitializer;
import org.xbib.netty.http.common.SecurityUtil;
import org.xbib.netty.http.common.security.SecurityUtil;
import org.xbib.netty.http.server.transport.HttpTransport;
import org.xbib.netty.http.server.transport.Http2Transport;
import org.xbib.netty.http.server.transport.Transport;
@ -83,8 +83,7 @@ public final class Server {
Class<? extends ServerSocketChannel> socketChannelClass) {
Objects.requireNonNull(serverConfig);
this.serverConfig = serverConfig;
this.byteBufAllocator = byteBufAllocator != null ?
byteBufAllocator : ByteBufAllocator.DEFAULT;
this.byteBufAllocator = byteBufAllocator != null ? byteBufAllocator : ByteBufAllocator.DEFAULT;
this.parentEventLoopGroup = createParentEventLoopGroup(serverConfig, parentEventLoopGroup);
this.childEventLoopGroup = createChildEventLoopGroup(serverConfig, childEventLoopGroup);
this.socketChannelClass = createSocketChannelClass(serverConfig, socketChannelClass);
@ -191,6 +190,7 @@ public final class Server {
parentEventLoopGroup.shutdownGracefully();
try {
if (channelFuture != null) {
// close channel and wait
channelFuture.channel().closeFuture().sync();
}
} catch (InterruptedException e) {

View file

@ -5,7 +5,7 @@ import org.xbib.netty.http.common.cookie.CookieEncoder;
import org.xbib.netty.http.common.cookie.CookieHeaderNames;
import org.xbib.netty.http.common.cookie.CookieUtil;
import org.xbib.netty.http.common.cookie.DefaultCookie;
import org.xbib.netty.http.common.util.DateTimeUtils;
import org.xbib.netty.http.common.util.DateTimeUtil;
import java.time.Instant;
import java.util.ArrayList;
@ -86,7 +86,7 @@ public final class ServerCookieEncoder extends CookieEncoder {
Instant expires = Instant.ofEpochMilli(cookie.maxAge() * 1000 + System.currentTimeMillis());
buf.append(CookieHeaderNames.EXPIRES);
buf.append(CookieUtil.EQUALS);
buf.append(DateTimeUtils.formatMillis(expires.toEpochMilli()));
buf.append(DateTimeUtil.formatMillis(expires.toEpochMilli()));
buf.append(CookieUtil.SEMICOLON);
buf.append(CookieUtil.SP);
}
@ -110,28 +110,6 @@ public final class ServerCookieEncoder extends CookieEncoder {
return CookieUtil.stripTrailingSeparator(buf);
}
/**
* Deduplicate a list of encoded cookies by keeping only the last instance with a given name.
*
* @param encoded The list of encoded cookies.
* @param nameToLastIndex A map from cookie name to index of last cookie instance.
* @return The encoded list with all but the last instance of a named cookie.
*/
private static List<String> dedup(List<String> encoded, Map<String, Integer> nameToLastIndex) {
boolean[] isLastInstance = new boolean[encoded.size()];
for (int idx : nameToLastIndex.values()) {
isLastInstance[idx] = true;
}
List<String> dedupd = new ArrayList<>(nameToLastIndex.size());
int n = encoded.size();
for (int i = 0; i < n; i++) {
if (isLastInstance[i]) {
dedupd.add(encoded.get(i));
}
}
return dedupd;
}
/**
* Batch encodes cookies into Set-Cookie header values.
*
@ -204,4 +182,26 @@ public final class ServerCookieEncoder extends CookieEncoder {
}
return hasDupdName ? dedup(encoded, nameToIndex) : encoded;
}
/**
* Deduplicate a list of encoded cookies by keeping only the last instance with a given name.
*
* @param encoded The list of encoded cookies.
* @param nameToLastIndex A map from cookie name to index of last cookie instance.
* @return The encoded list with all but the last instance of a named cookie.
*/
private static List<String> dedup(List<String> encoded, Map<String, Integer> nameToLastIndex) {
boolean[] isLastInstance = new boolean[encoded.size()];
for (int idx : nameToLastIndex.values()) {
isLastInstance[idx] = true;
}
List<String> dedupd = new ArrayList<>(nameToLastIndex.size());
int n = encoded.size();
for (int i = 0; i < n; i++) {
if (isLastInstance[i]) {
dedupd.add(encoded.get(i));
}
}
return dedupd;
}
}

View file

@ -32,7 +32,7 @@ public class HttpEndpointDescriptor implements EndpointDescriptor, Comparable<Ht
@Override
public String toString() {
return "[EndpointInfo:path=" + path + ",method=" + method + ",contentType=" + contentType + "]";
return "[HttpEndpointDescriptor:path=" + path + ",method=" + method + ",contentType=" + contentType + "]";
}
@Override

View file

@ -11,7 +11,6 @@ import java.io.IOException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
@ -44,7 +43,8 @@ public class HttpEndpointResolver {
HttpEndpointDescriptor httpEndpointDescriptor = serverRequest.getEndpointDescriptor();
endpointDescriptors.putIfAbsent(httpEndpointDescriptor, endpoints.stream()
.filter(endpoint -> endpoint.matches(httpEndpointDescriptor))
.sorted(new HttpEndpoint.EndpointPathComparator(httpEndpointDescriptor.getPath())).collect(Collectors.toList()));
.sorted(new HttpEndpoint.EndpointPathComparator(httpEndpointDescriptor.getPath()))
.collect(Collectors.toList()));
List<HttpEndpoint> matchingEndpoints = endpointDescriptors.get(httpEndpointDescriptor);
if (logger.isLoggable(Level.FINE)) {
logger.log(Level.FINE, () -> "endpoint = " + httpEndpointDescriptor +
@ -83,7 +83,7 @@ public class HttpEndpointResolver {
return endpointDescriptors;
}
protected HttpEndpoint createDefaultEndpoint() {
private HttpEndpoint createDefaultEndpoint() {
return HttpEndpoint.builder()
.setPath("/**")
.addMethod("GET")
@ -94,27 +94,6 @@ public class HttpEndpointResolver {
}).build();
}
/**
* A simple LRU cache, based on a {@link LinkedHashMap}.
*
* @param <K> the key type parameter
* @param <V> the vale type parameter
*/
@SuppressWarnings("serial")
private static class LRUCache<K, V> extends LinkedHashMap<K, V> {
private final int cacheSize;
LRUCache(int cacheSize) {
super(16, 0.75f, true);
this.cacheSize = cacheSize;
}
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > cacheSize;
}
}
public static Builder builder() {
return new Builder();
}

View file

@ -6,7 +6,7 @@ import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.stream.ChunkedNioStream;
import org.xbib.netty.http.common.util.DateTimeUtils;
import org.xbib.netty.http.common.util.DateTimeUtil;
import org.xbib.netty.http.server.ServerRequest;
import org.xbib.netty.http.server.ServerResponse;
import org.xbib.netty.http.server.util.MimeTypeUtils;
@ -55,14 +55,14 @@ public abstract class ResourceService implements Service {
long maxAgeSeconds = 24 * 3600;
long expirationMillis = System.currentTimeMillis() + 1000 * maxAgeSeconds;
if (isCacheResponseEnabled()) {
serverResponse.withHeader(HttpHeaderNames.EXPIRES, DateTimeUtils.formatMillis(expirationMillis))
serverResponse.withHeader(HttpHeaderNames.EXPIRES, DateTimeUtil.formatMillis(expirationMillis))
.withHeader(HttpHeaderNames.CACHE_CONTROL, "public, max-age=" + maxAgeSeconds);
}
boolean sent = false;
if (isETagResponseEnabled()) {
Instant lastModifiedInstant = resource.getLastModified();
String eTag = resource.getResourcePath().hashCode() + "/" + lastModifiedInstant.toEpochMilli() + "/" + resource.getLength();
Instant ifUnmodifiedSinceInstant = DateTimeUtils.parseDate(headers.get(HttpHeaderNames.IF_UNMODIFIED_SINCE));
Instant ifUnmodifiedSinceInstant = DateTimeUtil.parseDate(headers.get(HttpHeaderNames.IF_UNMODIFIED_SINCE));
if (ifUnmodifiedSinceInstant != null &&
ifUnmodifiedSinceInstant.plusMillis(1000L).isAfter(lastModifiedInstant)) {
ServerResponse.write(serverResponse, HttpResponseStatus.PRECONDITION_FAILED);
@ -76,20 +76,20 @@ public abstract class ResourceService implements Service {
String ifNoneMatch = headers.get(HttpHeaderNames.IF_NONE_MATCH);
if (ifNoneMatch != null && matches(ifNoneMatch, eTag)) {
serverResponse.withHeader(HttpHeaderNames.ETAG, eTag)
.withHeader(HttpHeaderNames.EXPIRES, DateTimeUtils.formatMillis(expirationMillis));
.withHeader(HttpHeaderNames.EXPIRES, DateTimeUtil.formatMillis(expirationMillis));
ServerResponse.write(serverResponse, HttpResponseStatus.NOT_MODIFIED);
return;
}
Instant ifModifiedSinceInstant = DateTimeUtils.parseDate(headers.get(HttpHeaderNames.IF_MODIFIED_SINCE));
Instant ifModifiedSinceInstant = DateTimeUtil.parseDate(headers.get(HttpHeaderNames.IF_MODIFIED_SINCE));
if (ifModifiedSinceInstant != null &&
ifModifiedSinceInstant.plusMillis(1000L).isAfter(lastModifiedInstant)) {
serverResponse.withHeader(HttpHeaderNames.ETAG, eTag)
.withHeader(HttpHeaderNames.EXPIRES, DateTimeUtils.formatMillis(expirationMillis));
.withHeader(HttpHeaderNames.EXPIRES, DateTimeUtil.formatMillis(expirationMillis));
ServerResponse.write(serverResponse, HttpResponseStatus.NOT_MODIFIED);
return;
}
serverResponse.withHeader(HttpHeaderNames.ETAG, eTag)
.withHeader(HttpHeaderNames.LAST_MODIFIED, DateTimeUtils.formatInstant(lastModifiedInstant));
.withHeader(HttpHeaderNames.LAST_MODIFIED, DateTimeUtil.formatInstant(lastModifiedInstant));
if (isRangeResponseEnabled()) {
performRangeResponse(serverRequest, serverResponse, resource, contentType, eTag, headers);
sent = true;
@ -119,7 +119,7 @@ public abstract class ResourceService implements Service {
String ifRange = headers.get(HttpHeaderNames.IF_RANGE);
if (ifRange != null && !ifRange.equals(eTag)) {
try {
Instant ifRangeTime = DateTimeUtils.parseDate(ifRange);
Instant ifRangeTime = DateTimeUtil.parseDate(ifRange);
if (ifRangeTime != null && ifRangeTime.plusMillis(1000).isBefore(resource.getLastModified())) {
ranges.add(full);
}

View file

@ -4,7 +4,7 @@ import org.junit.jupiter.api.Test;
import org.xbib.netty.http.client.cookie.ClientCookieEncoder;
import org.xbib.netty.http.common.cookie.Cookie;
import org.xbib.netty.http.common.cookie.DefaultCookie;
import org.xbib.netty.http.common.util.DateTimeUtils;
import org.xbib.netty.http.common.util.DateTimeUtil;
import org.xbib.netty.http.server.cookie.ServerCookieEncoder;
import java.time.Instant;
@ -35,7 +35,7 @@ class ServerCookieEncoderTest {
String encodedCookie = ServerCookieEncoder.STRICT.encode(cookie);
Matcher matcher = Pattern.compile(result).matcher(encodedCookie);
assertTrue(matcher.find());
Instant expire = DateTimeUtils.parseDate(matcher.group(1));
Instant expire = DateTimeUtil.parseDate(matcher.group(1));
long diff = (expire.toEpochMilli() - System.currentTimeMillis()) / 1000;
assertTrue(Math.abs(diff - maxAge) <= 2);
}