/*
 * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

/*
 * @test
 * @bug 8279842 8282293
 * @modules java.base/sun.security.util
 *          java.security.jgss/sun.security.jgss
 *          java.security.jgss/sun.security.jgss.krb5
 *          java.security.jgss/sun.security.jgss.krb5.internal
 *          java.security.jgss/sun.security.krb5.internal:+open
 *          java.security.jgss/sun.security.krb5:+open
 *          java.security.jgss/sun.security.krb5.internal.ccache
 *          java.security.jgss/sun.security.krb5.internal.crypto
 *          java.security.jgss/sun.security.krb5.internal.ktab
 *          jdk.security.auth
 *          jdk.security.jgss
 *          jdk.httpserver
 * @summary HTTPS Channel Binding support for Java GSS/Kerberos
 * @library /test/lib
 * @run main jdk.test.lib.FileInstaller TestHosts TestHosts
 * @run main/othervm -Djdk.net.hosts.file=TestHosts
 *          -Djdk.https.negotiate.cbt=always HttpsCB true true
 * @run main/othervm -Djdk.net.hosts.file=TestHosts
 *          -Djdk.https.negotiate.cbt=never HttpsCB false true
 * @run main/othervm -Djdk.net.hosts.file=TestHosts
 *          -Djdk.https.negotiate.cbt=invalid HttpsCB false true
 * @run main/othervm -Djdk.net.hosts.file=TestHosts
 *          HttpsCB false true
 * @run main/othervm -Djdk.net.hosts.file=TestHosts
 *          -Djdk.https.negotiate.cbt=domain:other.com HttpsCB false true
 * @run main/othervm -Djdk.net.hosts.file=TestHosts
 *          -Djdk.https.negotiate.cbt=domain:host.web.domain HttpsCB true true
 * @run main/othervm -Djdk.net.hosts.file=TestHosts
 *          -Djdk.https.negotiate.cbt=domain:HOST.WEB.DOMAIN HttpsCB true true
 * @run main/othervm -Djdk.net.hosts.file=TestHosts
 *          -Djdk.https.negotiate.cbt=domain:*.web.domain HttpsCB true true
 * @run main/othervm -Djdk.net.hosts.file=TestHosts
 *          -Djdk.https.negotiate.cbt=domain:*.WEB.Domain HttpsCB true true
 * @run main/othervm -Djdk.net.hosts.file=TestHosts
 *          -Djdk.https.negotiate.cbt=domain:*.Invalid,*.WEB.Domain HttpsCB true true
 */

import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import com.sun.net.httpserver.HttpPrincipal;
import com.sun.net.httpserver.HttpsConfigurator;
import com.sun.net.httpserver.HttpsExchange;
import com.sun.net.httpserver.HttpsServer;
import com.sun.security.auth.module.Krb5LoginModule;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.PasswordAuthentication;
import java.net.Proxy;
import java.net.Socket;
import java.net.URL;
import java.security.cert.X509Certificate;
import java.security.PrivilegedExceptionAction;
import java.util.HashMap;
import java.util.Map;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509ExtendedTrustManager;
import javax.security.auth.Subject;

import jdk.test.lib.Asserts;
import jdk.test.lib.net.SimpleSSLContext;
import org.ietf.jgss.GSSContext;
import org.ietf.jgss.GSSCredential;
import org.ietf.jgss.GSSManager;
import sun.security.jgss.GSSUtil;
import sun.security.jgss.krb5.internal.TlsChannelBindingImpl;
import sun.security.krb5.Config;
import sun.security.util.TlsChannelBinding;

import java.util.Base64;
import java.util.concurrent.Callable;

public class HttpsCB {

    final static String REALM_WEB = "WEB.DOMAIN";
    final static String KRB5_CONF = "web.conf";
    final static String KRB5_TAB = "web.ktab";

    final static String WEB_USER = "web";
    final static char[] WEB_PASS = "webby".toCharArray();
    final static String WEB_HOST = "host.web.domain";
    final static String CONTENT = "Hello, World!";

