All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.zeromq.ZAuth Maven / Gradle / Ivy

The newest version!
package org.zeromq;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.zeromq.ZMQ.PollItem;
import org.zeromq.ZMQ.Poller;
import org.zeromq.ZMQ.Socket;
import org.zeromq.ZThread.IAttachedRunnable;

/**
 * ZAuth takes over authentication for all incoming connections in its context.
 * Note that libzmq provides four levels of security: default NULL (which zauth
 * does not see), and authenticated NULL, PLAIN, and CURVE, which zauth can see.
 *
 * Based on zauth.c in
 * czmq
 *
 * @author cbusbey (at) connamara (dot) com
 */
public class ZAuth {

    private Socket pipe; //pipe to backend agent
    private boolean verbose;

    /**
     * A small class for working with ZAP requests and replies. 
     */
    public static class ZAPRequest {

        public Socket handler;              //socket we're talking to
        public String version;              //  Version number, must be "1.0"
        public String sequence;             //  Sequence number of request
        public String domain;               //  Server socket domain
        public String address;              //  Client IP address
        public String identity;             //  Server socket idenntity
        public String mechanism;            //  Security mechansim
        public String username;             //  PLAIN user name
        public String password;             //  PLAIN password, in clear text
        public String clientKey;            //  CURVE client public key in ASCII
        public String principal;            //  GSSAPI principal

        static ZAPRequest recvRequest(Socket handler) {
            if (ZMQ.getMajorVersion() == 4) {
                ZMsg request = ZMsg.recvMsg(handler);
                ZAPRequest self = new ZAPRequest();

                //  Store handler socket so we can send a reply easily
                self.handler = handler;

                //  Get all standard frames off the handler socket
                self.version = request.popString();
                self.sequence = request.popString();
                self.domain = request.popString();
                self.address = request.popString();
                self.identity = request.popString();
                self.mechanism = request.popString();

                //  If the version is wrong, we're linked with a bogus libzmq, so die
                assert (self.version.equals("1.0"));

                // Get mechanism-specific frames
                if (self.mechanism.equals("PLAIN")) {
                    self.username = request.popString();
                    self.password = request.popString();
                } else if (self.mechanism.equals("CURVE")) {
                    // TODO: Handle CURVE authentication
                } else if (self.mechanism.equals("GSSAPI")) {
                    self.principal = request.popString();
                }

                request.destroy();
                return self;
            } else {
                return null;
            }
        }

        /**
         * Send a zap reply to the handler socket
         *
         * @param request
         */
        static void reply(ZAPRequest request, String statusCode, String statusText) {
            if (request == null) {
                return;
            }

            ZMsg msg = new ZMsg();
            msg.add("1.0");
            msg.add(request.sequence);
            msg.add(statusCode);
            msg.add(statusText);
            msg.add("");
            msg.add("");
            msg.send(request.handler);
        }
    }

    /**
     * ZAuthAgent is the backend agent which we talk to over a pipe. This lets
     * the agent do work asynchronously in the background while our application
     * does other things. This is invisible to the caller, who sees a classic
     * API.
     */
    private static class ZAuthAgent implements IAttachedRunnable {

        private Socket pipe; //pipe back to application api
        private Socket handler; //ZAP handler socket
        private boolean verbose; //trace output to stdout
        private ConcurrentMap whitelist = new ConcurrentHashMap(); //whitelisted addresses
        private ConcurrentMap blacklist = new ConcurrentHashMap(); //blacklisted addresses
        private ConcurrentMap passwords = new ConcurrentHashMap(); // PLAIN passwords, if loaded
        private boolean terminated; //did api ask us to quit?
        private File passwords_file;
        private long passwords_modified;
        final private ZAuth auth; //our parent auth, used for authorization callbacks

        private ZAuthAgent(ZAuth auth) {
            this.auth = auth;
        }

