org.zeromq.ZAuth Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jeromq Show documentation
Show all versions of jeromq Show documentation
Pure Java implementation of libzmq
package org.zeromq;
import java.io.BufferedReader;
import java.io.Closeable;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.UUID;
import org.zeromq.ZMQ.Socket;
import org.zeromq.ZMQ.Socket.Mechanism;
import org.zeromq.util.ZMetadata;
import zmq.util.Objects;
/**
* A ZAuth actor takes over authentication for all incoming connections in
* its context. You can whitelist or blacklist peers based on IP address,
* and define policies for securing PLAIN, CURVE, and GSSAPI connections.
*
* 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
*/
public class ZAuth implements Closeable
{
public interface Auth
{
/**
* Configures with ad-hoc message.
* @param msg the configuration message.
* @param verbose true if the actor is verbose.
* @return true if correctly configured, otherwise false.
*/
boolean configure(ZMsg msg, boolean verbose);
/**
* Callback for authorizing a connection.
* @param request
* @param verbose
* @return true if the connection is authorized, false otherwise.
*/
boolean authorize(ZapRequest request, boolean verbose);
}
public static class SimplePlainAuth implements Auth
{
private final Properties passwords = new Properties(); // PLAIN passwords, if loaded
private File passwordsFile;
private long passwordsModified;
@Override
public boolean configure(ZMsg msg, boolean verbose)
{
assert (msg.size() == 2);
// For now we don't do anything with domains
@SuppressWarnings("unused")
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();
passwordsFile = new File(filename);
if (verbose) {
System.out.printf(
"ZAuth: activated plain-mechanism with password-file: %s%n",
passwordsFile.getAbsolutePath());
}
try {
loadPasswords(true);
}
catch (IOException e) {
// Ignore the exception, just don't read the file
}
return true;
}
@Override
public boolean authorize(ZapRequest request, boolean verbose)
{
// assert (request.username != null);
// Refresh the passwords map if the file changed
try {
loadPasswords(false);
}
catch (IOException e) {
// Ignore the exception, just don't read the file
}
String password = passwords.getProperty(request.username);
if (password != null && password.equals(request.password)) {
if (verbose) {
System.out.printf("ZAuth: Allowed (PLAIN) username=%s\n", request.username);
}
request.userId = request.username;
return true;
}
else {
if (verbose) {
System.out.printf("ZAuth: Denied (PLAIN) username=%s\n", request.username);
}
return false;
}
}
private void loadPasswords(boolean initial) throws IOException
{
if (!initial) {
final long lastModified = passwordsFile.lastModified();
final long age = System.currentTimeMillis() - lastModified;
if (lastModified > passwordsModified && age > 1000) {
// File has been modified and is stable, clear map
passwords.clear();
}
else {
return;
}
}
passwordsModified = passwordsFile.lastModified();
Reader br = new BufferedReader(new FileReader(passwordsFile));
try {
passwords.load(br);
}
catch (IOException | IllegalArgumentException ex) {
// Ignore the exception, just don't read the file
}
finally {
br.close();
}
}
}
public static class SimpleCurveAuth implements Auth
{
private final ZCertStore.Fingerprinter fingerprinter;
private ZCertStore certStore = null;
private boolean allowAny;
public SimpleCurveAuth()
{
this(new ZCertStore.Hasher());
}
public SimpleCurveAuth(ZCertStore.Fingerprinter fingerprinter)
{
this.fingerprinter = fingerprinter;
}
@Override
public boolean configure(ZMsg configuration, boolean verbose)
{
// If location is CURVE_ALLOW_ANY, allow all clients. Otherwise
// treat location as a directory that holds the certificates.
final String location = configuration.popString();
allowAny = location.equals(CURVE_ALLOW_ANY);
if (allowAny) {
if (verbose) {
System.out.println("ZAuth: Allowing all clients");
}
}
else {
if (verbose) {
System.out.printf("ZAuth: Using %s as certificates directory%n", location);
}
certStore = new ZCertStore(location, fingerprinter);
}
return true;
}
@Override
public boolean authorize(ZapRequest request, boolean verbose)
{
if (allowAny) {
if (verbose) {
System.out.println("ZAuth: allowed (CURVE allow any client)");
}
return true;
}
else {
if (certStore != null) {
if (certStore.containsPublicKey(request.clientKey)) {
// login allowed
if (verbose) {
System.out.printf("ZAuth: Allowed (CURVE) client_key=%s\n", request.clientKey);
}
request.userId = request.clientKey;
request.metadata = certStore.getMetadata(request.clientKey);
return true;
}
else {
// login not allowed. couldn't find certificate
if (verbose) {
System.out.printf("ZAuth: Denied (CURVE) client_key=%s\n", request.clientKey);
}
return false;
}
}
}
return false;
}
}
public static class SimpleNullAuth implements Auth
{
@Override
public boolean configure(ZMsg configuration, boolean verbose)
{
return true;
}
@Override
public boolean authorize(ZapRequest request, boolean verbose)
{
return true;
}
}
private static final String ZAP_VERSION = "1.0";
public static class ZapReply
{
public final String version; // Version number, must be "1.0"
public final String sequence; // Sequence number of request
public final int statusCode; // numeric status code
public final String statusText; // readable status
public final String userId; // User-Id
public final ZMetadata metadata; // optional metadata
public final String address; // not part of the ZAP protocol, but handy information for user
public final String identity; // not part of the ZAP protocol, but handy information for user
private ZapReply(String version, String sequence, int statusCode, String statusText, String userId,
ZMetadata metadata)
{
this(version, sequence, statusCode, statusText, userId, metadata, null, null);
}
private ZapReply(String version, String sequence, int statusCode, String statusText, String userId,
ZMetadata metadata, String address, String identity)
{
assert (ZAP_VERSION.equals(version));
this.version = version;
this.sequence = sequence;
this.statusCode = statusCode;
this.statusText = statusText;
this.userId = userId;
this.metadata = metadata;
this.address = address;
this.identity = identity;
}
private ZMsg msg()
{
ZMsg msg = new ZMsg();
msg.add(version);
msg.add(sequence);
msg.add(Integer.toString(statusCode));
msg.add(statusText);
msg.add(userId == null ? "" : userId);
msg.add(metadata == null ? new byte[0] : metadata.bytes());
return msg;
}
@Override
public String toString()
{
return "ZapReply [" + (version != null ? "version=" + version + ", " : "")
+ (sequence != null ? "sequence=" + sequence + ", " : "") + "statusCode=" + statusCode + ", "
+ (statusText != null ? "statusText=" + statusText + ", " : "")
+ (userId != null ? "userId=" + userId + ", " : "")
+ (metadata != null ? "metadata=" + metadata : "") + "]";
}
private static ZapReply recv(ZAgent agent, boolean wait)
{
return received(agent.recv(wait));
}
private static ZapReply recv(ZAgent agent, int timeout)
{
return received(agent.recv(timeout));
}
private static ZapReply received(ZMsg msg)
{
if (msg == null) {
return null;
}
assert (msg.size() == 8);
String version = msg.popString();
String sequence = msg.popString();
int statusCode = Integer.parseInt(msg.popString());
String statusText = msg.popString();
String userId = msg.popString();
ZMetadata metadata = ZMetadata.read(msg.popString());
String address = msg.popString();
String identity = msg.popString();
return new ZapReply(version, sequence, statusCode, statusText, userId, metadata, address, identity);
}
}
/**
* A small class for working with ZAP requests and replies.
*/
public static class ZapRequest
{
private final Socket handler; // socket we're talking to
public final String version; // Version number, must be "1.0"
public final String sequence; // Sequence number of request
public final String domain; // Server socket domain
public final String address; // Client IP address
public final String identity; // Server socket identity
public final String mechanism; // Security mechanism
public final String username; // PLAIN user name
public final String password; // PLAIN password, in clear text
public final String clientKey; // CURVE client public key in ASCII
public final String principal; // GSSAPI principal
public String userId; // User-Id to return in the ZAP Response
public ZMetadata metadata; // metadata to eventually return
private ZapRequest(Socket handler, ZMsg request)
{
// Store handler socket so we can send a reply easily
this.handler = handler;
// Get all standard frames off the handler socket
version = request.popString();
sequence = request.popString();
domain = request.popString();
address = request.popString();
identity = request.popString();
mechanism = request.popString();
// If the version is wrong, we're linked with a bogus libzmq, so die
assert (ZAP_VERSION.equals(version));
// Get mechanism-specific frames
if (Mechanism.PLAIN.name().equals(mechanism)) {
username = request.popString();
password = request.popString();
clientKey = null;
principal = null;
}
else if (Mechanism.CURVE.name().equals(mechanism)) {
ZFrame frame = request.pop();
byte[] clientPublicKey = frame.getData();
username = null;
password = null;
clientKey = ZMQ.Curve.z85Encode(clientPublicKey);
principal = null;
}
else if (zmq.io.mechanism.Mechanisms.GSSAPI.name().equals(mechanism)) {
// TOD handle GSSAPI as well
username = null;
password = null;
clientKey = null;
principal = request.popString();
}
else {
username = null;
password = null;
clientKey = null;
principal = null;
}
}
private static ZapRequest recvRequest(Socket handler, boolean wait)
{
ZMsg request = ZMsg.recvMsg(handler, wait);
if (request == null) {
return null;
}
ZapRequest self = new ZapRequest(handler, request);
// If the version is wrong, we're linked with a bogus libzmq, so die
assert (ZAP_VERSION.equals(self.version));
request.destroy();
return self;
}
/**
* Send a zap reply to the handler socket
*/
private void reply(int statusCode, String statusText, Socket replies)
{
ZapReply reply = new ZapReply(ZAP_VERSION, sequence, statusCode, statusText, userId, metadata);
ZMsg msg = reply.msg();
boolean destroy = replies == null;
msg.send(handler, destroy);
if (replies != null) {
// let's add other fields for convenience of listener
msg.add(address);
msg.add(identity);
msg.send(replies);
}
}
}
public static final String CURVE_ALLOW_ANY = "*";
private static final String VERBOSE = "VERBOSE";
private static final String REPLIES = "REPLIES";
private static final String ALLOW = "ALLOW";
private static final String DENY = "DENY";
private static final String TERMINATE = "TERMINATE";
private final ZAgent agent;
private final ZStar.Exit exit;
private final ZAgent replies;
private boolean repliesEnabled; // are replies enabled?
/**
* Install authentication for the specified context. Note that until you add
* policies, all incoming NULL connections are allowed (classic ZeroMQ
* behavior), and all PLAIN and CURVE connections are denied.
* @param ctx
*/
public ZAuth(ZContext ctx)
{
this(ctx, "ZAuth");
}
public ZAuth(ZContext ctx, ZCertStore.Fingerprinter fingerprinter)
{
this(ctx, "ZAuth", curveVariant(fingerprinter));
}
public ZAuth(ZContext ctx, String actorName)
{
this(ctx, actorName, makeSimpleAuths());
}
private static Map makeSimpleAuths()
{
Map auths = new HashMap<>();
auths.put(Mechanism.PLAIN.name(), new SimplePlainAuth());
auths.put(Mechanism.CURVE.name(), new SimpleCurveAuth());
auths.put(Mechanism.NULL.name(), new SimpleNullAuth());
// TODO add GSSAPI once it is implemented
return auths;
}
private static Map curveVariant(ZCertStore.Fingerprinter fingerprinter)
{
Map auths = makeSimpleAuths();
auths.put(Mechanism.CURVE.name(), new SimpleCurveAuth(fingerprinter));
return auths;
}
public ZAuth(final ZContext ctx, String actorName, Map auths)
{
Objects.requireNonNull(ctx, "ZAuth works only with a provided ZContext");
Objects.requireNonNull(actorName, "Actor name shall be defined");
Objects.requireNonNull(auths, "Authenticators shall be supplied as non-null map");
final AuthActor actor = new AuthActor(actorName, auths);
final ZActor zactor = new ZActor(ctx, actor, UUID.randomUUID().toString());
agent = zactor.agent();
exit = zactor.exit();
// wait for the start of the actor
agent.recv().destroy();
replies = actor.createAgent(ctx);
}
/**
* Enable verbose tracing of commands and activity
* @param verbose
*/
public ZAuth setVerbose(boolean verbose)
{
return verbose(verbose);
}
public ZAuth verbose(boolean verbose)
{
return send(VERBOSE, String.format("%b", verbose));
}
/**
* 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.
* @param address
*/
public ZAuth allow(String address)
{
Objects.requireNonNull(address, "Address has to be supplied for allowance");
return send(ALLOW, address);
}
/**
* 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.
* @param address
*/
public ZAuth deny(String address)
{
Objects.requireNonNull(address, "Address has to be supplied for denial");
return send(DENY, address);
}
/**
* 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 ZAuth configurePlain(String domain, String filename)
{
Objects.requireNonNull(domain, "Domain has to be supplied");
Objects.requireNonNull(filename, "File name has to be supplied");
return send(Mechanism.PLAIN.name(), domain, filename);
}
/**
* Configure CURVE authentication
*
* @param location Can be ZAuth.CURVE_ALLOW_ANY or a directory with public-keys that will be accepted
*/
public ZAuth configureCurve(String location)
{
Objects.requireNonNull(location, "Location has to be supplied");
return send(Mechanism.CURVE.name(), location);
}
public ZAuth replies(boolean enable)
{
repliesEnabled = enable;
return send(REPLIES, String.format("%b", enable));
}
/**
* Retrieves the next ZAP reply.
* @return the next reply or null if the actor is closed.
*/
public ZapReply nextReply()
{
return nextReply(true);
}
/**
* Retrieves the next ZAP reply.
* @param wait true to wait for the next reply, false to immediately return if there is no next reply.
* @return the next reply or null if the actor is closed or if there is no next reply yet.
*/
public ZapReply nextReply(boolean wait)
{
if (!repliesEnabled) {
System.out.println("ZAuth: replies are disabled. Please use replies(true);");
return null;
}
return ZapReply.recv(replies, wait);
}
/**
* Retrieves the next ZAP reply.
* @param timeout the timeout in milliseconds to wait for a reply before giving up and returning null.
* @return the next reply or null if the actor is closed or if there is no next reply after the elapsed timeout.
*/
public ZapReply nextReply(int timeout)
{
if (!repliesEnabled) {
System.out.println("ZAuth: replies are disabled. Please use replies(true);");
return null;
}
return ZapReply.recv(replies, timeout);
}
/**
* Destructor.
*/
@Override
public void close() throws IOException
{
destroy();
}
/**
* Destructor.
*/
public void destroy()
{
send(TERMINATE);
exit.awaitSilent();
agent.close();
replies.close();
}
protected ZAuth send(String command, String... datas)
{
ZMsg msg = new ZMsg();
msg.add(command);
for (String data : datas) {
msg.add(data);
}
agent.send(msg);
msg.destroy();
agent.recv();
return this;
}
/**
* AuthActor is the backend actor which we talk to over a pipe. This lets
* the actor 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 AuthActor extends ZActor.SimpleActor
{
private static final String OK = "OK";
private final String actorName;
private final Properties whitelist = new Properties(); // whitelisted addresses
private final Properties blacklist = new Properties(); // blacklisted addresses
private final Map auths = new HashMap<>();
private final String repliesAddress; // address of replies pipe AND safeguard lock for connected agent
private boolean repliesEnabled; // are replies enabled?
private Socket replies; // replies pipe
private boolean verbose; // trace behavior
private AuthActor(String actorName, Map auths)
{
assert (auths != null);
assert (actorName != null);
this.actorName = actorName;
this.auths.putAll(auths);
this.repliesAddress = "inproc://zauth-replies-" + UUID.randomUUID().toString();
}
private ZAgent createAgent(ZContext ctx)
{
Socket pipe = ctx.createSocket(SocketType.PAIR);
boolean rc = pipe.connect(repliesAddress);
assert (rc);
return new ZAgent.SimpleAgent(pipe, repliesAddress);
}
@Override
public String premiere(Socket pipe)
{
return actorName;
}
@Override
public List createSockets(ZContext ctx, Object... args)
{
//create replies pipe that will forward replies to user
replies = ctx.createSocket(SocketType.PAIR);
assert (replies != null);
//create ZAP handler and get ready for requests
Socket handler = ctx.createSocket(SocketType.REP);
assert (handler != null);
return Arrays.asList(handler, replies);
}
@Override
public void start(Socket pipe, List sockets, ZPoller poller)
{
boolean rc;
try {
rc = replies.bind(repliesAddress);
assert (rc);
Socket handler = sockets.get(0);
rc = handler.bind("inproc://zeromq.zap.01");
assert (rc);
rc = poller.register(handler, ZPoller.POLLIN);
assert (rc);
rc = pipe.send(OK);
assert (rc);
}
catch (ZMQException e) {
System.out.println("ZAuth: Error");
e.printStackTrace();
rc = pipe.send("ERROR");
assert (rc);
}
}
@Override
public boolean backstage(Socket pipe, ZPoller poller, int events)
{
ZMsg msg = ZMsg.recvMsg(pipe);
String command = msg.popString();
if (command == null) {
System.out.printf("ZAuth: Closing auth: No command%n");
return false; //interrupted
}
boolean rc;
if (ALLOW.equals(command)) {
String address = msg.popString();
if (verbose) {
System.out.printf("ZAuth: Whitelisting IP address=%s\n", address);
}
whitelist.put(address, OK);
rc = pipe.send(OK);
}
else if (DENY.equals(command)) {
String address = msg.popString();
if (verbose) {
System.out.printf("ZAuth: Blacklisting IP address=%s\n", address);
}
blacklist.put(address, OK);
rc = pipe.send(OK);
}
else if (VERBOSE.equals(command)) {
String verboseStr = msg.popString();
this.verbose = Boolean.parseBoolean(verboseStr);
rc = pipe.send(OK);
}
else if (REPLIES.equals(command)) {
repliesEnabled = Boolean.parseBoolean(msg.popString());
if (verbose) {
if (repliesEnabled) {
System.out.println("ZAuth: Enabled replies");
}
else {
System.out.println("ZAuth: Disabled replies");
}
}
rc = pipe.send(OK);
}
else if (TERMINATE.equals(command)) {
if (repliesEnabled) {
replies.send(repliesAddress); // lock replies agent
}
if (verbose) {
System.out.println("ZAuth: Terminated");
}
pipe.send(OK);
return false;
}
else {
final Auth authenticator = auths.get(command);
if (authenticator != null) {
if (authenticator.configure(msg, verbose)) {
rc = pipe.send(OK);
}
else {
rc = pipe.send("ERROR");
}
}
else {
System.out.printf("ZAuth: Invalid command %s%n", command);
rc = true;
}
}
msg.destroy();
if (!rc) {
System.out.printf("ZAuth: Command in error %s%n", command);
}
return rc;
}
@Override
public boolean stage(Socket socket, Socket pipe, ZPoller poller, int events)
{
ZapRequest request = ZapRequest.recvRequest(socket, true);
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("ZAuth: Passed (whitelist) address = %s\n", request.address);
}
}
else {
denied = true;
if (verbose) {
System.out.printf("ZAuth: 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("ZAuth: Denied (blacklist) address = %s\n", request.address);
}
}
else {
allowed = true;
if (verbose) {
System.out.printf("ZAuth: Passed (not in blacklist) address = %s\n", request.address);
}
}
}
//mechanism specific check
if (!denied) {
final Auth auth = auths.get(request.mechanism);
if (auth == null) {
System.out.printf("ZAuth E: Skipping unhandled mechanism %s%n", request.mechanism);
return false;
}
else {
allowed = auth.authorize(request, verbose);
}
}
final Socket reply = repliesEnabled ? replies : null;
if (allowed) {
request.reply(200, OK, reply);
}
else {
request.metadata = null;
request.reply(400, "NO ACCESS", reply);
}
return true;
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy