diff --git a/Source/JNA/pom.xml b/Source/JNA/pom.xml
index 307167d901..f7707aeb4e 100644
--- a/Source/JNA/pom.xml
+++ b/Source/JNA/pom.xml
@@ -100,6 +100,7 @@
waffle-tomcat85
waffle-tomcat9
waffle-tomcat10
+ waffle-tomcat11
diff --git a/Source/JNA/waffle-bom/pom.xml b/Source/JNA/waffle-bom/pom.xml
index d936b267b8..6ef54f5eb6 100644
--- a/Source/JNA/waffle-bom/pom.xml
+++ b/Source/JNA/waffle-bom/pom.xml
@@ -136,6 +136,11 @@
waffle-tomcat10
${project.version}
+
+ com.github.waffle
+ waffle-tomcat11
+ ${project.version}
+
diff --git a/Source/JNA/waffle-distro/pom.xml b/Source/JNA/waffle-distro/pom.xml
index 5b9e69ad5d..6391c13ac1 100644
--- a/Source/JNA/waffle-distro/pom.xml
+++ b/Source/JNA/waffle-distro/pom.xml
@@ -62,6 +62,9 @@
waffle.distro
+
+
+ true
@@ -157,6 +160,12 @@
${project.version}
runtime
+
+ com.github.waffle
+ waffle-tomcat11
+ ${project.version}
+ runtime
+
diff --git a/Source/JNA/waffle-tomcat11/format.xml b/Source/JNA/waffle-tomcat11/format.xml
new file mode 100644
index 0000000000..29273afe6f
--- /dev/null
+++ b/Source/JNA/waffle-tomcat11/format.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
diff --git a/Source/JNA/waffle-tomcat11/pom.xml b/Source/JNA/waffle-tomcat11/pom.xml
new file mode 100644
index 0000000000..07ad24aeca
--- /dev/null
+++ b/Source/JNA/waffle-tomcat11/pom.xml
@@ -0,0 +1,128 @@
+
+
+
+ 4.0.0
+
+
+ com.github.waffle
+ waffle
+ 3.3.1-SNAPSHOT
+
+
+ waffle-tomcat11
+ 3.3.1-SNAPSHOT
+ jar
+
+ waffle-tomcat11
+ Tomcat 11 integration for WAFFLE
+ https://waffle.github.io/waffle/
+
+
+ scm:git:ssh://git@github.com/waffle/waffle.git
+ scm:git:ssh://git@github.com/waffle/waffle.git
+ HEAD
+ https://github.com/Waffle/waffle
+
+
+
+
+ 11.0.0-M18
+
+
+ 21
+ 21
+
+
+ waffle.tomcat11
+
+
+ true
+
+
+ true
+
+
+ true
+
+
+
+
+ ${project.groupId}
+ waffle-jna-jakarta
+ ${project.version}
+ compile
+
+
+ ${project.groupId}
+ waffle-tests-jakarta
+ ${project.version}
+ test
+
+
+ org.apache.tomcat
+ tomcat-api
+ ${tomcat.version}
+ provided
+
+
+ org.apache.tomcat
+ tomcat-catalina
+ ${tomcat.version}
+ provided
+
+
+ org.apache.tomcat
+ tomcat-coyote
+ ${tomcat.version}
+ provided
+
+
+ org.apache.tomcat
+ tomcat-juli
+ ${tomcat.version}
+ provided
+
+
+ org.apache.tomcat
+ tomcat-servlet-api
+ ${tomcat.version}
+ provided
+
+
+ org.apache.tomcat
+ tomcat-util
+ ${tomcat.version}
+ provided
+
+
+ org.apache.tomcat
+ tomcat-jaspic-api
+ ${tomcat.version}
+ provided
+
+
+
diff --git a/Source/JNA/waffle-tomcat11/src/main/java/waffle/apache/GenericWindowsPrincipal.java b/Source/JNA/waffle-tomcat11/src/main/java/waffle/apache/GenericWindowsPrincipal.java
new file mode 100644
index 0000000000..57e7f686ca
--- /dev/null
+++ b/Source/JNA/waffle-tomcat11/src/main/java/waffle/apache/GenericWindowsPrincipal.java
@@ -0,0 +1,208 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2010-2024 The Waffle Project Contributors: https://github.com/Waffle/waffle/graphs/contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package waffle.apache;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.catalina.realm.GenericPrincipal;
+
+import waffle.windows.auth.IWindowsAccount;
+import waffle.windows.auth.IWindowsIdentity;
+import waffle.windows.auth.PrincipalFormat;
+import waffle.windows.auth.WindowsAccount;
+
+/**
+ * A Windows Principal.
+ */
+public class GenericWindowsPrincipal extends GenericPrincipal {
+
+ /** The Constant serialVersionUID. */
+ private static final long serialVersionUID = 1L;
+
+ /** The sid. */
+ private final byte[] sid;
+
+ /** The sid string. */
+ private final String sidString;
+
+ /** The groups. */
+ private final Map groups;
+
+ /**
+ * A windows principal.
+ *
+ * @param windowsIdentity
+ * Windows identity.
+ * @param principalFormat
+ * Principal format.
+ * @param roleFormat
+ * Role format.
+ */
+ public GenericWindowsPrincipal(final IWindowsIdentity windowsIdentity, final PrincipalFormat principalFormat,
+ final PrincipalFormat roleFormat) {
+ super(windowsIdentity.getFqn(), GenericWindowsPrincipal.getRoles(windowsIdentity, principalFormat, roleFormat));
+ this.sid = windowsIdentity.getSid();
+ this.sidString = windowsIdentity.getSidString();
+ this.groups = GenericWindowsPrincipal.getGroups(windowsIdentity.getGroups());
+ }
+
+ /**
+ * Gets the roles.
+ *
+ * @param windowsIdentity
+ * the windows identity
+ * @param principalFormat
+ * the principal format
+ * @param roleFormat
+ * the role format
+ *
+ * @return the roles
+ */
+ private static List getRoles(final IWindowsIdentity windowsIdentity, final PrincipalFormat principalFormat,
+ final PrincipalFormat roleFormat) {
+ final List roles = new ArrayList<>();
+ roles.addAll(GenericWindowsPrincipal.getPrincipalNames(windowsIdentity, principalFormat));
+ for (final IWindowsAccount group : windowsIdentity.getGroups()) {
+ roles.addAll(GenericWindowsPrincipal.getRoleNames(group, roleFormat));
+ }
+ return roles;
+ }
+
+ /**
+ * Gets the groups.
+ *
+ * @param groups
+ * the groups
+ *
+ * @return the groups
+ */
+ private static Map getGroups(final IWindowsAccount[] groups) {
+ final Map groupMap = new HashMap<>();
+ for (final IWindowsAccount group : groups) {
+ groupMap.put(group.getFqn(), new WindowsAccount(group));
+ }
+ return groupMap;
+ }
+
+ /**
+ * Windows groups that the user is a member of.
+ *
+ * @return A map of group names to groups.
+ */
+ public Map getGroups() {
+ return this.groups;
+ }
+
+ /**
+ * Byte representation of the SID.
+ *
+ * @return Array of bytes.
+ */
+ public byte[] getSid() {
+ return this.sid.clone();
+ }
+
+ /**
+ * String representation of the SID.
+ *
+ * @return String.
+ */
+ public String getSidString() {
+ return this.sidString;
+ }
+
+ /**
+ * Returns a list of role principal objects.
+ *
+ * @param group
+ * Windows group.
+ * @param principalFormat
+ * Principal format.
+ *
+ * @return List of role principal objects.
+ */
+ private static List getRoleNames(final IWindowsAccount group, final PrincipalFormat principalFormat) {
+ final List principals = new ArrayList<>();
+ switch (principalFormat) {
+ case FQN:
+ principals.add(group.getFqn());
+ break;
+ case SID:
+ principals.add(group.getSidString());
+ break;
+ case BOTH:
+ principals.add(group.getFqn());
+ principals.add(group.getSidString());
+ break;
+ case NONE:
+ default:
+ break;
+ }
+ return principals;
+ }
+
+ /**
+ * Returns a list of user principal objects.
+ *
+ * @param windowsIdentity
+ * Windows identity.
+ * @param principalFormat
+ * Principal format.
+ *
+ * @return A list of user principal objects.
+ */
+ private static List getPrincipalNames(final IWindowsIdentity windowsIdentity,
+ final PrincipalFormat principalFormat) {
+ final List principals = new ArrayList<>();
+ switch (principalFormat) {
+ case FQN:
+ principals.add(windowsIdentity.getFqn());
+ break;
+ case SID:
+ principals.add(windowsIdentity.getSidString());
+ break;
+ case BOTH:
+ principals.add(windowsIdentity.getFqn());
+ principals.add(windowsIdentity.getSidString());
+ break;
+ case NONE:
+ default:
+ break;
+ }
+ return principals;
+ }
+
+ /**
+ * Get an array of roles as a string.
+ *
+ * @return Role1, Role2, ...
+ */
+ public String getRolesString() {
+ return String.join(", ", this.getRoles());
+ }
+
+}
diff --git a/Source/JNA/waffle-tomcat11/src/main/java/waffle/apache/MixedAuthenticator.java b/Source/JNA/waffle-tomcat11/src/main/java/waffle/apache/MixedAuthenticator.java
new file mode 100644
index 0000000000..0e45986c00
--- /dev/null
+++ b/Source/JNA/waffle-tomcat11/src/main/java/waffle/apache/MixedAuthenticator.java
@@ -0,0 +1,310 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2010-2024 The Waffle Project Contributors: https://github.com/Waffle/waffle/graphs/contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package waffle.apache;
+
+import com.sun.jna.platform.win32.Win32Exception;
+
+import jakarta.servlet.RequestDispatcher;
+import jakarta.servlet.ServletContext;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
+
+import java.io.IOException;
+import java.security.Principal;
+import java.util.Base64;
+
+import org.apache.catalina.LifecycleException;
+import org.apache.catalina.connector.Request;
+import org.apache.catalina.realm.GenericPrincipal;
+import org.apache.tomcat.util.descriptor.web.LoginConfig;
+import org.slf4j.LoggerFactory;
+
+import waffle.util.AuthorizationHeader;
+import waffle.util.NtlmServletRequest;
+import waffle.windows.auth.IWindowsIdentity;
+import waffle.windows.auth.IWindowsSecurityContext;
+
+/**
+ * Mixed Negotiate + Form Authenticator.
+ */
+public class MixedAuthenticator extends WaffleAuthenticatorBase {
+
+ /**
+ * Instantiates a new mixed authenticator.
+ */
+ public MixedAuthenticator() {
+ super();
+ this.log = LoggerFactory.getLogger(MixedAuthenticator.class);
+ this.info = MixedAuthenticator.class.getSimpleName();
+ this.log.debug("[waffle.apache.MixedAuthenticator] loaded");
+ }
+
+ @Override
+ public synchronized void startInternal() throws LifecycleException {
+ this.log.info("[waffle.apache.MixedAuthenticator] started");
+ super.startInternal();
+ }
+
+ @Override
+ public synchronized void stopInternal() throws LifecycleException {
+ super.stopInternal();
+ this.log.info("[waffle.apache.MixedAuthenticator] stopped");
+ }
+
+ @Override
+ public boolean authenticate(final Request request, final HttpServletResponse response) {
+
+ // realm: fail if no realm is configured
+ if (this.context == null || this.context.getRealm() == null) {
+ this.log.warn("missing context/realm");
+ this.sendError(response, HttpServletResponse.SC_SERVICE_UNAVAILABLE);
+ return false;
+ }
+
+ this.log.debug("{} {}, contentlength: {}", request.getMethod(), request.getRequestURI(),
+ Integer.valueOf(request.getContentLength()));
+
+ final boolean negotiateCheck = request.getParameter("j_negotiate_check") != null;
+ this.log.debug("negotiateCheck: {}", Boolean.valueOf(negotiateCheck));
+ final boolean securityCheck = request.getParameter("j_security_check") != null;
+ this.log.debug("securityCheck: {}", Boolean.valueOf(securityCheck));
+
+ final Principal principal = request.getUserPrincipal();
+
+ final AuthorizationHeader authorizationHeader = new AuthorizationHeader(request);
+ final boolean ntlmPost = authorizationHeader.isNtlmType1PostAuthorizationHeader();
+ this.log.debug("authorization: {}, ntlm post: {}", authorizationHeader, Boolean.valueOf(ntlmPost));
+
+ final LoginConfig loginConfig = this.context.getLoginConfig();
+
+ if (principal != null && !ntlmPost) {
+ this.log.debug("previously authenticated user: {}", principal.getName());
+ return true;
+ } else if (negotiateCheck) {
+ if (!authorizationHeader.isNull()) {
+ boolean negotiateResult = this.negotiate(request, response, authorizationHeader);
+ if (!negotiateResult) {
+ this.redirectTo(request, response, loginConfig.getErrorPage());
+ }
+ return negotiateResult;
+ }
+ this.log.debug("authorization required");
+ this.sendUnauthorized(response);
+ return false;
+ } else if (securityCheck) {
+ final boolean postResult = this.post(request, response);
+ if (!postResult) {
+ this.redirectTo(request, response, loginConfig.getErrorPage());
+ }
+ return postResult;
+ } else {
+ this.redirectTo(request, response, loginConfig.getLoginPage());
+ return false;
+ }
+ }
+
+ /**
+ * Negotiate.
+ *
+ * @param request
+ * the request
+ * @param response
+ * the response
+ * @param authorizationHeader
+ * the authorization header
+ *
+ * @return true, if successful
+ */
+ private boolean negotiate(final Request request, final HttpServletResponse response,
+ final AuthorizationHeader authorizationHeader) {
+
+ final String securityPackage = authorizationHeader.getSecurityPackage();
+ // maintain a connection-based session for NTLM tokens
+ final String connectionId = NtlmServletRequest.getConnectionId(request);
+
+ this.log.debug("security package: {}, connection id: {}", securityPackage, connectionId);
+
+ final boolean ntlmPost = authorizationHeader.isNtlmType1PostAuthorizationHeader();
+
+ if (ntlmPost) {
+ // type 1 NTLM authentication message received
+ this.auth.resetSecurityToken(connectionId);
+ }
+
+ final byte[] tokenBuffer = authorizationHeader.getTokenBytes();
+ this.log.debug("token buffer: {} byte(s)", Integer.valueOf(tokenBuffer.length));
+
+ // log the user in using the token
+ IWindowsSecurityContext securityContext;
+ try {
+ securityContext = this.auth.acceptSecurityToken(connectionId, tokenBuffer, securityPackage);
+ } catch (final Win32Exception e) {
+ this.log.warn("error logging in user: {}", e.getMessage());
+ this.log.trace("", e);
+ this.sendUnauthorized(response);
+ return false;
+ }
+ this.log.debug("continue required: {}", Boolean.valueOf(securityContext.isContinue()));
+
+ final byte[] continueTokenBytes = securityContext.getToken();
+ if (continueTokenBytes != null && continueTokenBytes.length > 0) {
+ final String continueToken = Base64.getEncoder().encodeToString(continueTokenBytes);
+ this.log.debug("continue token: {}", continueToken);
+ response.addHeader("WWW-Authenticate", securityPackage + " " + continueToken);
+ }
+
+ try {
+ if (securityContext.isContinue() || ntlmPost) {
+ response.setHeader("Connection", "keep-alive");
+ response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
+ response.flushBuffer();
+ return false;
+ }
+ } catch (final IOException e) {
+ this.log.warn("error logging in user: {}", e.getMessage());
+ this.log.trace("", e);
+ this.sendUnauthorized(response);
+ return false;
+ }
+
+ // create and register the user principal with the session
+ final IWindowsIdentity windowsIdentity = securityContext.getIdentity();
+
+ // disable guest login
+ if (!this.allowGuestLogin && windowsIdentity.isGuest()) {
+ this.log.warn("guest login disabled: {}", windowsIdentity.getFqn());
+ this.sendUnauthorized(response);
+ return false;
+ }
+
+ try {
+
+ this.log.debug("logged in user: {} ({})", windowsIdentity.getFqn(), windowsIdentity.getSidString());
+
+ final GenericPrincipal genericPrincipal = this.createPrincipal(windowsIdentity);
+
+ if (this.log.isDebugEnabled()) {
+ this.log.debug("roles: {}", String.join(", ", genericPrincipal.getRoles()));
+ }
+
+ // create a session associated with this request if there's none
+ final HttpSession session = request.getSession(true);
+ this.log.debug("session id: {}", session == null ? "null" : session.getId());
+
+ this.register(request, response, genericPrincipal, securityPackage, genericPrincipal.getName(), null);
+ this.log.info("successfully logged in user: {}", genericPrincipal.getName());
+
+ } finally {
+ windowsIdentity.dispose();
+ }
+
+ return true;
+ }
+
+ /**
+ * Post.
+ *
+ * @param request
+ * the request
+ * @param response
+ * the response
+ *
+ * @return true, if successful
+ */
+ private boolean post(final Request request, final HttpServletResponse response) {
+
+ final String username = request.getParameter("j_username");
+ final String password = request.getParameter("j_password");
+
+ this.log.debug("logging in: {}", username);
+
+ IWindowsIdentity windowsIdentity;
+ try {
+ windowsIdentity = this.auth.logonUser(username, password);
+ } catch (final Exception e) {
+ this.log.error(e.getMessage());
+ this.log.trace("", e);
+ return false;
+ }
+
+ // disable guest login
+ if (!this.allowGuestLogin && windowsIdentity.isGuest()) {
+ this.log.warn("guest login disabled: {}", windowsIdentity.getFqn());
+ return false;
+ }
+
+ try {
+ this.log.debug("successfully logged in {} ({})", username, windowsIdentity.getSidString());
+
+ final GenericPrincipal genericPrincipal = this.createPrincipal(windowsIdentity);
+
+ if (this.log.isDebugEnabled()) {
+ this.log.debug("roles: {}", String.join(", ", genericPrincipal.getRoles()));
+ }
+
+ // create a session associated with this request if there's none
+ final HttpSession session = request.getSession(true);
+ this.log.debug("session id: {}", session == null ? "null" : session.getId());
+
+ this.register(request, response, genericPrincipal, "FORM", genericPrincipal.getName(), null);
+ this.log.info("successfully logged in user: {}", genericPrincipal.getName());
+ } finally {
+ windowsIdentity.dispose();
+ }
+
+ return true;
+ }
+
+ /**
+ * Redirect to.
+ *
+ * @param request
+ * the request
+ * @param response
+ * the response
+ * @param url
+ * the url
+ */
+ private void redirectTo(final Request request, final HttpServletResponse response, final String url) {
+ try {
+ this.log.debug("redirecting to: {}", url);
+ final ServletContext servletContext = this.context.getServletContext();
+ final RequestDispatcher disp = servletContext.getRequestDispatcher(url);
+ disp.forward(request.getRequest(), response);
+ } catch (final IOException | ServletException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * XXX The 'doAuthenticate' is intended to replace 'authenticate' for needs like ours. In order to support old and
+ * new at this time, we will continue to have both for time being.
+ */
+ @Override
+ protected boolean doAuthenticate(final Request request, final HttpServletResponse response) throws IOException {
+ return this.authenticate(request, response);
+ }
+
+}
diff --git a/Source/JNA/waffle-tomcat11/src/main/java/waffle/apache/NegotiateAuthenticator.java b/Source/JNA/waffle-tomcat11/src/main/java/waffle/apache/NegotiateAuthenticator.java
new file mode 100644
index 0000000000..b204fcea7c
--- /dev/null
+++ b/Source/JNA/waffle-tomcat11/src/main/java/waffle/apache/NegotiateAuthenticator.java
@@ -0,0 +1,197 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2010-2024 The Waffle Project Contributors: https://github.com/Waffle/waffle/graphs/contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package waffle.apache;
+
+import com.sun.jna.platform.win32.Win32Exception;
+
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
+
+import java.io.IOException;
+import java.security.Principal;
+import java.util.Base64;
+
+import org.apache.catalina.LifecycleException;
+import org.apache.catalina.connector.Request;
+import org.apache.catalina.realm.GenericPrincipal;
+import org.slf4j.LoggerFactory;
+
+import waffle.util.AuthorizationHeader;
+import waffle.util.NtlmServletRequest;
+import waffle.windows.auth.IWindowsIdentity;
+import waffle.windows.auth.IWindowsSecurityContext;
+
+/**
+ * An Apache Negotiate (NTLM, Kerberos) Authenticator.
+ */
+public class NegotiateAuthenticator extends WaffleAuthenticatorBase {
+
+ /**
+ * Instantiates a new negotiate authenticator.
+ */
+ public NegotiateAuthenticator() {
+ super();
+ this.log = LoggerFactory.getLogger(NegotiateAuthenticator.class);
+ this.info = NegotiateAuthenticator.class.getSimpleName();
+ this.log.debug("[waffle.apache.NegotiateAuthenticator] loaded");
+ }
+
+ @Override
+ public synchronized void startInternal() throws LifecycleException {
+ this.log.info("[waffle.apache.NegotiateAuthenticator] started");
+ super.startInternal();
+ }
+
+ @Override
+ public synchronized void stopInternal() throws LifecycleException {
+ super.stopInternal();
+ this.log.info("[waffle.apache.NegotiateAuthenticator] stopped");
+ }
+
+ @Override
+ public boolean authenticate(final Request request, final HttpServletResponse response) {
+
+ Principal principal = request.getUserPrincipal();
+ final AuthorizationHeader authorizationHeader = new AuthorizationHeader(request);
+ final boolean ntlmPost = authorizationHeader.isNtlmType1PostAuthorizationHeader();
+
+ this.log.debug("{} {}, contentlength: {}", request.getMethod(), request.getRequestURI(),
+ Integer.valueOf(request.getContentLength()));
+ this.log.debug("authorization: {}, ntlm post: {}", authorizationHeader, Boolean.valueOf(ntlmPost));
+
+ if (principal != null && !ntlmPost) {
+ // user already authenticated
+ this.log.debug("previously authenticated user: {}", principal.getName());
+ return true;
+ }
+
+ // authenticate user
+ if (!authorizationHeader.isNull()) {
+
+ final String securityPackage = authorizationHeader.getSecurityPackage();
+ // maintain a connection-based session for NTLM tokens
+ final String connectionId = NtlmServletRequest.getConnectionId(request);
+
+ this.log.debug("security package: {}, connection id: {}", securityPackage, connectionId);
+
+ if (ntlmPost) {
+ // type 1 NTLM authentication message received
+ this.auth.resetSecurityToken(connectionId);
+ }
+
+ final byte[] tokenBuffer = authorizationHeader.getTokenBytes();
+ this.log.debug("token buffer: {} byte(s)", Integer.valueOf(tokenBuffer.length));
+
+ // log the user in using the token
+ IWindowsSecurityContext securityContext;
+ try {
+ securityContext = this.auth.acceptSecurityToken(connectionId, tokenBuffer, securityPackage);
+ } catch (final Win32Exception e) {
+ this.log.warn("error logging in user: {}", e.getMessage());
+ this.log.trace("", e);
+ this.sendUnauthorized(response);
+ return false;
+ }
+ this.log.debug("continue required: {}", Boolean.valueOf(securityContext.isContinue()));
+
+ final byte[] continueTokenBytes = securityContext.getToken();
+ if (continueTokenBytes != null && continueTokenBytes.length > 0) {
+ final String continueToken = Base64.getEncoder().encodeToString(continueTokenBytes);
+ this.log.debug("continue token: {}", continueToken);
+ response.addHeader("WWW-Authenticate", securityPackage + " " + continueToken);
+ }
+
+ try {
+ if (securityContext.isContinue()) {
+ response.setHeader("Connection", "keep-alive");
+ response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
+ response.flushBuffer();
+ return false;
+ }
+ } catch (final IOException e) {
+ this.log.warn("error logging in user: {}", e.getMessage());
+ this.log.trace("", e);
+ this.sendUnauthorized(response);
+ return false;
+ }
+
+ // realm: fail if no realm is configured
+ if (this.context == null || this.context.getRealm() == null) {
+ this.log.warn("missing context/realm");
+ this.sendError(response, HttpServletResponse.SC_SERVICE_UNAVAILABLE);
+ return false;
+ }
+
+ // create and register the user principal with the session
+ final IWindowsIdentity windowsIdentity = securityContext.getIdentity();
+
+ // disable guest login
+ if (!this.allowGuestLogin && windowsIdentity.isGuest()) {
+ this.log.warn("guest login disabled: {}", windowsIdentity.getFqn());
+ this.sendUnauthorized(response);
+ return false;
+ }
+
+ try {
+ this.log.debug("logged in user: {} ({})", windowsIdentity.getFqn(), windowsIdentity.getSidString());
+
+ final GenericPrincipal genericPrincipal = this.createPrincipal(windowsIdentity);
+
+ if (this.log.isDebugEnabled()) {
+ this.log.debug("roles: {}", String.join(", ", genericPrincipal.getRoles()));
+ }
+
+ principal = genericPrincipal;
+
+ // create a session associated with this request if there's none
+ final HttpSession session = request.getSession(true);
+ this.log.debug("session id: {}", session == null ? "null" : session.getId());
+
+ // register the authenticated principal
+ this.register(request, response, principal, securityPackage, principal.getName(), null);
+ this.log.info("successfully logged in user: {}", principal.getName());
+
+ } finally {
+ windowsIdentity.dispose();
+ securityContext.dispose();
+ }
+
+ return true;
+ }
+
+ this.log.debug("authorization required");
+ this.sendUnauthorized(response);
+ return false;
+ }
+
+ /**
+ * XXX The 'doAuthenticate' is intended to replace 'authenticate' for needs like ours. In order to support old and
+ * new at this time, we will continue to have both for time being.
+ */
+ @Override
+ protected boolean doAuthenticate(final Request request, final HttpServletResponse response) throws IOException {
+ return this.authenticate(request, response);
+ }
+
+}
diff --git a/Source/JNA/waffle-tomcat11/src/main/java/waffle/apache/WaffleAuthenticatorBase.java b/Source/JNA/waffle-tomcat11/src/main/java/waffle/apache/WaffleAuthenticatorBase.java
new file mode 100644
index 0000000000..4a2db5e88f
--- /dev/null
+++ b/Source/JNA/waffle-tomcat11/src/main/java/waffle/apache/WaffleAuthenticatorBase.java
@@ -0,0 +1,305 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2010-2024 The Waffle Project Contributors: https://github.com/Waffle/waffle/graphs/contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package waffle.apache;
+
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.security.Principal;
+import java.util.Arrays;
+import java.util.LinkedHashSet;
+import java.util.Locale;
+import java.util.Set;
+
+import org.apache.catalina.LifecycleException;
+import org.apache.catalina.authenticator.AuthenticatorBase;
+import org.apache.catalina.connector.Request;
+import org.apache.catalina.realm.GenericPrincipal;
+import org.slf4j.Logger;
+
+import waffle.windows.auth.IWindowsAuthProvider;
+import waffle.windows.auth.IWindowsIdentity;
+import waffle.windows.auth.PrincipalFormat;
+import waffle.windows.auth.impl.WindowsAuthProviderImpl;
+
+/**
+ * The Class WaffleAuthenticatorBase.
+ */
+abstract class WaffleAuthenticatorBase extends AuthenticatorBase {
+
+ /** The Constant SUPPORTED_PROTOCOLS. */
+ private static final Set SUPPORTED_PROTOCOLS = new LinkedHashSet<>(Arrays.asList("Negotiate", "NTLM"));
+
+ /** The info. */
+ protected String info;
+
+ /** The log. */
+ protected Logger log;
+
+ /** The principal format. */
+ protected PrincipalFormat principalFormat = PrincipalFormat.FQN;
+
+ /** The role format. */
+ protected PrincipalFormat roleFormat = PrincipalFormat.FQN;
+
+ /** The allow guest login. */
+ protected boolean allowGuestLogin = true;
+
+ /** The protocols. */
+ protected Set protocols = WaffleAuthenticatorBase.SUPPORTED_PROTOCOLS;
+
+ /** The auth continueContextsTimeout configuration. */
+ protected int continueContextsTimeout = WindowsAuthProviderImpl.CONTINUE_CONTEXT_TIMEOUT;
+
+ /** The auth. */
+ protected IWindowsAuthProvider auth;
+
+ /**
+ * Gets the continue context time out configuration.
+ *
+ * @return the continue contexts timeout
+ */
+ public int getContinueContextsTimeout() {
+ return this.continueContextsTimeout;
+ }
+
+ /**
+ * Sets the continue context time out configuration.
+ *
+ * @param continueContextsTimeout
+ * the new continue contexts timeout
+ */
+ public void setContinueContextsTimeout(final int continueContextsTimeout) {
+ this.continueContextsTimeout = continueContextsTimeout;
+ }
+
+ /**
+ * Windows authentication provider.
+ *
+ * @return IWindowsAuthProvider.
+ */
+ public IWindowsAuthProvider getAuth() {
+ return this.auth;
+ }
+
+ /**
+ * Set Windows auth provider.
+ *
+ * @param provider
+ * Class implements IWindowsAuthProvider.
+ */
+ public void setAuth(final IWindowsAuthProvider provider) {
+ this.auth = provider;
+ }
+
+ /**
+ * Gets the info.
+ *
+ * @return the info
+ */
+ public String getInfo() {
+ return this.info;
+ }
+
+ /**
+ * Set the principal format.
+ *
+ * @param format
+ * Principal format.
+ */
+ public void setPrincipalFormat(final String format) {
+ this.principalFormat = PrincipalFormat.valueOf(format.toUpperCase(Locale.ENGLISH));
+ this.log.debug("principal format: {}", this.principalFormat);
+ }
+
+ /**
+ * Principal format.
+ *
+ * @return Principal format.
+ */
+ public PrincipalFormat getPrincipalFormat() {
+ return this.principalFormat;
+ }
+
+ /**
+ * Set the principal format.
+ *
+ * @param format
+ * Role format.
+ */
+ public void setRoleFormat(final String format) {
+ this.roleFormat = PrincipalFormat.valueOf(format.toUpperCase(Locale.ENGLISH));
+ this.log.debug("role format: {}", this.roleFormat);
+ }
+
+ /**
+ * Principal format.
+ *
+ * @return Role format.
+ */
+ public PrincipalFormat getRoleFormat() {
+ return this.roleFormat;
+ }
+
+ /**
+ * True if Guest login permitted.
+ *
+ * @return True if Guest login permitted, false otherwise.
+ */
+ public boolean isAllowGuestLogin() {
+ return this.allowGuestLogin;
+ }
+
+ /**
+ * Set whether Guest login is permitted. Default is true, if the Guest account is enabled, an invalid
+ * username/password results in a Guest login.
+ *
+ * @param value
+ * True or false.
+ */
+ public void setAllowGuestLogin(final boolean value) {
+ this.allowGuestLogin = value;
+ }
+
+ /**
+ * Set the authentication protocols. Default is "Negotiate, NTLM".
+ *
+ * @param value
+ * Authentication protocols
+ */
+ public void setProtocols(final String value) {
+ this.protocols = new LinkedHashSet<>();
+ final String[] protocolNames = value.split(",", -1);
+ for (String protocolName : protocolNames) {
+ protocolName = protocolName.trim();
+ if (!protocolName.isEmpty()) {
+ this.log.debug("init protocol: {}", protocolName);
+ if (WaffleAuthenticatorBase.SUPPORTED_PROTOCOLS.contains(protocolName)) {
+ this.protocols.add(protocolName);
+ } else {
+ this.log.error("unsupported protocol: {}", protocolName);
+ throw new RuntimeException("Unsupported protocol: " + protocolName);
+ }
+ }
+ }
+ }
+
+ /**
+ * Send a 401 Unauthorized along with protocol authentication headers.
+ *
+ * @param response
+ * HTTP Response
+ */
+ protected void sendUnauthorized(final HttpServletResponse response) {
+ try {
+ for (final String protocol : this.protocols) {
+ response.addHeader("WWW-Authenticate", protocol);
+ }
+ response.setHeader("Connection", "close");
+ response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
+ response.flushBuffer();
+ } catch (final IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Send an error code.
+ *
+ * @param response
+ * HTTP Response
+ * @param code
+ * Error Code
+ */
+ protected void sendError(final HttpServletResponse response, final int code) {
+ try {
+ response.sendError(code);
+ } catch (final IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ protected String getAuthMethod() {
+ return null;
+ }
+
+ @Override
+ protected Principal doLogin(final Request request, final String username, final String password)
+ throws ServletException {
+ this.log.debug("logging in: {}", username);
+ IWindowsIdentity windowsIdentity;
+ try {
+ windowsIdentity = this.auth.logonUser(username, password);
+ } catch (final Exception e) {
+ this.log.error(e.getMessage());
+ this.log.trace("", e);
+ return super.doLogin(request, username, password);
+ }
+ // disable guest login
+ if (!this.allowGuestLogin && windowsIdentity.isGuest()) {
+ this.log.warn("guest login disabled: {}", windowsIdentity.getFqn());
+ return super.doLogin(request, username, password);
+ }
+ try {
+ this.log.debug("successfully logged in {} ({})", username, windowsIdentity.getSidString());
+ final GenericPrincipal genericPrincipal = this.createPrincipal(windowsIdentity);
+ if (this.log.isDebugEnabled()) {
+ this.log.debug("roles: {}", String.join(", ", genericPrincipal.getRoles()));
+ }
+ return genericPrincipal;
+ } finally {
+ windowsIdentity.dispose();
+ }
+ }
+
+ /**
+ * This method will create an instance of a IWindowsIdentity based GenericPrincipal. It is used for creating custom
+ * implementation within subclasses.
+ *
+ * @param windowsIdentity
+ * the windows identity to initialize the principal
+ *
+ * @return the Generic Principal
+ */
+ protected GenericPrincipal createPrincipal(final IWindowsIdentity windowsIdentity) {
+ return new GenericWindowsPrincipal(windowsIdentity, this.principalFormat, this.roleFormat);
+ }
+
+ /**
+ * Hook to the start and to set up the dependencies.
+ *
+ * @throws LifecycleException
+ * the lifecycle exception
+ */
+ @Override
+ public synchronized void startInternal() throws LifecycleException {
+ this.log.debug("Creating a windows authentication provider with continueContextsTimeout property set to: {}",
+ this.continueContextsTimeout);
+ this.auth = new WindowsAuthProviderImpl(this.continueContextsTimeout);
+ super.startInternal();
+ }
+
+}
diff --git a/Source/JNA/waffle-tomcat11/src/main/java/waffle/apache/WindowsRealm.java b/Source/JNA/waffle-tomcat11/src/main/java/waffle/apache/WindowsRealm.java
new file mode 100644
index 0000000000..159f167089
--- /dev/null
+++ b/Source/JNA/waffle-tomcat11/src/main/java/waffle/apache/WindowsRealm.java
@@ -0,0 +1,45 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2010-2024 The Waffle Project Contributors: https://github.com/Waffle/waffle/graphs/contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package waffle.apache;
+
+import java.security.Principal;
+
+import org.apache.catalina.realm.RealmBase;
+
+/**
+ * A rudimentary Windows realm.
+ */
+public class WindowsRealm extends RealmBase {
+
+ @Override
+ protected String getPassword(final String value) {
+ return null;
+ }
+
+ @Override
+ protected Principal getPrincipal(final String value) {
+ return null;
+ }
+
+}
diff --git a/Source/JNA/waffle-tomcat11/src/main/java/waffle/apache/package-info.java b/Source/JNA/waffle-tomcat11/src/main/java/waffle/apache/package-info.java
new file mode 100644
index 0000000000..eb148d4f03
--- /dev/null
+++ b/Source/JNA/waffle-tomcat11/src/main/java/waffle/apache/package-info.java
@@ -0,0 +1,27 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2010-2024 The Waffle Project Contributors: https://github.com/Waffle/waffle/graphs/contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+/**
+ * Waffle Tomcat Package.
+ */
+package waffle.apache;
diff --git a/Source/JNA/waffle-tomcat11/src/site/resources/images/waffle.jpg b/Source/JNA/waffle-tomcat11/src/site/resources/images/waffle.jpg
new file mode 100644
index 0000000000..00455a8db4
Binary files /dev/null and b/Source/JNA/waffle-tomcat11/src/site/resources/images/waffle.jpg differ
diff --git a/Source/JNA/waffle-tomcat11/src/site/site.xml b/Source/JNA/waffle-tomcat11/src/site/site.xml
new file mode 100644
index 0000000000..25dd2499bf
--- /dev/null
+++ b/Source/JNA/waffle-tomcat11/src/site/site.xml
@@ -0,0 +1,54 @@
+
+
+
+
+ /images/waffle.jpg
+ https://github.com/Waffle/waffle
+
+
+ /images/waffle.jpg
+ https://github.com/Waffle/waffle
+
+
+ org.apache.maven.skins
+ maven-fluido-skin
+ 1.11.2
+
+
+
+ true
+ true
+
+
+
+
+
+
+
+
+
diff --git a/Source/JNA/waffle-tomcat11/src/test/java/waffle/apache/MixedAuthenticatorTest.java b/Source/JNA/waffle-tomcat11/src/test/java/waffle/apache/MixedAuthenticatorTest.java
new file mode 100644
index 0000000000..22d11ffc3c
--- /dev/null
+++ b/Source/JNA/waffle-tomcat11/src/test/java/waffle/apache/MixedAuthenticatorTest.java
@@ -0,0 +1,400 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2010-2024 The Waffle Project Contributors: https://github.com/Waffle/waffle/graphs/contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package waffle.apache;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import com.sun.jna.platform.win32.Sspi;
+import com.sun.jna.platform.win32.SspiUtil.ManagedSecBufferDesc;
+
+import jakarta.servlet.ServletException;
+
+import java.util.Base64;
+import java.util.Collections;
+
+import mockit.Expectations;
+import mockit.Mocked;
+
+import org.apache.catalina.Context;
+import org.apache.catalina.Engine;
+import org.apache.catalina.LifecycleException;
+import org.apache.catalina.realm.GenericPrincipal;
+import org.apache.coyote.Response;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import waffle.apache.catalina.SimpleHttpRequest;
+import waffle.apache.catalina.SimpleHttpResponse;
+import waffle.mock.MockWindowsAuthProvider;
+import waffle.windows.auth.IWindowsCredentialsHandle;
+import waffle.windows.auth.IWindowsIdentity;
+import waffle.windows.auth.PrincipalFormat;
+import waffle.windows.auth.impl.WindowsAccountImpl;
+import waffle.windows.auth.impl.WindowsCredentialsHandleImpl;
+import waffle.windows.auth.impl.WindowsSecurityContextImpl;
+
+/**
+ * Waffle Tomcat Mixed Authenticator Test.
+ */
+class MixedAuthenticatorTest {
+
+ /** The authenticator. */
+ MixedAuthenticator authenticator;
+
+ /** The context. */
+ @Mocked
+ Context context;
+
+ /** The engine. */
+ @Mocked
+ Engine engine;
+
+ /**
+ * Sets the up.
+ *
+ * @throws LifecycleException
+ * the lifecycle exception
+ */
+ @BeforeEach
+ void setUp() throws LifecycleException {
+ this.authenticator = new MixedAuthenticator();
+ this.authenticator.setContainer(this.context);
+ Assertions.assertNotNull(new Expectations() {
+ {
+ MixedAuthenticatorTest.this.context.getParent();
+ this.result = MixedAuthenticatorTest.this.engine;
+ MixedAuthenticatorTest.this.context.getParent();
+ this.result = null;
+ }
+ });
+ this.authenticator.start();
+ }
+
+ /**
+ * Tear down.
+ *
+ * @throws LifecycleException
+ * the lifecycle exception
+ */
+ @AfterEach
+ void tearDown() throws LifecycleException {
+ this.authenticator.stop();
+ }
+
+ /**
+ * Test challenge get.
+ */
+ @Test
+ void testChallengeGET() {
+ final SimpleHttpRequest request = new SimpleHttpRequest();
+ request.setMethod("GET");
+ request.setQueryString("j_negotiate_check");
+ final SimpleHttpResponse response = new SimpleHttpResponse(new Response());
+ this.authenticator.authenticate(request, response);
+ final String[] wwwAuthenticates = response.getHeaderValues("WWW-Authenticate");
+ Assertions.assertNotNull(wwwAuthenticates);
+ Assertions.assertEquals(2, wwwAuthenticates.length);
+ Assertions.assertEquals("Negotiate", wwwAuthenticates[0]);
+ Assertions.assertEquals("NTLM", wwwAuthenticates[1]);
+ Assertions.assertEquals("close", response.getHeader("Connection"));
+ Assertions.assertEquals(2, response.getHeaderNames().size());
+ Assertions.assertEquals(401, response.getStatus());
+ }
+
+ /**
+ * Test challenge post.
+ */
+ @Test
+ void testChallengePOST() {
+ final String securityPackage = "Negotiate";
+ IWindowsCredentialsHandle clientCredentials = null;
+ WindowsSecurityContextImpl clientContext = null;
+ try {
+ // client credentials handle
+ clientCredentials = WindowsCredentialsHandleImpl.getCurrent(securityPackage);
+ clientCredentials.initialize();
+ // initial client security context
+ clientContext = new WindowsSecurityContextImpl();
+ clientContext.setPrincipalName(WindowsAccountImpl.getCurrentUsername());
+ clientContext.setCredentialsHandle(clientCredentials);
+ clientContext.setSecurityPackage(securityPackage);
+ clientContext.initialize(null, null, WindowsAccountImpl.getCurrentUsername());
+ final SimpleHttpRequest request = new SimpleHttpRequest();
+ request.setQueryString("j_negotiate_check");
+ request.setMethod("POST");
+ request.setContentLength(0);
+ final String clientToken = Base64.getEncoder().encodeToString(clientContext.getToken());
+ request.addHeader("Authorization", securityPackage + " " + clientToken);
+ final SimpleHttpResponse response = new SimpleHttpResponse(new Response());
+ this.authenticator.authenticate(request, response);
+ Assertions.assertTrue(response.getHeader("WWW-Authenticate").startsWith(securityPackage + " "));
+ Assertions.assertEquals("keep-alive", response.getHeader("Connection"));
+ Assertions.assertEquals(2, response.getHeaderNames().size());
+ Assertions.assertEquals(401, response.getStatus());
+ } finally {
+ if (clientContext != null) {
+ clientContext.dispose();
+ }
+ if (clientCredentials != null) {
+ clientCredentials.dispose();
+ }
+ }
+ }
+
+ /**
+ * Test get.
+ */
+ @Test
+ void testGet() {
+ final SimpleHttpRequest request = new SimpleHttpRequest();
+ final SimpleHttpResponse response = new SimpleHttpResponse(new Response());
+ Assertions.assertFalse(this.authenticator.authenticate(request, response));
+ }
+
+ /**
+ * Test get info.
+ */
+ @Test
+ void testGetInfo() {
+ assertThat(this.authenticator.getInfo()).isNotEmpty();
+ }
+
+ /**
+ * Test negotiate.
+ */
+ @Test
+ void testNegotiate() {
+ final String securityPackage = "Negotiate";
+ IWindowsCredentialsHandle clientCredentials = null;
+ WindowsSecurityContextImpl clientContext = null;
+ try {
+ // client credentials handle
+ clientCredentials = WindowsCredentialsHandleImpl.getCurrent(securityPackage);
+ clientCredentials.initialize();
+ // initial client security context
+ clientContext = new WindowsSecurityContextImpl();
+ clientContext.setPrincipalName(WindowsAccountImpl.getCurrentUsername());
+ clientContext.setCredentialsHandle(clientCredentials);
+ clientContext.setSecurityPackage(securityPackage);
+ clientContext.initialize(null, null, WindowsAccountImpl.getCurrentUsername());
+ // negotiate
+ boolean authenticated = false;
+ final SimpleHttpRequest request = new SimpleHttpRequest();
+ request.setQueryString("j_negotiate_check");
+ String clientToken;
+ while (true) {
+ clientToken = Base64.getEncoder().encodeToString(clientContext.getToken());
+ request.addHeader("Authorization", securityPackage + " " + clientToken);
+
+ final SimpleHttpResponse response = new SimpleHttpResponse(new Response());
+ authenticated = this.authenticator.authenticate(request, response);
+
+ if (authenticated) {
+ assertThat(response.getHeaderNames().size()).isNotNegative();
+ break;
+ }
+
+ Assertions.assertTrue(response.getHeader("WWW-Authenticate").startsWith(securityPackage + " "));
+ Assertions.assertEquals("keep-alive", response.getHeader("Connection"));
+ Assertions.assertEquals(2, response.getHeaderNames().size());
+ Assertions.assertEquals(401, response.getStatus());
+ final String continueToken = response.getHeader("WWW-Authenticate")
+ .substring(securityPackage.length() + 1);
+ final byte[] continueTokenBytes = Base64.getDecoder().decode(continueToken);
+ assertThat(continueTokenBytes).isNotEmpty();
+ final ManagedSecBufferDesc continueTokenBuffer = new ManagedSecBufferDesc(Sspi.SECBUFFER_TOKEN,
+ continueTokenBytes);
+ clientContext.initialize(clientContext.getHandle(), continueTokenBuffer,
+ WindowsAccountImpl.getCurrentUsername());
+ }
+ Assertions.assertTrue(authenticated);
+ } finally {
+ if (clientContext != null) {
+ clientContext.dispose();
+ }
+ if (clientCredentials != null) {
+ clientCredentials.dispose();
+ }
+ }
+ }
+
+ /**
+ * Test post security check.
+ */
+ @Test
+ void testPostSecurityCheck() {
+ final SimpleHttpRequest request = new SimpleHttpRequest();
+ request.setQueryString("j_security_check");
+ request.addParameter("j_username", "username");
+ request.addParameter("j_password", "password");
+ final SimpleHttpResponse response = new SimpleHttpResponse(new Response());
+ Assertions.assertFalse(this.authenticator.authenticate(request, response));
+ }
+
+ /**
+ * Test programmatic security BOTH.
+ *
+ * @param identity
+ * the identity
+ *
+ * @throws ServletException
+ * the servlet exception
+ */
+ @Test
+ void testProgrammaticSecurityBoth(@Mocked final IWindowsIdentity identity) throws ServletException {
+ this.authenticator.setAuth(new MockWindowsAuthProvider());
+ final SimpleHttpRequest request = new SimpleHttpRequest();
+ request.getMappingData().context = (Context) this.authenticator.getContainer();
+
+ request.login(WindowsAccountImpl.getCurrentUsername(), "");
+
+ Assertions.assertNotNull(new Expectations() {
+ {
+ identity.getFqn();
+ this.result = "fqn";
+ identity.getSidString();
+ this.result = "S-1234";
+ }
+ });
+ request.setUserPrincipal(new GenericWindowsPrincipal(identity, PrincipalFormat.BOTH, PrincipalFormat.BOTH));
+
+ Assertions.assertTrue(request.getUserPrincipal() instanceof GenericWindowsPrincipal);
+ final GenericWindowsPrincipal windowsPrincipal = (GenericWindowsPrincipal) request.getUserPrincipal();
+ Assertions.assertTrue(windowsPrincipal.getSidString().startsWith("S-"));
+ }
+
+ /**
+ * Test programmatic security SID.
+ *
+ * @param identity
+ * the identity
+ *
+ * @throws ServletException
+ * the servlet exception
+ */
+ @Test
+ void testProgrammaticSecuritySID(@Mocked final IWindowsIdentity identity) throws ServletException {
+ this.authenticator.setAuth(new MockWindowsAuthProvider());
+ final SimpleHttpRequest request = new SimpleHttpRequest();
+ request.getMappingData().context = (Context) this.authenticator.getContainer();
+
+ request.login(WindowsAccountImpl.getCurrentUsername(), "");
+
+ Assertions.assertNotNull(new Expectations() {
+ {
+ identity.getSidString();
+ this.result = "S-1234";
+ }
+ });
+ request.setUserPrincipal(new GenericWindowsPrincipal(identity, PrincipalFormat.SID, PrincipalFormat.SID));
+
+ Assertions.assertTrue(request.getUserPrincipal() instanceof GenericWindowsPrincipal);
+ final GenericWindowsPrincipal windowsPrincipal = (GenericWindowsPrincipal) request.getUserPrincipal();
+ Assertions.assertTrue(windowsPrincipal.getSidString().startsWith("S-"));
+ }
+
+ /**
+ * Test programmatic security NONE.
+ *
+ * @param identity
+ * the identity
+ *
+ * @throws ServletException
+ * the servlet exception
+ */
+ @Test
+ void testProgrammaticSecurityNone(@Mocked final IWindowsIdentity identity) throws ServletException {
+ this.authenticator.setAuth(new MockWindowsAuthProvider());
+ final SimpleHttpRequest request = new SimpleHttpRequest();
+ request.getMappingData().context = (Context) this.authenticator.getContainer();
+
+ request.login(WindowsAccountImpl.getCurrentUsername(), "");
+
+ request.setUserPrincipal(new GenericWindowsPrincipal(identity, PrincipalFormat.NONE, PrincipalFormat.NONE));
+
+ Assertions.assertTrue(request.getUserPrincipal() instanceof GenericWindowsPrincipal);
+ final GenericWindowsPrincipal windowsPrincipal = (GenericWindowsPrincipal) request.getUserPrincipal();
+ Assertions.assertNull(windowsPrincipal.getSidString());
+ }
+
+ /**
+ * Test security check parameters.
+ */
+ @Test
+ void testSecurityCheckParameters() {
+ this.authenticator.setAuth(new MockWindowsAuthProvider());
+ final SimpleHttpRequest request = new SimpleHttpRequest();
+ request.addParameter("j_security_check", "");
+ request.addParameter("j_username", WindowsAccountImpl.getCurrentUsername());
+ request.addParameter("j_password", "");
+ final SimpleHttpResponse response = new SimpleHttpResponse(new Response());
+ Assertions.assertTrue(this.authenticator.authenticate(request, response));
+ }
+
+ /**
+ * Test security check query string.
+ */
+ @Test
+ void testSecurityCheckQueryString() {
+ this.authenticator.setAuth(new MockWindowsAuthProvider());
+ final SimpleHttpRequest request = new SimpleHttpRequest();
+ request.setQueryString("j_security_check");
+ request.addParameter("j_username", WindowsAccountImpl.getCurrentUsername());
+ request.addParameter("j_password", "");
+ final SimpleHttpResponse response = new SimpleHttpResponse(new Response());
+ Assertions.assertTrue(this.authenticator.authenticate(request, response));
+ }
+
+ @Test
+ void testCustomPrincipal() throws LifecycleException {
+ final GenericPrincipal genericPrincipal = new GenericPrincipal("my-principal", Collections.emptyList());
+ final MixedAuthenticator customAuthenticator = new MixedAuthenticator() {
+ @Override
+ protected GenericPrincipal createPrincipal(final IWindowsIdentity windowsIdentity) {
+ return genericPrincipal;
+ }
+ };
+ try {
+ customAuthenticator.setContainer(this.context);
+ customAuthenticator.setAlwaysUseSession(true);
+ customAuthenticator.start();
+
+ customAuthenticator.setAuth(new MockWindowsAuthProvider());
+ final SimpleHttpRequest request = new SimpleHttpRequest();
+ request.addParameter("j_security_check", "");
+ request.addParameter("j_username", WindowsAccountImpl.getCurrentUsername());
+ request.addParameter("j_password", "");
+ final SimpleHttpResponse response = new SimpleHttpResponse(new Response());
+ Assertions.assertTrue(customAuthenticator.authenticate(request, response));
+
+ Assertions.assertEquals(genericPrincipal, request.getUserPrincipal());
+ } finally {
+ customAuthenticator.stop();
+ }
+
+ }
+
+}
diff --git a/Source/JNA/waffle-tomcat11/src/test/java/waffle/apache/NegotiateAuthenticatorTest.java b/Source/JNA/waffle-tomcat11/src/test/java/waffle/apache/NegotiateAuthenticatorTest.java
new file mode 100644
index 0000000000..427584c0cf
--- /dev/null
+++ b/Source/JNA/waffle-tomcat11/src/test/java/waffle/apache/NegotiateAuthenticatorTest.java
@@ -0,0 +1,330 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2010-2024 The Waffle Project Contributors: https://github.com/Waffle/waffle/graphs/contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package waffle.apache;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import com.sun.jna.platform.win32.Sspi;
+import com.sun.jna.platform.win32.SspiUtil.ManagedSecBufferDesc;
+
+import java.util.Base64;
+
+import mockit.Expectations;
+import mockit.Mocked;
+
+import org.apache.catalina.Context;
+import org.apache.catalina.Engine;
+import org.apache.catalina.LifecycleException;
+import org.apache.coyote.Response;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import waffle.apache.catalina.SimpleHttpRequest;
+import waffle.apache.catalina.SimpleHttpResponse;
+import waffle.windows.auth.IWindowsCredentialsHandle;
+import waffle.windows.auth.PrincipalFormat;
+import waffle.windows.auth.impl.WindowsAccountImpl;
+import waffle.windows.auth.impl.WindowsAuthProviderImpl;
+import waffle.windows.auth.impl.WindowsCredentialsHandleImpl;
+import waffle.windows.auth.impl.WindowsSecurityContextImpl;
+
+/**
+ * Waffle Tomcat Authenticator Test.
+ */
+class NegotiateAuthenticatorTest {
+
+ /** The authenticator. */
+ private NegotiateAuthenticator authenticator;
+
+ @Mocked
+ Context context;
+
+ @Mocked
+ Engine engine;
+
+ /**
+ * Sets the up.
+ *
+ * @throws LifecycleException
+ * the lifecycle exception
+ */
+ @BeforeEach
+ void setUp() throws LifecycleException {
+ this.authenticator = new NegotiateAuthenticator();
+ this.authenticator.setContainer(this.context);
+ Assertions.assertNotNull(new Expectations() {
+ {
+ NegotiateAuthenticatorTest.this.context.getParent();
+ this.result = NegotiateAuthenticatorTest.this.engine;
+ NegotiateAuthenticatorTest.this.context.getParent();
+ this.result = null;
+ }
+ });
+ this.authenticator.start();
+ }
+
+ /**
+ * Tear down.
+ *
+ * @throws LifecycleException
+ * the lifecycle exception
+ */
+ @AfterEach
+ void tearDown() throws LifecycleException {
+ this.authenticator.stop();
+ }
+
+ /**
+ * Test allow guest login.
+ */
+ @Test
+ void testAllowGuestLogin() {
+ Assertions.assertTrue(this.authenticator.isAllowGuestLogin());
+ this.authenticator.setAllowGuestLogin(false);
+ Assertions.assertFalse(this.authenticator.isAllowGuestLogin());
+ }
+
+ /**
+ * Test challenge get.
+ */
+ @Test
+ void testChallengeGET() {
+ final SimpleHttpRequest request = new SimpleHttpRequest();
+ request.setMethod("GET");
+ final SimpleHttpResponse response = new SimpleHttpResponse(new Response());
+ this.authenticator.authenticate(request, response);
+ final String[] wwwAuthenticates = response.getHeaderValues("WWW-Authenticate");
+ Assertions.assertNotNull(wwwAuthenticates);
+ Assertions.assertEquals(2, wwwAuthenticates.length);
+ Assertions.assertEquals("Negotiate", wwwAuthenticates[0]);
+ Assertions.assertEquals("NTLM", wwwAuthenticates[1]);
+ Assertions.assertEquals("close", response.getHeader("Connection"));
+ Assertions.assertEquals(2, response.getHeaderNames().size());
+ Assertions.assertEquals(401, response.getStatus());
+ }
+
+ /**
+ * Test challenge post.
+ */
+ @Test
+ void testChallengePOST() {
+ final String securityPackage = "Negotiate";
+ IWindowsCredentialsHandle clientCredentials = null;
+ WindowsSecurityContextImpl clientContext = null;
+ try {
+ // client credentials handle
+ clientCredentials = WindowsCredentialsHandleImpl.getCurrent(securityPackage);
+ clientCredentials.initialize();
+ // initial client security context
+ clientContext = new WindowsSecurityContextImpl();
+ clientContext.setPrincipalName(WindowsAccountImpl.getCurrentUsername());
+ clientContext.setCredentialsHandle(clientCredentials);
+ clientContext.setSecurityPackage(securityPackage);
+ clientContext.initialize(null, null, WindowsAccountImpl.getCurrentUsername());
+ final SimpleHttpRequest request = new SimpleHttpRequest();
+ request.setMethod("POST");
+ request.setContentLength(0);
+ final String clientToken = Base64.getEncoder().encodeToString(clientContext.getToken());
+ request.addHeader("Authorization", securityPackage + " " + clientToken);
+ final SimpleHttpResponse response = new SimpleHttpResponse(new Response());
+ this.authenticator.authenticate(request, response);
+ Assertions.assertTrue(response.getHeader("WWW-Authenticate").startsWith(securityPackage + " "));
+ Assertions.assertEquals("keep-alive", response.getHeader("Connection"));
+ Assertions.assertEquals(2, response.getHeaderNames().size());
+ Assertions.assertEquals(401, response.getStatus());
+ } finally {
+ if (clientContext != null) {
+ clientContext.dispose();
+ }
+ if (clientCredentials != null) {
+ clientCredentials.dispose();
+ }
+ }
+ }
+
+ /**
+ * Test get info.
+ */
+ @Test
+ void testGetInfo() {
+ assertThat(this.authenticator.getInfo()).isNotEmpty();
+ Assertions.assertTrue(this.authenticator.getAuth() instanceof WindowsAuthProviderImpl);
+ }
+
+ /**
+ * Test negotiate.
+ */
+ @Test
+ void testNegotiate() {
+ final String securityPackage = "Negotiate";
+ IWindowsCredentialsHandle clientCredentials = null;
+ WindowsSecurityContextImpl clientContext = null;
+ try {
+ // client credentials handle
+ clientCredentials = WindowsCredentialsHandleImpl.getCurrent(securityPackage);
+ clientCredentials.initialize();
+ // initial client security context
+ clientContext = new WindowsSecurityContextImpl();
+ clientContext.setPrincipalName(WindowsAccountImpl.getCurrentUsername());
+ clientContext.setCredentialsHandle(clientCredentials);
+ clientContext.setSecurityPackage(securityPackage);
+ clientContext.initialize(null, null, WindowsAccountImpl.getCurrentUsername());
+ // negotiate
+ boolean authenticated = false;
+ final SimpleHttpRequest request = new SimpleHttpRequest();
+ while (true) {
+ final String clientToken = Base64.getEncoder().encodeToString(clientContext.getToken());
+ request.addHeader("Authorization", securityPackage + " " + clientToken);
+
+ final SimpleHttpResponse response = new SimpleHttpResponse(new Response());
+ authenticated = this.authenticator.authenticate(request, response);
+
+ if (authenticated) {
+ Assertions.assertNotNull(request.getUserPrincipal());
+ Assertions.assertTrue(request.getUserPrincipal() instanceof GenericWindowsPrincipal);
+ final GenericWindowsPrincipal windowsPrincipal = (GenericWindowsPrincipal) request
+ .getUserPrincipal();
+ Assertions.assertTrue(windowsPrincipal.getSidString().startsWith("S-"));
+ assertThat(windowsPrincipal.getSid()).isNotEmpty();
+ Assertions.assertTrue(windowsPrincipal.getGroups().containsKey("Everyone"));
+ assertThat(response.getHeaderNames()).hasSizeLessThanOrEqualTo(1);
+ break;
+ }
+
+ Assertions.assertTrue(response.getHeader("WWW-Authenticate").startsWith(securityPackage + " "));
+ Assertions.assertEquals("keep-alive", response.getHeader("Connection"));
+ Assertions.assertEquals(2, response.getHeaderNames().size());
+ Assertions.assertEquals(401, response.getStatus());
+ final String continueToken = response.getHeader("WWW-Authenticate")
+ .substring(securityPackage.length() + 1);
+ final byte[] continueTokenBytes = Base64.getDecoder().decode(continueToken);
+ assertThat(continueTokenBytes).isNotEmpty();
+ final ManagedSecBufferDesc continueTokenBuffer = new ManagedSecBufferDesc(Sspi.SECBUFFER_TOKEN,
+ continueTokenBytes);
+ clientContext.initialize(clientContext.getHandle(), continueTokenBuffer,
+ WindowsAccountImpl.getCurrentUsername());
+ }
+ Assertions.assertTrue(authenticated);
+ } finally {
+ if (clientContext != null) {
+ clientContext.dispose();
+ }
+ if (clientCredentials != null) {
+ clientCredentials.dispose();
+ }
+ }
+ }
+
+ /**
+ * Test post empty.
+ */
+ @Test
+ void testPOSTEmpty() {
+ final String securityPackage = "Negotiate";
+ IWindowsCredentialsHandle clientCredentials = null;
+ WindowsSecurityContextImpl clientContext = null;
+ try {
+ // client credentials handle
+ clientCredentials = WindowsCredentialsHandleImpl.getCurrent(securityPackage);
+ clientCredentials.initialize();
+ // initial client security context
+ clientContext = new WindowsSecurityContextImpl();
+ clientContext.setPrincipalName(WindowsAccountImpl.getCurrentUsername());
+ clientContext.setCredentialsHandle(clientCredentials);
+ clientContext.setSecurityPackage(securityPackage);
+ clientContext.initialize(null, null, WindowsAccountImpl.getCurrentUsername());
+ // negotiate
+ boolean authenticated = false;
+ final SimpleHttpRequest request = new SimpleHttpRequest();
+ request.setMethod("POST");
+ request.setContentLength(0);
+ String clientToken;
+ String continueToken;
+ byte[] continueTokenBytes;
+ SimpleHttpResponse response;
+ ManagedSecBufferDesc continueTokenBuffer;
+ while (true) {
+ clientToken = Base64.getEncoder().encodeToString(clientContext.getToken());
+ request.addHeader("Authorization", securityPackage + " " + clientToken);
+
+ response = new SimpleHttpResponse(new Response());
+ authenticated = this.authenticator.authenticate(request, response);
+
+ if (authenticated) {
+ assertThat(response.getHeaderNames().size()).isNotNegative();
+ break;
+ }
+
+ if (response.getHeader("WWW-Authenticate").startsWith(securityPackage + ",")) {
+ Assertions.assertEquals("close", response.getHeader("Connection"));
+ Assertions.assertEquals(2, response.getHeaderNames().size());
+ Assertions.assertEquals(401, response.getStatus());
+ return;
+ }
+
+ Assertions.assertTrue(response.getHeader("WWW-Authenticate").startsWith(securityPackage + " "));
+ Assertions.assertEquals("keep-alive", response.getHeader("Connection"));
+ Assertions.assertEquals(2, response.getHeaderNames().size());
+ Assertions.assertEquals(401, response.getStatus());
+ continueToken = response.getHeader("WWW-Authenticate").substring(securityPackage.length() + 1);
+ continueTokenBytes = Base64.getDecoder().decode(continueToken);
+ assertThat(continueTokenBytes).isNotEmpty();
+ continueTokenBuffer = new ManagedSecBufferDesc(Sspi.SECBUFFER_TOKEN, continueTokenBytes);
+ clientContext.initialize(clientContext.getHandle(), continueTokenBuffer,
+ WindowsAccountImpl.getCurrentUsername());
+ }
+ Assertions.assertTrue(authenticated);
+ } finally {
+ if (clientContext != null) {
+ clientContext.dispose();
+ }
+ if (clientCredentials != null) {
+ clientCredentials.dispose();
+ }
+ }
+ }
+
+ /**
+ * Test principal format.
+ */
+ @Test
+ void testPrincipalFormat() {
+ Assertions.assertEquals(PrincipalFormat.FQN, this.authenticator.getPrincipalFormat());
+ this.authenticator.setPrincipalFormat("both");
+ Assertions.assertEquals(PrincipalFormat.BOTH, this.authenticator.getPrincipalFormat());
+ }
+
+ /**
+ * Test role format.
+ */
+ @Test
+ void testRoleFormat() {
+ Assertions.assertEquals(PrincipalFormat.FQN, this.authenticator.getRoleFormat());
+ this.authenticator.setRoleFormat("both");
+ Assertions.assertEquals(PrincipalFormat.BOTH, this.authenticator.getRoleFormat());
+ }
+
+}
diff --git a/Source/JNA/waffle-tomcat11/src/test/java/waffle/apache/WaffleAuthenticatorBaseTest.java b/Source/JNA/waffle-tomcat11/src/test/java/waffle/apache/WaffleAuthenticatorBaseTest.java
new file mode 100644
index 0000000000..46ba0c7e61
--- /dev/null
+++ b/Source/JNA/waffle-tomcat11/src/test/java/waffle/apache/WaffleAuthenticatorBaseTest.java
@@ -0,0 +1,111 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2010-2024 The Waffle Project Contributors: https://github.com/Waffle/waffle/graphs/contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package waffle.apache;
+
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+
+import org.apache.catalina.connector.Request;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Waffle Authenticator Base Test.
+ */
+class WaffleAuthenticatorBaseTest {
+
+ /** The waffle authenticator base. */
+ private WaffleAuthenticatorBase waffleAuthenticatorBase;
+
+ /**
+ * Inits the.
+ */
+ @BeforeEach
+ void init() {
+ this.waffleAuthenticatorBase = new WaffleAuthenticatorBase() {
+ {
+ this.log = LoggerFactory.getLogger(WaffleAuthenticatorBaseTest.class);
+ }
+
+ @Override
+ public boolean authenticate(final Request request, final HttpServletResponse response) throws IOException {
+ return false;
+ }
+
+ @Override
+ protected boolean doAuthenticate(final Request request, final HttpServletResponse response)
+ throws IOException {
+ return false;
+ }
+ };
+ }
+
+ /**
+ * Should_accept_both_protocols.
+ */
+ @Test
+ void should_accept_both_protocols() {
+ this.waffleAuthenticatorBase.setProtocols(" NTLM , , Negotiate ");
+
+ Assertions.assertEquals(2, this.waffleAuthenticatorBase.protocols.size(), "Two protocols added");
+ Assertions.assertTrue(this.waffleAuthenticatorBase.protocols.contains("NTLM"), "NTLM has been added");
+ Assertions.assertTrue(this.waffleAuthenticatorBase.protocols.contains("Negotiate"), "Negotiate has been added");
+ }
+
+ /**
+ * Should_accept_ negotiate_protocol.
+ */
+ @Test
+ void should_accept_Negotiate_protocol() {
+ this.waffleAuthenticatorBase.setProtocols(" Negotiate ");
+
+ Assertions.assertEquals(1, this.waffleAuthenticatorBase.protocols.size(), "One protocol added");
+ Assertions.assertEquals("Negotiate", this.waffleAuthenticatorBase.protocols.iterator().next());
+ }
+
+ /**
+ * Should_accept_ ntl m_protocol.
+ */
+ @Test
+ void should_accept_NTLM_protocol() {
+ this.waffleAuthenticatorBase.setProtocols(" NTLM ");
+
+ Assertions.assertEquals(1, this.waffleAuthenticatorBase.protocols.size(), "One protocol added");
+ Assertions.assertEquals("NTLM", this.waffleAuthenticatorBase.protocols.iterator().next());
+ }
+
+ /**
+ * Should_refuse_other_protocol.
+ */
+ @Test
+ void should_refuse_other_protocol() {
+ Assertions.assertThrows(RuntimeException.class, () -> {
+ this.waffleAuthenticatorBase.setProtocols(" NTLM , OTHER, Negotiate ");
+ });
+ }
+
+}
diff --git a/Source/JNA/waffle-tomcat11/src/test/java/waffle/apache/WindowsAccountTest.java b/Source/JNA/waffle-tomcat11/src/test/java/waffle/apache/WindowsAccountTest.java
new file mode 100644
index 0000000000..fb696f12d2
--- /dev/null
+++ b/Source/JNA/waffle-tomcat11/src/test/java/waffle/apache/WindowsAccountTest.java
@@ -0,0 +1,110 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2010-2024 The Waffle Project Contributors: https://github.com/Waffle/waffle/graphs/contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package waffle.apache;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import waffle.mock.MockWindowsAccount;
+import waffle.windows.auth.WindowsAccount;
+
+/**
+ * Windows Account Test.
+ */
+class WindowsAccountTest {
+
+ /** The mock windows account. */
+ private final MockWindowsAccount mockWindowsAccount = new MockWindowsAccount("localhost\\Administrator");
+
+ /** The windows account. */
+ private WindowsAccount windowsAccount;
+
+ /**
+ * Sets the up.
+ */
+ @BeforeEach
+ void setUp() {
+ this.windowsAccount = new WindowsAccount(this.mockWindowsAccount);
+ }
+
+ /**
+ * Test equals.
+ */
+ @Test
+ void testEquals() {
+ Assertions.assertEquals(this.windowsAccount, new WindowsAccount(this.mockWindowsAccount));
+ final MockWindowsAccount mockWindowsAccount2 = new MockWindowsAccount("localhost\\Administrator2");
+ Assertions.assertNotEquals(this.windowsAccount, new WindowsAccount(mockWindowsAccount2));
+ }
+
+ /**
+ * Test is serializable.
+ *
+ * @throws IOException
+ * Signals that an I/O exception has occurred.
+ * @throws ClassNotFoundException
+ * the class not found exception
+ */
+ @Test
+ void testIsSerializable() throws IOException, ClassNotFoundException {
+ // serialize
+ final ByteArrayOutputStream out = new ByteArrayOutputStream();
+ try (ObjectOutputStream oos = new ObjectOutputStream(out)) {
+ oos.writeObject(this.windowsAccount);
+ }
+ assertThat(out.toByteArray()).isNotEmpty();
+ // deserialize
+ final InputStream in = new ByteArrayInputStream(out.toByteArray());
+ final ObjectInputStream ois = new ObjectInputStream(in);
+ final WindowsAccount copy = (WindowsAccount) ois.readObject();
+ // test
+ Assertions.assertEquals(this.windowsAccount, copy);
+ Assertions.assertEquals(this.windowsAccount.getDomain(), copy.getDomain());
+ Assertions.assertEquals(this.windowsAccount.getFqn(), copy.getFqn());
+ Assertions.assertEquals(this.windowsAccount.getName(), copy.getName());
+ Assertions.assertEquals(this.windowsAccount.getSidString(), copy.getSidString());
+ }
+
+ /**
+ * Test properties.
+ */
+ @Test
+ void testProperties() {
+ Assertions.assertEquals("localhost", this.windowsAccount.getDomain());
+ Assertions.assertEquals("localhost\\Administrator", this.windowsAccount.getFqn());
+ Assertions.assertEquals("Administrator", this.windowsAccount.getName());
+ Assertions.assertTrue(this.windowsAccount.getSidString().startsWith("S-"));
+ }
+
+}
diff --git a/Source/JNA/waffle-tomcat11/src/test/java/waffle/apache/WindowsRealmTest.java b/Source/JNA/waffle-tomcat11/src/test/java/waffle/apache/WindowsRealmTest.java
new file mode 100644
index 0000000000..2e2973c65c
--- /dev/null
+++ b/Source/JNA/waffle-tomcat11/src/test/java/waffle/apache/WindowsRealmTest.java
@@ -0,0 +1,45 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2010-2024 The Waffle Project Contributors: https://github.com/Waffle/waffle/graphs/contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package waffle.apache;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Windows Realm Test.
+ */
+class WindowsRealmTest {
+
+ /**
+ * Test properties.
+ */
+ @Test
+ void testProperties() {
+ final WindowsRealm realm = new WindowsRealm();
+ Assertions.assertNull(realm.getPassword(null));
+ Assertions.assertNull(realm.getPrincipal(null));
+ Assertions.assertEquals("WindowsRealm", realm.getClass().getSimpleName());
+ }
+
+}
diff --git a/Source/JNA/waffle-tomcat11/src/test/java/waffle/apache/catalina/SimpleHttpRequest.java b/Source/JNA/waffle-tomcat11/src/test/java/waffle/apache/catalina/SimpleHttpRequest.java
new file mode 100644
index 0000000000..b24a731fed
--- /dev/null
+++ b/Source/JNA/waffle-tomcat11/src/test/java/waffle/apache/catalina/SimpleHttpRequest.java
@@ -0,0 +1,258 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2010-2024 The Waffle Project Contributors: https://github.com/Waffle/waffle/graphs/contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package waffle.apache.catalina;
+
+import jakarta.servlet.http.HttpSession;
+
+import java.security.Principal;
+import java.util.HashMap;
+import java.util.Map;
+
+import mockit.Mocked;
+
+import org.apache.catalina.connector.Request;
+
+/**
+ * Simple HTTP Request.
+ */
+public class SimpleHttpRequest extends Request {
+
+ /** The remote port s. */
+ private static int remotePortS;
+
+ /**
+ * Next remote port.
+ *
+ * @return the int
+ */
+ public synchronized static int nextRemotePort() {
+ return ++SimpleHttpRequest.remotePortS;
+ }
+
+ /**
+ * Reset remote port.
+ */
+ public synchronized static void resetRemotePort() {
+ SimpleHttpRequest.remotePortS = 0;
+ }
+
+ /** The request uri. */
+ private String requestURI;
+
+ /** The query string. */
+ private String queryString;
+
+ /** The remote user. */
+ private String remoteUser;
+
+ /** The method. */
+ private String method = "GET";
+
+ /** The headers. */
+ private final Map headers = new HashMap<>();
+
+ /** The parameters. */
+ private final Map parameters = new HashMap<>();
+
+ /** The content. */
+ private byte[] content;
+
+ /** The http session. */
+ @Mocked
+ private HttpSession httpSession;
+
+ /** The principal. */
+ private Principal principal;
+
+ /**
+ * Instantiates a new simple http request.
+ */
+ public SimpleHttpRequest() {
+ // Tomcat notes that null on connector here may be ok for testing
+ super(null, null);
+ this.remotePort = SimpleHttpRequest.nextRemotePort();
+ }
+
+ /**
+ * Adds the header.
+ *
+ * @param headerName
+ * the header name
+ * @param headerValue
+ * the header value
+ */
+ public void addHeader(final String headerName, final String headerValue) {
+ this.headers.put(headerName, headerValue);
+ }
+
+ /**
+ * Adds the parameter.
+ *
+ * @param parameterName
+ * the parameter name
+ * @param parameterValue
+ * the parameter value
+ */
+ public void addParameter(final String parameterName, final String parameterValue) {
+ this.parameters.put(parameterName, parameterValue);
+ }
+
+ @Override
+ public int getContentLength() {
+ return this.content == null ? -1 : this.content.length;
+ }
+
+ @Override
+ public String getHeader(final String headerName) {
+ return this.headers.get(headerName);
+ }
+
+ @Override
+ public String getMethod() {
+ return this.method;
+ }
+
+ @Override
+ public String getParameter(final String parameterName) {
+ return this.parameters.get(parameterName);
+ }
+
+ @Override
+ public String getQueryString() {
+ return this.queryString;
+ }
+
+ @Override
+ public String getRemoteAddr() {
+ return this.remoteAddr;
+ }
+
+ @Override
+ public String getRemoteHost() {
+ return this.remoteHost;
+ }
+
+ @Override
+ public int getRemotePort() {
+ return this.remotePort;
+ }
+
+ @Override
+ public String getRemoteUser() {
+ return this.remoteUser;
+ }
+
+ @Override
+ public String getRequestURI() {
+ return this.requestURI;
+ }
+
+ @Override
+ public HttpSession getSession() {
+ return this.httpSession;
+ }
+
+ @Override
+ public HttpSession getSession(final boolean create) {
+ return this.httpSession;
+ }
+
+ @Override
+ public Principal getUserPrincipal() {
+ return this.principal;
+ }
+
+ /**
+ * Sets the content length.
+ *
+ * @param length
+ * the new content length
+ */
+ public void setContentLength(final int length) {
+ this.content = new byte[length];
+ }
+
+ /**
+ * Sets the method.
+ *
+ * @param value
+ * the new method
+ */
+ public void setMethod(final String value) {
+ this.method = value;
+ }
+
+ /**
+ * Sets the query string.
+ *
+ * @param queryValue
+ * the new query string
+ */
+ public void setQueryString(final String queryValue) {
+ this.queryString = queryValue;
+ if (this.queryString != null) {
+ for (final String eachParameter : this.queryString.split("[&]", -1)) {
+ final String[] pair = eachParameter.split("=", -1);
+ final String value = pair.length == 2 ? pair[1] : "";
+ this.addParameter(pair[0], value);
+ }
+ }
+ }
+
+ @Override
+ public void setRemoteAddr(final String value) {
+ this.remoteAddr = value;
+ }
+
+ @Override
+ public void setRemoteHost(final String value) {
+ this.remoteHost = value;
+ }
+
+ /**
+ * Sets the remote user.
+ *
+ * @param value
+ * the new remote user
+ */
+ public void setRemoteUser(final String value) {
+ this.remoteUser = value;
+ }
+
+ /**
+ * Sets the request uri.
+ *
+ * @param value
+ * the new request uri
+ */
+ public void setRequestURI(final String value) {
+ this.requestURI = value;
+ }
+
+ @Override
+ public void setUserPrincipal(final Principal value) {
+ this.principal = value;
+ }
+
+}
diff --git a/Source/JNA/waffle-tomcat11/src/test/java/waffle/apache/catalina/SimpleHttpResponse.java b/Source/JNA/waffle-tomcat11/src/test/java/waffle/apache/catalina/SimpleHttpResponse.java
new file mode 100644
index 0000000000..cb117c037f
--- /dev/null
+++ b/Source/JNA/waffle-tomcat11/src/test/java/waffle/apache/catalina/SimpleHttpResponse.java
@@ -0,0 +1,145 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2010-2024 The Waffle Project Contributors: https://github.com/Waffle/waffle/graphs/contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package waffle.apache.catalina;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.catalina.connector.Response;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Simple HTTP Response.
+ */
+public class SimpleHttpResponse extends Response {
+
+ /**
+ * Instantiates a new simple http response.
+ *
+ * @param coyoteResponse
+ * the coyote response
+ */
+ public SimpleHttpResponse(org.apache.coyote.Response coyoteResponse) {
+ super(coyoteResponse);
+ }
+
+ /** The Constant LOGGER. */
+ private static final Logger LOGGER = LoggerFactory.getLogger(SimpleHttpResponse.class);
+
+ /** The status. */
+ private int status = 500;
+
+ /** The headers. */
+ private final Map> headers = new HashMap<>();
+
+ @Override
+ public void addHeader(final String headerName, final String headerValue) {
+ List current = this.headers.get(headerName);
+ if (current == null) {
+ current = new ArrayList<>();
+ }
+ current.add(headerValue);
+ this.headers.put(headerName, current);
+ }
+
+ @Override
+ public void flushBuffer() {
+ SimpleHttpResponse.LOGGER.info("{} {}", Integer.valueOf(this.status), this.getStatusString());
+ for (final String header : this.headers.keySet()) {
+ for (final String headerValue : this.headers.get(header)) {
+ SimpleHttpResponse.LOGGER.info("{}: {}", header, headerValue);
+ }
+ }
+ }
+
+ @Override
+ public String getHeader(final String headerName) {
+ final List headerValues = this.headers.get(headerName);
+ return headerValues == null ? null : String.join(", ", headerValues);
+ }
+
+ @Override
+ public Collection getHeaderNames() {
+ return this.headers.keySet();
+ }
+
+ /**
+ * Gets the header values.
+ *
+ * @param headerName
+ * the header name
+ *
+ * @return the header values
+ */
+ public String[] getHeaderValues(final String headerName) {
+ final List headerValues = this.headers.get(headerName);
+ return headerValues == null ? null : headerValues.toArray(new String[0]);
+ }
+
+ @Override
+ public int getStatus() {
+ return this.status;
+ }
+
+ /**
+ * Gets the status string.
+ *
+ * @return the status string
+ */
+ public String getStatusString() {
+ return this.status == 401 ? "Unauthorized" : "Unknown";
+ }
+
+ @Override
+ public void sendError(final int rc) {
+ this.status = rc;
+ }
+
+ @Override
+ public void sendError(final int rc, final String message) {
+ this.status = rc;
+ }
+
+ @Override
+ public void setHeader(final String headerName, final String headerValue) {
+ List current = this.headers.get(headerName);
+ if (current == null) {
+ current = new ArrayList<>();
+ } else {
+ current.clear();
+ }
+ current.add(headerValue);
+ this.headers.put(headerName, current);
+ }
+
+ @Override
+ public void setStatus(final int value) {
+ this.status = value;
+ }
+
+}