    static int webPort;
    static URL cbtURL;
    static URL normalURL;

    public static void main(String[] args)
            throws Exception {

        boolean expectCBT = Boolean.parseBoolean(args[0]);
        boolean expectNoCBT = Boolean.parseBoolean(args[1]);

        System.setProperty("sun.security.krb5.debug", "true");

        KDC kdcw = KDC.create(REALM_WEB);
        kdcw.addPrincipal(WEB_USER, WEB_PASS);
        kdcw.addPrincipalRandKey("krbtgt/" + REALM_WEB);
        kdcw.addPrincipalRandKey("HTTP/" + WEB_HOST);

        KDC.saveConfig(KRB5_CONF, kdcw,
                "default_keytab_name = " + KRB5_TAB,
                "[domain_realm]",
                "",
                ".web.domain="+REALM_WEB);

        System.setProperty("java.security.krb5.conf", KRB5_CONF);
        Config.refresh();
        KDC.writeMultiKtab(KRB5_TAB, kdcw);

        // Write a customized JAAS conf file, so that any kinit cache
        // will be ignored.
        System.setProperty("java.security.auth.login.config", OneKDC.JAAS_CONF);
        File f = new File(OneKDC.JAAS_CONF);
        FileOutputStream fos = new FileOutputStream(f);
        fos.write((
                "com.sun.security.jgss.krb5.initiate {\n" +
                "    com.sun.security.auth.module.Krb5LoginModule required;\n};\n"
                ).getBytes());
        fos.close();

        HttpServer h1 = httpd("Negotiate",
                "HTTP/" + WEB_HOST + "@" + REALM_WEB, KRB5_TAB);
        webPort = h1.getAddress().getPort();

        cbtURL = new URL("https://" + WEB_HOST +":" + webPort + "/cbt");
        normalURL = new URL("https://" + WEB_HOST +":" + webPort + "/normal");

        java.net.Authenticator.setDefault(new java.net.Authenticator() {
            public PasswordAuthentication getPasswordAuthentication () {
                return new PasswordAuthentication(
                        WEB_USER+"@"+REALM_WEB, WEB_PASS);
            }
        });

        // Client-side SSLContext needs to ignore hostname mismatch
        // and untrusted certificate.
        SSLContext sc = SSLContext.getInstance("SSL");
        sc.init(null, new TrustManager[] {
                new X509ExtendedTrustManager() {
                    public X509Certificate[] getAcceptedIssuers() {
                        return null;
                    }
                    public void checkClientTrusted(X509Certificate[] chain,
                            String authType, Socket socket) { }
                    public void checkServerTrusted(X509Certificate[] chain,
                            String authType, Socket socket) { }
                    public void checkClientTrusted(X509Certificate[] chain,
                            String authType, SSLEngine engine) { }
                    public void checkServerTrusted(X509Certificate[] chain,
                            String authType, SSLEngine engine) { }
                    public void checkClientTrusted(X509Certificate[] certs,
                            String authType) { }
                    public void checkServerTrusted(X509Certificate[] certs,
                            String authType) { }
                }
        }, null);

        Asserts.assertEQ(visit(sc, cbtURL), expectCBT);
        Asserts.assertEQ(visit(sc, normalURL), expectNoCBT);
    }

    static boolean visit(SSLContext sc, URL url) {
        try {
            HttpsURLConnection conn = (HttpsURLConnection)
                    url.openConnection(Proxy.NO_PROXY);
            conn.setSSLSocketFactory(sc.getSocketFactory());
            BufferedReader reader;
            reader = new BufferedReader(new InputStreamReader(
                    conn.getInputStream()));
            return reader.readLine().equals(CONTENT);
        } catch (IOException e) {
            e.printStackTrace(System.out);
            return false;
        }
    }

    static HttpServer httpd(String scheme, String principal, String ktab)
            throws Exception {
        MyHttpHandler h = new MyHttpHandler();
        HttpsServer server = HttpsServer.create(new InetSocketAddress(0), 0);
        server.setHttpsConfigurator(
                new HttpsConfigurator(new SimpleSSLContext().get()));
        server.createContext("/", h).setAuthenticator(
                new MyServerAuthenticator(scheme, principal, ktab));
        server.start();
        return server;
    }

