Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
2d9da1a
feat: add UrlSigner interface for customizable URL signing
uriahcarpenter Mar 16, 2026
7e0b3b9
feat: update expiration handling in URL builders to use IllegalArgume…
uriahcarpenter Mar 16, 2026
8c68cc9
Check input
uriahcarpenter Mar 17, 2026
294df24
Null check
uriahcarpenter Mar 17, 2026
c3b98c2
No path returns `/`
uriahcarpenter Mar 17, 2026
a4b639f
Fix JavaDoc
uriahcarpenter Mar 17, 2026
ac525dd
Fix JavaDoc
uriahcarpenter Mar 17, 2026
684f314
Fix JavaDoc
uriahcarpenter Mar 17, 2026
70cf40a
copilot nits
uriahcarpenter Mar 17, 2026
631067f
feat: add support for raw query parameters in SigningContext implemen…
uriahcarpenter Mar 25, 2026
b43d361
feat: add version management for dependencies in libs.versions.toml
uriahcarpenter Mar 25, 2026
1d0ebda
feat: implement URL-safe Base64 encoding utility and update HMAC sign…
uriahcarpenter Mar 25, 2026
0552d98
fix javadoc
uriahcarpenter Mar 25, 2026
3e6f661
enable logging
uriahcarpenter Mar 25, 2026
70f398c
fix all javadoc errors
uriahcarpenter Mar 25, 2026
347a7a8
Update src/main/java/com/widen/urlbuilder/UrlBuilder.java
uriahcarpenter Mar 25, 2026
c63efdc
Update src/main/java/com/widen/urlbuilder/UrlSigner.java
uriahcarpenter Mar 25, 2026
ec2ba2a
Update src/main/java/com/widen/urlbuilder/UrlSigner.java
uriahcarpenter Mar 25, 2026
8888d08
Update src/main/java/com/widen/urlbuilder/S3UrlBuilder.java
uriahcarpenter Mar 25, 2026
ad7bd0d
fix: align expireAt(Instant) @since to 3.1.0 in S3 and CloudFront bui…
uriahcarpenter Apr 7, 2026
de71221
fix: wrap checked crypto exceptions in README signing example
uriahcarpenter Apr 7, 2026
8ff18d1
fix: remove accidental trailing comma in HmacSigningExampleTest signi…
uriahcarpenter Apr 7, 2026
deeb21c
fix: correct indentation in CloudfrontUrlBuilder.expireAt(Date)
uriahcarpenter Apr 7, 2026
26f4472
fix: change SigningContext.getPort() from int/-1 to Integer/null for …
uriahcarpenter Apr 7, 2026
2f9f00c
fix: cache static URL-safe Base64 encoder in UrlSafeBase64
uriahcarpenter Apr 7, 2026
d26563a
chore: add project AGENTS.md with mise exec Java command convention
uriahcarpenter Apr 7, 2026
2443f13
revert: change @since 3.1.0 back to 3.0.0 (not yet released)
uriahcarpenter Apr 7, 2026
87afbcc
simplify readme example
uriahcarpenter Apr 7, 2026
3936169
chore(deps): bump org.bouncycastle:bcprov-jdk15to18 from 1.80 to 1.83…
dependabot[bot] Apr 11, 2026
33e621d
fix: restore bouncycastle bundle from master
uriahcarpenter Apr 11, 2026
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
13 changes: 13 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# AGENTS.md

## Java Commands

