Skip to content
Merged
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
4 changes: 2 additions & 2 deletions docs/design/sdk-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ public interface Rift extends AutoCloseable {
// -- intercept (TLS-MITM) --
Intercept intercept(); // default options
Intercept intercept(InterceptOptions options); // InterceptOptions.attach() discovers a running listener
Optional<InterceptStatus> interceptStatus(); // GET /intercept (404 → empty); embedded: own listener, else the admin plane's
Optional<InterceptStatus> interceptStatus(); // GET /intercept (404 → empty); embedded: the admin plane's once served, else own listener

// -- async facade (secondary surface) --
RiftAsync async(); // createAsync(ImposterSpec), deleteAllAsync(), impostersAsync()
Expand Down Expand Up @@ -288,7 +288,7 @@ public interface RiftTransport extends AutoCloseable {
void spaceDelete(int port, String flowId);
// intercept
JsonValue startIntercept(JsonValue options);
default Optional<JsonValue> interceptStatus(); // GET /intercept | embedded: the FFI-started listener, else the admin plane's GET /intercept
default Optional<JsonValue> interceptStatus(); // GET /intercept | embedded: the admin plane's GET /intercept once one is served (it shares the FFI slot, rift#521), else the FFI-started listener
void interceptAddRules(JsonValue rules);
default void interceptReplaceRules(JsonValue rules); // PUT /intercept/rules | rift_intercept_replace_rules (>= 0.20.0)
JsonValue interceptListRules(); void interceptClearRules();
Expand Down
5 changes: 3 additions & 2 deletions docs/intercept.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,9 @@ the way a listener it started is reached: through `ConnectOptions.interceptAddre
wildcard bind (`0.0.0.0`) replaced by the admin host. No listener running → `IllegalStateException`,
and intercept stays available. The explicit `attach(host, port)` form never consults
`interceptAddress`: the endpoint you give is used as is. On the embedded transport
`interceptStatus()` reports the listener the handle started until it is closed, or one another client
started through the engine's admin plane (`rift.adminUri()`).
`interceptStatus()` reports the engine's one listener, whether the handle started it or another client
started it through the engine's admin plane (`rift.adminUri()`). A client that stops or restarts it
through that plane is reflected too.

To start the listener at runtime instead — with your own CA, say — expose the port without
launching a listener, then start it bound to the container's interface. The handle is mapped to
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,9 @@ default Intercept intercept() {
/**
* The intercept listener the engine reports running, or empty when none is. Over the admin API
* this is {@code GET /intercept} and sees a listener started by anyone — at engine launch, by
* another client. On the embedded transport it reports the listener this handle started until it is
* closed, or one another client started through this engine's {@link #adminUri() admin plane}.
* another client. On the embedded transport it reports the engine's one listener, whoever started
* it: this handle, or another client through this engine's {@link #adminUri() admin plane}, which
* can also stop or restart it. Asking does not start the admin plane.
*/
Optional<InterceptStatus> interceptStatus();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,11 @@ public final class EmbeddedTransport implements RiftTransport {
private volatile RemoteTransport admin;
/**
* The listener this handle started over FFI, as {@code {interceptPort, interceptUrl}} (never the
* CA a start can return); null when none is. The engine runs one listener, but its admin plane's
* {@code GET /intercept} reports only one started through that plane (rift 0.21.0), and there is
* no FFI status symbol — so this record answers for an FFI start, the admin plane for the rest.
* CA a start can return); null when none is. There is no FFI status symbol, so this answers
* {@link #interceptStatus()} only while no admin plane is served: then nothing but this handle can
* change the engine's one intercept slot. Once a plane is served, its {@code GET /intercept}
* reads that same slot (the C-ABI and the plane have shared it since rift#521) and is the answer instead,
* since another client can stop or restart the listener through it.
*/
private volatile JsonValue runningIntercept;

Expand Down Expand Up @@ -332,13 +334,11 @@ public Optional<JsonValue> interceptStatus() {
if (closed.get()) {
throw new IllegalStateException("the embedded rift engine is closed");
}
JsonValue started = runningIntercept;
if (started != null) {
return Optional.of(started);
}
// Started by another client through this engine's admin plane, if it is serving one.
RemoteTransport a = admin;
return a == null ? Optional.empty() : a.interceptStatus();
if (a != null) {
return a.interceptStatus();
}
return Optional.ofNullable(runningIntercept);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,49 @@ void interceptStatusReportsAListenerAnotherClientStartedThroughTheAdminPlane() {
}
}

@Test
void anFfiStartedListenerStoppedThroughTheAdminPlaneIsNoLongerReported() throws Exception {
// the C-ABI and the admin plane share one intercept slot (rift#521): the plane is the truth
try (Rift rift = embedded()) {
rift.intercept();
assertTrue(rift.interceptStatus().isPresent());

assertEquals(204, deleteIntercept(rift.adminUri()), "the admin plane stops the FFI-started listener");
assertEquals(Optional.empty(), rift.interceptStatus(), "no listener runs any more");
}
}

@Test
void aListenerRestartedThroughTheAdminPlaneIsReportedWithItsNewPort() throws Exception {
try (Rift rift = embedded();
Rift viaAdmin = Rift.connect(ConnectOptions.builder(rift.adminUri()).versionCheck(VersionCheck.OFF).build())) {
int ffiPort = rift.intercept().address().getPort();
assertEquals(204, deleteIntercept(rift.adminUri()));

int theirPort = viaAdmin.intercept().address().getPort();
org.junit.jupiter.api.Assumptions.assumeTrue(theirPort != ffiPort,
"the OS handed the freed port straight back, so a stale report would look right");
assertEquals(theirPort, rift.interceptStatus().orElseThrow().port(),
"the report follows the shared slot, not this handle's start (" + ffiPort + ")");
}
}

@Test
void anFfiStartAfterTheAdminPlaneIsServedIsReported() {
// this handle's own start, answered by the plane rather than the cached record: the one case
// that fails if the plane stops seeing an FFI-started listener
try (Rift rift = embedded()) {
rift.adminUri();
int port = rift.intercept().address().getPort();
assertEquals(port, rift.interceptStatus().orElseThrow().port());
}
}

private static int deleteIntercept(URI adminUri) throws Exception {
HttpRequest delete = HttpRequest.newBuilder(adminUri.resolve("/intercept")).timeout(Duration.ofSeconds(5)).DELETE().build();
return HttpClient.newHttpClient().send(delete, HttpResponse.BodyHandlers.discarding()).statusCode();
}

@Test
void interceptStatusOnAClosedEngineIsRefused() {
Rift rift = embedded();
Expand Down
Loading