Introduce CMS client
authorMathieu Baudier <mbaudier@argeo.org>
Mon, 31 Oct 2022 09:56:54 +0000 (10:56 +0100)
committerMathieu Baudier <mbaudier@argeo.org>
Mon, 31 Oct 2022 09:56:54 +0000 (10:56 +0100)
org.argeo.cms.cli/src/org/argeo/cms/cli/CmsCommands.java
org.argeo.cms.cli/src/org/argeo/cms/cli/EventCommands.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/client/CmsClient.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/client/SpnegoHttpClient.java [deleted file]
org.argeo.cms/src/org/argeo/cms/client/WebSocketEventClient.java
org.argeo.cms/src/org/argeo/cms/client/jaas-client-ipa.cfg
org.argeo.cms/src/org/argeo/cms/internal/http/CmsAuthenticator.java

index 6f2db4ff22db5e495f5374f8cc162af1edab80e2..50977d1e1045a58bc0132cccd4389f78a631bf66 100644 (file)
@@ -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<Void> {
+       class Get implements DescribedCommand<String> {
 
                @Override
                public Options getOptions() {
@@ -70,32 +72,56 @@ public class CmsCommands extends CommandsCli {
                }
 
                @Override
-               public Void apply(List<String> t) {
+               public String apply(List<String> t) {
                        CommandLine line = toCommandLine(t);
                        List<String> 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<String> {
+
+               @Override
+               public Options getOptions() {
+                       Options options = new Options();
+                       options.addOption(connectOption);
+                       return options;
+               }
+
+               @Override
+               public String apply(List<String> 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 (file)
index 0000000..009ad45
--- /dev/null
@@ -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<Void> {
+
+               @Override
+               public Options getOptions() {
+                       Options options = new Options();
+                       options.addOption(CmsCommands.connectOption);
+                       return options;
+               }
+
+               @Override
+               public Void apply(List<String> t) {
+                       CommandLine line = toCommandLine(t);
+                       List<String> 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 (file)
index 0000000..af06560
--- /dev/null
@@ -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<String> bodyHandler = BodyHandlers.ofString();
+                       HttpResponse<String> 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<WebSocket> newWebSocket(WebSocket.Listener listener) {
+               return newWebSocket(uri, listener);
+       }
+
+       public CompletableFuture<WebSocket> newWebSocket(URI uri, WebSocket.Listener listener) {
+               CompletableFuture<WebSocket> ws = getHttpClient().newWebSocketBuilder()
+                               .header(HttpHeader.AUTHORIZATION.getHeaderName(), HttpHeader.NEGOTIATE + " " + getGssToken())
+                               .buildAsync(uri, listener);
+               return ws;
+       }
+
+       @SuppressWarnings("unchecked")
+       protected SSLContext ipaSslContext() {
+               try {
+                       final Collection<X509Certificate> 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<X509Certificate>) 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 (file)
index 444f4ef..0000000
+++ /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=<principal@REALM> "
-                                       + SpnegoHttpClient.class.getName() + " <url>");
-                       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<String> bodyHandler = BodyHandlers.ofString();
-                       HttpResponse<String> 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<X509Certificate> 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<X509Certificate>) 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);
-               }
-       }
-
-}
index ed20e95b271a7b7168ad43821fcf0fbf96b9896c..e8dd2fa523f4c2305e42b6fd21353ff93fc9a61e 100644 (file)
@@ -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<String> 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<WebSocket> ws = client.newWebSocketBuilder()
-                                       .header(HttpHeader.AUTHORIZATION.getHeaderName(), HttpHeader.NEGOTIATE + " " + token)
-                                       .buildAsync(uri, listener);
+                       CompletableFuture<WebSocket> 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() + " <url>");
-//                     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<String> res = CompletableFuture.completedStage(message.toString());
+                       return res;
+               }
+
+               @Override
+               public CompletionStage<?> onPong(WebSocket webSocket, ByteBuffer message) {
+                       // System.out.println("Pong received.");
+                       return null;
+               }
 
+       }
 }
index 2921a339738ebdc3164323a134e5064db04dc261..b776c2c5ce6ed90ca7c6774a48e771997037d7f9 100644 (file)
@@ -1,4 +1,4 @@
 CLIENT {
     com.sun.security.auth.module.Krb5LoginModule required
-    useTicketCache=true;
+     useTicketCache=true;
 };
index 5d96244d8ed4cdb1cbb9e77e8346f2168e430af3..caa7810098e30d012e02202944b9a52d388d15c3 100644 (file)
@@ -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<Void>() {
-//
-//                     @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);