Skip to content

Commit 0313c41

Browse files
committed
Use HTTP server for TUF conformance testing
Signed-off-by: Aaron Lew <[email protected]>
1 parent cbc0836 commit 0313c41

File tree

7 files changed

+219
-5
lines changed

7 files changed

+219
-5
lines changed

.github/workflows/tuf-conformance.yml

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
name: TUF Conformance Tests
2+
permissions:
3+
contents: read
24

35
on:
46
push:
@@ -37,11 +39,14 @@ jobs:
3739
- name: Setup Gradle
3840
uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
3941

40-
- name: Build tuf cli
41-
run: ./gradlew :tuf-cli:build
42+
- name: Build tuf cli and server jar
43+
run: ./gradlew :tuf-cli:serverShadowJar
4244

43-
- name: Unpack tuf distribution
44-
run: tar -xvf ${{ github.workspace }}/tuf-cli/build/distributions/tuf-cli-*.tar --strip-components 1
45+
- name: Start test server in background
46+
run: java -jar ${{ github.workspace }}/tuf-cli/build/libs/tuf-cli-server-all.jar &
47+
48+
- name: Wait for server to be ready
49+
run: curl --retry-connrefused --retry 10 --retry-delay 1 --fail http://localhost:8080/
4550

4651
- name: Set up JDK ${{ matrix.java-version }}
4752
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
@@ -51,5 +56,5 @@ jobs:
5156

5257
- uses: theupdateframework/tuf-conformance@9bfc222a371e30ad5511eb17449f68f855fb9d8f # v2.3.0
5358
with:
54-
entrypoint: ${{ github.workspace }}/bin/tuf-cli
59+
entrypoint: ${{ github.workspace }}/tuf-cli/tuf-cli-server
5560
artifact-name: test repositories for tuf-cli java ${{ matrix.java-version }}

tuf-cli/build.gradle.kts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
2+
13
plugins {
24
id("build-logic.java")
35
id("application")
6+
id("com.gradleup.shadow") version "9.0.0-rc3"
47
}
58

69
repositories {
@@ -15,6 +18,11 @@ dependencies {
1518
implementation(platform("com.google.oauth-client:google-oauth-client-bom:1.39.0"))
1619
implementation("com.google.oauth-client:google-oauth-client")
1720

21+
implementation("org.eclipse.jetty:jetty-server:11.0.24")
22+
implementation("org.eclipse.jetty:jetty-servlet:11.0.24")
23+
24+
implementation("org.slf4j:slf4j-simple:2.0.17")
25+
1826
annotationProcessor("info.picocli:picocli-codegen:4.7.6")
1927
}
2028

@@ -37,3 +45,25 @@ distributions.main {
3745
tasks.run.configure {
3846
workingDir = rootProject.projectDir
3947
}
48+
49+
tasks.register<ShadowJar>("serverShadowJar") {
50+
archiveBaseName.set("tuf-cli-server")
51+
archiveClassifier.set("all")
52+
archiveVersion.set("")
53+
54+
mergeServiceFiles()
55+
56+
from(sourceSets.main.get().output)
57+
configurations = listOf(project.configurations.runtimeClasspath.get())
58+
59+
from(project.file("tuf-cli.xfails")) {
60+
rename { "tuf-cli-server.xfails" }
61+
into("bin")
62+
}
63+
64+
exclude("META-INF/*.SF", "META-INF/*.DSA", "META-INF/*.RSA")
65+
66+
manifest {
67+
attributes("Main-Class" to "dev.sigstore.tuf.cli.TufConformanceServer")
68+
}
69+
}

tuf-cli/src/main/java/dev/sigstore/tuf/cli/Download.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ public Integer call() throws Exception {
3838
var targetDir = tufCommand.getTargetDir();
3939
var targetBaseUrl = tufCommand.getTargetBaseUrl();
4040
var targetName = tufCommand.getTargetName();
41+
var clock = tufCommand.getClock();
4142

4243
var fsStore = FileSystemTufStore.newFileSystemStore(metadataDir, targetDir);
4344
var tuf =
@@ -49,6 +50,7 @@ public Integer call() throws Exception {
4950
.setMetaFetcher(MetaFetcher.newFetcher(HttpFetcher.newFetcher(metadataUrl)))
5051
.setTargetFetcher(HttpFetcher.newFetcher(targetBaseUrl))
5152
.setTargetStore(fsStore)
53+
.setClock(clock)
5254
.build();
5355
// the java client isn't one shot like other clients, so downloadTarget doesn't call update
5456
// for the sake of conformance updateMeta here

tuf-cli/src/main/java/dev/sigstore/tuf/cli/Refresh.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ public class Refresh implements Callable<Integer> {
3535
public Integer call() throws Exception {
3636
var metadataDir = tufCommand.getMetadataDir();
3737
var metadataUrl = tufCommand.getMetadataUrl();
38+
var clock = tufCommand.getClock();
3839

3940
var fsStore = FileSystemTufStore.newFileSystemStore(metadataDir);
4041
var tuf =
@@ -44,6 +45,7 @@ public Integer call() throws Exception {
4445
PassthroughCacheMetaStore.newPassthroughMetaCache(fsStore)))
4546
.setTrustedRootPath(RootProvider.fromFile(metadataDir.resolve("root.json")))
4647
.setMetaFetcher(MetaFetcher.newFetcher(HttpFetcher.newFetcher(metadataUrl)))
48+
.setClock(clock)
4749
.build();
4850
tuf.updateMeta();
4951
return 0;

tuf-cli/src/main/java/dev/sigstore/tuf/cli/Tuf.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717

1818
import java.net.URI;
1919
import java.nio.file.Path;
20+
import java.time.Clock;
21+
import java.time.Instant;
22+
import java.time.ZoneOffset;
2023
import picocli.CommandLine;
2124
import picocli.CommandLine.Command;
2225
import picocli.CommandLine.Model.CommandSpec;
@@ -65,6 +68,15 @@ public CommandSpec getSpec() {
6568
paramLabel = "<TARGET_DIR>")
6669
private Path targetDir;
6770

71+
private Clock clock = Clock.systemUTC();
72+
73+
@Option(
74+
names = {"--time"},
75+
required = false)
76+
public void setTime(String epochSecond) {
77+
this.clock = Clock.fixed(Instant.ofEpochSecond(Long.parseLong(epochSecond)), ZoneOffset.UTC);
78+
}
79+
6880
Path getMetadataDir() {
6981
if (metadataDir == null) {
7082
throw new ParameterException(spec.commandLine(), "--metadata-dir not set");
@@ -100,6 +112,10 @@ Path getTargetDir() {
100112
return targetDir;
101113
}
102114

115+
public Clock getClock() {
116+
return clock;
117+
}
118+
103119
public static void main(String[] args) {
104120
int exitCode = new CommandLine(new Tuf()).execute(args);
105121
System.exit(exitCode);
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/*
2+
* Copyright 2025 The Sigstore Authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package dev.sigstore.tuf.cli;
17+
18+
import com.google.gson.Gson;
19+
import jakarta.servlet.ServletException;
20+
import jakarta.servlet.http.HttpServletRequest;
21+
import jakarta.servlet.http.HttpServletResponse;
22+
import java.io.ByteArrayOutputStream;
23+
import java.io.IOException;
24+
import java.io.InputStream;
25+
import java.io.OutputStream;
26+
import java.io.PrintStream;
27+
import java.nio.charset.StandardCharsets;
28+
import java.util.Arrays;
29+
import java.util.List;
30+
import java.util.Map;
31+
import org.eclipse.jetty.server.Request;
32+
import org.eclipse.jetty.server.Server;
33+
import org.eclipse.jetty.server.handler.AbstractHandler;
34+
35+
public class TufConformanceServer {
36+
37+
private static final Gson GSON = new Gson();
38+
private static boolean debug = false;
39+
40+
private static class ExecuteRequest {
41+
String[] args;
42+
String faketime;
43+
}
44+
45+
public static void main(String[] args) throws Exception {
46+
if (args.length > 0 && "--debug".equals(args[0])) {
47+
debug = true;
48+
}
49+
int port = 8080;
50+
Server server = new Server(port);
51+
server.setHandler(new TufConformanceHandler());
52+
server.start();
53+
server.join();
54+
}
55+
56+
public static class TufConformanceHandler extends AbstractHandler {
57+
@Override
58+
public void handle(
59+
String target,
60+
Request baseRequest,
61+
HttpServletRequest request,
62+
HttpServletResponse response)
63+
throws IOException, ServletException {
64+
if ("/".equals(target)) {
65+
handleHealthCheck(response);
66+
} else if ("/execute".equals(target) && "POST".equals(request.getMethod())) {
67+
handleExecute(request, response);
68+
}
69+
baseRequest.setHandled(true);
70+
}
71+
}
72+
73+
private static void handleExecute(HttpServletRequest request, HttpServletResponse response)
74+
throws IOException {
75+
ExecuteRequest executeRequest;
76+
try (InputStream is = request.getInputStream()) {
77+
String requestBody = new String(is.readAllBytes(), StandardCharsets.UTF_8);
78+
executeRequest = GSON.fromJson(requestBody, ExecuteRequest.class);
79+
}
80+
81+
// Tests should not be run in parallel, to ensure orderly input/output
82+
PrintStream originalOut = System.out;
83+
PrintStream originalErr = System.err;
84+
85+
ByteArrayOutputStream outContent = new ByteArrayOutputStream();
86+
ByteArrayOutputStream errContent = new ByteArrayOutputStream();
87+
88+
try (PrintStream outPs = new PrintStream(outContent, true, StandardCharsets.UTF_8);
89+
PrintStream errPs = new PrintStream(errContent, true, StandardCharsets.UTF_8)) {
90+
if (!debug) {
91+
System.setOut(outPs);
92+
System.setErr(errPs);
93+
}
94+
95+
List<String> args = new java.util.ArrayList<>();
96+
args.add("--time");
97+
args.add(executeRequest.faketime);
98+
args.addAll(Arrays.asList(executeRequest.args));
99+
100+
int exitCode = new picocli.CommandLine(new Tuf()).execute(args.toArray(String[]::new));
101+
102+
Map<String, Object> responseMap =
103+
Map.of(
104+
"stdout", outContent.toString(StandardCharsets.UTF_8),
105+
"stderr", errContent.toString(StandardCharsets.UTF_8),
106+
"exitCode", exitCode);
107+
String jsonResponse = GSON.toJson(responseMap);
108+
109+
response.setStatus(HttpServletResponse.SC_OK);
110+
response.setContentType("application/json");
111+
byte[] responseBytes = jsonResponse.getBytes(StandardCharsets.UTF_8);
112+
response.setContentLength(responseBytes.length);
113+
114+
try (OutputStream os = response.getOutputStream()) {
115+
os.write(responseBytes);
116+
}
117+
} finally {
118+
if (!debug) {
119+
System.setOut(originalOut);
120+
System.setErr(originalErr);
121+
}
122+
}
123+
}
124+
125+
private static void handleHealthCheck(HttpServletResponse response) throws IOException {
126+
response.setStatus(HttpServletResponse.SC_OK);
127+
response.getWriter().println("OK");
128+
}
129+
}

tuf-cli/tuf-cli-server

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#!/bin/bash
2+
3+
set -o pipefail -o errexit -o nounset
4+
5+
CWD=$PWD
6+
7+
ARGS_JSON="["
8+
for arg in "$@"; do
9+
escaped_arg=$(echo -n "$arg" | jq -R -s '.')
10+
ARGS_JSON="$ARGS_JSON$escaped_arg,"
11+
done
12+
if [[ $ARGS_JSON == *, ]]; then
13+
ARGS_JSON="${ARGS_JSON%,}"
14+
fi
15+
ARGS_JSON="$ARGS_JSON]"
16+
17+
DATE_VAL="$(date +%s)"
18+
19+
JSON_PAYLOAD=$(jq -nc --arg cwd "$CWD" --argjson args "$ARGS_JSON" --arg faketime "$DATE_VAL" '{"cwd": $cwd, "args": $args, "faketime": $faketime}')
20+
21+
RESPONSE=$(curl -s -X POST --header "Content-Type: application/json" --data-binary "$JSON_PAYLOAD" http://localhost:8080/execute)
22+
23+
STDOUT=$(echo "$RESPONSE" | jq -r .stdout)
24+
STDERR=$(echo "$RESPONSE" | jq -r .stderr)
25+
EXIT_CODE=$(echo "$RESPONSE" | jq .exitCode)
26+
27+
echo -n "$STDOUT"
28+
echo -n "$STDERR" >&2
29+
30+
exit "$EXIT_CODE"

0 commit comments

Comments
 (0)