From: Mathieu Baudier Date: Mon, 31 Oct 2022 09:56:54 +0000 (+0100) Subject: Introduce CMS client X-Git-Tag: v2.3.11~55 X-Git-Url: https://git.argeo.org/?p=lgpl%2Fargeo-commons.git;a=commitdiff_plain;h=c9100383d67d1be4c5797f084169a3faf513f5fb Introduce CMS client --- 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/CmsClient.java b/org.argeo.cms/src/org/argeo/cms/client/CmsClient.java new file mode 100644 index 000000000..af0656097 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/client/CmsClient.java @@ -0,0 +1,172 @@ +package org.argeo.cms.client; + +import java.io.BufferedInputStream; +import java.io.IOException; +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; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +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.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; + +/** Utility to connect to a remote CMS node. */ +public class CmsClient { + public final static String CLIENT_LOGIN_CONTEXT = "CLIENT"; + + private URI uri; + + private HttpClient httpClient; + private String gssToken; + + public CmsClient(URI uri) { + this.uri = uri; + } + + 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 { + + } + } + + public String getAsString() { + return getAsString(uri); + } + + 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); + 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; + } + + public CompletableFuture newWebSocket(WebSocket.Listener listener) { + return newWebSocket(uri, listener); + } + + 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") + protected SSLContext ipaSslContext() { + try { + final Collection certificates; + Path caCertificatePath = Paths.get("/etc/ipa/ca.crt"); + if (Files.exists(caCertificatePath)) { + CertificateFactory certificateFactory = CertificateFactory.getInstance("X509"); + try (BufferedInputStream in = new BufferedInputStream(Files.newInputStream(caCertificatePath))) { + certificates = (Collection) certificateFactory.generateCertificates(in); + } + } else { + certificates = null; + } + TrustManager[] noopTrustManager = new TrustManager[] { new X509TrustManager() { + public void checkClientTrusted(X509Certificate[] xcs, String string) { + } + + public void checkServerTrusted(X509Certificate[] xcs, String string) { + } + + public X509Certificate[] getAcceptedIssuers() { + if (certificates == null) + return null; + return certificates.toArray(new X509Certificate[certificates.size()]); + } + } }; + + SSLContext sc = SSLContext.getInstance("ssl"); + sc.init(null, noopTrustManager, null); + return sc; + } catch (KeyManagementException | NoSuchAlgorithmException | CertificateException | IOException e) { + throw new IllegalStateException("Cannot create SSL context ", e); + } + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/client/SpnegoHttpClient.java b/org.argeo.cms/src/org/argeo/cms/client/SpnegoHttpClient.java deleted file mode 100644 index 444f4efb5..000000000 --- a/org.argeo.cms/src/org/argeo/cms/client/SpnegoHttpClient.java +++ /dev/null @@ -1,113 +0,0 @@ -package org.argeo.cms.client; - -import java.io.BufferedInputStream; -import java.io.IOException; -import java.net.MalformedURLException; -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.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.security.KeyManagementException; -import java.security.NoSuchAlgorithmException; -import java.security.cert.CertificateException; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; -import java.util.Collection; - -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 org.argeo.cms.auth.ConsoleCallbackHandler; -import org.argeo.cms.auth.RemoteAuthUtils; -import org.argeo.util.http.HttpHeader; - -public class SpnegoHttpClient { - 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(); - - URL jaasUrl = SpnegoHttpClient.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(); - - HttpClient httpClient = openHttpClient(lc.getSubject()); - String token = RemoteAuthUtils.createGssToken(lc.getSubject(), "HTTP", server); - - HttpRequest request = HttpRequest.newBuilder().uri(u.toURI()) // - .header(HttpHeader.AUTHORIZATION.getHeaderName(), HttpHeader.NEGOTIATE + " " + token) // - .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(); - } - } - - static HttpClient openHttpClient(Subject subject) { - HttpClient client = HttpClient.newBuilder() // - .sslContext(ipaSslContext()) // - .version(HttpClient.Version.HTTP_1_1) // - .build(); - - return client; - } - - @SuppressWarnings("unchecked") - static SSLContext ipaSslContext() { - try { - final Collection certificates; - Path caCertificatePath = Paths.get("/etc/ipa/ca.crt"); - if (Files.exists(caCertificatePath)) { - CertificateFactory certificateFactory = CertificateFactory.getInstance("X509"); - try (BufferedInputStream in = new BufferedInputStream(Files.newInputStream(caCertificatePath))) { - certificates = (Collection) certificateFactory.generateCertificates(in); - } - } else { - certificates = null; - } - TrustManager[] noopTrustManager = new TrustManager[] { new X509TrustManager() { - public void checkClientTrusted(X509Certificate[] xcs, String string) { - } - - public void checkServerTrusted(X509Certificate[] xcs, String string) { - } - - public X509Certificate[] getAcceptedIssuers() { - if (certificates == null) - return null; - return certificates.toArray(new X509Certificate[certificates.size()]); - } - } }; - - SSLContext sc = SSLContext.getInstance("ssl"); - sc.init(null, noopTrustManager, null); - return sc; - } catch (KeyManagementException | NoSuchAlgorithmException | CertificateException | IOException e) { - throw new IllegalStateException("Cannot create SSL context ", e); - } - } - -} 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);