/*
 * Copyright (c) 2013, 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.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * 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.
 */


package sun.security.krb5.internal.rcache;

import java.io.*;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.PosixFilePermission;
import java.security.AccessController;
import java.util.*;

import sun.security.action.GetPropertyAction;
import sun.security.krb5.internal.KerberosTime;
import sun.security.krb5.internal.Krb5;
import sun.security.krb5.internal.KrbApErrException;
import sun.security.krb5.internal.ReplayCache;


/**
 * A dfl file is used to sustores AuthTime entries when the system property
 * sun.security.krb5.rcache is set to
 *
 *    dfl(|:path/|:path/name|:name)
 *
 * The file will be path/name. If path is not given, it will be
 *
 *    System.getProperty("java.io.tmpdir")
 *
 * If name is not given, it will be
 *
 *    service_euid
 *
 * Java does not have a method to get euid, so uid is used instead. This
 * should normally to be since a Java program is seldom used as a setuid app.
 *
 * The file has a header:
 *
 *    i16 0x0501 (KRB5_RC_VNO) in network order
 *    i32 number of seconds for lifespan (in native order, same below)
 *
 * followed by cache entries concatenated, which can be encoded in
 * 2 styles:
 *
 * The traditional style is:
 *
 *    LC of client principal
 *    LC of server principal
 *    i32 cusec of Authenticator
 *    i32 ctime of Authenticator
 *
 * The new style has a hash:
 *
 *    LC of ""
 *    LC of "HASH:%s %lu:%s %lu:%s" of (hash, clientlen, client, serverlen,
 *          server) where msghash is 32 char (lower case) text mode md5sum
 *          of the ciphertext of authenticator.
 *    i32 cusec of Authenticator
 *    i32 ctime of Authenticator
 *
 * where LC of a string means
 *
 *    i32 strlen(string) + 1
 *    octets of string, with the \0x00 ending
 *
 * The old style block is always created by MIT krb5 used even if a new style
 * is available, which means there can be 2 entries for a single Authenticator.
 * Java also does this way.
 *
 * See src/lib/krb5/rcache/rc_io.c and src/lib/krb5/rcache/rc_dfl.c.
 */
public class DflCache extends ReplayCache {

    private static final int KRB5_RV_VNO = 0x501;
    private static final int EXCESSREPS = 30;   // if missed-hit>this, recreate

    private final String source;

    private static int uid;
    static {
        try {
            // Available on Solaris, Linux and Mac. Otherwise, no _euid suffix
            Class<?> clazz = Class.forName("com.sun.security.auth.module.UnixSystem");
            uid = (int)(long)(Long)
                    clazz.getMethod("getUid").invoke(clazz.newInstance());
        } catch (Exception e) {
            uid = -1;
        }
    }

    public DflCache (String source) {
        this.source = source;
    }

    private static String defaultPath() {
        return AccessController.doPrivileged(
                new GetPropertyAction("java.io.tmpdir"));
    }

    private static String defaultFile(String server) {
        // service/host@REALM -> service
        int slash = server.indexOf('/');
        if (slash == -1) {
            // A normal principal? say, dummy@REALM
            slash = server.indexOf('@');
        }
        if (slash != -1) {
            // Should not happen, but be careful
            server= server.substring(0, slash);
        }
        if (uid != -1) {
            server += "_" + uid;
        }
        return server;
    }

    private static Path getFileName(String source, String server) {
        String path, file;
        if (source.equals("dfl")) {
            path = defaultPath();
            file = defaultFile(server);
        } else if (source.startsWith("dfl:")) {
            source = source.substring(4);
            int pos = source.lastIndexOf('/');
            int pos1 = source.lastIndexOf('\\');
            if (pos1 > pos) pos = pos1;
            if (pos == -1) {
                // Only file name
                path = defaultPath();
                file = source;
            } else if (new File(source).isDirectory()) {
                // Only path
                path = source;
                file = defaultFile(server);
            } else {
                // Full pathname
                path = null;
                file = source;
            }
        } else {
            throw new IllegalArgumentException();
        }
        return new File(path, file).toPath();
    }

    @Override
    public void checkAndStore(KerberosTime currTime, AuthTimeWithHash time)
            throws KrbApErrException {
        try {
            checkAndStore0(currTime, time);
        } catch (IOException ioe) {
            KrbApErrException ke = new KrbApErrException(Krb5.KRB_ERR_GENERIC);
            ke.initCause(ioe);
            throw ke;
        }
    }

    private synchronized void checkAndStore0(KerberosTime currTime, AuthTimeWithHash time)
            throws IOException, KrbApErrException {
        Path p = getFileName(source, time.server);
        int missed = 0;
        try (Storage s = new Storage()) {
            try {
                missed = s.loadAndCheck(p, time, currTime);
            } catch (IOException ioe) {
                // Non-existing or invalid file
                Storage.create(p);
                missed = s.loadAndCheck(p, time, currTime);
            }
            s.append(time);
        }
        if (missed > EXCESSREPS) {
            Storage.expunge(p, currTime);
        }
    }


    private static class Storage implements Closeable {
        // Static methods
        @SuppressWarnings("try")
        private static void create(Path p) throws IOException {
            try (SeekableByteChannel newChan = createNoClose(p)) {
                // Do nothing, wait for close
            }
            makeMine(p);
        }

