From commits-return-7320-archive-asf-public=cust-asf.ponee.io@zookeeper.apache.org Wed Oct 24 11:32:25 2018 Return-Path: X-Original-To: archive-asf-public@cust-asf.ponee.io Delivered-To: archive-asf-public@cust-asf.ponee.io Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by mx-eu-01.ponee.io (Postfix) with SMTP id 221AC1807B5 for ; Wed, 24 Oct 2018 11:32:19 +0200 (CEST) Received: (qmail 87067 invoked by uid 500); 24 Oct 2018 09:32:19 -0000 Mailing-List: contact commits-help@zookeeper.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: dev@zookeeper.apache.org Delivered-To: mailing list commits@zookeeper.apache.org Received: (qmail 85810 invoked by uid 99); 24 Oct 2018 09:32:18 -0000 Received: from git1-us-west.apache.org (HELO git1-us-west.apache.org) (140.211.11.23) by apache.org (qpsmtpd/0.29) with ESMTP; Wed, 24 Oct 2018 09:32:18 +0000 Received: by git1-us-west.apache.org (ASF Mail Server at git1-us-west.apache.org, from userid 33) id 03E97E11BA; Wed, 24 Oct 2018 09:32:18 +0000 (UTC) Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit From: andor@apache.org To: commits@zookeeper.apache.org Date: Wed, 24 Oct 2018 09:32:30 -0000 Message-Id: In-Reply-To: <0d0a9cdffdf445fca95a27db2fa4a2e1@git.apache.org> References: <0d0a9cdffdf445fca95a27db2fa4a2e1@git.apache.org> X-Mailer: ASF-Git Admin Mailer Subject: [14/36] zookeeper git commit: ZOOKEEPER-3032: MAVEN MIGRATION - branch-3.4 - zookeeper-server http://git-wip-us.apache.org/repos/asf/zookeeper/blob/7291e47c/zookeeper-server/src/main/java/org/apache/zookeeper/ZooKeeperMain.java ---------------------------------------------------------------------- diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/ZooKeeperMain.java b/zookeeper-server/src/main/java/org/apache/zookeeper/ZooKeeperMain.java new file mode 100644 index 0000000..6ca538b --- /dev/null +++ b/zookeeper-server/src/main/java/org/apache/zookeeper/ZooKeeperMain.java @@ -0,0 +1,871 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.zookeeper; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; + +import org.apache.yetus.audience.InterfaceAudience; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.apache.zookeeper.AsyncCallback.DataCallback; +import org.apache.zookeeper.ZooDefs.Ids; +import org.apache.zookeeper.data.ACL; +import org.apache.zookeeper.data.Id; +import org.apache.zookeeper.data.Stat; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * The command line client to ZooKeeper. + * + */ +@InterfaceAudience.Public +public class ZooKeeperMain { + private static final Logger LOG = LoggerFactory.getLogger(ZooKeeperMain.class); + static final Map commandMap = new HashMap( ); + + protected MyCommandOptions cl = new MyCommandOptions(); + protected HashMap history = new HashMap( ); + protected int commandCount = 0; + protected boolean printWatches = true; + + protected ZooKeeper zk; + protected String host = ""; + + public boolean getPrintWatches( ) { + return printWatches; + } + + static { + commandMap.put("connect", "host:port"); + commandMap.put("close",""); + commandMap.put("create", "[-s] [-e] path data acl"); + commandMap.put("delete","path [version]"); + commandMap.put("rmr","path"); + commandMap.put("set","path data [version]"); + commandMap.put("get","path [watch]"); + commandMap.put("ls","path [watch]"); + commandMap.put("ls2","path [watch]"); + commandMap.put("getAcl","path"); + commandMap.put("setAcl","path acl"); + commandMap.put("stat","path [watch]"); + commandMap.put("sync","path"); + commandMap.put("setquota","-n|-b val path"); + commandMap.put("listquota","path"); + commandMap.put("delquota","[-n|-b] path"); + commandMap.put("history",""); + commandMap.put("redo","cmdno"); + commandMap.put("printwatches", "on|off"); + commandMap.put("quit",""); + commandMap.put("addauth", "scheme auth"); + } + + static void usage() { + System.err.println("ZooKeeper -server host:port cmd args"); + for (Map.Entry entry : commandMap.entrySet()) { + System.err.println("\t" + entry.getKey() + " " + entry.getValue()); + } + } + + private class MyWatcher implements Watcher { + public void process(WatchedEvent event) { + if (getPrintWatches()) { + ZooKeeperMain.printMessage("WATCHER::"); + ZooKeeperMain.printMessage(event.toString()); + } + } + } + + static private int getPermFromString(String permString) { + int perm = 0; + for (int i = 0; i < permString.length(); i++) { + switch (permString.charAt(i)) { + case 'r': + perm |= ZooDefs.Perms.READ; + break; + case 'w': + perm |= ZooDefs.Perms.WRITE; + break; + case 'c': + perm |= ZooDefs.Perms.CREATE; + break; + case 'd': + perm |= ZooDefs.Perms.DELETE; + break; + case 'a': + perm |= ZooDefs.Perms.ADMIN; + break; + default: + System.err + .println("Unknown perm type: " + permString.charAt(i)); + } + } + return perm; + } + + private static void printStat(Stat stat) { + System.err.println("cZxid = 0x" + Long.toHexString(stat.getCzxid())); + System.err.println("ctime = " + new Date(stat.getCtime()).toString()); + System.err.println("mZxid = 0x" + Long.toHexString(stat.getMzxid())); + System.err.println("mtime = " + new Date(stat.getMtime()).toString()); + System.err.println("pZxid = 0x" + Long.toHexString(stat.getPzxid())); + System.err.println("cversion = " + stat.getCversion()); + System.err.println("dataVersion = " + stat.getVersion()); + System.err.println("aclVersion = " + stat.getAversion()); + System.err.println("ephemeralOwner = 0x" + + Long.toHexString(stat.getEphemeralOwner())); + System.err.println("dataLength = " + stat.getDataLength()); + System.err.println("numChildren = " + stat.getNumChildren()); + } + + /** + * A storage class for both command line options and shell commands. + * + */ + static class MyCommandOptions { + + private Map options = new HashMap(); + private List cmdArgs = null; + private String command = null; + public static final Pattern ARGS_PATTERN = Pattern.compile("\\s*([^\"\']\\S*|\"[^\"]*\"|'[^']*')\\s*"); + public static final Pattern QUOTED_PATTERN = Pattern.compile("^([\'\"])(.*)(\\1)$"); + + public MyCommandOptions() { + options.put("server", "localhost:2181"); + options.put("timeout", "30000"); + } + + public String getOption(String opt) { + return options.get(opt); + } + + public String getCommand( ) { + return command; + } + + public String getCmdArgument( int index ) { + return cmdArgs.get(index); + } + + public int getNumArguments( ) { + return cmdArgs.size(); + } + + public String[] getArgArray() { + return cmdArgs.toArray(new String[0]); + } + + /** + * Parses a command line that may contain one or more flags + * before an optional command string + * @param args command line arguments + * @return true if parsing succeeded, false otherwise. + */ + public boolean parseOptions(String[] args) { + List argList = Arrays.asList(args); + Iterator it = argList.iterator(); + + while (it.hasNext()) { + String opt = it.next(); + try { + if (opt.equals("-server")) { + options.put("server", it.next()); + } else if (opt.equals("-timeout")) { + options.put("timeout", it.next()); + } else if (opt.equals("-r")) { + options.put("readonly", "true"); + } + } catch (NoSuchElementException e){ + System.err.println("Error: no argument found for option " + + opt); + return false; + } + + if (!opt.startsWith("-")) { + command = opt; + cmdArgs = new ArrayList( ); + cmdArgs.add( command ); + while (it.hasNext()) { + cmdArgs.add(it.next()); + } + return true; + } + } + return true; + } + + /** + * Breaks a string into command + arguments. + * @param cmdstring string of form "cmd arg1 arg2..etc" + * @return true if parsing succeeded. + */ + public boolean parseCommand( String cmdstring ) { + Matcher matcher = ARGS_PATTERN.matcher(cmdstring); + + List args = new LinkedList(); + while (matcher.find()) { + String value = matcher.group(1); + if (QUOTED_PATTERN.matcher(value).matches()) { + // Strip off the surrounding quotes + value = value.substring(1, value.length() - 1); + } + args.add(value); + } + if (args.isEmpty()){ + return false; + } + command = args.get(0); + cmdArgs = args; + return true; + } + } + + + /** + * Makes a list of possible completions, either for commands + * or for zk nodes if the token to complete begins with / + * + */ + + + protected void addToHistory(int i,String cmd) { + history.put(i, cmd); + } + + public static List getCommands() { + return new LinkedList(commandMap.keySet()); + } + + protected String getPrompt() { + return "[zk: " + host + "("+zk.getState()+")" + " " + commandCount + "] "; + } + + public static void printMessage(String msg) { + System.out.println("\n"+msg); + } + + protected void connectToZK(String newHost) throws InterruptedException, IOException { + if (zk != null && zk.getState().isAlive()) { + zk.close(); + } + host = newHost; + boolean readOnly = cl.getOption("readonly") != null; + zk = new ZooKeeper(host, + Integer.parseInt(cl.getOption("timeout")), + new MyWatcher(), readOnly); + } + + public static void main(String args[]) + throws KeeperException, IOException, InterruptedException + { + ZooKeeperMain main = new ZooKeeperMain(args); + main.run(); + } + + public ZooKeeperMain(String args[]) throws IOException, InterruptedException { + cl.parseOptions(args); + System.out.println("Connecting to " + cl.getOption("server")); + connectToZK(cl.getOption("server")); + //zk = new ZooKeeper(cl.getOption("server"), +// Integer.parseInt(cl.getOption("timeout")), new MyWatcher()); + } + + public ZooKeeperMain(ZooKeeper zk) { + this.zk = zk; + } + + @SuppressWarnings("unchecked") + void run() throws KeeperException, IOException, InterruptedException { + if (cl.getCommand() == null) { + System.out.println("Welcome to ZooKeeper!"); + + boolean jlinemissing = false; + // only use jline if it's in the classpath + try { + Class consoleC = Class.forName("jline.ConsoleReader"); + Class completorC = + Class.forName("org.apache.zookeeper.JLineZNodeCompletor"); + + System.out.println("JLine support is enabled"); + + Object console = + consoleC.getConstructor().newInstance(); + + Object completor = + completorC.getConstructor(ZooKeeper.class).newInstance(zk); + Method addCompletor = consoleC.getMethod("addCompletor", + Class.forName("jline.Completor")); + addCompletor.invoke(console, completor); + + String line; + Method readLine = consoleC.getMethod("readLine", String.class); + while ((line = (String)readLine.invoke(console, getPrompt())) != null) { + executeLine(line); + } + } catch (ClassNotFoundException e) { + LOG.debug("Unable to start jline", e); + jlinemissing = true; + } catch (NoSuchMethodException e) { + LOG.debug("Unable to start jline", e); + jlinemissing = true; + } catch (InvocationTargetException e) { + LOG.debug("Unable to start jline", e); + jlinemissing = true; + } catch (IllegalAccessException e) { + LOG.debug("Unable to start jline", e); + jlinemissing = true; + } catch (InstantiationException e) { + LOG.debug("Unable to start jline", e); + jlinemissing = true; + } + + if (jlinemissing) { + System.out.println("JLine support is disabled"); + BufferedReader br = + new BufferedReader(new InputStreamReader(System.in)); + + String line; + while ((line = br.readLine()) != null) { + executeLine(line); + } + } + } else { + // Command line args non-null. Run what was passed. + processCmd(cl); + } + } + + public void executeLine(String line) + throws InterruptedException, IOException, KeeperException { + if (!line.equals("")) { + cl.parseCommand(line); + addToHistory(commandCount,line); + processCmd(cl); + commandCount++; + } + } + + private static DataCallback dataCallback = new DataCallback() { + + public void processResult(int rc, String path, Object ctx, byte[] data, + Stat stat) { + System.out.println("rc = " + rc + " path = " + path + " data = " + + (data == null ? "null" : new String(data)) + " stat = "); + printStat(stat); + } + + }; + + /** + * trim the quota tree to recover unwanted tree elements + * in the quota's tree + * @param zk the zookeeper client + * @param path the path to start from and go up and see if their + * is any unwanted parent in the path. + * @return true if sucessful + * @throws KeeperException + * @throws IOException + * @throws InterruptedException + */ + private static boolean trimProcQuotas(ZooKeeper zk, String path) + throws KeeperException, IOException, InterruptedException + { + if (Quotas.quotaZookeeper.equals(path)) { + return true; + } + List children = zk.getChildren(path, false); + if (children.size() == 0) { + zk.delete(path, -1); + String parent = path.substring(0, path.lastIndexOf('/')); + return trimProcQuotas(zk, parent); + } else { + return true; + } + } + + /** + * this method deletes quota for a node. + * @param zk the zookeeper client + * @param path the path to delete quota for + * @param bytes true if number of bytes needs to + * be unset + * @param numNodes true if number of nodes needs + * to be unset + * @return true if quota deletion is successful + * @throws KeeperException + * @throws IOException + * @throws InterruptedException + */ + public static boolean delQuota(ZooKeeper zk, String path, + boolean bytes, boolean numNodes) + throws KeeperException, IOException, InterruptedException + { + String parentPath = Quotas.quotaZookeeper + path; + String quotaPath = Quotas.quotaZookeeper + path + "/" + Quotas.limitNode; + if (zk.exists(quotaPath, false) == null) { + System.out.println("Quota does not exist for " + path); + return true; + } + byte[] data = null; + try { + data = zk.getData(quotaPath, false, new Stat()); + } catch(KeeperException.NoNodeException ne) { + System.err.println("quota does not exist for " + path); + return true; + } + StatsTrack strack = new StatsTrack(new String(data)); + if (bytes && !numNodes) { + strack.setBytes(-1L); + zk.setData(quotaPath, strack.toString().getBytes(), -1); + } else if (!bytes && numNodes) { + strack.setCount(-1); + zk.setData(quotaPath, strack.toString().getBytes(), -1); + } else if (bytes && numNodes) { + // delete till you can find a node with more than + // one child + List children = zk.getChildren(parentPath, false); + /// delete the direct children first + for (String child: children) { + zk.delete(parentPath + "/" + child, -1); + } + // cut the tree till their is more than one child + trimProcQuotas(zk, parentPath); + } + return true; + } + + private static void checkIfParentQuota(ZooKeeper zk, String path) + throws InterruptedException, KeeperException + { + final String[] splits = path.split("/"); + String quotaPath = Quotas.quotaZookeeper; + for (String str: splits) { + if (str.length() == 0) { + // this should only be for the beginning of the path + // i.e. "/..." - split(path)[0] is empty string before first '/' + continue; + } + quotaPath += "/" + str; + List children = null; + try { + children = zk.getChildren(quotaPath, false); + } catch(KeeperException.NoNodeException ne) { + LOG.debug("child removed during quota check", ne); + return; + } + if (children.size() == 0) { + return; + } + for (String child: children) { + if (Quotas.limitNode.equals(child)) { + throw new IllegalArgumentException(path + " has a parent " + + quotaPath + " which has a quota"); + } + } + } + } + + /** + * this method creates a quota node for the path + * @param zk the ZooKeeper client + * @param path the path for which quota needs to be created + * @param bytes the limit of bytes on this path + * @param numNodes the limit of number of nodes on this path + * @return true if its successful and false if not. + */ + public static boolean createQuota(ZooKeeper zk, String path, + long bytes, int numNodes) + throws KeeperException, IOException, InterruptedException + { + // check if the path exists. We cannot create + // quota for a path that already exists in zookeeper + // for now. + Stat initStat = zk.exists(path, false); + if (initStat == null) { + throw new IllegalArgumentException(path + " does not exist."); + } + // now check if their is already existing + // parent or child that has quota + + String quotaPath = Quotas.quotaZookeeper; + // check for more than 2 children -- + // if zookeeper_stats and zookeeper_qutoas + // are not the children then this path + // is an ancestor of some path that + // already has quota + String realPath = Quotas.quotaZookeeper + path; + try { + List children = zk.getChildren(realPath, false); + for (String child: children) { + if (!child.startsWith("zookeeper_")) { + throw new IllegalArgumentException(path + " has child " + + child + " which has a quota"); + } + } + } catch(KeeperException.NoNodeException ne) { + // this is fine + } + + //check for any parent that has been quota + checkIfParentQuota(zk, path); + + // this is valid node for quota + // start creating all the parents + if (zk.exists(quotaPath, false) == null) { + try { + zk.create(Quotas.procZookeeper, null, Ids.OPEN_ACL_UNSAFE, + CreateMode.PERSISTENT); + zk.create(Quotas.quotaZookeeper, null, Ids.OPEN_ACL_UNSAFE, + CreateMode.PERSISTENT); + } catch(KeeperException.NodeExistsException ne) { + // do nothing + } + } + + // now create the direct children + // and the stat and quota nodes + String[] splits = path.split("/"); + StringBuilder sb = new StringBuilder(); + sb.append(quotaPath); + for (int i=1; i 2; + String path = null; + List acl = Ids.OPEN_ACL_UNSAFE; + LOG.debug("Processing " + cmd); + + if (cmd.equals("quit")) { + System.out.println("Quitting..."); + zk.close(); + System.exit(0); + } else if (cmd.equals("redo") && args.length >= 2) { + Integer i = Integer.decode(args[1]); + if (commandCount <= i || i < 0){ // don't allow redoing this redo + System.out.println("Command index out of range"); + return false; + } + cl.parseCommand(history.get(i)); + if (cl.getCommand().equals( "redo" )){ + System.out.println("No redoing redos"); + return false; + } + history.put(commandCount, history.get(i)); + processCmd( cl); + } else if (cmd.equals("history")) { + for (int i=commandCount - 10;i<=commandCount;++i) { + if (i < 0) continue; + System.out.println(i + " - " + history.get(i)); + } + } else if (cmd.equals("printwatches")) { + if (args.length == 1) { + System.out.println("printwatches is " + (printWatches ? "on" : "off")); + } else { + printWatches = args[1].equals("on"); + } + } else if (cmd.equals("connect")) { + if (args.length >=2) { + connectToZK(args[1]); + } else { + connectToZK(host); + } + } + + // Below commands all need a live connection + if (zk == null || !zk.getState().isAlive()) { + System.out.println("Not connected"); + return false; + } + + if (cmd.equals("create") && args.length >= 3) { + int first = 0; + CreateMode flags = CreateMode.PERSISTENT; + if ((args[1].equals("-e") && args[2].equals("-s")) + || (args[1]).equals("-s") && (args[2].equals("-e"))) { + first+=2; + flags = CreateMode.EPHEMERAL_SEQUENTIAL; + } else if (args[1].equals("-e")) { + first++; + flags = CreateMode.EPHEMERAL; + } else if (args[1].equals("-s")) { + first++; + flags = CreateMode.PERSISTENT_SEQUENTIAL; + } + if (args.length == first + 4) { + acl = parseACLs(args[first+3]); + } + path = args[first + 1]; + String newPath = zk.create(path, args[first+2].getBytes(), acl, + flags); + System.err.println("Created " + newPath); + } else if (cmd.equals("delete") && args.length >= 2) { + path = args[1]; + zk.delete(path, watch ? Integer.parseInt(args[2]) : -1); + } else if (cmd.equals("rmr") && args.length >= 2) { + path = args[1]; + ZKUtil.deleteRecursive(zk, path); + } else if (cmd.equals("set") && args.length >= 3) { + path = args[1]; + stat = zk.setData(path, args[2].getBytes(), + args.length > 3 ? Integer.parseInt(args[3]) : -1); + printStat(stat); + } else if (cmd.equals("aget") && args.length >= 2) { + path = args[1]; + zk.getData(path, watch, dataCallback, path); + } else if (cmd.equals("get") && args.length >= 2) { + path = args[1]; + byte data[] = zk.getData(path, watch, stat); + data = (data == null)? "null".getBytes() : data; + System.out.println(new String(data)); + printStat(stat); + } else if (cmd.equals("ls") && args.length >= 2) { + path = args[1]; + List children = zk.getChildren(path, watch); + System.out.println(children); + } else if (cmd.equals("ls2") && args.length >= 2) { + path = args[1]; + List children = zk.getChildren(path, watch, stat); + System.out.println(children); + printStat(stat); + } else if (cmd.equals("getAcl") && args.length >= 2) { + path = args[1]; + acl = zk.getACL(path, stat); + for (ACL a : acl) { + System.out.println(a.getId() + ": " + + getPermString(a.getPerms())); + } + } else if (cmd.equals("setAcl") && args.length >= 3) { + path = args[1]; + stat = zk.setACL(path, parseACLs(args[2]), + args.length > 4 ? Integer.parseInt(args[3]) : -1); + printStat(stat); + } else if (cmd.equals("stat") && args.length >= 2) { + path = args[1]; + stat = zk.exists(path, watch); + if (stat == null) { + throw new KeeperException.NoNodeException(path); + } + printStat(stat); + } else if (cmd.equals("listquota") && args.length >= 2) { + path = args[1]; + String absolutePath = Quotas.quotaZookeeper + path + "/" + Quotas.limitNode; + byte[] data = null; + try { + System.err.println("absolute path is " + absolutePath); + data = zk.getData(absolutePath, false, stat); + StatsTrack st = new StatsTrack(new String(data)); + System.out.println("Output quota for " + path + " " + + st.toString()); + + data = zk.getData(Quotas.quotaZookeeper + path + "/" + + Quotas.statNode, false, stat); + System.out.println("Output stat for " + path + " " + + new StatsTrack(new String(data)).toString()); + } catch(KeeperException.NoNodeException ne) { + System.err.println("quota for " + path + " does not exist."); + } + } else if (cmd.equals("setquota") && args.length >= 4) { + String option = args[1]; + String val = args[2]; + path = args[3]; + System.err.println("Comment: the parts are " + + "option " + option + + " val " + val + + " path " + path); + if ("-b".equals(option)) { + // we are setting the bytes quota + createQuota(zk, path, Long.parseLong(val), -1); + } else if ("-n".equals(option)) { + // we are setting the num quota + createQuota(zk, path, -1L, Integer.parseInt(val)); + } else { + usage(); + } + + } else if (cmd.equals("delquota") && args.length >= 2) { + //if neither option -n or -b is specified, we delete + // the quota node for thsi node. + if (args.length == 3) { + //this time we have an option + String option = args[1]; + path = args[2]; + if ("-b".equals(option)) { + delQuota(zk, path, true, false); + } else if ("-n".equals(option)) { + delQuota(zk, path, false, true); + } + } else if (args.length == 2) { + path = args[1]; + // we dont have an option specified. + // just delete whole quota node + delQuota(zk, path, true, true); + } else if (cmd.equals("help")) { + usage(); + } + } else if (cmd.equals("close")) { + zk.close(); + } else if (cmd.equals("sync") && args.length >= 2) { + path = args[1]; + zk.sync(path, new AsyncCallback.VoidCallback() { public void processResult(int rc, String path, Object ctx) { System.out.println("Sync returned " + rc); } }, null ); + } else if (cmd.equals("addauth") && args.length >=2 ) { + byte[] b = null; + if (args.length >= 3) + b = args[2].getBytes(); + + zk.addAuthInfo(args[1], b); + } else if (!commandMap.containsKey(cmd)) { + usage(); + } + return watch; + } + + private static String getPermString(int perms) { + StringBuilder p = new StringBuilder(); + if ((perms & ZooDefs.Perms.CREATE) != 0) { + p.append('c'); + } + if ((perms & ZooDefs.Perms.DELETE) != 0) { + p.append('d'); + } + if ((perms & ZooDefs.Perms.READ) != 0) { + p.append('r'); + } + if ((perms & ZooDefs.Perms.WRITE) != 0) { + p.append('w'); + } + if ((perms & ZooDefs.Perms.ADMIN) != 0) { + p.append('a'); + } + return p.toString(); + } + + private static List parseACLs(String aclString) { + List acl; + String acls[] = aclString.split(","); + acl = new ArrayList(); + for (String a : acls) { + int firstColon = a.indexOf(':'); + int lastColon = a.lastIndexOf(':'); + if (firstColon == -1 || lastColon == -1 || firstColon == lastColon) { + System.err + .println(a + " does not have the form scheme:id:perm"); + continue; + } + ACL newAcl = new ACL(); + newAcl.setId(new Id(a.substring(0, firstColon), a.substring( + firstColon + 1, lastColon))); + newAcl.setPerms(getPermFromString(a.substring(lastColon + 1))); + acl.add(newAcl); + } + return acl; + } +} http://git-wip-us.apache.org/repos/asf/zookeeper/blob/7291e47c/zookeeper-server/src/main/java/org/apache/zookeeper/ZooKeeperTestable.java ---------------------------------------------------------------------- diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/ZooKeeperTestable.java b/zookeeper-server/src/main/java/org/apache/zookeeper/ZooKeeperTestable.java new file mode 100644 index 0000000..775d1a2 --- /dev/null +++ b/zookeeper-server/src/main/java/org/apache/zookeeper/ZooKeeperTestable.java @@ -0,0 +1,47 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.zookeeper; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class ZooKeeperTestable implements Testable { + private static final Logger LOG = LoggerFactory + .getLogger(ZooKeeperTestable.class); + + private final ZooKeeper zooKeeper; + private final ClientCnxn clientCnxn; + + ZooKeeperTestable(ZooKeeper zooKeeper, ClientCnxn clientCnxn) { + this.zooKeeper = zooKeeper; + this.clientCnxn = clientCnxn; + } + + @Override + public void injectSessionExpiration() { + LOG.info("injectSessionExpiration() called"); + + clientCnxn.eventThread.queueEvent(new WatchedEvent( + Watcher.Event.EventType.None, + Watcher.Event.KeeperState.Expired, null)); + clientCnxn.eventThread.queueEventOfDeath(); + clientCnxn.sendThread.getClientCnxnSocket().wakeupCnxn(); + clientCnxn.state = ZooKeeper.States.CLOSED; + } +} http://git-wip-us.apache.org/repos/asf/zookeeper/blob/7291e47c/zookeeper-server/src/main/java/org/apache/zookeeper/client/ConnectStringParser.java ---------------------------------------------------------------------- diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/client/ConnectStringParser.java b/zookeeper-server/src/main/java/org/apache/zookeeper/client/ConnectStringParser.java new file mode 100644 index 0000000..ff0425a --- /dev/null +++ b/zookeeper-server/src/main/java/org/apache/zookeeper/client/ConnectStringParser.java @@ -0,0 +1,87 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.zookeeper.client; + +import java.net.InetSocketAddress; +import java.util.ArrayList; + +import org.apache.zookeeper.common.PathUtils; + +/** + * A parser for ZooKeeper Client connect strings. + * + * This class is not meant to be seen or used outside of ZooKeeper itself. + * + * The chrootPath member should be replaced by a Path object in issue + * ZOOKEEPER-849. + * + * @see org.apache.zookeeper.ZooKeeper + */ +public final class ConnectStringParser { + private static final int DEFAULT_PORT = 2181; + + private final String chrootPath; + + private final ArrayList serverAddresses = new ArrayList(); + + /** + * + * @throws IllegalArgumentException + * for an invalid chroot path. + */ + public ConnectStringParser(String connectString) { + // parse out chroot, if any + int off = connectString.indexOf('/'); + if (off >= 0) { + String chrootPath = connectString.substring(off); + // ignore "/" chroot spec, same as null + if (chrootPath.length() == 1) { + this.chrootPath = null; + } else { + PathUtils.validatePath(chrootPath); + this.chrootPath = chrootPath; + } + connectString = connectString.substring(0, off); + } else { + this.chrootPath = null; + } + + String hostsList[] = connectString.split(","); + for (String host : hostsList) { + int port = DEFAULT_PORT; + int pidx = host.lastIndexOf(':'); + if (pidx >= 0) { + // otherwise : is at the end of the string, ignore + if (pidx < host.length() - 1) { + port = Integer.parseInt(host.substring(pidx + 1)); + } + host = host.substring(0, pidx); + } + serverAddresses.add(InetSocketAddress.createUnresolved(host, port)); + } + } + + public String getChrootPath() { + return chrootPath; + } + + public ArrayList getServerAddresses() { + return serverAddresses; + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/zookeeper/blob/7291e47c/zookeeper-server/src/main/java/org/apache/zookeeper/client/FourLetterWordMain.java ---------------------------------------------------------------------- diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/client/FourLetterWordMain.java b/zookeeper-server/src/main/java/org/apache/zookeeper/client/FourLetterWordMain.java new file mode 100644 index 0000000..a4175e4 --- /dev/null +++ b/zookeeper-server/src/main/java/org/apache/zookeeper/client/FourLetterWordMain.java @@ -0,0 +1,107 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.zookeeper.client; + +import org.apache.log4j.Logger; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketTimeoutException; + +import org.apache.yetus.audience.InterfaceAudience; + +@InterfaceAudience.Public +public class FourLetterWordMain { + //in milliseconds, socket should connect/read within this period otherwise SocketTimeoutException + private static final int DEFAULT_SOCKET_TIMEOUT = 5000; + protected static final Logger LOG = Logger.getLogger(FourLetterWordMain.class); + + /** + * Send the 4letterword + * @param host the destination host + * @param port the destination port + * @param cmd the 4letterword + * @return server response + * @throws java.io.IOException + */ + public static String send4LetterWord(String host, int port, String cmd) + throws IOException + { + return send4LetterWord(host, port, cmd, DEFAULT_SOCKET_TIMEOUT); + } + /** + * Send the 4letterword + * @param host the destination host + * @param port the destination port + * @param cmd the 4letterword + * @param timeout in milliseconds, maximum time to wait while connecting/reading data + * @return server response + * @throws java.io.IOException + */ + public static String send4LetterWord(String host, int port, String cmd, int timeout) + throws IOException + { + LOG.info("connecting to " + host + " " + port); + Socket sock = new Socket(); + InetSocketAddress hostaddress= host != null ? new InetSocketAddress(host, port) : + new InetSocketAddress(InetAddress.getByName(null), port); + BufferedReader reader = null; + try { + sock.setSoTimeout(timeout); + sock.connect(hostaddress, timeout); + OutputStream outstream = sock.getOutputStream(); + outstream.write(cmd.getBytes()); + outstream.flush(); + // this replicates NC - close the output stream before reading + sock.shutdownOutput(); + + reader = + new BufferedReader( + new InputStreamReader(sock.getInputStream())); + StringBuilder sb = new StringBuilder(); + String line; + while((line = reader.readLine()) != null) { + sb.append(line + "\n"); + } + return sb.toString(); + } catch (SocketTimeoutException e) { + throw new IOException("Exception while executing four letter word: " + cmd, e); + } finally { + sock.close(); + if (reader != null) { + reader.close(); + } + } + } + + public static void main(String[] args) + throws IOException + { + if (args.length != 3) { + System.out.println("Usage: FourLetterWordMain "); + } else { + System.out.println(send4LetterWord(args[0], Integer.parseInt(args[1]), args[2])); + } + } +} http://git-wip-us.apache.org/repos/asf/zookeeper/blob/7291e47c/zookeeper-server/src/main/java/org/apache/zookeeper/client/HostProvider.java ---------------------------------------------------------------------- diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/client/HostProvider.java b/zookeeper-server/src/main/java/org/apache/zookeeper/client/HostProvider.java new file mode 100644 index 0000000..61d9108 --- /dev/null +++ b/zookeeper-server/src/main/java/org/apache/zookeeper/client/HostProvider.java @@ -0,0 +1,66 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.zookeeper.client; + +import org.apache.yetus.audience.InterfaceAudience; + +import java.net.InetSocketAddress; +import java.net.UnknownHostException; + +/** + * A set of hosts a ZooKeeper client should connect to. + * + * Classes implementing this interface must guarantee the following: + * + * * Every call to next() returns an InetSocketAddress. So the iterator never + * ends. + * + * * The size() of a HostProvider may never be zero. + * + * A HostProvider must return resolved InetSocketAddress instances on next(), + * but it's up to the HostProvider, when it wants to do the resolving. + * + * Different HostProvider could be imagined: + * + * * A HostProvider that loads the list of Hosts from an URL or from DNS + * * A HostProvider that re-resolves the InetSocketAddress after a timeout. + * * A HostProvider that prefers nearby hosts. + */ +@InterfaceAudience.Public +public interface HostProvider { + public int size(); + + /** + * The next host to try to connect to. + * + * For a spinDelay of 0 there should be no wait. + * + * @param spinDelay Milliseconds to wait if all hosts have been tried once. + * @return The next host to try to connect to with resolved address. If the host is not resolvable, the unresolved + * address will be returned. + */ + public InetSocketAddress next(long spinDelay); + + /** + * Notify the HostProvider of a successful connection. + * + * The HostProvider may use this notification to reset it's inner state. + */ + public void onConnected(); +} http://git-wip-us.apache.org/repos/asf/zookeeper/blob/7291e47c/zookeeper-server/src/main/java/org/apache/zookeeper/client/StaticHostProvider.java ---------------------------------------------------------------------- diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/client/StaticHostProvider.java b/zookeeper-server/src/main/java/org/apache/zookeeper/client/StaticHostProvider.java new file mode 100644 index 0000000..0005942 --- /dev/null +++ b/zookeeper-server/src/main/java/org/apache/zookeeper/client/StaticHostProvider.java @@ -0,0 +1,179 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.zookeeper.client; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import org.apache.yetus.audience.InterfaceAudience; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Most simple HostProvider, resolves on every next() call. + * + * Please be aware that although this class doesn't do any DNS caching, there're multiple levels of caching already + * present across the stack like in JVM, OS level, hardware, etc. The best we could do here is to get the most recent + * address from the underlying system which is considered up-to-date. + * + */ +@InterfaceAudience.Public +public final class StaticHostProvider implements HostProvider { + public interface Resolver { + InetAddress[] getAllByName(String name) throws UnknownHostException; + } + + private static final Logger LOG = LoggerFactory + .getLogger(StaticHostProvider.class); + + private final List serverAddresses = new ArrayList(5); + + private int lastIndex = -1; + + private int currentIndex = -1; + + private Resolver resolver; + + /** + * Constructs a SimpleHostSet. + * + * @param serverAddresses + * possibly unresolved ZooKeeper server addresses + * @throws IllegalArgumentException + * if serverAddresses is empty or resolves to an empty list + */ + public StaticHostProvider(Collection serverAddresses) { + this.resolver = new Resolver() { + @Override + public InetAddress[] getAllByName(String name) throws UnknownHostException { + return InetAddress.getAllByName(name); + } + }; + init(serverAddresses); + } + + /** + * Introduced for testing purposes. getAllByName() is a static method of InetAddress, therefore cannot be easily mocked. + * By abstraction of Resolver interface we can easily inject a mocked implementation in tests. + * + * @param serverAddresses + * possibly unresolved ZooKeeper server addresses + * @param resolver + * custom resolver implementation + * @throws IllegalArgumentException + * if serverAddresses is empty or resolves to an empty list + */ + public StaticHostProvider(Collection serverAddresses, Resolver resolver) { + this.resolver = resolver; + init(serverAddresses); + } + + /** + * Common init method for all constructors. + * Resolve all unresolved server addresses, put them in a list and shuffle. + */ + private void init(Collection serverAddresses) { + if (serverAddresses.isEmpty()) { + throw new IllegalArgumentException( + "A HostProvider may not be empty!"); + } + + this.serverAddresses.addAll(serverAddresses); + Collections.shuffle(this.serverAddresses); + } + + /** + * Evaluate to a hostname if one is available and otherwise it returns the + * string representation of the IP address. + * + * In Java 7, we have a method getHostString, but earlier versions do not support it. + * This method is to provide a replacement for InetSocketAddress.getHostString(). + * + * @param addr + * @return Hostname string of address parameter + */ + private String getHostString(InetSocketAddress addr) { + String hostString = ""; + + if (addr == null) { + return hostString; + } + if (!addr.isUnresolved()) { + InetAddress ia = addr.getAddress(); + + // If the string starts with '/', then it has no hostname + // and we want to avoid the reverse lookup, so we return + // the string representation of the address. + if (ia.toString().startsWith("/")) { + hostString = ia.getHostAddress(); + } else { + hostString = addr.getHostName(); + } + } else { + // According to the Java 6 documentation, if the hostname is + // unresolved, then the string before the colon is the hostname. + String addrString = addr.toString(); + hostString = addrString.substring(0, addrString.lastIndexOf(':')); + } + + return hostString; + } + + public int size() { + return serverAddresses.size(); + } + + public InetSocketAddress next(long spinDelay) { + currentIndex = ++currentIndex % serverAddresses.size(); + if (currentIndex == lastIndex && spinDelay > 0) { + try { + Thread.sleep(spinDelay); + } catch (InterruptedException e) { + LOG.warn("Unexpected exception", e); + } + } else if (lastIndex == -1) { + // We don't want to sleep on the first ever connect attempt. + lastIndex = 0; + } + + InetSocketAddress curAddr = serverAddresses.get(currentIndex); + try { + String curHostString = getHostString(curAddr); + List resolvedAddresses = new ArrayList(Arrays.asList(this.resolver.getAllByName(curHostString))); + if (resolvedAddresses.isEmpty()) { + return curAddr; + } + Collections.shuffle(resolvedAddresses); + return new InetSocketAddress(resolvedAddresses.get(0), curAddr.getPort()); + } catch (UnknownHostException e) { + return curAddr; + } + } + + @Override + public void onConnected() { + lastIndex = currentIndex; + } +} http://git-wip-us.apache.org/repos/asf/zookeeper/blob/7291e47c/zookeeper-server/src/main/java/org/apache/zookeeper/client/ZooKeeperSaslClient.java ---------------------------------------------------------------------- diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/client/ZooKeeperSaslClient.java b/zookeeper-server/src/main/java/org/apache/zookeeper/client/ZooKeeperSaslClient.java new file mode 100644 index 0000000..af3303c --- /dev/null +++ b/zookeeper-server/src/main/java/org/apache/zookeeper/client/ZooKeeperSaslClient.java @@ -0,0 +1,439 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.zookeeper.client; + +import java.io.IOException; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; + +import javax.security.auth.Subject; +import javax.security.auth.login.AppConfigurationEntry; +import javax.security.auth.login.Configuration; +import javax.security.auth.login.LoginException; +import javax.security.sasl.SaslClient; +import javax.security.sasl.SaslException; + +import org.apache.zookeeper.AsyncCallback; +import org.apache.zookeeper.ClientCnxn; +import org.apache.zookeeper.Environment; +import org.apache.zookeeper.Login; +import org.apache.zookeeper.SaslClientCallbackHandler; +import org.apache.zookeeper.Watcher.Event.KeeperState; +import org.apache.zookeeper.ZooDefs; +import org.apache.zookeeper.data.Stat; +import org.apache.zookeeper.proto.GetSASLRequest; +import org.apache.zookeeper.proto.SetSASLResponse; +import org.apache.zookeeper.util.SecurityUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class manages SASL authentication for the client. It + * allows ClientCnxn to authenticate using SASL with a Zookeeper server. + */ +public class ZooKeeperSaslClient { + public static final String LOGIN_CONTEXT_NAME_KEY = "zookeeper.sasl.clientconfig"; + public static final String ENABLE_CLIENT_SASL_KEY = "zookeeper.sasl.client"; + public static final String ENABLE_CLIENT_SASL_DEFAULT = "true"; + private static volatile boolean initializedLogin = false; + + /** + * Returns true if the SASL client is enabled. By default, the client + * is enabled but can be disabled by setting the system property + * zookeeper.sasl.client to false. See + * ZOOKEEPER-1657 for more information. + * + * @return If the SASL client is enabled. + */ + public static boolean isEnabled() { + return Boolean.valueOf(System.getProperty(ENABLE_CLIENT_SASL_KEY, ENABLE_CLIENT_SASL_DEFAULT)); + } + + private static final Logger LOG = LoggerFactory.getLogger(ZooKeeperSaslClient.class); + private static Login login = null; + private SaslClient saslClient; + private boolean isSASLConfigured = true; + + private byte[] saslToken = new byte[0]; + + public enum SaslState { + INITIAL,INTERMEDIATE,COMPLETE,FAILED + } + + private SaslState saslState = SaslState.INITIAL; + + private boolean gotLastPacket = false; + /** informational message indicating the current configuration status */ + private final String configStatus; + + public SaslState getSaslState() { + return saslState; + } + + public String getLoginContext() { + if (login != null) + return login.getLoginContextName(); + return null; + } + + public ZooKeeperSaslClient(final String serverPrincipal) + throws LoginException { + /** + * ZOOKEEPER-1373: allow system property to specify the JAAS + * configuration section that the zookeeper client should use. + * Default to "Client". + */ + String clientSection = System.getProperty(ZooKeeperSaslClient.LOGIN_CONTEXT_NAME_KEY, "Client"); + // Note that 'Configuration' here refers to javax.security.auth.login.Configuration. + AppConfigurationEntry entries[] = null; + RuntimeException runtimeException = null; + try { + entries = Configuration.getConfiguration().getAppConfigurationEntry(clientSection); + } catch (SecurityException e) { + // handle below: might be harmless if the user doesn't intend to use JAAS authentication. + runtimeException = e; + } catch (IllegalArgumentException e) { + // third party customized getAppConfigurationEntry could throw IllegalArgumentException when JAAS + // configuration isn't set. We can reevaluate whether to catch RuntimeException instead when more + // different types of RuntimeException found + runtimeException = e; + } + if (entries != null) { + this.configStatus = "Will attempt to SASL-authenticate using Login Context section '" + clientSection + "'"; + this.saslClient = createSaslClient(serverPrincipal, clientSection); + } else { + // Handle situation of clientSection's being null: it might simply because the client does not intend to + // use SASL, so not necessarily an error. + saslState = SaslState.FAILED; + String explicitClientSection = System.getProperty(ZooKeeperSaslClient.LOGIN_CONTEXT_NAME_KEY); + if (explicitClientSection != null) { + // If the user explicitly overrides the default Login Context, they probably expected SASL to + // succeed. But if we got here, SASL failed. + if (runtimeException != null) { + throw new LoginException("Zookeeper client cannot authenticate using the " + explicitClientSection + + " section of the supplied JAAS configuration: '" + + System.getProperty(Environment.JAAS_CONF_KEY) + "' because of a " + + "RuntimeException: " + runtimeException); + } else { + throw new LoginException("Client cannot SASL-authenticate because the specified JAAS configuration " + + "section '" + explicitClientSection + "' could not be found."); + } + } else { + // The user did not override the default context. It might be that they just don't intend to use SASL, + // so log at INFO, not WARN, since they don't expect any SASL-related information. + String msg = "Will not attempt to authenticate using SASL "; + if (runtimeException != null) { + msg += "(" + runtimeException + ")"; + } else { + msg += "(unknown error)"; + } + this.configStatus = msg; + this.isSASLConfigured = false; + } + if (System.getProperty(Environment.JAAS_CONF_KEY) != null) { + // Again, the user explicitly set something SASL-related, so they probably expected SASL to succeed. + if (runtimeException != null) { + throw new LoginException("Zookeeper client cannot authenticate using the '" + + System.getProperty(ZooKeeperSaslClient.LOGIN_CONTEXT_NAME_KEY, "Client") + + "' section of the supplied JAAS configuration: '" + + System.getProperty(Environment.JAAS_CONF_KEY) + "' because of a " + + "RuntimeException: " + runtimeException); + } else { + throw new LoginException("No JAAS configuration section named '" + + System.getProperty(ZooKeeperSaslClient.LOGIN_CONTEXT_NAME_KEY, "Client") + + "' was found in specified JAAS configuration file: '" + + System.getProperty(Environment.JAAS_CONF_KEY) + "'."); + } + } + } + } + + /** + * @return informational message indicating the current configuration status. + */ + public String getConfigStatus() { + return configStatus; + } + + public boolean isComplete() { + return (saslState == SaslState.COMPLETE); + } + + public boolean isFailed() { + return (saslState == SaslState.FAILED); + } + + public static class ServerSaslResponseCallback implements AsyncCallback.DataCallback { + public void processResult(int rc, String path, Object ctx, byte data[], Stat stat) { + // processResult() is used by ClientCnxn's sendThread to respond to + // data[] contains the Zookeeper Server's SASL token. + // ctx is the ZooKeeperSaslClient object. We use this object's respondToServer() method + // to reply to the Zookeeper Server's SASL token + ZooKeeperSaslClient client = ((ClientCnxn)ctx).zooKeeperSaslClient; + if (client == null) { + LOG.warn("sasl client was unexpectedly null: cannot respond to Zookeeper server."); + return; + } + byte[] usedata = data; + if (data != null) { + LOG.debug("ServerSaslResponseCallback(): saslToken server response: (length="+usedata.length+")"); + } + else { + usedata = new byte[0]; + LOG.debug("ServerSaslResponseCallback(): using empty data[] as server response (length="+usedata.length+")"); + } + client.respondToServer(usedata, (ClientCnxn)ctx); + } + } + + private SaslClient createSaslClient(final String servicePrincipal, + final String loginContext) throws LoginException { + try { + if (!initializedLogin) { + synchronized (ZooKeeperSaslClient.class) { + if (login == null) { + if (LOG.isDebugEnabled()) { + LOG.debug("JAAS loginContext is: " + loginContext); + } + // note that the login object is static: it's shared amongst all zookeeper-related connections. + // in order to ensure the login is initialized only once, it must be synchronized the code snippet. + login = new Login(loginContext, new SaslClientCallbackHandler(null, "Client")); + login.startThreadIfNeeded(); + initializedLogin = true; + } + } + } + return SecurityUtils.createSaslClient(login.getSubject(), + servicePrincipal, "zookeeper", "zk-sasl-md5", LOG, "Client"); + } catch (LoginException e) { + // We throw LoginExceptions... + throw e; + } catch (Exception e) { + // ..but consume (with a log message) all other types of exceptions. + LOG.error("Exception while trying to create SASL client: " + e); + return null; + } + } + + public void respondToServer(byte[] serverToken, ClientCnxn cnxn) { + if (saslClient == null) { + LOG.error("saslClient is unexpectedly null. Cannot respond to server's SASL message; ignoring."); + return; + } + + if (!(saslClient.isComplete())) { + try { + saslToken = createSaslToken(serverToken); + if (saslToken != null) { + sendSaslPacket(saslToken, cnxn); + } + } catch (SaslException e) { + LOG.error("SASL authentication failed using login context '" + + this.getLoginContext() + "' with exception: {}", e); + saslState = SaslState.FAILED; + gotLastPacket = true; + } + } + + if (saslClient.isComplete()) { + // GSSAPI: server sends a final packet after authentication succeeds + // or fails. + if ((serverToken == null) && (saslClient.getMechanismName().equals("GSSAPI"))) + gotLastPacket = true; + // non-GSSAPI: no final packet from server. + if (!saslClient.getMechanismName().equals("GSSAPI")) { + gotLastPacket = true; + } + // SASL authentication is completed, successfully or not: + // enable the socket's writable flag so that any packets waiting for authentication to complete in + // the outgoing queue will be sent to the Zookeeper server. + cnxn.enableWrite(); + } + } + + private byte[] createSaslToken() throws SaslException { + saslState = SaslState.INTERMEDIATE; + return createSaslToken(saslToken); + } + + private byte[] createSaslToken(final byte[] saslToken) throws SaslException { + if (saslToken == null) { + // TODO: introspect about runtime environment (such as jaas.conf) + saslState = SaslState.FAILED; + throw new SaslException("Error in authenticating with a Zookeeper Quorum member: the quorum member's saslToken is null."); + } + + Subject subject = login.getSubject(); + if (subject != null) { + synchronized(login) { + try { + final byte[] retval = + Subject.doAs(subject, new PrivilegedExceptionAction() { + public byte[] run() throws SaslException { + LOG.debug("saslClient.evaluateChallenge(len="+saslToken.length+")"); + return saslClient.evaluateChallenge(saslToken); + } + }); + return retval; + } + catch (PrivilegedActionException e) { + String error = "An error: (" + e + ") occurred when evaluating Zookeeper Quorum Member's " + + " received SASL token."; + // Try to provide hints to use about what went wrong so they can fix their configuration. + // TODO: introspect about e: look for GSS information. + final String UNKNOWN_SERVER_ERROR_TEXT = + "(Mechanism level: Server not found in Kerberos database (7) - UNKNOWN_SERVER)"; + if (e.toString().indexOf(UNKNOWN_SERVER_ERROR_TEXT) > -1) { + error += " This may be caused by Java's being unable to resolve the Zookeeper Quorum Member's" + + " hostname correctly. You may want to try to adding" + + " '-Dsun.net.spi.nameservice.provider.1=dns,sun' to your client's JVMFLAGS environment."; + } + error += " Zookeeper Client will go to AUTH_FAILED state."; + LOG.error(error); + saslState = SaslState.FAILED; + throw new SaslException(error); + } + } + } + else { + throw new SaslException("Cannot make SASL token without subject defined. " + + "For diagnosis, please look for WARNs and ERRORs in your log related to the Login class."); + } + } + + private void sendSaslPacket(byte[] saslToken, ClientCnxn cnxn) + throws SaslException{ + if (LOG.isDebugEnabled()) { + LOG.debug("ClientCnxn:sendSaslPacket:length="+saslToken.length); + } + + GetSASLRequest request = new GetSASLRequest(); + request.setToken(saslToken); + SetSASLResponse response = new SetSASLResponse(); + ServerSaslResponseCallback cb = new ServerSaslResponseCallback(); + + try { + cnxn.sendPacket(request,response,cb, ZooDefs.OpCode.sasl); + } catch (IOException e) { + throw new SaslException("Failed to send SASL packet to server.", + e); + } + } + + private void sendSaslPacket(ClientCnxn cnxn) throws SaslException { + if (LOG.isDebugEnabled()) { + LOG.debug("ClientCnxn:sendSaslPacket:length="+saslToken.length); + } + GetSASLRequest request = new GetSASLRequest(); + request.setToken(createSaslToken()); + SetSASLResponse response = new SetSASLResponse(); + ServerSaslResponseCallback cb = new ServerSaslResponseCallback(); + try { + cnxn.sendPacket(request,response,cb, ZooDefs.OpCode.sasl); + } catch (IOException e) { + throw new SaslException("Failed to send SASL packet to server due " + + "to IOException:", e); + } + } + + // used by ClientCnxn to know whether to emit a SASL-related event: either AuthFailed or SaslAuthenticated, + // or none, if not ready yet. Sets saslState to COMPLETE as a side-effect. + public KeeperState getKeeperState() { + if (saslClient != null) { + if (saslState == SaslState.FAILED) { + return KeeperState.AuthFailed; + } + if (saslClient.isComplete()) { + if (saslState == SaslState.INTERMEDIATE) { + saslState = SaslState.COMPLETE; + return KeeperState.SaslAuthenticated; + } + } + } + // No event ready to emit yet. + return null; + } + + // Initialize the client's communications with the Zookeeper server by sending the server the first + // authentication packet. + public void initialize(ClientCnxn cnxn) throws SaslException { + if (saslClient == null) { + saslState = SaslState.FAILED; + throw new SaslException("saslClient failed to initialize properly: it's null."); + } + if (saslState == SaslState.INITIAL) { + if (saslClient.hasInitialResponse()) { + sendSaslPacket(cnxn); + } + else { + byte[] emptyToken = new byte[0]; + sendSaslPacket(emptyToken, cnxn); + } + saslState = SaslState.INTERMEDIATE; + } + } + + public boolean clientTunneledAuthenticationInProgress() { + if (!isSASLConfigured) { + return false; + } + // TODO: Rather than checking a disjunction here, should be a single member + // variable or method in this class to determine whether the client is + // configured to use SASL. (see also ZOOKEEPER-1455). + try { + if ((System.getProperty(Environment.JAAS_CONF_KEY) != null) || + ((javax.security.auth.login.Configuration.getConfiguration() != null) && + (javax.security.auth.login.Configuration.getConfiguration(). + getAppConfigurationEntry(System. + getProperty(ZooKeeperSaslClient.LOGIN_CONTEXT_NAME_KEY,"Client")) + != null))) { + // Client is configured to use a valid login Configuration, so + // authentication is either in progress, successful, or failed. + + // 1. Authentication hasn't finished yet: we must wait for it to do so. + if ((isComplete() == false) && + (isFailed() == false)) { + return true; + } + + // 2. SASL authentication has succeeded or failed.. + if (isComplete() || isFailed()) { + if (gotLastPacket == false) { + // ..but still in progress, because there is a final SASL + // message from server which must be received. + return true; + } + } + } + // Either client is not configured to use a tunnelled authentication + // scheme, or tunnelled authentication has completed (successfully or + // not), and all server SASL messages have been received. + return false; + } catch (SecurityException e) { + // Thrown if the caller does not have permission to retrieve the Configuration. + // In this case, simply returning false is correct. + if (LOG.isDebugEnabled() == true) { + LOG.debug("Could not retrieve login configuration: " + e); + } + return false; + } + } + + +} http://git-wip-us.apache.org/repos/asf/zookeeper/blob/7291e47c/zookeeper-server/src/main/java/org/apache/zookeeper/common/AtomicFileOutputStream.java ---------------------------------------------------------------------- diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/common/AtomicFileOutputStream.java b/zookeeper-server/src/main/java/org/apache/zookeeper/common/AtomicFileOutputStream.java new file mode 100644 index 0000000..2584d3f --- /dev/null +++ b/zookeeper-server/src/main/java/org/apache/zookeeper/common/AtomicFileOutputStream.java @@ -0,0 +1,126 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.zookeeper.common; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FilterOutputStream; +import java.io.IOException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/* + * This code is originally from HDFS, see the similarly named files there + * in case of bug fixing, history, etc... + */ + +/** + * A FileOutputStream that has the property that it will only show up at its + * destination once it has been entirely written and flushed to disk. While + * being written, it will use a .tmp suffix. + * + * When the output stream is closed, it is flushed, fsynced, and will be moved + * into place, overwriting any file that already exists at that location. + * + * NOTE: on Windows platforms, it will not atomically replace the target + * file - instead the target file is deleted before this one is moved into + * place. + */ +public class AtomicFileOutputStream extends FilterOutputStream { + private static final String TMP_EXTENSION = ".tmp"; + + private final static Logger LOG = LoggerFactory + .getLogger(AtomicFileOutputStream.class); + + private final File origFile; + private final File tmpFile; + + public AtomicFileOutputStream(File f) throws FileNotFoundException { + // Code unfortunately must be duplicated below since we can't assign + // anything + // before calling super + super(new FileOutputStream(new File(f.getParentFile(), f.getName() + + TMP_EXTENSION))); + origFile = f.getAbsoluteFile(); + tmpFile = new File(f.getParentFile(), f.getName() + TMP_EXTENSION) + .getAbsoluteFile(); + } + + /** + * The default write method in FilterOutputStream does not call the write + * method of its underlying input stream with the same arguments. Instead + * it writes the data byte by byte, override it here to make it more + * efficient. + */ + @Override + public void write(byte b[], int off, int len) throws IOException { + out.write(b, off, len); + } + + @Override + public void close() throws IOException { + boolean triedToClose = false, success = false; + try { + flush(); + ((FileOutputStream) out).getChannel().force(true); + + triedToClose = true; + super.close(); + success = true; + } finally { + if (success) { + boolean renamed = tmpFile.renameTo(origFile); + if (!renamed) { + // On windows, renameTo does not replace. + if (!origFile.delete() || !tmpFile.renameTo(origFile)) { + throw new IOException( + "Could not rename temporary file " + tmpFile + + " to " + origFile); + } + } + } else { + if (!triedToClose) { + // If we failed when flushing, try to close it to not leak + // an FD + IOUtils.closeStream(out); + } + // close wasn't successful, try to delete the tmp file + if (!tmpFile.delete()) { + LOG.warn("Unable to delete tmp file " + tmpFile); + } + } + } + } + + /** + * Close the atomic file, but do not "commit" the temporary file on top of + * the destination. This should be used if there is a failure in writing. + */ + public void abort() { + try { + super.close(); + } catch (IOException ioe) { + LOG.warn("Unable to abort file " + tmpFile, ioe); + } + if (!tmpFile.delete()) { + LOG.warn("Unable to delete tmp file during abort " + tmpFile); + } + } +} http://git-wip-us.apache.org/repos/asf/zookeeper/blob/7291e47c/zookeeper-server/src/main/java/org/apache/zookeeper/common/IOUtils.java ---------------------------------------------------------------------- diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/common/IOUtils.java b/zookeeper-server/src/main/java/org/apache/zookeeper/common/IOUtils.java new file mode 100644 index 0000000..16aea4e --- /dev/null +++ b/zookeeper-server/src/main/java/org/apache/zookeeper/common/IOUtils.java @@ -0,0 +1,123 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.zookeeper.common; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintStream; + +import org.slf4j.Logger; + +/* + * This code is originally from HDFS, see the similarly named files there + * in case of bug fixing, history, etc... + */ + +public class IOUtils { + /** + * Closes the stream ignoring {@link IOException}. Must only be called in + * cleaning up from exception handlers. + * + * @param stream + * the Stream to close + */ + public static void closeStream(Closeable stream) { + cleanup(null, stream); + } + + /** + * Close the Closeable objects and ignore any {@link IOException} or + * null pointers. Must only be used for cleanup in exception handlers. + * + * @param log + * the log to record problems to at debug level. Can be null. + * @param closeables + * the objects to close + */ + public static void cleanup(Logger log, Closeable... closeables) { + for (Closeable c : closeables) { + if (c != null) { + try { + c.close(); + } catch (IOException e) { + if (log != null) { + log.warn("Exception in closing " + c, e); + } + } + } + } + } + + /** + * Copies from one stream to another. + * + * @param in + * InputStrem to read from + * @param out + * OutputStream to write to + * @param buffSize + * the size of the buffer + * @param close + * whether or not close the InputStream and OutputStream at the + * end. The streams are closed in the finally clause. + */ + public static void copyBytes(InputStream in, OutputStream out, + int buffSize, boolean close) throws IOException { + try { + copyBytes(in, out, buffSize); + if (close) { + out.close(); + out = null; + in.close(); + in = null; + } + } finally { + if (close) { + closeStream(out); + closeStream(in); + } + } + } + + /** + * Copies from one stream to another. + * + * @param in + * InputStrem to read from + * @param out + * OutputStream to write to + * @param buffSize + * the size of the buffer + */ + public static void copyBytes(InputStream in, OutputStream out, int buffSize) + throws IOException { + PrintStream ps = out instanceof PrintStream ? (PrintStream) out : null; + byte buf[] = new byte[buffSize]; + int bytesRead = in.read(buf); + while (bytesRead >= 0) { + out.write(buf, 0, bytesRead); + if ((ps != null) && ps.checkError()) { + throw new IOException("Unable to write to output stream."); + } + bytesRead = in.read(buf); + } + } + +}