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
11 changes: 10 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
# 5.2.1 (unreleased)

* Fix Maven publication dependency scopes so dependencies required by the public API are available on consumers' compile classpaths (#219, #233).
* Preserve non-default endpoint ports when building the VAPID JWT audience origin (#153).
* Migrate the Gradle Maven Central publishing flow from the retired OSSRH service to the Central Portal OSSRH Staging API compatibility service.
* Include the fixes that were prepared after 5.1.2 for the tagged but unpublished 5.2.0 release:
* Encode the VAPID public application server key as unpadded URL-safe Base64 (#216).
* Use AES128GCM as the default content encoding.
* Upgrade `org.bitbucket.b_c:jose4j` to 0.9.6.

# 5.1.1

* Target Java 8 instead of Java 7.
Expand All @@ -24,4 +34,3 @@
* Support [aes128gcm content encoding](https://tools.ietf.org/html/draft-ietf-httpbis-encryption-encoding-09#section-2) (#72)
* Use `PushService.send(Notification, Encoding)` or the analogous `sendAsync` with `Encoding.AES128GCM`.
* Remove Guava dependency (#69)

14 changes: 8 additions & 6 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
plugins {
id 'application'
id 'java-library'
id 'com.github.johnrengelman.shadow' version '7.1.1'

// Used by release.gradle
id 'maven-publish'
id 'signing'
id 'io.codearte.nexus-staging' version '0.30.0'
id 'io.github.gradle-nexus.publish-plugin' version '2.0.0' apply false
}

apply plugin: 'application'
Expand All @@ -21,19 +22,19 @@ repositories {

dependencies {
// For CLI
implementation group: 'com.beust', name: 'jcommander', version: '1.81'
api group: 'com.beust', name: 'jcommander', version: '1.81'

// For making HTTP requests
implementation group: 'org.apache.httpcomponents', name: 'httpasyncclient', version: '4.1.5'
api group: 'org.apache.httpcomponents', name: 'httpasyncclient', version: '4.1.5'

// For making async HTTP requests
implementation group: 'org.asynchttpclient', name: 'async-http-client', version: '2.12.4'
api group: 'org.asynchttpclient', name: 'async-http-client', version: '2.12.4'

// For cryptographic operations
shadow group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.70'

// For creating and signing JWT
implementation group: 'org.bitbucket.b_c', name: 'jose4j', version: '0.9.6'
api group: 'org.bitbucket.b_c', name: 'jose4j', version: '0.9.6'

// For parsing JSON
testImplementation group: 'com.google.code.gson', name: 'gson', version: '2.8.9'
Expand All @@ -58,7 +59,7 @@ dependencies {
}

wrapper {
gradleVersion = '5.1'
gradleVersion = '7.2'
}

compileJava {
Expand Down Expand Up @@ -104,5 +105,6 @@ artifacts {
}

if (hasProperty('release')) {
apply plugin: 'io.github.gradle-nexus.publish-plugin'
apply from: 'release.gradle'
}
55 changes: 43 additions & 12 deletions release.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,17 @@ publishing {
}
}
}

}
}
}

nexusPublishing {
repositories {
maven {
credentials {
username ossrhUsername
password ossrhPassword
}
url getRepositoryUrl()
sonatype {
nexusUrl.set(uri('https://ossrh-staging-api.central.sonatype.com/service/local/'))
snapshotRepositoryUrl.set(uri('https://central.sonatype.com/repository/maven-snapshots/'))
username = project.findProperty('sonatypeUsername') ?: project.findProperty('ossrhUsername')
password = project.findProperty('sonatypePassword') ?: project.findProperty('ossrhPassword')
}
}
}
Expand All @@ -45,10 +46,40 @@ signing {
sign publishing.publications.mavenJava
}

def getRepositoryUrl() {
if (version.endsWith('SNAPSHOT')) {
return 'https://oss.sonatype.org/content/repositories/snapshots/'
} else {
return 'https://oss.sonatype.org/service/local/staging/deploy/maven2/'
tasks.register('verifyPublicationPom') {
dependsOn 'generatePomFileForMavenJavaPublication'

doLast {
def pomFile = file("$buildDir/publications/mavenJava/pom-default.xml")
def pom = new XmlSlurper().parse(pomFile)
def scopes = [:]

pom.dependencies.dependency.each { dependency ->
scopes["${dependency.groupId.text()}:${dependency.artifactId.text()}"] = dependency.scope.text()
}

[
'com.beust:jcommander',
'org.apache.httpcomponents:httpasyncclient',
'org.asynchttpclient:async-http-client',
'org.bitbucket.b_c:jose4j'
].each { dependency ->
if (scopes[dependency] != 'compile') {
throw new GradleException("Expected ${dependency} to be published with compile scope, got '${scopes[dependency]}'")
}
}
}
}

// Keep the historical release commands stable while using the current Sonatype plugin internally.
gradle.projectsEvaluated {
tasks.named('publish') {
dependsOn 'publishToSonatype'
}
}

tasks.register('closeAndReleaseRepository') {
group = 'publishing'
description = 'Closes and releases the current Sonatype staging repository.'
dependsOn 'findSonatypeStagingRepository', 'closeAndReleaseSonatypeStagingRepository'
}
17 changes: 7 additions & 10 deletions scripts/version.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,16 @@
set -eu

if [ "$#" -ne 2 ]; then
echo "Usage: version.sh OLD_VERSION NEW_VERSION" && exit 1
echo "Usage: version.sh OLD_VERSION NEW_VERSION" >&2
exit 1
fi

OLD_VERSION=$1
NEW_VERSION=$2
OLD_VERSION_REGEX=$(printf '%s' "$OLD_VERSION" | sed 's/\./\\./g')

files=(
"build.gradle"
"README.md"
)

for file in ${files[@]}; do
sed -i '' "s/$OLD_VERSION/$NEW_VERSION/g" $file
for file in build.gradle README.md; do
tmp_file="${file}.tmp"
sed "s/${OLD_VERSION_REGEX}/${NEW_VERSION}/g" "$file" > "$tmp_file"
mv "$tmp_file" "$file"
done


12 changes: 7 additions & 5 deletions src/main/java/nl/martijndwars/webpush/Notification.java
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,10 @@ public Notification(String endpoint, String userPublicKey, String userAuth, Stri
this(endpoint, Utils.loadPublicKey(userPublicKey), Base64.getUrlDecoder().decode(userAuth), payload.getBytes(UTF_8));
}

public Notification(String endpoint, String userPublicKey, String userAuth, String payload, Urgency urgency) throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException {
this(endpoint, Utils.loadPublicKey(userPublicKey), Base64.getUrlDecoder().decode(userAuth), payload.getBytes(UTF_8));
this.urgency = urgency;
}
public Notification(String endpoint, String userPublicKey, String userAuth, String payload, Urgency urgency) throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException {
this(endpoint, Utils.loadPublicKey(userPublicKey), Base64.getUrlDecoder().decode(userAuth), payload.getBytes(UTF_8));
this.urgency = urgency;
}

public Notification(Subscription subscription, String payload) throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException {
this(subscription.endpoint, subscription.keys.p256dh, subscription.keys.auth, payload);
Expand Down Expand Up @@ -156,8 +156,10 @@ public String getTopic() {

public String getOrigin() throws MalformedURLException {
URL url = new URL(getEndpoint());
int port = url.getPort();
boolean includePort = port != -1 && port != url.getDefaultPort();

return url.getProtocol() + "://" + url.getHost();
return url.getProtocol() + "://" + url.getHost() + (includePort ? ":" + port : "");
}

public static NotificationBuilder builder() {
Expand Down
59 changes: 59 additions & 0 deletions src/test/java/nl/martijndwars/webpush/AbstractPushServiceTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package nl.martijndwars.webpush;

import org.bouncycastle.jce.ECNamedCurveTable;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.Security;
import java.util.Base64;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

class AbstractPushServiceTest {

private static final String USER_PUBLIC_KEY = "BGu3hOwCLOBfdMReXf7-SD2x5tKs_vPapOneyngBOnu6PgNYdgLPKFAodfBnG60MqkXC0McPFehN2Kyuh6TKm14=";

@BeforeAll
public static void addSecurityProvider() {
Security.addProvider(new BouncyCastleProvider());
}

@Test
public void vapidCryptoKeyUsesUnpaddedBase64Url() throws Exception {
ECNamedCurveParameterSpec parameterSpec = ECNamedCurveTable.getParameterSpec("prime256v1");
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("ECDH", "BC");
keyPairGenerator.initialize(parameterSpec);

TestPushService service = new TestPushService(keyPairGenerator.generateKeyPair());
String userAuth = Base64.getUrlEncoder().withoutPadding().encodeToString(new byte[16]);
Notification notification = new Notification(
"https://push.example.test/send/123",
USER_PUBLIC_KEY,
userAuth,
"payload"
);

HttpRequest request = service.prepare(notification, Encoding.AES128GCM);
String cryptoKey = request.getHeaders().get("Crypto-Key");

assertNotNull(cryptoKey);
assertTrue(cryptoKey.startsWith("p256ecdsa="));
assertFalse(cryptoKey.substring("p256ecdsa=".length()).contains("="));
}

private static class TestPushService extends AbstractPushService<TestPushService> {
TestPushService(KeyPair keyPair) {
super(keyPair);
}

HttpRequest prepare(Notification notification, Encoding encoding) throws Exception {
return prepareRequest(notification, encoding);
}
}
}
18 changes: 18 additions & 0 deletions src/test/java/nl/martijndwars/webpush/NotificationTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,22 @@ public void testDefaultTtl() throws GeneralSecurityException {
.build();
assertEquals(28 * oneDayDurationInSeconds, notification.getTTL());
}

@Test
public void testOriginOmitsDefaultPort() throws Exception {
Notification notification = Notification.builder()
.endpoint("https://push.example.test:443/send/123")
.build();

assertEquals("https://push.example.test", notification.getOrigin());
}

@Test
public void testOriginIncludesNonDefaultPort() throws Exception {
Notification notification = Notification.builder()
.endpoint("https://push.example.test:8443/send/123")
.build();

assertEquals("https://push.example.test:8443", notification.getOrigin());
}
}