        /**
         * handle a message from the front end api
         *
         * @return
         */
        private boolean controlMessage() {
            ZMsg msg = ZMsg.recvMsg(pipe);

            String command = msg.popString();
            if (command == null) {
                return false; //interrupted
            }
            if (command.equals("ALLOW")) {
                String address = msg.popString();
                whitelist.put(address, "OK");
            } else if (command.equals("DENY")) {
                String address = msg.popString();
                blacklist.put(address, "OK");
            } else if (command.equals("PLAIN")) {
                // For now we don't do anything with domains
                String domain = msg.popString();
                // Get password file and load into HashMap
                // If the file doesn't exist we'll get an empty map
                String filename = msg.popString();
                this.passwords_file = new File(filename);
                this.loadPasswords(true);

                ZMsg reply = new ZMsg();
                reply.add("OK");
                reply.send(pipe);
                reply.destroy();
            } else if (command.equals("GSSAPI")) {
                //for now, we don't do anything with domains
                String domain = msg.popString();
            } else if (command.equals("VERBOSE")) {
                String verboseStr = msg.popString();
                this.verbose = verboseStr.equals("true");
            } else if (command.equals("TERMINATE")) {
                this.terminated = true;
                ZMsg reply = new ZMsg();
                reply.add("OK");
                reply.send(pipe);
                reply.destroy();
            }

            msg.destroy();

            return true;
        }

        private boolean authenticate() {
            ZAPRequest request = ZAPRequest.recvRequest(handler);
            if (request == null) {
                return false;
            }

            //is the address explicitly whitelisted or blacklisted?
            boolean allowed = false;
            boolean denied = false;

            if (!whitelist.isEmpty()) {
                if (whitelist.containsKey(request.address)) {
                    allowed = true;
                    if (verbose) {
                        System.out.printf("I: PASSED (whitelist) address = %s\n", request.address);
                    }
                } else {
                    denied = true;
                    if (verbose) {
                        System.out.printf("I: DENIED (not in whitelist) address = %s\n", request.address);
                    }
                }
            } else if (!blacklist.isEmpty()) {
                if (blacklist.containsKey(request.address)) {
                    denied = true;
                    if (verbose) {
                        System.out.printf("I: DENIED (blacklist) address = %s\n", request.address);
                    }
                } else {
                    allowed = true;
                    if (verbose) {
                        System.out.printf("I: PASSED (not in blacklist) address = %s\n", request.address);
                    }
                }
            }

            //mechanism specific check
            if (!denied) {
                if (request.mechanism.equals("NULL") && !allowed) {
                    //  For NULL, we allow if the address wasn't blacklisted
                    if (verbose) {
                        System.out.printf("I: ALLOWED (NULL)\n");
                    }
                    allowed = true;
                } else if (request.mechanism.equals("PLAIN")) {
                    // For PLAIN, even a whitelisted address must authenticate
                    allowed = authenticatePlain(request);
                } else if (request.mechanism.equals("CURVE")) {
                    // For CURVE, even a whitelisted address must authenticate
                    // TODO: Handle CURVE authentication
                } else if (request.mechanism.equals("GSSAPI")) {
                    // At this point, the request is authenticated, send to 
                    //zauth callback for complete authorization
                    allowed = auth.authenticateGSS(request);
                } else {
                    System.out.printf("Skipping unknown mechanism%n");
                }
            }

            if (allowed) {
                ZAPRequest.reply(request, "200", "OK");
            } else {
                ZAPRequest.reply(request, "400", "NO ACCESS");
            }

            return true;
        }

        private boolean authenticatePlain(ZAPRequest request) {
            // Refresh the passwords map if the file changed
            this.loadPasswords(false);

            String password = this.passwords.get(request.username);
            if (password != null && password.equals(request.password)) {
                if (this.verbose) {
                    System.out.printf("ZAUTH I: ALLOWED (PLAIN) username=%s password=%s\n",
                                      request.username, request.password);
                }

                return true;
            } else {
                if (this.verbose) {
                    System.out.printf("ZAUTH I: DENIED (PLAIN) username=%s password=%s\n",
                                      request.username, request.password);
                }

                return false;
            }
        }

        @Override
        public void run(Object[] args, ZContext ctx, Socket pipe) {
            this.pipe = pipe;

            //create ZAP handler and get ready for requests
            handler = ctx.createSocket(ZMQ.REP);
            try {
                handler.bind("inproc://zeromq.zap.01");
            } catch (ZMQException e) {
                pipe.send("ERROR");
                return;
            }

            pipe.send("OK");

            PollItem[] pollItems = {new PollItem(pipe, Poller.POLLIN), new PollItem(handler, Poller.POLLIN)};
            while (!terminated && !Thread.currentThread().isInterrupted()) {
                int rc = ZMQ.poll(pollItems, -1);
                if (rc == -1) {
                    break; //interrupt

                }

                if (pollItems[0].isReadable()) {
                    if (!controlMessage()) {
                        break;
                    }
                }

                if (pollItems[1].isReadable()) {
                    if (!authenticate()) {
                        break;
                    }
                }
            }
        }