All `java`, `javac`, and related JVM commands must be prefixed with `mise exec --` to ensure the correct Java version (as specified by [mise](https://mise.jdx.dev/)) is used. Examples:

```bash
mise exec -- java -version
mise exec -- ./gradlew build
mise exec -- ./gradlew test
```

This applies to any direct `java` invocation and to any tool that shells out to the JVM.
118 changes: 118 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ Made with :heart: by Widen.
* [UrlBuilderQueryParamsTest](/src/test/java/com/widen/urlbuilder/UrlBuilderQueryParamsTest.java) - query parameters
* [UrlBuilderPortSslTest](/src/test/java/com/widen/urlbuilder/UrlBuilderPortSslTest.java) - port and SSL
* [UrlBuilderModesTest](/src/test/java/com/widen/urlbuilder/UrlBuilderModesTest.java) - output modes
* [UrlBuilderSigningTest](/src/test/java/com/widen/urlbuilder/UrlBuilderSigningTest.java) - URL signing

## Installation

Expand Down Expand Up @@ -180,6 +181,123 @@ new CloudfrontUrlBuilder("d1234.cloudfront.net", "files/document.pdf", "APKAEIBA
.toString()
```

## URL Signing

UrlBuilder supports signing URLs during generation using the `UrlSigner` interface. This is useful for implementing HMAC signatures, RSA signing (like CloudFront), or other URL signing schemes.

The `sign` method receives a `SigningContext` and returns a `Map<String, String>`. Each key/value pair in that map is appended as a query parameter to the final generated URL.

### Basic Usage

Sign URLs using a lambda function:

```java
import java.util.Collections;

String SECRET_KEY = "my-secret-key";

UrlBuilder builder = new UrlBuilder("cdn.example.com", "/videos/movie.mp4");
builder.addParameter("user", "john");
builder.usingUrlSigner(context -> {
// NOTE: use a strong hash like HmacSHA256 in production
String signature = Integer.toHexString((context.getUrl() + SECRET_KEY).hashCode());
return Collections.singletonMap("signature", signature);
});
Comment thread
uriahcarpenter marked this conversation as resolved.
String signedUrl = builder.toString();

// Result: http://cdn.example.com/videos/movie.mp4?user=john&signature=abc123...
```

### Reusable Signers

Create reusable signer classes:

```java
public class HmacUrlSigner implements UrlSigner {
private final String secretKey;

public HmacUrlSigner(String secretKey) {
this.secretKey = secretKey;
}

@Override
public Map<String, String> sign(SigningContext context) {
String signature = hmacSha256(context.getUrl(), secretKey);

Map<String, String> params = new HashMap<>();
params.put("signature", signature);
params.put("timestamp", String.valueOf(System.currentTimeMillis() / 1000));
return params;
}
}

// Usage
HmacUrlSigner signer = new HmacUrlSigner("my-secret");
UrlBuilder builder = new UrlBuilder("cdn.example.com", "/media/video.mp4");
builder.usingUrlSigner(signer);
String signedUrl = builder.toString();
```

### Expiring URLs

Create signed URLs with expiration:

```java
public class ExpiringHmacSigner implements UrlSigner {
private final String secretKey;
private final long expirationSeconds;

public ExpiringHmacSigner(String secretKey, long expirationSeconds) {
this.secretKey = secretKey;
this.expirationSeconds = expirationSeconds;
}

@Override
public Map<String, String> sign(SigningContext context) {
long expiresAt = System.currentTimeMillis() / 1000 + expirationSeconds;

// Sign URL + expiration
String toSign = context.getUrl() + expiresAt;
String signature = hmacSha256(toSign, secretKey);

Map<String, String> params = new LinkedHashMap<>();
params.put("expires", String.valueOf(expiresAt));
params.put("signature", signature);
return params;
}
}

// Create URL that expires in 1 hour
ExpiringHmacSigner signer = new ExpiringHmacSigner("my-secret", 3600);
UrlBuilder builder = new UrlBuilder("api.example.com", "/v1/data");
builder.usingUrlSigner(signer);
String signedUrl = builder.toString();
```

### Signing Context

The `SigningContext` provides access to URL components:

- `getUrl()` - Complete unsigned URL (what you typically sign)
- `getProtocol()` - Protocol (http/https)
- `getHostname()` - Hostname
- `getPort()` - Port number or -1
- `getEncodedPath()` - URL-encoded path
- `getEncodedQuery()` - URL-encoded query string
- `getFragment()` - Fragment (not typically signed)
- `isSsl()` - Whether using HTTPS
- `getGenerationMode()` - URL generation mode

### Best Practices

1. **Keep signers stateless** - Signers should be thread-safe
2. **Sign the complete URL** - Use `context.getUrl()` in most cases
3. **Don't include fragments** - Fragments are client-side only
4. **Use base64/hex encoding** - Signature values are not URL-encoded by default
5. **Add expiration** - Prevent signature reuse with expiration timestamps

See [`HmacSigningExampleTest`](/src/test/java/com/widen/urlbuilder/examples/HmacSigningExampleTest.java) for more examples.

## License

Licensed under the Apache Version 2.0 license. See [the license file](LICENSE.md) for details.
9 changes: 7 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,23 @@ repositories {
dependencies {
implementation(libs.bundles.bouncycastle)

// JUnit 5
testImplementation(platform(libs.junit.bom))
testImplementation(libs.junit.jupiter)
testRuntimeOnly(libs.junit.platform.launcher)

// Other test dependencies
testImplementation(libs.commons.io)
testImplementation(libs.slf4j.simple)
}

tasks.test {
useJUnitPlatform()
testLogging {
events("started", "passed", "skipped", "failed")
exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
showExceptions = true
showCauses = true
showStackTraces = true
}
}

nexusPublishing {
Expand Down
Loading
Loading