Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prototype of rewrite of CLI to not use Tyrus #9688

Draft
wants to merge 8 commits into
base: master
Choose a base branch
from
10 changes: 0 additions & 10 deletions cli/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,6 @@
<version>${mina-sshd.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.glassfish.tyrus.bundles</groupId>
<artifactId>tyrus-standalone-client-jdk</artifactId>
<version>2.2.0</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.jenkins-ci</groupId>
<artifactId>annotation-indexer</artifactId>
Expand Down Expand Up @@ -141,10 +135,6 @@
<pattern>com</pattern>
<shadedPattern>io.jenkins.cli.shaded.com</shadedPattern>
</relocation>
<relocation>
<pattern>jakarta</pattern>
<shadedPattern>io.jenkins.cli.shaded.jakarta</shadedPattern>
</relocation>
</relocations>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
Expand Down
106 changes: 49 additions & 57 deletions cli/src/main/java/hudson/cli/CLI.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,7 @@

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import hudson.cli.client.Messages;
import jakarta.websocket.ClientEndpointConfig;
import jakarta.websocket.Endpoint;
import jakarta.websocket.EndpointConfig;
import jakarta.websocket.HandshakeResponse;
import jakarta.websocket.Session;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.IOException;
Expand All @@ -42,6 +38,9 @@
import java.net.URI;
import java.net.URL;
import java.net.URLConnection;
import java.net.http.HttpClient;
import java.net.http.WebSocket;
import java.net.http.WebSocketHandshakeException;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.file.Files;
Expand All @@ -54,19 +53,17 @@
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import org.glassfish.tyrus.client.ClientManager;
import org.glassfish.tyrus.client.ClientProperties;
import org.glassfish.tyrus.client.SslEngineConfigurator;
import org.glassfish.tyrus.client.exception.DeploymentHandshakeException;
import org.glassfish.tyrus.container.jdk.client.JdkClientContainer;

/**
* CLI entry point to Jenkins.
Expand Down Expand Up @@ -336,71 +333,66 @@ private static File getFileFromArguments(List<String> args) {

private static int webSocketConnection(String url, List<String> args, CLIConnectionFactory factory) throws Exception {
LOGGER.fine(() -> "Trying to connect to " + url + " via plain protocol over WebSocket");
class CLIEndpoint extends Endpoint {
@Override
public void onOpen(Session session, EndpointConfig config) {}
var wsb = HttpClient.newBuilder().followRedirects(HttpClient.Redirect.NORMAL).build().newWebSocketBuilder();
if (factory.authorization != null) {
wsb.header("Authorization", factory.authorization);
}

class Authenticator extends ClientEndpointConfig.Configurator {
HandshakeResponse hr;
@Override
public void beforeRequest(Map<String, List<String>> headers) {
if (factory.authorization != null) {
headers.put("Authorization", List.of(factory.authorization));
}
}
@Override
public void afterResponse(HandshakeResponse hr) {
this.hr = hr;
}
}
var authenticator = new Authenticator();

ClientManager client = ClientManager.createClient(JdkClientContainer.class.getName()); // ~ ContainerProvider.getWebSocketContainer()
client.getProperties().put(ClientProperties.REDIRECT_ENABLED, true); // https://tyrus-project.github.io/documentation/1.13.1/index/tyrus-proprietary-config.html#d0e1775
if (factory.noCertificateCheck) {
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, new TrustManager[] {new NoCheckTrustManager()}, new SecureRandom());
/* TODO
SslEngineConfigurator sslEngineConfigurator = new SslEngineConfigurator(sslContext);
sslEngineConfigurator.setHostnameVerifier((s, sslSession) -> true);
client.getProperties().put(ClientProperties.SSL_ENGINE_CONFIGURATOR, sslEngineConfigurator);
*/
}
Session session;
try {
session = client.connectToServer(new CLIEndpoint(), ClientEndpointConfig.Builder.create().configurator(authenticator).build(), URI.create(url.replaceFirst("^http", "ws") + "cli/ws"));
} catch (DeploymentHandshakeException x) {
System.err.println("CLI handshake failed with status code " + x.getHttpStatusCode());
if (authenticator.hr != null) {
for (var entry : authenticator.hr.getHeaders().entrySet()) {
// org.glassfish.tyrus.core.Utils.parseHeaderValue improperly splits values like Date at commas, so undo that:
System.err.println(entry.getKey() + ": " + String.join(", ", entry.getValue()));
}
// UpgradeResponse.getReasonPhrase is useless since Jetty generates it from the code,
// and the body is not accessible at all.
}
return 15; // compare CLICommand.main
}
PlainCLIProtocol.Output out = new PlainCLIProtocol.Output() {
var ws = new AtomicReference<WebSocket>();
var out = new PlainCLIProtocol.Output() {
@Override
public void send(byte[] data) throws IOException {
session.getBasicRemote().sendBinary(ByteBuffer.wrap(data));
ws.get().sendBinary(ByteBuffer.wrap(data), false);
jglick marked this conversation as resolved.
Show resolved Hide resolved
}

@Override
public void close() throws IOException {
session.close();
var _ws = ws.get();
if (_ws != null) {
_ws.sendClose(WebSocket.NORMAL_CLOSURE, "");
}
}
};
try (ClientSideImpl connection = new ClientSideImpl(out)) {
session.addMessageHandler(InputStream.class, is -> {
try {
connection.handle(new DataInputStream(is));
} catch (IOException x) {
LOGGER.log(Level.WARNING, null, x);
ws.set(wsb.buildAsync(URI.create(url.replaceFirst("^http", "ws") + "cli/ws"), new WebSocket.Listener() {
@Override
public CompletionStage<?> onBinary(WebSocket webSocket, ByteBuffer data, boolean last) {
var f = new CompletableFuture<Void>();
try {
connection.handle(new DataInputStream(new ByteArrayInputStream(data.array())));
f.complete(null);
} catch (IOException x) {
f.completeExceptionally(x);
}
return f;
}
});
}).get());
connection.start(args);
return connection.exit();
} catch (ExecutionException x) {
var cause = x.getCause();
if (cause instanceof WebSocketHandshakeException) {
var rsp = ((WebSocketHandshakeException) cause).getResponse();
System.err.println("CLI handshake failed with status code " + rsp.statusCode());
for (var entry : rsp.headers().map().entrySet()) {
for (var value : entry.getValue()) {
System.err.println(entry.getKey() + ": " + value);
}
}
// WebSocketHandshakeException.getResponse is available but HttpResponse.body is empty
return 15; // compare CLICommand.main
} else if (cause instanceof Exception x2) {
throw x2;
} else {
throw new Exception(cause);
}
}
}

Expand Down
5 changes: 2 additions & 3 deletions test/src/test/java/hudson/cli/CLIActionTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@
import org.apache.commons.io.input.NullInputStream;
import org.apache.commons.io.output.CountingOutputStream;
import org.apache.commons.io.output.TeeOutputStream;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
Expand Down Expand Up @@ -136,7 +135,7 @@ private void assertExitCode(int code, boolean useApiToken, File jar, String... a
assertEquals(code, proc.join());
}

@Ignore("TODO flaky test") @Test public void authenticationFailed() throws Exception {
@Test public void authenticationFailed() throws Exception {
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy().grant(Jenkins.ADMINISTER).everywhere().toAuthenticated());
var jar = tmp.newFile("jenkins-cli.jar");
Expand All @@ -145,7 +144,7 @@ private void assertExitCode(int code, boolean useApiToken, File jar, String... a
var exitStatus = new Launcher.LocalLauncher(StreamTaskListener.fromStderr()).launch().cmds(
"java", "-jar", jar.getAbsolutePath(), "-s", j.getURL().toString(), "-auth", "user:bogustoken", "who-am-i"
).stdout(baos).start().join();
assertThat(baos.toString(), allOf(containsString("status code 401"), containsString("Server: Jetty")));
assertThat(baos.toString(), allOf(containsString("status code 401"), containsString("erver: Jetty")));
jglick marked this conversation as resolved.
Show resolved Hide resolved
assertThat(exitStatus, is(15));
}

Expand Down
Loading