From c9100383d67d1be4c5797f084169a3faf513f5fb Mon Sep 17 00:00:00 2001 From: Mathieu Baudier Date: Mon, 31 Oct 2022 10:56:54 +0100 Subject: [PATCH] Introduce CMS client --- .../src/org/argeo/cms/cli/CmsCommands.java | 62 ++++++--- .../src/org/argeo/cms/cli/EventCommands.java | 64 +++++++++ .../{SpnegoHttpClient.java => CmsClient.java} | 121 +++++++++++++----- .../cms/client/WebSocketEventClient.java | 66 +++------- .../org/argeo/cms/client/jaas-client-ipa.cfg | 2 +- .../cms/internal/http/CmsAuthenticator.java | 16 --- 6 files changed, 219 insertions(+), 112 deletions(-) create mode 100644 org.argeo.cms.cli/src/org/argeo/cms/cli/EventCommands.java rename org.argeo.cms/src/org/argeo/cms/client/{SpnegoHttpClient.java => CmsClient.java} (50%) diff --git a/org.argeo.cms.cli/src/org/argeo/cms/cli/CmsCommands.java b/org.argeo.cms.cli/src/org/argeo/cms/cli/CmsCommands.java index 6f2db4ff2..50977d1e1 100644 --- a/org.argeo.cms.cli/src/org/argeo/cms/cli/CmsCommands.java +++ b/org.argeo.cms.cli/src/org/argeo/cms/cli/CmsCommands.java @@ -6,12 +6,12 @@ import java.util.List; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.Option; import org.apache.commons.cli.Options; -import org.argeo.api.cli.CommandArgsException; import org.argeo.api.cli.CommandsCli; import org.argeo.api.cli.DescribedCommand; -import org.argeo.cms.client.WebSocketEventClient; +import org.argeo.cms.client.CmsClient; import org.argeo.cms.client.WebSocketPing; +/** Commands dealing with CMS. */ public class CmsCommands extends CommandsCli { final static Option connectOption = Option.builder().option("c").longOpt("connect").desc("server to connect to") .hasArg(true).build(); @@ -19,7 +19,9 @@ public class CmsCommands extends CommandsCli { public CmsCommands(String commandName) { super(commandName); addCommand("ping", new Ping()); - addCommand("event", new Events()); + addCommand("get", new Get()); + addCommand("status", new Status()); + addCommand("event", new EventCommands("event")); } @Override @@ -60,7 +62,7 @@ public class CmsCommands extends CommandsCli { } - class Events implements DescribedCommand { + class Get implements DescribedCommand { @Override public Options getOptions() { @@ -70,32 +72,56 @@ public class CmsCommands extends CommandsCli { } @Override - public Void apply(List t) { + public String apply(List t) { CommandLine line = toCommandLine(t); List remaining = line.getArgList(); - if (remaining.size() == 0) { - throw new CommandArgsException("There must be at least one argument"); + String additionalUri = null; + if (remaining.size() != 0) { + additionalUri = remaining.get(0); } - String topic = remaining.get(0); - String uriArg = line.getOptionValue(connectOption); - // TODO make it more robust (trailing /, etc.) - URI uri = URI.create(uriArg); - if ("".equals(uri.getPath())) { - uri = URI.create(uri.toString() + "/cms/status/event/" + topic); - } - new WebSocketEventClient(uri).run(); - return null; + String connectUri = line.getOptionValue(connectOption); + CmsClient cmsClient = new CmsClient(URI.create(connectUri)); + return additionalUri != null ? cmsClient.getAsString(URI.create(additionalUri)) : cmsClient.getAsString(); + } + + @Override + public String getUsage() { + return "[URI]"; + } + + @Override + public String getDescription() { + return "Retrieve this URI as a string"; + } + + } + + class Status implements DescribedCommand { + + @Override + public Options getOptions() { + Options options = new Options(); + options.addOption(connectOption); + return options; + } + + @Override + public String apply(List t) { + CommandLine line = toCommandLine(t); + String connectUri = line.getOptionValue(connectOption); + CmsClient cmsClient = new CmsClient(URI.create(connectUri)); + return cmsClient.getAsString(URI.create("/cms/status")); } @Override public String getUsage() { - return "TOPIC"; + return "[URI]"; } @Override public String getDescription() { - return "Listen to events on a topic"; + return "Retrieve the CMS status as a string"; } } diff --git a/org.argeo.cms.cli/src/org/argeo/cms/cli/EventCommands.java b/org.argeo.cms.cli/src/org/argeo/cms/cli/EventCommands.java new file mode 100644 index 000000000..009ad455b --- /dev/null +++ b/org.argeo.cms.cli/src/org/argeo/cms/cli/EventCommands.java @@ -0,0 +1,64 @@ +package org.argeo.cms.cli; + +import java.net.URI; +import java.util.List; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Options; +import org.argeo.api.cli.CommandArgsException; +import org.argeo.api.cli.CommandsCli; +import org.argeo.api.cli.DescribedCommand; +import org.argeo.cms.client.WebSocketEventClient; + +/** Commands dealing with CMS events. */ +public class EventCommands extends CommandsCli { + public EventCommands(String commandName) { + super(commandName); + addCommand("listen", new EventListent()); + } + + @Override + public String getDescription() { + return "Utilities related to an Argeo CMS"; + } + + class EventListent implements DescribedCommand { + + @Override + public Options getOptions() { + Options options = new Options(); + options.addOption(CmsCommands.connectOption); + return options; + } + + @Override + public Void apply(List t) { + CommandLine line = toCommandLine(t); + List remaining = line.getArgList(); + if (remaining.size() == 0) { + throw new CommandArgsException("There must be at least one argument"); + } + String topic = remaining.get(0); + + String uriArg = line.getOptionValue(CmsCommands.connectOption); + // TODO make it more robust (trailing /, etc.) + URI uri = URI.create(uriArg); + if ("".equals(uri.getPath())) { + uri = URI.create(uri.toString() + "/cms/status/event/" + topic); + } + new WebSocketEventClient(uri).run(); + return null; + } + + @Override + public String getUsage() { + return "TOPIC"; + } + + @Override + public String getDescription() { + return "Listen to events on a topic"; + } + + } +} diff --git a/org.argeo.cms/src/org/argeo/cms/client/SpnegoHttpClient.java b/org.argeo.cms/src/org/argeo/cms/client/CmsClient.java similarity index 50% rename from org.argeo.cms/src/org/argeo/cms/client/SpnegoHttpClient.java rename to org.argeo.cms/src/org/argeo/cms/client/CmsClient.java index 444f4efb5..af0656097 100644 --- a/org.argeo.cms/src/org/argeo/cms/client/SpnegoHttpClient.java +++ b/org.argeo.cms/src/org/argeo/cms/client/CmsClient.java @@ -2,13 +2,15 @@ package org.argeo.cms.client; import java.io.BufferedInputStream; import java.io.IOException; -import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; import java.net.URL; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandler; import java.net.http.HttpResponse.BodyHandlers; +import java.net.http.WebSocket; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -18,65 +20,122 @@ import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.util.Collection; +import java.util.concurrent.CompletableFuture; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; -import javax.security.auth.Subject; import javax.security.auth.login.LoginContext; +import javax.security.auth.login.LoginException; import org.argeo.cms.auth.ConsoleCallbackHandler; import org.argeo.cms.auth.RemoteAuthUtils; import org.argeo.util.http.HttpHeader; -public class SpnegoHttpClient { +/** Utility to connect to a remote CMS node. */ +public class CmsClient { public final static String CLIENT_LOGIN_CONTEXT = "CLIENT"; - public static void main(String[] args) throws MalformedURLException { -// String principal = System.getProperty("javax.security.auth.login.name"); - if (args.length == 0) { - System.err.println("usage: java -Djavax.security.auth.login.name= " - + SpnegoHttpClient.class.getName() + " "); - System.exit(1); - return; - } - String url = args[0]; - URL u = new URL(url); - String server = u.getHost(); + private URI uri; + + private HttpClient httpClient; + private String gssToken; + + public CmsClient(URI uri) { + this.uri = uri; + } - URL jaasUrl = SpnegoHttpClient.class.getResource("jaas-client-ipa.cfg"); + public void login() { + String server = uri.getHost(); + + URL jaasUrl = CmsClient.class.getResource("jaas-client-ipa.cfg"); System.setProperty("java.security.auth.login.config", jaasUrl.toExternalForm()); try { LoginContext lc = new LoginContext(CLIENT_LOGIN_CONTEXT, new ConsoleCallbackHandler()); lc.login(); + gssToken = RemoteAuthUtils.createGssToken(lc.getSubject(), "HTTP", server); + } catch (LoginException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } finally { + + } + } - HttpClient httpClient = openHttpClient(lc.getSubject()); - String token = RemoteAuthUtils.createGssToken(lc.getSubject(), "HTTP", server); + public String getAsString() { + return getAsString(uri); + } - HttpRequest request = HttpRequest.newBuilder().uri(u.toURI()) // - .header(HttpHeader.AUTHORIZATION.getHeaderName(), HttpHeader.NEGOTIATE + " " + token) // + public String getAsString(URI uri) { + uri = normalizeUri(uri); + try { + HttpClient httpClient = getHttpClient(); + + HttpRequest request = HttpRequest.newBuilder().uri(uri) // + .header(HttpHeader.AUTHORIZATION.getHeaderName(), HttpHeader.NEGOTIATE + " " + getGssToken()) // .build(); BodyHandler bodyHandler = BodyHandlers.ofString(); HttpResponse response = httpClient.send(request, bodyHandler); - System.out.println(response.body()); - int responseCode = response.statusCode(); - System.exit(responseCode); - } catch (Exception e) { - e.printStackTrace(); + return response.body(); +// int responseCode = response.statusCode(); +// System.exit(responseCode); + } catch (IOException | InterruptedException e) { + throw new RuntimeException("Cannot read " + uri + " as a string", e); + } + } + + protected URI normalizeUri(URI uri) { + if (uri.getHost() != null) + return uri; + try { + String path = uri.getPath(); + if (path.startsWith("/")) {// absolute + return new URI(this.uri.getScheme(), this.uri.getUserInfo(), this.uri.getHost(), this.uri.getPort(), + path, uri.getQuery(), uri.getFragment()); + } else { + String thisUriStr = this.uri.toString(); + if (!thisUriStr.endsWith("/")) + thisUriStr = thisUriStr + "/"; + return URI.create(thisUriStr + path); + } + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Cannot interpret " + uri, e); + } + } + + public URI getUri() { + return uri; + } + + String getGssToken() { + return gssToken; + } + + public HttpClient getHttpClient() { + if (httpClient == null) { + login(); + HttpClient client = HttpClient.newBuilder() // + .sslContext(ipaSslContext()) // + .version(HttpClient.Version.HTTP_1_1) // + .build(); + httpClient = client; } + return httpClient; } - static HttpClient openHttpClient(Subject subject) { - HttpClient client = HttpClient.newBuilder() // - .sslContext(ipaSslContext()) // - .version(HttpClient.Version.HTTP_1_1) // - .build(); + public CompletableFuture newWebSocket(WebSocket.Listener listener) { + return newWebSocket(uri, listener); + } - return client; + public CompletableFuture newWebSocket(URI uri, WebSocket.Listener listener) { + CompletableFuture ws = getHttpClient().newWebSocketBuilder() + .header(HttpHeader.AUTHORIZATION.getHeaderName(), HttpHeader.NEGOTIATE + " " + getGssToken()) + .buildAsync(uri, listener); + return ws; } @SuppressWarnings("unchecked") - static SSLContext ipaSslContext() { + protected SSLContext ipaSslContext() { try { final Collection certificates; Path caCertificatePath = Paths.get("/etc/ipa/ca.crt"); diff --git a/org.argeo.cms/src/org/argeo/cms/client/WebSocketEventClient.java b/org.argeo.cms/src/org/argeo/cms/client/WebSocketEventClient.java index ed20e95b2..e8dd2fa52 100644 --- a/org.argeo.cms/src/org/argeo/cms/client/WebSocketEventClient.java +++ b/org.argeo.cms/src/org/argeo/cms/client/WebSocketEventClient.java @@ -1,61 +1,30 @@ package org.argeo.cms.client; import java.net.URI; -import java.net.URL; -import java.net.http.HttpClient; import java.net.http.WebSocket; import java.nio.ByteBuffer; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; import java.util.concurrent.ExecutionException; -import javax.security.auth.login.LoginContext; -import javax.security.auth.login.LoginException; - -import org.argeo.cms.auth.RemoteAuthUtils; -import org.argeo.util.http.HttpHeader; - /** Tests connectivity to the web socket server. */ public class WebSocketEventClient implements Runnable { private final URI uri; private WebSocket webSocket; - + + private CmsClient cmsClient; + public WebSocketEventClient(URI uri) { this.uri = uri; + cmsClient = new CmsClient(uri); } @Override public void run() { try { - WebSocket.Listener listener = new WebSocket.Listener() { - - public CompletionStage onText(WebSocket webSocket, CharSequence message, boolean last) { - System.out.println(message); - CompletionStage res = CompletableFuture.completedStage(message.toString()); - return res; - } - - @Override - public CompletionStage onPong(WebSocket webSocket, ByteBuffer message) { - // System.out.println("Pong received."); - return null; - } - - }; - - // SPNEGO - URL jaasUrl = SpnegoHttpClient.class.getResource("jaas-client-ipa.cfg"); - System.setProperty("java.security.auth.login.config", jaasUrl.toExternalForm()); - LoginContext lc = new LoginContext(SpnegoHttpClient.CLIENT_LOGIN_CONTEXT); - lc.login(); - String token = RemoteAuthUtils.createGssToken(lc.getSubject(), "HTTP", uri.getHost()); - - HttpClient client = SpnegoHttpClient.openHttpClient(lc.getSubject()); - CompletableFuture ws = client.newWebSocketBuilder() - .header(HttpHeader.AUTHORIZATION.getHeaderName(), HttpHeader.NEGOTIATE + " " + token) - .buildAsync(uri, listener); + CompletableFuture ws = cmsClient.newWebSocket(new WsEventListener()); WebSocket webSocket = ws.get(); webSocket.request(Long.MAX_VALUE); @@ -66,21 +35,26 @@ public class WebSocketEventClient implements Runnable { webSocket.sendPing(ByteBuffer.allocate(0)); Thread.sleep(10000); } - }catch (InterruptedException e) { + } catch (InterruptedException e) { if (webSocket != null) webSocket.sendClose(WebSocket.NORMAL_CLOSURE, ""); - } catch (ExecutionException | LoginException e) { + } catch (ExecutionException e) { throw new RuntimeException("Cannot listent to " + uri, e.getCause()); } } -// public static void main(String[] args) throws Exception { -// if (args.length == 0) { -// System.err.println("usage: java " + WebSocketEventClient.class.getName() + " "); -// System.exit(1); -// return; -// } -// URI uri = URI.create(args[0]); -// } + private class WsEventListener implements WebSocket.Listener { + public CompletionStage onText(WebSocket webSocket, CharSequence message, boolean last) { + System.out.println(message); + CompletionStage res = CompletableFuture.completedStage(message.toString()); + return res; + } + + @Override + public CompletionStage onPong(WebSocket webSocket, ByteBuffer message) { + // System.out.println("Pong received."); + return null; + } + } } diff --git a/org.argeo.cms/src/org/argeo/cms/client/jaas-client-ipa.cfg b/org.argeo.cms/src/org/argeo/cms/client/jaas-client-ipa.cfg index 2921a3397..b776c2c5c 100644 --- a/org.argeo.cms/src/org/argeo/cms/client/jaas-client-ipa.cfg +++ b/org.argeo.cms/src/org/argeo/cms/client/jaas-client-ipa.cfg @@ -1,4 +1,4 @@ CLIENT { com.sun.security.auth.module.Krb5LoginModule required - useTicketCache=true; + useTicketCache=true; }; diff --git a/org.argeo.cms/src/org/argeo/cms/internal/http/CmsAuthenticator.java b/org.argeo.cms/src/org/argeo/cms/internal/http/CmsAuthenticator.java index 5d96244d8..caa781009 100644 --- a/org.argeo.cms/src/org/argeo/cms/internal/http/CmsAuthenticator.java +++ b/org.argeo.cms/src/org/argeo/cms/internal/http/CmsAuthenticator.java @@ -23,8 +23,6 @@ public class CmsAuthenticator extends Authenticator { @Override public Result authenticate(HttpExchange exch) { -// if (log.isTraceEnabled()) -// HttpUtils.logRequestHeaders(log, request); RemoteAuthHttpExchange remoteAuthExchange = new RemoteAuthHttpExchange(exch); ClassLoader currentThreadContextClassLoader = Thread.currentThread().getContextClassLoader(); Thread.currentThread().setContextClassLoader(CmsAuthenticator.class.getClassLoader()); @@ -49,20 +47,6 @@ public class CmsAuthenticator extends Authenticator { Subject subject = lc.getSubject(); -// CurrentSubject.callAs(subject, () -> { -// RemoteAuthUtils.configureRequestSecurity(remoteAuthExchange); -// return null; -// }); -// Subject.doAs(subject, new PrivilegedAction() { -// -// @Override -// public Void run() { -// // TODO also set login context in order to log out ? -// RemoteAuthUtils.configureRequestSecurity(new ServletHttpRequest(request)); -// return null; -// } -// -// }); String username = CurrentUser.getUsername(subject); HttpPrincipal httpPrincipal = new HttpPrincipal(username, httpAuthRealm); return new Authenticator.Success(httpPrincipal); -- 2.30.2