    static class MyHttpHandler implements HttpHandler {
        public void handle(HttpExchange t) throws IOException {
            t.sendResponseHeaders(200, 0);
            t.getResponseBody().write(CONTENT.getBytes());
            t.close();
        }
    }

    static class MyServerAuthenticator
            extends com.sun.net.httpserver.Authenticator {
        Subject s = new Subject();
        GSSManager m;
        GSSCredential cred;
        String scheme = null;
        String reqHdr = "WWW-Authenticate";
        String respHdr = "Authorization";
        int err = HttpURLConnection.HTTP_UNAUTHORIZED;

        public MyServerAuthenticator(String scheme,
                String principal, String ktab) throws Exception {

            this.scheme = scheme;
            Krb5LoginModule krb5 = new Krb5LoginModule();
            Map<String, String> map = new HashMap<>();
            Map<String, Object> shared = new HashMap<>();

            map.put("storeKey", "true");
            map.put("isInitiator", "false");
            map.put("useKeyTab", "true");
            map.put("keyTab", ktab);
            map.put("principal", principal);
            krb5.initialize(s, null, shared, map);
            krb5.login();
            krb5.commit();
            m = GSSManager.getInstance();
            cred = Subject.doAs(s, new PrivilegedExceptionAction<GSSCredential>() {
                @Override
                public GSSCredential run() throws Exception {
                    System.err.println("Creating GSSCredential");
                    return m.createCredential(
                            null,
                            GSSCredential.INDEFINITE_LIFETIME,
                            MyServerAuthenticator.this.scheme
                                        .equalsIgnoreCase("Negotiate") ?
                                    GSSUtil.GSS_SPNEGO_MECH_OID :
                                    GSSUtil.GSS_KRB5_MECH_OID,
                            GSSCredential.ACCEPT_ONLY);
                }
            });
        }

        @Override
        public Result authenticate(HttpExchange exch) {
            // The GSContext is stored in an HttpContext attribute named
            // "GSSContext" and is created at the first request.
            GSSContext c = null;
            String auth = exch.getRequestHeaders().getFirst(respHdr);
            try {
                c = (GSSContext)exch.getHttpContext()
                        .getAttributes().get("GSSContext");
                if (auth == null) {                 // First request
                    Headers map = exch.getResponseHeaders();
                    map.set (reqHdr, scheme);        // Challenge!
                    c = Subject.doAs(s, new PrivilegedExceptionAction<GSSContext>() {
                        @Override
                        public GSSContext run() throws Exception {
                            return m.createContext(cred);
                        }
                    });
                    // CBT is required for cbtURL
                    if (exch instanceof HttpsExchange sexch
                            && exch.getRequestURI().toString().equals("/cbt")) {
                        TlsChannelBinding b = TlsChannelBinding.create(
                                (X509Certificate) sexch.getSSLSession()
                                        .getLocalCertificates()[0]);
                        c.setChannelBinding(
                                new TlsChannelBindingImpl(b.getData()));
                    }
                    exch.getHttpContext().getAttributes().put("GSSContext", c);
                    return new com.sun.net.httpserver.Authenticator.Retry(err);
                } else {                            // Later requests
                    byte[] token = Base64.getMimeDecoder()
                            .decode(auth.split(" ")[1]);
                    token = c.acceptSecContext(token, 0, token.length);
                    Headers map = exch.getResponseHeaders();
                    map.set (reqHdr, scheme + " " + Base64.getMimeEncoder()
                            .encodeToString(token).replaceAll("\\s", ""));
                    if (c.isEstablished()) {
                        return new com.sun.net.httpserver.Authenticator.Success(
                                new HttpPrincipal(c.getSrcName().toString(), ""));
                    } else {
                        return new com.sun.net.httpserver.Authenticator.Retry(err);
                    }
                }
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }
}