        private static void makeMine(Path p) throws IOException {
            // chmod to owner-rw only, otherwise MIT krb5 rejects
            try {
                Set<PosixFilePermission> attrs = new HashSet<>();
                attrs.add(PosixFilePermission.OWNER_READ);
                attrs.add(PosixFilePermission.OWNER_WRITE);
                Files.setPosixFilePermissions(p, attrs);
            } catch (UnsupportedOperationException uoe) {
                // No POSIX permission. That's OK.
            }
        }

        private static SeekableByteChannel createNoClose(Path p)
                throws IOException {
            SeekableByteChannel newChan = Files.newByteChannel(
                    p, StandardOpenOption.CREATE,
                        StandardOpenOption.TRUNCATE_EXISTING,
                        StandardOpenOption.WRITE);
            ByteBuffer buffer = ByteBuffer.allocate(6);
            buffer.putShort((short)KRB5_RV_VNO);
            buffer.order(ByteOrder.nativeOrder());
            buffer.putInt(KerberosTime.getDefaultSkew());
            buffer.flip();
            newChan.write(buffer);
            return newChan;
        }

        private static void expunge(Path p, KerberosTime currTime)
                throws IOException {
            Path p2 = Files.createTempFile(p.getParent(), "rcache", null);
            try (SeekableByteChannel oldChan = Files.newByteChannel(p);
                    SeekableByteChannel newChan = createNoClose(p2)) {
                long timeLimit = currTime.getSeconds() - readHeader(oldChan);
                while (true) {
                    try {
                        AuthTime at = AuthTime.readFrom(oldChan);
                        if (at.ctime > timeLimit) {
                            ByteBuffer bb = ByteBuffer.wrap(at.encode(true));
                            newChan.write(bb);
                        }
                    } catch (BufferUnderflowException e) {
                        break;
                    }
                }
            }
            makeMine(p2);
            Files.move(p2, p,
                    StandardCopyOption.REPLACE_EXISTING,
                    StandardCopyOption.ATOMIC_MOVE);
        }

        // Instance methods
        SeekableByteChannel chan;
        private int loadAndCheck(Path p, AuthTimeWithHash time,
                KerberosTime currTime)
                throws IOException, KrbApErrException {
            int missed = 0;
            if (Files.isSymbolicLink(p)) {
                throw new IOException("Symlink not accepted");
            }
            try {
                Set<PosixFilePermission> perms =
                        Files.getPosixFilePermissions(p);
                if (uid != -1 &&
                        (Integer)Files.getAttribute(p, "unix:uid") != uid) {
                    throw new IOException("Not mine");
                }
                if (perms.contains(PosixFilePermission.GROUP_READ) ||
                        perms.contains(PosixFilePermission.GROUP_WRITE) ||
                        perms.contains(PosixFilePermission.GROUP_EXECUTE) ||
                        perms.contains(PosixFilePermission.OTHERS_READ) ||
                        perms.contains(PosixFilePermission.OTHERS_WRITE) ||
                        perms.contains(PosixFilePermission.OTHERS_EXECUTE)) {
                    throw new IOException("Accessible by someone else");
                }
            } catch (UnsupportedOperationException uoe) {
                // No POSIX permissions? Ignore it.
            }
            chan = Files.newByteChannel(p, StandardOpenOption.WRITE,
                    StandardOpenOption.READ);

            long timeLimit = currTime.getSeconds() - readHeader(chan);

            long pos = 0;
            boolean seeNewButNotSame = false;
            while (true) {
                try {
                    pos = chan.position();
                    AuthTime a = AuthTime.readFrom(chan);
                    if (a instanceof AuthTimeWithHash) {
                        if (time.equals(a)) {
                            // Exact match, must be a replay
                            throw new KrbApErrException(Krb5.KRB_AP_ERR_REPEAT);
                        } else if (time.isSameIgnoresHash(a)) {
                            // Two different authenticators in the same second.
                            // Remember it
                            seeNewButNotSame = true;
                        }
                    } else {
                        if (time.isSameIgnoresHash(a)) {
                            // Two authenticators in the same second. Considered
                            // same if we haven't seen a new style version of it
                            if (!seeNewButNotSame) {
                                throw new KrbApErrException(Krb5.KRB_AP_ERR_REPEAT);
                            }
                        }
                    }
                    if (a.ctime < timeLimit) {
                        missed++;
                    } else {
                        missed--;
                    }
                } catch (BufferUnderflowException e) {
                    // Half-written file?
                    chan.position(pos);
                    break;
                }
            }
            return missed;
        }

        private static int readHeader(SeekableByteChannel chan)
                throws IOException {
            ByteBuffer bb = ByteBuffer.allocate(6);
            chan.read(bb);
            if (bb.getShort(0) != KRB5_RV_VNO) {
                throw new IOException("Not correct rcache version");
            }
            bb.order(ByteOrder.nativeOrder());
            return bb.getInt(2);
        }

        private void append(AuthTimeWithHash at) throws IOException {
            // Write an entry with hash, to be followed by one without it,
            // for the benefit of old implementations.
            ByteBuffer bb;
            bb = ByteBuffer.wrap(at.encode(true));
            chan.write(bb);
            bb = ByteBuffer.wrap(at.encode(false));
            chan.write(bb);
        }

        @Override
        public void close() throws IOException {
            if (chan != null) chan.close();
            chan = null;
        }
    }
}