        private void loadPasswords(boolean initial) {
            if (!initial) {
                long lastModified = this.passwords_file.lastModified();
                long age = System.currentTimeMillis() - lastModified;
                if (lastModified > this.passwords_modified && age > 1000) {
                    // File has been modified and is stable, clear hashmap
                    this.passwords.clear();
                } else {
                    return;
                }
            }

            this.passwords_modified = this.passwords_file.lastModified();
            try {
                BufferedReader br = new BufferedReader(new FileReader(this.passwords_file));
                String line;
                while ((line = br.readLine()) != null) {
                    // Skip lines starting with "#" or that do not look like name=value data
                    int equals = line.indexOf('=');
                    if (line.charAt(0) == '#' || equals == -1 || equals == line.length() - 1) {
                        continue;
                    }

                    this.passwords.put(line.substring(0, equals), line.substring(equals + 1, line.length()));
                }
                br.close();
            } catch (Exception ex) {
                // Ignore the exception, just don't read the file
            }
        }
    }

    /**
     * Install authentication for the specified context. Note that until you add
     * policies, all incoming NULL connections are allowed (classic ZeroMQ
     * behaviour), and all PLAIN and CURVE connections are denied.
     */
    public ZAuth(ZContext ctx) {
        pipe = ZThread.fork(ctx, new ZAuthAgent(this));
        ZMsg msg = ZMsg.recvMsg(pipe);
        String response = msg.popString();

        msg.destroy();
    }

    /**
     * Enable verbose tracing of commands and activity
     *
     * @param verbose
     */
    public void setVerbose(boolean verbose) {
        this.verbose = verbose;

        //agent should also be verbose
        ZMsg msg = new ZMsg();
        msg.add("VERBOSE");
        msg.add(String.format("%b", verbose));
        msg.send(pipe);
        msg.destroy();
    }

    /**
     * Allow (whitelist) a single IP address. For NULL, all clients from this
     * address will be accepted. For PLAIN and CURVE, they will be allowed to
     * continue with authentication. You can call this method multiple times to
     * whitelist multiple IP addresses. If you whitelist a single address, any
     * non-whitelisted addresses are treated as blacklisted.
     *
     */
    public void allow(String address) {
        assert (address != null);

        ZMsg msg = new ZMsg();
        msg.add("ALLOW");
        msg.add(address);
        msg.send(pipe);
        msg.destroy();
    }

    /**
     * Deny (blacklist) a single IP address. For all security mechanisms, this
     * rejects the connection without any further authentication. Use either a
     * whitelist, or a blacklist, not not both. If you define both a whitelist
     * and a blacklist, only the whitelist takes effect.
     */
    public void deny(String address) {
        assert (address != null);

        ZMsg msg = new ZMsg();
        msg.add("DENY");
        msg.add(address);
        msg.send(pipe);
        msg.destroy();
    }

    /**
     * Configure PLAIN authentication for a given domain. PLAIN authentication
     * uses a plain-text password file. To cover all domains, use "*". You can
     * modify the password file at any time; it is reloaded automatically.
     *
     * @param domain
     * @param filename
     */
    public void configurePlain(String domain, String filename) {
        assert (domain != null);
        assert (filename != null);

        ZMsg msg = new ZMsg();
        msg.add("PLAIN");
        msg.add(domain);
        msg.add(filename);
        msg.send(pipe);
        msg.destroy();
    }

    /**
     * Destructor.
     */
    public void destroy() {
        ZMsg request = new ZMsg();
        request.add("TERMINATE");
        request.send(pipe);
        request.destroy();

        ZMsg reply = ZMsg.recvMsg(pipe);
        reply.destroy();
    }

    /*Configure GSSAPI authentication for a given domain. GSSAPI authentication
     uses an underlying mechanism (usually Kerberos) to establish a secure
     context and perform mutual authentication.  To cover all domains, use "*". */
    public void configureGSSAPI(String domain) {
        assert (domain != null);
        ZMsg msg = new ZMsg();
        msg.add("GSSAPI");
        msg.add(domain);
        msg.send(pipe);
        msg.destroy();
    }

    /*
     * Callback for authorizing an authenticated GSS connection.  Returns true 
     * if the connection is authorized, false otherwise.  Default implementation 
     * authorizes all authenticated connections.
     */
    protected boolean authenticateGSS(ZAPRequest request) {
        if (verbose) {
            System.out.printf("I: ALLOWED (GSSAPI allow any client) principal = %s identity = %s%n", request.principal, request.identity);
        }

        return true;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy