From 81d9084e2c9fd9d33ca1d864171d28f9564647d8 Mon Sep 17 00:00:00 2001 From: Mathieu Baudier Date: Sun, 17 Jul 2022 15:32:57 +0200 Subject: [PATCH] Start supporting JDK HTTP server API --- .../src/org/argeo/cms/cli}/CmsCli.java | 2 +- .../org/argeo/cms/cli}/StaticCmsLaunch.java | 3 +- .../httpserver/HttpContextServlet.java | 50 +++ .../httpserver/ServletHttpExchange.java | 187 ++++++++++ .../http/jetty/EquinoxJettyServer.java | 6 +- org.argeo.cms.lib.jetty/build.properties | 4 + .../org/argeo/cms/jetty/CmsJettyServer.java | 201 ++--------- .../org/argeo/cms/jetty/JettyHttpContext.java | 147 ++++++++ .../org/argeo/cms/jetty/JettyHttpServer.java | 326 ++++++++++++++++++ org.argeo.cms/bnd.bnd | 1 - org.argeo.cms/build.properties | 14 +- .../src/org/argeo/cms/auth/CmsAuthUtils.java | 6 +- .../RemoteCmsSessionImpl.java} | 7 +- .../cms/internal/http/CmsAuthenticator.java | 114 ++++++ .../internal/http/RemoteAuthHttpExchange.java | 76 ++++ .../http/client/HttpCredentialProvider.java | 20 -- .../http/client/SpnegoAuthScheme.java | 165 --------- .../http/client/SpnegoCredentials.java | 7 - .../cms/internal/runtime/CmsUserAdmin.java | 17 - .../org/argeo/util/dav/DavServerHandler.java | 25 ++ .../org/argeo/util/http/HttpServerUtils.java | 32 ++ 21 files changed, 1006 insertions(+), 404 deletions(-) rename {org.argeo.cms/src/org/argeo/cms/runtime => org.argeo.cms.cli/src/org/argeo/cms/cli}/CmsCli.java (92%) rename {org.argeo.cms/src/org/argeo/cms/runtime => org.argeo.cms.cli/src/org/argeo/cms/cli}/StaticCmsLaunch.java (95%) create mode 100644 org.argeo.cms.ee/src/org/argeo/cms/servlet/httpserver/HttpContextServlet.java create mode 100644 org.argeo.cms.ee/src/org/argeo/cms/servlet/httpserver/ServletHttpExchange.java create mode 100644 org.argeo.cms.lib.jetty/src/org/argeo/cms/jetty/JettyHttpContext.java create mode 100644 org.argeo.cms.lib.jetty/src/org/argeo/cms/jetty/JettyHttpServer.java rename org.argeo.cms/src/org/argeo/cms/internal/{http/WebCmsSessionImpl.java => auth/RemoteCmsSessionImpl.java} (72%) create mode 100644 org.argeo.cms/src/org/argeo/cms/internal/http/CmsAuthenticator.java create mode 100644 org.argeo.cms/src/org/argeo/cms/internal/http/RemoteAuthHttpExchange.java delete mode 100644 org.argeo.cms/src/org/argeo/cms/internal/http/client/HttpCredentialProvider.java delete mode 100644 org.argeo.cms/src/org/argeo/cms/internal/http/client/SpnegoAuthScheme.java delete mode 100644 org.argeo.cms/src/org/argeo/cms/internal/http/client/SpnegoCredentials.java create mode 100644 org.argeo.util/src/org/argeo/util/dav/DavServerHandler.java create mode 100644 org.argeo.util/src/org/argeo/util/http/HttpServerUtils.java diff --git a/org.argeo.cms/src/org/argeo/cms/runtime/CmsCli.java b/org.argeo.cms.cli/src/org/argeo/cms/cli/CmsCli.java similarity index 92% rename from org.argeo.cms/src/org/argeo/cms/runtime/CmsCli.java rename to org.argeo.cms.cli/src/org/argeo/cms/cli/CmsCli.java index 425bc30f9..516c7688f 100644 --- a/org.argeo.cms/src/org/argeo/cms/runtime/CmsCli.java +++ b/org.argeo.cms.cli/src/org/argeo/cms/cli/CmsCli.java @@ -1,4 +1,4 @@ -package org.argeo.cms.runtime; +package org.argeo.cms.cli; import org.argeo.api.cli.CommandsCli; diff --git a/org.argeo.cms/src/org/argeo/cms/runtime/StaticCmsLaunch.java b/org.argeo.cms.cli/src/org/argeo/cms/cli/StaticCmsLaunch.java similarity index 95% rename from org.argeo.cms/src/org/argeo/cms/runtime/StaticCmsLaunch.java rename to org.argeo.cms.cli/src/org/argeo/cms/cli/StaticCmsLaunch.java index b4cfb906a..9b5359832 100644 --- a/org.argeo.cms/src/org/argeo/cms/runtime/StaticCmsLaunch.java +++ b/org.argeo.cms.cli/src/org/argeo/cms/cli/StaticCmsLaunch.java @@ -1,4 +1,4 @@ -package org.argeo.cms.runtime; +package org.argeo.cms.cli; import java.lang.management.ManagementFactory; import java.nio.file.Path; @@ -9,6 +9,7 @@ import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.Option; import org.apache.commons.cli.Options; import org.argeo.api.cli.DescribedCommand; +import org.argeo.cms.runtime.StaticCms; public class StaticCmsLaunch implements DescribedCommand { private Option dataOption; diff --git a/org.argeo.cms.ee/src/org/argeo/cms/servlet/httpserver/HttpContextServlet.java b/org.argeo.cms.ee/src/org/argeo/cms/servlet/httpserver/HttpContextServlet.java new file mode 100644 index 000000000..c81bad7bc --- /dev/null +++ b/org.argeo.cms.ee/src/org/argeo/cms/servlet/httpserver/HttpContextServlet.java @@ -0,0 +1,50 @@ +package org.argeo.cms.servlet.httpserver; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import com.sun.net.httpserver.Authenticator; +import com.sun.net.httpserver.HttpContext; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpPrincipal; + +public class HttpContextServlet extends HttpServlet { + private static final long serialVersionUID = 2321612280413662738L; + + private final HttpContext httpContext; + + public HttpContextServlet(HttpContext httpContext) { + this.httpContext = httpContext; + } + + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + try (ServletHttpExchange httpExchange = new ServletHttpExchange(httpContext, req, resp)) { + Authenticator authenticator = httpContext.getAuthenticator(); + if (authenticator != null) { + Authenticator.Result authenticationResult = authenticator.authenticate(httpExchange); + if (authenticationResult instanceof Authenticator.Success) { + HttpPrincipal httpPrincipal = ((Authenticator.Success) authenticationResult).getPrincipal(); + httpExchange.setPrincipal(httpPrincipal); + } else if (authenticationResult instanceof Authenticator.Retry) { + resp.setStatus(((Authenticator.Retry) authenticationResult).getResponseCode()); + return; + } else if (authenticationResult instanceof Authenticator.Failure) { + resp.setStatus(((Authenticator.Failure) authenticationResult).getResponseCode()); + return; + } else { + throw new UnsupportedOperationException( + "Authentication result " + authenticationResult.getClass().getName() + " is not supported"); + } + } + + HttpHandler httpHandler = httpContext.getHandler(); + httpHandler.handle(httpExchange); + } + } + +} diff --git a/org.argeo.cms.ee/src/org/argeo/cms/servlet/httpserver/ServletHttpExchange.java b/org.argeo.cms.ee/src/org/argeo/cms/servlet/httpserver/ServletHttpExchange.java new file mode 100644 index 000000000..5a29fbe84 --- /dev/null +++ b/org.argeo.cms.ee/src/org/argeo/cms/servlet/httpserver/ServletHttpExchange.java @@ -0,0 +1,187 @@ +package org.argeo.cms.servlet.httpserver; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.net.URI; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; + +import javax.net.ssl.SSLSession; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import com.sun.net.httpserver.Headers; +import com.sun.net.httpserver.HttpContext; +import com.sun.net.httpserver.HttpPrincipal; +import com.sun.net.httpserver.HttpsExchange; + +public class ServletHttpExchange extends HttpsExchange { + private final HttpContext httpContext; + private final HttpServletRequest httpServletRequest; + private final HttpServletResponse httpServletResponse; + + private final Headers requestHeaders; + private final Headers responseHeaders; + + private InputStream filteredIn; + private OutputStream filteredOut; + + private HttpPrincipal principal; + + public ServletHttpExchange(HttpContext httpContext, HttpServletRequest httpServletRequest, + HttpServletResponse httpServletResponse) { + this.httpContext = httpContext; + this.httpServletRequest = httpServletRequest; + this.httpServletResponse = httpServletResponse; + + // request headers + requestHeaders = new Headers(); + for (Enumeration headerNames = httpServletRequest.getHeaderNames(); headerNames.hasMoreElements();) { + String headerName = headerNames.nextElement(); + List values = new ArrayList<>(); + for (Enumeration headerValues = httpServletRequest.getHeaders(headerName); headerValues + .hasMoreElements();) + values.add(headerValues.nextElement()); + requestHeaders.put(headerName, values); + } + + responseHeaders = new Headers(); + } + + @Override + public SSLSession getSSLSession() { + Object obj = httpServletRequest.getAttribute("javax.net.ssl.session"); + if (obj == null || !(obj instanceof SSLSession)) + throw new IllegalStateException("SSL session not found"); + return (SSLSession) obj; + } + + @Override + public Headers getRequestHeaders() { + return requestHeaders; + } + + @Override + public Headers getResponseHeaders() { + return responseHeaders; + } + + @Override + public URI getRequestURI() { + return URI.create(httpServletRequest.getRequestURI()); + } + + @Override + public String getRequestMethod() { + return httpServletRequest.getMethod(); + } + + @Override + public HttpContext getHttpContext() { + return httpContext; + } + + @Override + public void close() { + try { + httpServletRequest.getInputStream().close(); + } catch (IOException e) { + // TODO use proper logging + e.printStackTrace(); + } + try { + httpServletResponse.getOutputStream().close(); + } catch (IOException e) { + // TODO use proper logging + e.printStackTrace(); + } + + } + + @Override + public InputStream getRequestBody() { + try { + if (filteredIn != null) + return filteredIn; + else + return httpServletRequest.getInputStream(); + } catch (IOException e) { + throw new IllegalStateException("Cannot get request body", e); + } + } + + @Override + public OutputStream getResponseBody() { + try { + if (filteredOut != null) + return filteredOut; + else + return httpServletResponse.getOutputStream(); + } catch (IOException e) { + throw new IllegalStateException("Cannot get response body", e); + } + } + + @Override + public void sendResponseHeaders(int rCode, long responseLength) throws IOException { + for (String headerName : responseHeaders.keySet()) { + for (String headerValue : responseHeaders.get(headerName)) { + httpServletResponse.addHeader(headerName, headerValue); + } + } + // TODO deal with content length etc. + httpServletResponse.setStatus(rCode); + } + + @Override + public InetSocketAddress getRemoteAddress() { + return new InetSocketAddress(httpServletRequest.getRemoteHost(), httpServletRequest.getRemotePort()); + } + + @Override + public int getResponseCode() { + return httpServletResponse.getStatus(); + } + + @Override + public InetSocketAddress getLocalAddress() { + return new InetSocketAddress(httpServletRequest.getLocalName(), httpServletRequest.getLocalPort()); + } + + @Override + public String getProtocol() { + return httpServletRequest.getProtocol(); + } + + @Override + public Object getAttribute(String name) { + return httpServletRequest.getAttribute(name); + } + + @Override + public void setAttribute(String name, Object value) { + httpServletRequest.setAttribute(name, value); + } + + @Override + public void setStreams(InputStream i, OutputStream o) { + if (i != null) + filteredIn = i; + if (o != null) + filteredOut = o; + + } + + @Override + public HttpPrincipal getPrincipal() { + return principal; + } + + void setPrincipal(HttpPrincipal principal) { + this.principal = principal; + } + +} diff --git a/org.argeo.cms.lib.equinox/src/org/argeo/cms/equinox/http/jetty/EquinoxJettyServer.java b/org.argeo.cms.lib.equinox/src/org/argeo/cms/equinox/http/jetty/EquinoxJettyServer.java index d73bdfd1b..1b1b42961 100644 --- a/org.argeo.cms.lib.equinox/src/org/argeo/cms/equinox/http/jetty/EquinoxJettyServer.java +++ b/org.argeo.cms.lib.equinox/src/org/argeo/cms/equinox/http/jetty/EquinoxJettyServer.java @@ -25,7 +25,7 @@ public class EquinoxJettyServer extends CmsJettyServer { private static final String INTERNAL_CONTEXT_CLASSLOADER = "org.eclipse.equinox.http.jetty.internal.ContextClassLoader"; @Override - protected void addServlets(ServletContextHandler servletContextHandler) throws ServletException { + protected void addServlets(ServletContextHandler rootContextHandler) throws ServletException { ServletHolder holder = new ServletHolder(new InternalHttpServiceServlet()); holder.setInitOrder(0); holder.setInitParameter(Constants.SERVICE_VENDOR, "Eclipse.org"); //$NON-NLS-1$ @@ -33,10 +33,10 @@ public class EquinoxJettyServer extends CmsJettyServer { // holder.setInitParameter(JettyConstants.CONTEXT_PATH, // httpContext.getContextPath()); - servletContextHandler.addServlet(holder, "/*"); + rootContextHandler.addServlet(holder, "/*"); // post-start - SessionHandler sessionManager = servletContextHandler.getSessionHandler(); + SessionHandler sessionManager = rootContextHandler.getSessionHandler(); sessionManager.addEventListener((HttpSessionIdListener) holder.getServlet()); } diff --git a/org.argeo.cms.lib.jetty/build.properties b/org.argeo.cms.lib.jetty/build.properties index 34d2e4d2d..62f58d95a 100644 --- a/org.argeo.cms.lib.jetty/build.properties +++ b/org.argeo.cms.lib.jetty/build.properties @@ -2,3 +2,7 @@ source.. = src/ output.. = bin/ bin.includes = META-INF/,\ . +additional.bundles = org.eclipse.jetty.io,\ + org.eclipse.jetty.security,\ + org.argeo.ext.slf4j,\ + org.slf4j.api diff --git a/org.argeo.cms.lib.jetty/src/org/argeo/cms/jetty/CmsJettyServer.java b/org.argeo.cms.lib.jetty/src/org/argeo/cms/jetty/CmsJettyServer.java index 39fc66d9d..7f33beeb6 100644 --- a/org.argeo.cms.lib.jetty/src/org/argeo/cms/jetty/CmsJettyServer.java +++ b/org.argeo.cms.lib.jetty/src/org/argeo/cms/jetty/CmsJettyServer.java @@ -1,5 +1,6 @@ package org.argeo.cms.jetty; +import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -9,189 +10,71 @@ import javax.websocket.DeploymentException; import javax.websocket.server.ServerContainer; import javax.websocket.server.ServerEndpointConfig; -import org.argeo.api.cms.CmsLog; import org.argeo.api.cms.CmsState; import org.argeo.cms.CmsDeployProperty; import org.argeo.cms.websocket.javax.server.CmsWebSocketConfigurator; import org.argeo.cms.websocket.javax.server.TestEndpoint; -import org.eclipse.jetty.http.UriCompliance; -import org.eclipse.jetty.server.HttpConfiguration; -import org.eclipse.jetty.server.HttpConnectionFactory; -import org.eclipse.jetty.server.SecureRequestCustomizer; -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.server.ServerConnector; -import org.eclipse.jetty.server.SslConnectionFactory; import org.eclipse.jetty.server.session.SessionHandler; import org.eclipse.jetty.servlet.ServletContextHandler; -import org.eclipse.jetty.util.ssl.SslContextFactory; -import org.eclipse.jetty.util.thread.QueuedThreadPool; - import org.eclipse.jetty.websocket.javax.server.config.JavaxWebSocketServletContainerInitializer; import org.eclipse.jetty.websocket.javax.server.config.JavaxWebSocketServletContainerInitializer.Configurator; -public class CmsJettyServer { - private static final CmsLog log = CmsLog.getLog(CmsJettyServer.class); - - private static final int DEFAULT_IDLE_TIMEOUT = 30000; +public class CmsJettyServer extends JettyHttpServer { private static final String CONTEXT_TEMPDIR = "javax.servlet.context.tempdir"; - // Equinox compatibility private static final String INTERNAL_CONTEXT_CLASSLOADER = "org.eclipse.equinox.http.jetty.internal.ContextClassLoader"; +// private static final CmsLog log = CmsLog.getLog(CmsJettyServer.class); - private Server server; +// private Server server; +// private Path tempDir; +// +// private ServerConnector httpConnector; +// private ServerConnector httpsConnector; private Path tempDir; - private ServerConnector httpConnector; - private ServerConnector httpsConnector; - // WebSocket private ServerContainer wsServerContainer; private ServerEndpointConfig.Configurator wsEndpointConfigurator; private CmsState cmsState; + protected void addServlets(ServletContextHandler servletContextHandler) throws ServletException { + } + + @Override public void start() { try { tempDir = Files.createTempDirectory("jetty"); - - server = new Server(new QueuedThreadPool(10, 1)); - - configure(); - // context.addServlet(new ServletHolder(new RWTServlet()), "/" + entryPoint); - // Required to serve rwt-resources. It is important that this is last. -// ServletHolder holderPwd = new ServletHolder("default", DefaultServlet.class); -// context.addServlet(holderPwd, "/"); - - if (httpConnector != null) { - httpConnector.open(); - server.addConnector(httpConnector); - } - - if (httpsConnector != null) { - httpsConnector.open(); - server.addConnector(httpsConnector); - } - - // holder - - // context - ServletContextHandler httpContext = createHttpContext(); - // httpContext.addServlet(holder, "/*"); - addServlets(httpContext); - enableWebSocket(httpContext); - server.setHandler(httpContext); - - // - // START - server.start(); - // - - Runtime.getRuntime().addShutdownHook(new Thread(() -> stop(), "Jetty shutdown")); - - log.info(httpPortsMsg()); - } catch (Exception e) { - throw new IllegalStateException("Cannot start Jetty HTTPS server", e); + } catch (IOException e) { + throw new IllegalStateException("Cannot create temp dir", e); } + super.start(); } - protected void addServlets(ServletContextHandler servletContextHandler) throws ServletException { - } + protected ServletContextHandler createRootContextHandler() { + ServletContextHandler servletContextHandler = new ServletContextHandler(); + servletContextHandler.setAttribute(INTERNAL_CONTEXT_CLASSLOADER, + Thread.currentThread().getContextClassLoader()); + servletContextHandler.setClassLoader(this.getClass().getClassLoader()); + servletContextHandler.setContextPath("/"); - public Integer getHttpPort() { - if (httpConnector == null) - return null; - return httpConnector.getLocalPort(); - } - - public Integer getHttpsPort() { - if (httpsConnector == null) - return null; - return httpsConnector.getLocalPort(); - } - - public void stop() { - try { - // serverConnector.close(); - server.stop(); - // TODO delete temp dir - } catch (Exception e) { - e.printStackTrace(); - } + servletContextHandler.setAttribute(CONTEXT_TEMPDIR, tempDir.toAbsolutePath().toFile()); + SessionHandler handler = new SessionHandler(); + handler.setMaxInactiveInterval(-1); + servletContextHandler.setSessionHandler(handler); + return servletContextHandler; } - protected void configure() { - HttpConfiguration http_config = new HttpConfiguration(); - - String httpPortStr = getFrameworkProp(CmsDeployProperty.HTTP_PORT); - String httpsPortStr = getFrameworkProp(CmsDeployProperty.HTTPS_PORT); - - /// TODO make it more generic - String httpHost = getFrameworkProp(CmsDeployProperty.HOST); -// String httpsHost = getFrameworkProp( -// JettyConfig.JETTY_PROPERTY_PREFIX + CmsHttpConstants.HTTPS_HOST); - - // try { - if (httpPortStr != null || httpsPortStr != null) { - boolean httpEnabled = httpPortStr != null; - // props.put(JettyHttpConstants.HTTP_ENABLED, httpEnabled); - boolean httpsEnabled = httpsPortStr != null; - // props.put(JettyHttpConstants.HTTPS_ENABLED, httpsEnabled); - if (httpsEnabled) { - int httpsPort = Integer.parseInt(httpsPortStr); - http_config.setSecureScheme("https"); - http_config.setSecurePort(httpsPort); - } - - if (httpEnabled) { - int httpPort = Integer.parseInt(httpPortStr); - httpConnector = new ServerConnector(server, new HttpConnectionFactory(http_config)); - httpConnector.setPort(httpPort); - httpConnector.setHost(httpHost); - httpConnector.setIdleTimeout(DEFAULT_IDLE_TIMEOUT); - } - - if (httpsEnabled) { - - SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); - // sslContextFactory.setKeyStore(KeyS) - - sslContextFactory.setKeyStoreType(getFrameworkProp(CmsDeployProperty.SSL_KEYSTORETYPE)); - sslContextFactory.setKeyStorePath(getFrameworkProp(CmsDeployProperty.SSL_KEYSTORE)); - sslContextFactory.setKeyStorePassword(getFrameworkProp(CmsDeployProperty.SSL_PASSWORD)); - // sslContextFactory.setKeyManagerPassword(getFrameworkProp(CmsDeployProperty.SSL_KEYPASSWORD)); - sslContextFactory.setProtocol("TLS"); - - sslContextFactory.setTrustStoreType(getFrameworkProp(CmsDeployProperty.SSL_TRUSTSTORETYPE)); - sslContextFactory.setTrustStorePath(getFrameworkProp(CmsDeployProperty.SSL_TRUSTSTORE)); - sslContextFactory.setTrustStorePassword(getFrameworkProp(CmsDeployProperty.SSL_TRUSTSTOREPASSWORD)); - - String wantClientAuth = getFrameworkProp(CmsDeployProperty.SSL_WANTCLIENTAUTH); - if (wantClientAuth != null && wantClientAuth.equals(Boolean.toString(true))) - sslContextFactory.setWantClientAuth(true); - String needClientAuth = getFrameworkProp(CmsDeployProperty.SSL_NEEDCLIENTAUTH); - if (needClientAuth != null && needClientAuth.equals(Boolean.toString(true))) - sslContextFactory.setNeedClientAuth(true); - - // HTTPS Configuration - HttpConfiguration https_config = new HttpConfiguration(http_config); - https_config.addCustomizer(new SecureRequestCustomizer()); - https_config.setUriCompliance(UriCompliance.LEGACY); - - // HTTPS connector - httpsConnector = new ServerConnector(server, new SslConnectionFactory(sslContextFactory, "http/1.1"), - new HttpConnectionFactory(https_config)); - int httpsPort = Integer.parseInt(httpsPortStr); - httpsConnector.setPort(httpsPort); - httpsConnector.setHost(httpHost); - } - - } + @Override + protected void configureRootContextHandler(ServletContextHandler servletContextHandler) throws ServletException { + addServlets(servletContextHandler); + enableWebSocket(servletContextHandler); } protected void enableWebSocket(ServletContextHandler servletContextHandler) { - String webSocketEnabled = getFrameworkProp(CmsDeployProperty.WEBSOCKET_ENABLED); + String webSocketEnabled = getDeployProperty(CmsDeployProperty.WEBSOCKET_ENABLED); // web socket if (webSocketEnabled != null && webSocketEnabled.equals(Boolean.toString(true))) { JavaxWebSocketServletContainerInitializer.configure(servletContextHandler, new Configurator() { @@ -216,27 +99,7 @@ public class CmsJettyServer { } } - protected ServletContextHandler createHttpContext() { - ServletContextHandler httpContext = new ServletContextHandler(); - httpContext.setAttribute(INTERNAL_CONTEXT_CLASSLOADER, Thread.currentThread().getContextClassLoader()); - httpContext.setClassLoader(this.getClass().getClassLoader()); - httpContext.setContextPath("/"); - - httpContext.setAttribute(CONTEXT_TEMPDIR, tempDir.toAbsolutePath().toFile()); - SessionHandler handler = new SessionHandler(); - handler.setMaxInactiveInterval(-1); - httpContext.setSessionHandler(handler); - - return httpContext; - } - - private String httpPortsMsg() { - - return (httpConnector != null ? "HTTP " + getHttpPort() + " " : " ") - + (httpsConnector != null ? "HTTPS " + getHttpsPort() : ""); - } - - private String getFrameworkProp(CmsDeployProperty deployProperty) { + protected String getDeployProperty(CmsDeployProperty deployProperty) { return cmsState.getDeployProperty(deployProperty.getProperty()); } diff --git a/org.argeo.cms.lib.jetty/src/org/argeo/cms/jetty/JettyHttpContext.java b/org.argeo.cms.lib.jetty/src/org/argeo/cms/jetty/JettyHttpContext.java new file mode 100644 index 000000000..2aa4abc43 --- /dev/null +++ b/org.argeo.cms.lib.jetty/src/org/argeo/cms/jetty/JettyHttpContext.java @@ -0,0 +1,147 @@ +package org.argeo.cms.jetty; + +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import org.argeo.cms.servlet.httpserver.HttpContextServlet; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.handler.ContextHandler; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; + +import com.sun.net.httpserver.Authenticator; +import com.sun.net.httpserver.Filter; +import com.sun.net.httpserver.HttpContext; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; + +/** Trivial implementation of @{HttpContext}. */ +class JettyHttpContext extends HttpContext { + private final JettyHttpServer httpServer; + private final String path; + private final ContextHandler contextHandler; + private final ContextAttributes attributes; + private final List filters = new ArrayList<>(); + + private HttpHandler handler; + private Authenticator authenticator; + + public JettyHttpContext(JettyHttpServer httpServer, String path) { + this.httpServer = httpServer; + this.path = path; + + ServletContextHandler servletContextHandler = new ServletContextHandler(); + servletContextHandler.setContextPath(path); + HttpContextServlet servlet = new HttpContextServlet(this); + servletContextHandler.addServlet(new ServletHolder(servlet), "/*"); + contextHandler = servletContextHandler; + + attributes = new ContextAttributes(); + } + + @Override + public HttpHandler getHandler() { + return handler; + } + + @Override + public void setHandler(HttpHandler handler) { + if (this.handler != null) + throw new IllegalArgumentException("Handler is already set"); + Objects.requireNonNull(handler); + this.handler = handler; + + if (httpServer.isStarted()) + try { + contextHandler.start(); + } catch (Exception e) { + throw new IllegalStateException("Cannot start context handler", e); + } + } + + @Override + public String getPath() { + return path; + } + + @Override + public HttpServer getServer() { + return httpServer; + } + + @Override + public Map getAttributes() { + return attributes; + } + + @Override + public List getFilters() { + return filters; + } + + @Override + public Authenticator setAuthenticator(Authenticator auth) { + Authenticator previousAuthenticator = authenticator; + this.authenticator = auth; + return previousAuthenticator; + } + + @Override + public Authenticator getAuthenticator() { + return authenticator; + } + + public Handler getContextHandler() { + return contextHandler; + } + + private class ContextAttributes extends AbstractMap { + @Override + public Set> entrySet() { + Set> entries = new HashSet<>(); + for (Enumeration keys = contextHandler.getAttributeNames(); keys.hasMoreElements();) { + entries.add(new ContextAttributeEntry(keys.nextElement())); + } + return entries; + } + + @Override + public Object put(String key, Object value) { + Object previousValue = get(key); + contextHandler.setAttribute(key, value); + return previousValue; + } + + private class ContextAttributeEntry implements Map.Entry { + private final String key; + + public ContextAttributeEntry(String key) { + this.key = key; + } + + @Override + public String getKey() { + return key; + } + + @Override + public Object getValue() { + return contextHandler.getAttribute(key); + } + + @Override + public Object setValue(Object value) { + Object previousValue = getValue(); + contextHandler.setAttribute(key, value); + return previousValue; + } + + } + } +} diff --git a/org.argeo.cms.lib.jetty/src/org/argeo/cms/jetty/JettyHttpServer.java b/org.argeo.cms.lib.jetty/src/org/argeo/cms/jetty/JettyHttpServer.java new file mode 100644 index 000000000..ea9705720 --- /dev/null +++ b/org.argeo.cms.lib.jetty/src/org/argeo/cms/jetty/JettyHttpServer.java @@ -0,0 +1,326 @@ +package org.argeo.cms.jetty; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.Map; +import java.util.TreeMap; +import java.util.concurrent.Executor; +import java.util.concurrent.ThreadPoolExecutor; + +import javax.servlet.ServletException; + +import org.argeo.api.cms.CmsLog; +import org.argeo.cms.CmsDeployProperty; +import org.argeo.util.http.HttpServerUtils; +import org.eclipse.jetty.http.UriCompliance; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.SecureRequestCustomizer; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.SslConnectionFactory; +import org.eclipse.jetty.server.handler.ContextHandlerCollection; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.eclipse.jetty.util.thread.ExecutorThreadPool; +import org.eclipse.jetty.util.thread.QueuedThreadPool; +import org.eclipse.jetty.util.thread.ThreadPool; + +import com.sun.net.httpserver.HttpContext; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpsConfigurator; +import com.sun.net.httpserver.HttpsServer; + +public class JettyHttpServer extends HttpsServer { + private final static CmsLog log = CmsLog.getLog(JettyHttpServer.class); + + private static final int DEFAULT_IDLE_TIMEOUT = 30000; + + private Server server; + + protected ServerConnector httpConnector; + protected ServerConnector httpsConnector; + + private InetSocketAddress address; + + private ThreadPoolExecutor executor; + + private HttpsConfigurator httpsConfigurator; + + private final Map contexts = new TreeMap<>(); + + protected final ContextHandlerCollection contextHandlerCollection = new ContextHandlerCollection(); + + private boolean started; + + @Override + public void bind(InetSocketAddress addr, int backlog) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public void start() { + try { + + ThreadPool threadPool = null; + if (executor != null) { + threadPool = new ExecutorThreadPool(executor); + } else { + // TODO make it configurable + threadPool = new QueuedThreadPool(10, 1); + } + + server = new Server(threadPool); + + configureConnectors(); + + if (httpConnector != null) { + httpConnector.open(); + server.addConnector(httpConnector); + } + + if (httpsConnector != null) { + httpsConnector.open(); + server.addConnector(httpsConnector); + } + + // holder + + // context + ServletContextHandler rootContextHandler = createRootContextHandler(); + // httpContext.addServlet(holder, "/*"); + if (rootContextHandler != null) + configureRootContextHandler(rootContextHandler); +// server.setHandler(rootContextHandler); + + ContextHandlerCollection contextHandlers = new ContextHandlerCollection(); + if (rootContextHandler != null && !contexts.containsKey("/")) + contextHandlers.addHandler(rootContextHandler); + for (String contextPath : contexts.keySet()) { + JettyHttpContext ctx = contexts.get(contextPath); + contextHandlers.addHandler(ctx.getContextHandler()); + } + + server.setHandler(contextHandlerCollection); + + // + // START + server.start(); + // + + Runtime.getRuntime().addShutdownHook(new Thread(() -> stop(), "Jetty shutdown")); + + log.info(httpPortsMsg()); + started = true; + } catch (Exception e) { + throw new IllegalStateException("Cannot start Jetty HTTPS server", e); + } + } + + @Override + public void stop(int delay) { + // TODO wait for processing to complete + stop(); + + } + + public void stop() { + try { + // serverConnector.close(); + server.stop(); + // TODO delete temp dir + started = false; + } catch (Exception e) { + e.printStackTrace(); + } + + } + + @Override + public void setExecutor(Executor executor) { + if (!(executor instanceof ThreadPoolExecutor)) + throw new IllegalArgumentException("Only " + ThreadPoolExecutor.class.getName() + " are supported"); + this.executor = (ThreadPoolExecutor) executor; + } + + @Override + public Executor getExecutor() { + return executor; + } + + @Override + public synchronized HttpContext createContext(String path, HttpHandler handler) { + HttpContext httpContext = createContext(path); + httpContext.setHandler(handler); + return httpContext; + } + + @Override + public synchronized HttpContext createContext(String path) { + if (contexts.containsKey(path)) + throw new IllegalArgumentException("Context " + path + " already exists"); + JettyHttpContext httpContext = new JettyHttpContext(this, path); + contexts.put(path, httpContext); + + contextHandlerCollection.addHandler(httpContext.getContextHandler()); + return httpContext; + } + + @Override + public synchronized void removeContext(String path) throws IllegalArgumentException { + if (!contexts.containsKey(path)) + throw new IllegalArgumentException("Context " + path + " does not exist"); + JettyHttpContext httpContext = contexts.remove(path); + // TODO stop handler first? + contextHandlerCollection.removeHandler(httpContext.getContextHandler()); + } + + @Override + public synchronized void removeContext(HttpContext context) { + removeContext(context.getPath()); + } + + @Override + public InetSocketAddress getAddress() { + return address; + } + + @Override + public void setHttpsConfigurator(HttpsConfigurator config) { + this.httpsConfigurator = config; + } + + @Override + public HttpsConfigurator getHttpsConfigurator() { + return httpsConfigurator; + } + + + + protected void configureConnectors() { + HttpConfiguration httpConfiguration = new HttpConfiguration(); + + String httpPortStr = getDeployProperty(CmsDeployProperty.HTTP_PORT); + String httpsPortStr = getDeployProperty(CmsDeployProperty.HTTPS_PORT); + + /// TODO make it more generic + String httpHost = getDeployProperty(CmsDeployProperty.HOST); +// String httpsHost = getFrameworkProp( +// JettyConfig.JETTY_PROPERTY_PREFIX + CmsHttpConstants.HTTPS_HOST); + + // try { + if (httpPortStr != null || httpsPortStr != null) { + boolean httpEnabled = httpPortStr != null; + // props.put(JettyHttpConstants.HTTP_ENABLED, httpEnabled); + boolean httpsEnabled = httpsPortStr != null; + // props.put(JettyHttpConstants.HTTPS_ENABLED, httpsEnabled); + if (httpsEnabled) { + int httpsPort = Integer.parseInt(httpsPortStr); + httpConfiguration.setSecureScheme("https"); + httpConfiguration.setSecurePort(httpsPort); + } + + if (httpEnabled) { + int httpPort = Integer.parseInt(httpPortStr); + httpConnector = new ServerConnector(server, new HttpConnectionFactory(httpConfiguration)); + httpConnector.setPort(httpPort); + httpConnector.setHost(httpHost); + httpConnector.setIdleTimeout(DEFAULT_IDLE_TIMEOUT); + } + + if (httpsEnabled) { + + SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); + // sslContextFactory.setKeyStore(KeyS) + + sslContextFactory.setKeyStoreType(getDeployProperty(CmsDeployProperty.SSL_KEYSTORETYPE)); + sslContextFactory.setKeyStorePath(getDeployProperty(CmsDeployProperty.SSL_KEYSTORE)); + sslContextFactory.setKeyStorePassword(getDeployProperty(CmsDeployProperty.SSL_PASSWORD)); + // sslContextFactory.setKeyManagerPassword(getFrameworkProp(CmsDeployProperty.SSL_KEYPASSWORD)); + sslContextFactory.setProtocol("TLS"); + + sslContextFactory.setTrustStoreType(getDeployProperty(CmsDeployProperty.SSL_TRUSTSTORETYPE)); + sslContextFactory.setTrustStorePath(getDeployProperty(CmsDeployProperty.SSL_TRUSTSTORE)); + sslContextFactory.setTrustStorePassword(getDeployProperty(CmsDeployProperty.SSL_TRUSTSTOREPASSWORD)); + + String wantClientAuth = getDeployProperty(CmsDeployProperty.SSL_WANTCLIENTAUTH); + if (wantClientAuth != null && wantClientAuth.equals(Boolean.toString(true))) + sslContextFactory.setWantClientAuth(true); + String needClientAuth = getDeployProperty(CmsDeployProperty.SSL_NEEDCLIENTAUTH); + if (needClientAuth != null && needClientAuth.equals(Boolean.toString(true))) + sslContextFactory.setNeedClientAuth(true); + + // HTTPS Configuration + HttpConfiguration https_config = new HttpConfiguration(httpConfiguration); + https_config.addCustomizer(new SecureRequestCustomizer()); + https_config.setUriCompliance(UriCompliance.LEGACY); + + // HTTPS connector + httpsConnector = new ServerConnector(server, new SslConnectionFactory(sslContextFactory, "http/1.1"), + new HttpConnectionFactory(https_config)); + int httpsPort = Integer.parseInt(httpsPortStr); + httpsConnector.setPort(httpsPort); + httpsConnector.setHost(httpHost); + } + + } + + } + + protected String getDeployProperty(CmsDeployProperty deployProperty) { + return System.getProperty(deployProperty.getProperty()); + } + + private String httpPortsMsg() { + + return (httpConnector != null ? "HTTP " + getHttpPort() + " " : " ") + + (httpsConnector != null ? "HTTPS " + getHttpsPort() : ""); + } + + public Integer getHttpPort() { + if (httpConnector == null) + return null; + return httpConnector.getLocalPort(); + } + + public Integer getHttpsPort() { + if (httpsConnector == null) + return null; + return httpsConnector.getLocalPort(); + } + + protected ServletContextHandler createRootContextHandler() { + return null; + } + + protected void configureRootContextHandler(ServletContextHandler servletContextHandler) throws ServletException { + + } + + + public boolean isStarted() { + return started; + } + + public static void main(String... args) { + JettyHttpServer httpServer = new JettyHttpServer(); + System.setProperty("argeo.http.port", "8080"); + httpServer.createContext("/", (exchange) -> { + exchange.getResponseBody().write("Hello World!".getBytes()); + }); + httpServer.start(); + httpServer.createContext("/sub/context", (exchange) -> { + final String key = "count"; + Integer count = (Integer) exchange.getHttpContext().getAttributes().get(key); + if (count == null) + exchange.getHttpContext().getAttributes().put(key, 0); + else + exchange.getHttpContext().getAttributes().put(key, count + 1); + StringBuilder sb = new StringBuilder(); + sb.append("Subcontext:"); + sb.append(" " + key + "=" + exchange.getHttpContext().getAttributes().get(key)); + sb.append(" relativePath=" + HttpServerUtils.relativize(exchange)); + exchange.getResponseBody().write(sb.toString().getBytes()); + }); + } +} diff --git a/org.argeo.cms/bnd.bnd b/org.argeo.cms/bnd.bnd index d0047dcc6..12114c2a8 100644 --- a/org.argeo.cms/bnd.bnd +++ b/org.argeo.cms/bnd.bnd @@ -1,7 +1,6 @@ Bundle-Activator: org.argeo.cms.internal.osgi.CmsActivator Import-Package: \ -org.apache.commons.httpclient.cookie;resolution:=optional,\ !com.sun.security.jgss,\ org.osgi.*;version=0.0.0,\ org.apache.xerces.jaxp;resolution:=optional,\ diff --git a/org.argeo.cms/build.properties b/org.argeo.cms/build.properties index 8fec5e54b..67b98f4d0 100644 --- a/org.argeo.cms/build.properties +++ b/org.argeo.cms/build.properties @@ -2,17 +2,5 @@ output.. = bin/ bin.includes = META-INF/,\ .,\ bin/,\ - OSGI-INF/,\ - OSGI-INF/simpleTransactionManager.xml,\ - OSGI-INF/cmsState.xml,\ - OSGI-INF/nodeUserAdmin.xml,\ - OSGI-INF/deployConfig.xml,\ - OSGI-INF/cmsDeployment.xml,\ - OSGI-INF/cmsContext.xml,\ - OSGI-INF/acrContentRepository.xml,\ - OSGI-INF/uuidFactory.xml + OSGI-INF/ source.. = src/ -additional.bundles = org.argeo.ext.slf4j,\ - org.slf4j.commons.logging,\ - org.slf4j.api,\ - org.apache.commons.codec diff --git a/org.argeo.cms/src/org/argeo/cms/auth/CmsAuthUtils.java b/org.argeo.cms/src/org/argeo/cms/auth/CmsAuthUtils.java index 9e00ed458..711edba2f 100644 --- a/org.argeo.cms/src/org/argeo/cms/auth/CmsAuthUtils.java +++ b/org.argeo.cms/src/org/argeo/cms/auth/CmsAuthUtils.java @@ -24,7 +24,7 @@ import org.argeo.api.cms.CmsSessionId; import org.argeo.api.cms.DataAdminPrincipal; import org.argeo.cms.internal.auth.CmsSessionImpl; import org.argeo.cms.internal.auth.ImpliedByPrincipal; -import org.argeo.cms.internal.http.WebCmsSessionImpl; +import org.argeo.cms.internal.auth.RemoteCmsSessionImpl; import org.argeo.cms.internal.runtime.CmsContextImpl; import org.argeo.osgi.useradmin.AuthenticatingUser; import org.osgi.service.useradmin.Authorization; @@ -151,7 +151,7 @@ class CmsAuthUtils { currentLocalSession.close(); // new CMS session UUID cmsSessionUuid = CmsContextImpl.getCmsContext().getUuidFactory().timeUUID(); - cmsSession = new WebCmsSessionImpl(cmsSessionUuid, subject, authorization, locale, request); + cmsSession = new RemoteCmsSessionImpl(cmsSessionUuid, subject, authorization, locale, request); CmsContextImpl.getCmsContext().registerCmsSession(cmsSession); } else if (!authorization.getName().equals(currentLocalSession.getAuthorization().getName())) { throw new IllegalStateException("Inconsistent user " + authorization.getName() @@ -176,7 +176,7 @@ class CmsAuthUtils { } else { // new CMS session UUID cmsSessionUuid = CmsContextImpl.getCmsContext().getUuidFactory().timeUUID(); - cmsSession = new WebCmsSessionImpl(cmsSessionUuid, subject, authorization, locale, request); + cmsSession = new RemoteCmsSessionImpl(cmsSessionUuid, subject, authorization, locale, request); CmsContextImpl.getCmsContext().registerCmsSession(cmsSession); } diff --git a/org.argeo.cms/src/org/argeo/cms/internal/http/WebCmsSessionImpl.java b/org.argeo.cms/src/org/argeo/cms/internal/auth/RemoteCmsSessionImpl.java similarity index 72% rename from org.argeo.cms/src/org/argeo/cms/internal/http/WebCmsSessionImpl.java rename to org.argeo.cms/src/org/argeo/cms/internal/auth/RemoteCmsSessionImpl.java index 4b4a77641..41ee7797b 100644 --- a/org.argeo.cms/src/org/argeo/cms/internal/http/WebCmsSessionImpl.java +++ b/org.argeo.cms/src/org/argeo/cms/internal/auth/RemoteCmsSessionImpl.java @@ -1,4 +1,4 @@ -package org.argeo.cms.internal.http; +package org.argeo.cms.internal.auth; import java.util.Locale; import java.util.UUID; @@ -7,15 +7,14 @@ import javax.security.auth.Subject; import org.argeo.cms.auth.RemoteAuthRequest; import org.argeo.cms.auth.RemoteAuthSession; -import org.argeo.cms.internal.auth.CmsSessionImpl; import org.osgi.service.useradmin.Authorization; /** CMS session implementation in a web context. */ -public class WebCmsSessionImpl extends CmsSessionImpl { +public class RemoteCmsSessionImpl extends CmsSessionImpl { private static final long serialVersionUID = -5178883380637048025L; private RemoteAuthSession httpSession; - public WebCmsSessionImpl(UUID uuid, Subject initialSubject, Authorization authorization, Locale locale, + public RemoteCmsSessionImpl(UUID uuid, Subject initialSubject, Authorization authorization, Locale locale, RemoteAuthRequest request) { super(uuid, initialSubject, authorization, locale, request.getSession().getId()); httpSession = request.getSession(); 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 new file mode 100644 index 000000000..54849fc03 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/internal/http/CmsAuthenticator.java @@ -0,0 +1,114 @@ +package org.argeo.cms.internal.http; + +import javax.security.auth.Subject; +import javax.security.auth.login.LoginContext; +import javax.security.auth.login.LoginException; + +import org.argeo.api.cms.CmsAuth; +import org.argeo.api.cms.CmsLog; +import org.argeo.cms.auth.CurrentUser; +import org.argeo.cms.auth.RemoteAuthCallbackHandler; +import org.argeo.cms.auth.SpnegoLoginModule; + +import com.sun.net.httpserver.Authenticator; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpPrincipal; + +public class CmsAuthenticator extends Authenticator { + final static String HEADER_AUTHORIZATION = "Authorization"; + final static String HEADER_WWW_AUTHENTICATE = "WWW-Authenticate"; + + private final static CmsLog log = CmsLog.getLog(CmsAuthenticator.class); + + // TODO make it configurable + private final String httpAuthRealm = "Argeo"; + private final boolean forceBasic = false; + + @Override + public Result authenticate(HttpExchange exch) { +// if (log.isTraceEnabled()) +// HttpUtils.logRequestHeaders(log, request); + RemoteAuthHttpExchange remoteAuthHttpExchange = new RemoteAuthHttpExchange(exch); + ClassLoader currentThreadContextClassLoader = Thread.currentThread().getContextClassLoader(); + Thread.currentThread().setContextClassLoader(CmsAuthenticator.class.getClassLoader()); + LoginContext lc; + try { + lc = CmsAuth.USER + .newLoginContext(new RemoteAuthCallbackHandler(remoteAuthHttpExchange, remoteAuthHttpExchange)); + lc.login(); + } catch (LoginException e) { + // FIXME better analyse failure so as not to try endlessly + if (authIsRequired(exch)) { + return askForWwwAuth(exch); + } else { + lc = processUnauthorized(exch); +// if (log.isTraceEnabled()) +// HttpUtils.logResponseHeaders(log, response); + } + if (lc == null) + return new Authenticator.Failure(403); + } finally { + Thread.currentThread().setContextClassLoader(currentThreadContextClassLoader); + } + + Subject subject = lc.getSubject(); + +// 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); + } + + protected boolean authIsRequired(HttpExchange httpExchange) { + return false; + } + + protected LoginContext processUnauthorized(HttpExchange httpExchange) { + + RemoteAuthHttpExchange remoteAuthExchange = new RemoteAuthHttpExchange(httpExchange); + // anonymous + ClassLoader currentContextClassLoader = Thread.currentThread().getContextClassLoader(); + try { + Thread.currentThread().setContextClassLoader(CmsAuthenticator.class.getClassLoader()); + LoginContext lc = CmsAuth.ANONYMOUS + .newLoginContext(new RemoteAuthCallbackHandler(remoteAuthExchange, remoteAuthExchange)); + lc.login(); + return lc; + } catch (LoginException e1) { + if (log.isDebugEnabled()) + log.error("Cannot log in as anonymous", e1); + return null; + } finally { + Thread.currentThread().setContextClassLoader(currentContextClassLoader); + } + } + + protected Authenticator.Retry askForWwwAuth(HttpExchange httpExchange) { + // response.setHeader(HttpUtils.HEADER_WWW_AUTHENTICATE, "basic + // realm=\"" + httpAuthRealm + "\""); + if (SpnegoLoginModule.hasAcceptorCredentials() && !forceBasic)// SPNEGO + httpExchange.getResponseHeaders().set(HEADER_WWW_AUTHENTICATE, "Negotiate"); + else + httpExchange.getResponseHeaders().set(HEADER_WWW_AUTHENTICATE, "Basic realm=\"" + httpAuthRealm + "\""); + + // response.setDateHeader("Date", System.currentTimeMillis()); + // response.setDateHeader("Expires", System.currentTimeMillis() + (24 * + // 60 * 60 * 1000)); + // response.setHeader("Accept-Ranges", "bytes"); + // response.setHeader("Connection", "Keep-Alive"); + // response.setHeader("Keep-Alive", "timeout=5, max=97"); + // response.setContentType("text/html; charset=UTF-8"); + + return new Authenticator.Retry(401); + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/internal/http/RemoteAuthHttpExchange.java b/org.argeo.cms/src/org/argeo/cms/internal/http/RemoteAuthHttpExchange.java new file mode 100644 index 000000000..68e0b1e3e --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/internal/http/RemoteAuthHttpExchange.java @@ -0,0 +1,76 @@ +package org.argeo.cms.internal.http; + +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +import org.argeo.cms.auth.RemoteAuthRequest; +import org.argeo.cms.auth.RemoteAuthResponse; +import org.argeo.cms.auth.RemoteAuthSession; + +import com.sun.net.httpserver.HttpExchange; + +public class RemoteAuthHttpExchange implements RemoteAuthRequest, RemoteAuthResponse { + private HttpExchange httpExchange; + + public RemoteAuthHttpExchange(HttpExchange httpExchange) { + this.httpExchange = httpExchange; + } + + @Override + public void setHeader(String keys, String value) { + httpExchange.getResponseHeaders().put(keys, Collections.singletonList(value)); + } + + @Override + public RemoteAuthSession getSession() { + // TODO Auto-generated method stub + return null; + } + + @Override + public RemoteAuthSession createSession() { + // TODO Auto-generated method stub + return null; + } + + @Override + public Locale getLocale() { + // TODO Auto-generated method stub + return null; + } + + @Override + public Object getAttribute(String key) { + return httpExchange.getAttribute(key); + } + + @Override + public void setAttribute(String key, Object object) { + httpExchange.setAttribute(key, object); + } + + @Override + public String getHeader(String key) { + List lst = httpExchange.getRequestHeaders().get(key); + if (lst == null || lst.size() == 0) + return null; + return lst.get(0); + } + + @Override + public int getLocalPort() { + return httpExchange.getLocalAddress().getPort(); + } + + @Override + public String getRemoteAddr() { + return httpExchange.getRemoteAddress().getHostName(); + } + + @Override + public int getRemotePort() { + return httpExchange.getRemoteAddress().getPort(); + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/internal/http/client/HttpCredentialProvider.java b/org.argeo.cms/src/org/argeo/cms/internal/http/client/HttpCredentialProvider.java deleted file mode 100644 index 4a9392c83..000000000 --- a/org.argeo.cms/src/org/argeo/cms/internal/http/client/HttpCredentialProvider.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.argeo.cms.internal.http.client; - -import org.apache.commons.httpclient.Credentials; -import org.apache.commons.httpclient.auth.AuthScheme; -import org.apache.commons.httpclient.auth.CredentialsNotAvailableException; -import org.apache.commons.httpclient.auth.CredentialsProvider; - -/** SPNEGO credential provider */ -public class HttpCredentialProvider implements CredentialsProvider { - - @Override - public Credentials getCredentials(AuthScheme scheme, String host, int port, boolean proxy) - throws CredentialsNotAvailableException { - if (scheme instanceof SpnegoAuthScheme) - return new SpnegoCredentials(); - else - throw new UnsupportedOperationException("Auth scheme " + scheme.getSchemeName() + " not supported"); - } - -} diff --git a/org.argeo.cms/src/org/argeo/cms/internal/http/client/SpnegoAuthScheme.java b/org.argeo.cms/src/org/argeo/cms/internal/http/client/SpnegoAuthScheme.java deleted file mode 100644 index a8aa29bbb..000000000 --- a/org.argeo.cms/src/org/argeo/cms/internal/http/client/SpnegoAuthScheme.java +++ /dev/null @@ -1,165 +0,0 @@ -package org.argeo.cms.internal.http.client; - -import java.net.URL; -import java.security.PrivilegedExceptionAction; -import java.util.ArrayList; - -import javax.security.auth.Subject; -import javax.security.auth.login.LoginContext; - -import org.apache.commons.httpclient.Credentials; -import org.apache.commons.httpclient.HttpClient; -import org.apache.commons.httpclient.HttpMethod; -import org.apache.commons.httpclient.auth.AuthPolicy; -import org.apache.commons.httpclient.auth.AuthScheme; -import org.apache.commons.httpclient.auth.AuthenticationException; -import org.apache.commons.httpclient.auth.CredentialsProvider; -import org.apache.commons.httpclient.auth.MalformedChallengeException; -import org.apache.commons.httpclient.methods.GetMethod; -import org.apache.commons.httpclient.params.DefaultHttpParams; -import org.apache.commons.httpclient.params.HttpParams; -import org.argeo.cms.auth.RemoteAuthUtils; - -/** Implementation of the SPNEGO auth scheme. */ -public class SpnegoAuthScheme implements AuthScheme { -// private final static Log log = LogFactory.getLog(SpnegoAuthScheme.class); - - public static final String NAME = "Negotiate"; -// private final static Oid KERBEROS_OID; -// static { -// try { -// KERBEROS_OID = new Oid("1.3.6.1.5.5.2"); -// } catch (GSSException e) { -// throw new IllegalStateException("Cannot create Kerberos OID", e); -// } -// } - - private final static String DEFAULT_KERBEROS_SERVICE = "HTTP"; - - private boolean complete = false; - private String realm; - - @Override - public void processChallenge(String challenge) throws MalformedChallengeException { - // if(tokenStr!=null){ - // log.error("Received challenge while there is a token. Failing."); - // complete = false; - // } - - } - - @Override - public String getSchemeName() { - return NAME; - } - - @Override - public String getParameter(String name) { - return null; - } - - @Override - public String getRealm() { - return realm; - } - - @Override - public String getID() { - return NAME; - } - - @Override - public boolean isConnectionBased() { - return true; - } - - @Override - public boolean isComplete() { - return complete; - } - - @Override - public String authenticate(Credentials credentials, String method, String uri) throws AuthenticationException { - // log.debug("authenticate " + method + " " + uri); - // return null; - throw new UnsupportedOperationException(); - } - - @Override - public String authenticate(Credentials credentials, HttpMethod method) throws AuthenticationException { -// GSSContext context = null; - String hostname; - try { - hostname = method.getURI().getHost(); - String tokenStr = RemoteAuthUtils.getGssToken(null, DEFAULT_KERBEROS_SERVICE, hostname); - return "Negotiate " + tokenStr; - } catch (Exception e1) { - complete = true; - throw new AuthenticationException("Cannot authenticate " + method, e1); - } -// String serverPrinc = DEFAULT_KERBEROS_SERVICE + "@" + hostname; -// -// try { -// // Get service's principal name -// GSSManager manager = GSSManager.getInstance(); -// GSSName serverName = manager.createName(serverPrinc, GSSName.NT_HOSTBASED_SERVICE, KERBEROS_OID); -// -// // Get the context for authentication -// context = manager.createContext(serverName, KERBEROS_OID, null, GSSContext.DEFAULT_LIFETIME); -// // context.requestMutualAuth(true); // Request mutual authentication -// // context.requestConf(true); // Request confidentiality -// context.requestCredDeleg(true); -// -// byte[] token = new byte[0]; -// -// // token is ignored on the first call -// token = context.initSecContext(token, 0, token.length); -// -// // Send a token to the server if one was generated by -// // initSecContext -// if (token != null) { -// tokenStr = Base64.getEncoder().encodeToString(token); -// // complete=true; -// } -// } catch (GSSException e) { -// complete = true; -// throw new AuthenticationException("Cannot authenticate to " + serverPrinc, e); -// } - } - - public static void main(String[] args) { - String principal = System.getProperty("javax.security.auth.login.name"); - if (args.length == 0 || principal == null) { - System.err.println("usage: java -Djavax.security.auth.login.name= " - + SpnegoAuthScheme.class.getName() + " "); - System.exit(1); - return; - } - String url = args[0]; - - URL jaasUrl = SpnegoAuthScheme.class.getResource("jaas.cfg"); - System.setProperty("java.security.auth.login.config", jaasUrl.toExternalForm()); - try { - LoginContext lc = new LoginContext("SINGLE_USER"); - lc.login(); - - AuthPolicy.registerAuthScheme(SpnegoAuthScheme.NAME, SpnegoAuthScheme.class); - HttpParams params = DefaultHttpParams.getDefaultParams(); - ArrayList schemes = new ArrayList<>(); - schemes.add(SpnegoAuthScheme.NAME); - params.setParameter(AuthPolicy.AUTH_SCHEME_PRIORITY, schemes); - params.setParameter(CredentialsProvider.PROVIDER, new HttpCredentialProvider()); - - int responseCode = Subject.doAs(lc.getSubject(), new PrivilegedExceptionAction() { - public Integer run() throws Exception { - HttpClient httpClient = new HttpClient(); - return httpClient.executeMethod(new GetMethod(url)); - } - }); - System.out.println("Reponse code: " + responseCode); - } catch (Exception e) { - e.printStackTrace(); - } - } - -} diff --git a/org.argeo.cms/src/org/argeo/cms/internal/http/client/SpnegoCredentials.java b/org.argeo.cms/src/org/argeo/cms/internal/http/client/SpnegoCredentials.java deleted file mode 100644 index f59b72a0b..000000000 --- a/org.argeo.cms/src/org/argeo/cms/internal/http/client/SpnegoCredentials.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.argeo.cms.internal.http.client; - -import org.apache.commons.httpclient.Credentials; - -public class SpnegoCredentials implements Credentials { - -} diff --git a/org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsUserAdmin.java b/org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsUserAdmin.java index 9675c2542..4f0ba10aa 100644 --- a/org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsUserAdmin.java +++ b/org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsUserAdmin.java @@ -25,19 +25,12 @@ import javax.security.auth.kerberos.KerberosPrincipal; import javax.security.auth.login.LoginContext; import javax.security.auth.login.LoginException; -import org.apache.commons.httpclient.auth.AuthPolicy; -import org.apache.commons.httpclient.auth.CredentialsProvider; -import org.apache.commons.httpclient.params.DefaultHttpParams; -import org.apache.commons.httpclient.params.HttpMethodParams; -import org.apache.commons.httpclient.params.HttpParams; import org.apache.commons.io.FileUtils; import org.argeo.api.cms.CmsAuth; import org.argeo.api.cms.CmsConstants; import org.argeo.api.cms.CmsLog; import org.argeo.api.cms.CmsState; import org.argeo.cms.CmsDeployProperty; -import org.argeo.cms.internal.http.client.HttpCredentialProvider; -import org.argeo.cms.internal.http.client.SpnegoAuthScheme; import org.argeo.osgi.useradmin.AggregatingUserAdmin; import org.argeo.osgi.useradmin.DirectoryUserAdmin; import org.argeo.osgi.useradmin.UserDirectory; @@ -298,16 +291,6 @@ public class CmsUserAdmin extends AggregatingUserAdmin { } } - // Register client-side SPNEGO auth scheme - AuthPolicy.registerAuthScheme(SpnegoAuthScheme.NAME, SpnegoAuthScheme.class); - HttpParams params = DefaultHttpParams.getDefaultParams(); - ArrayList schemes = new ArrayList<>(); - schemes.add(SpnegoAuthScheme.NAME);// SPNEGO preferred - // schemes.add(AuthPolicy.BASIC);// incompatible with Basic - params.setParameter(AuthPolicy.AUTH_SCHEME_PRIORITY, schemes); - params.setParameter(CredentialsProvider.PROVIDER, new HttpCredentialProvider()); - params.setParameter(HttpMethodParams.COOKIE_POLICY, KernelConstants.COOKIE_POLICY_BROWSER_COMPATIBILITY); - // params.setCookiePolicy(CookiePolicy.BROWSER_COMPATIBILITY); } } diff --git a/org.argeo.util/src/org/argeo/util/dav/DavServerHandler.java b/org.argeo.util/src/org/argeo/util/dav/DavServerHandler.java new file mode 100644 index 000000000..4fc73f3ce --- /dev/null +++ b/org.argeo.util/src/org/argeo/util/dav/DavServerHandler.java @@ -0,0 +1,25 @@ +package org.argeo.util.dav; + +import java.io.IOException; + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; + +public class DavServerHandler implements HttpHandler { + + @Override + public void handle(HttpExchange exchange) throws IOException { + String method = exchange.getRequestMethod(); + if (DavMethod.PROPFIND.name().equals(method)) { + handle(exchange); + } else { + throw new IllegalArgumentException("Unsupported method " + method); + } + + } + + protected DavResponse handlePROPFIND(HttpExchange exchange) { + return null; + } + +} diff --git a/org.argeo.util/src/org/argeo/util/http/HttpServerUtils.java b/org.argeo.util/src/org/argeo/util/http/HttpServerUtils.java new file mode 100644 index 000000000..6c6e88414 --- /dev/null +++ b/org.argeo.util/src/org/argeo/util/http/HttpServerUtils.java @@ -0,0 +1,32 @@ +package org.argeo.util.http; + +import java.net.URI; +import java.util.Objects; + +import com.sun.net.httpserver.HttpContext; +import com.sun.net.httpserver.HttpExchange; + +public class HttpServerUtils { + + public static String relativize(HttpContext httpContext, String path) { + Objects.requireNonNull(path); + if (!path.startsWith(httpContext.getPath())) + throw new IllegalArgumentException(path + " does not belong to context" + httpContext.getPath()); + String relativePath = path.substring(httpContext.getPath().length()); + // TODO optimise? + if (relativePath.startsWith("/")) + relativePath = relativePath.substring(1); + return relativePath; + } + + public static String relativize(HttpExchange exchange) { + URI uri = exchange.getRequestURI(); + HttpContext httpContext = exchange.getHttpContext(); + return relativize(httpContext, uri.getPath()); + } + + /** singleton */ + private HttpServerUtils() { + + } +} -- 2.30.2