com.fluffypeople.managesieve.ManageSieveClient.java Source code

Java tutorial

Introduction

Here is the source code for com.fluffypeople.managesieve.ManageSieveClient.java

Source

/*
 * The MIT License
 *
 * Copyright 2013 "Osric Wilkinson" <osric@fluffypeople.com>.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package com.fluffypeople.managesieve;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StreamTokenizer;
import java.net.InetAddress;
import java.net.Socket;
import java.net.SocketException;
import java.nio.charset.Charset;
import java.security.Principal;
import java.util.List;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.x500.X500Principal;
import javax.security.sasl.Sasl;
import javax.security.sasl.SaslClient;
import javax.security.sasl.SaslException;
import org.apache.commons.codec.binary.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A client for the Manage Sieve protocol. Manage sieve (<a
 * href="http://tools.ietf.org/html/rfc5804">rfc5804</a>) is used to manage the
 * sieve mail filtering scripts on a server. (Sieve is defined in <a
 * href="http://tools.ietf.org/html/rfc5228">rfc5228>/a>).
 * <p>
 * This class manages the client side of the connection. The basic pattern is
 * connect, upgrade to TLS, authenticate, issue commands, logout, close
 * connection.
 * <p>
 * Most commands take string arguments and return {@link ManageSieveResponse}
 * objects. {@link #putscript} takes an {@link SieveScript} as an argument and
 * stores the result in that object.
 *
 * @author "Osric Wilkinson" <osric@fluffypeople.com>
 */
public class ManageSieveClient {

    private static final Logger log = LoggerFactory.getLogger(ManageSieveClient.class);
    private static final Charset UTF8 = Charset.forName("UTF-8");
    private static final char DQUOTE = '"';
    private static final char LEFT_CURRLY_BRACE = '{';
    private static final char LEFT_BRACKET = '(';
    private static final char RIGHT_BRACKET = ')';
    private static final String CRLF = "\r\n";
    private static final char SP = ' ';
    private Socket socket = null;
    private SSLSocket secureSocket = null;
    private ServerCapabilities cap;
    private StreamTokenizer in;
    private BufferedInputStream byteStream;
    private PrintWriter out;
    private String hostname;
    private int socketTimeout = 0; // Default socket timeout is zero, or don't time out.

    /**
     * Public constructor.
     */
    public ManageSieveClient() {
    }

    /**
     * Get the current known server capabilities. Will return null if called
     * before the server has been connected.
     *
     * @return
     */
    public ServerCapabilities getCapabilities() {
        return cap;
    }

    /**
     * Returns setting for SO_TIMEOUT. 0 returns implies that the option is
     * disabled (i.e., timeout of infinity).</p>
     *
     * If the socket isn't connected, return the cached value that will be set
     * once the socket does connect.</p>
     *
     *
     * @return the setting for SO_TIMEOUT
     * @throws SocketException - if there is an error in the underlying
     * protocol, such as a TCP error.
     * @see java.net.Socket#getSoTimeout(int)
     */
    public int getSocketTimeout() throws SocketException {
        return socket != null ? socket.getSoTimeout() : socketTimeout;
    }

    /**
     * Set SO_TIMEOUT. Updates a connected socket (and is stored for use when a
     * socket connects).</p>
     *
     * From <code>Socket.setSoTimeout</code>: "Enable/disable SO_TIMEOUT with
     * the specified timeout, in milliseconds. With this option set to a
     * non-zero timeout, a read() call on the InputStream associated with this
     * Socket will block for only this amount of time. If the timeout expires, a
     * java.net.SocketTimeoutException is raised, though the Socket is still
     * valid. The option must be enabled prior to entering the blocking
     * operation to have effect. The timeout must be > 0. A timeout of zero is
     * interpreted as an infinite timeout."</p>
     *
     * @param timeout the specified timeout, in milliseconds.
     * @throws SocketException if there is an error in the underlying protocol,
     * such as a TCP error.
     *
     * @see java.net.Socket#setSoTimeout(int)
     */
    public void setSocketTimeout(int timeout) throws SocketException {
        this.socketTimeout = timeout;
        if (socket != null) {
            socket.setSoTimeout(timeout);
        }
    }

    /**
     * Connect to remote server
     *
     * @return ManageSieveResponse OK on connect, NO on connection problems
     * @throws IOException if there are underlying IO issues
     * @throws ParseException if we can't parse the response from the server
     */
    public synchronized ManageSieveResponse connect(final String host, final int port)
            throws IOException, ParseException {
        hostname = host;
        socket = new Socket(InetAddress.getByName(hostname), port);

        setupAfterConnect(socket);
        return parseCapabilities();
    }

    /**
     * Returns true if the underlying socket is connected.
     *
     * @return
     */
    public synchronized boolean isConnected() {
        return socket.isConnected();
    }

    /**
     * Upgrade connection to TLS. Should be called before authenticating,
     * especially if you are using the PLAIN scheme.
     *
     * @return ManageSieveResponse OK on successful upgrade, NO on error or if
     * the server doesn't support SSL
     * @throws IOException
     * @throws ParseException
     */
    public synchronized ManageSieveResponse starttls() throws IOException, ParseException {
        return starttls((SSLSocketFactory) SSLSocketFactory.getDefault(), true);
    }

    /**
     * Upgrade connection to TLS. Should be called before authenticating,
     * especially if you are using the PLAIN scheme.
     *
     * @param sslSocketFactory
     * @return ManageSieveResponse OK on successful upgrade, NO on error or if
     * the server doesn't support SSL
     * @throws IOException
     * @throws ParseException
     */
    public synchronized ManageSieveResponse starttls(final SSLSocketFactory sslSocketFactory,
            final boolean rfcCheck) throws IOException, ParseException {
        sendCommand("STARTTLS");
        ManageSieveResponse resp = parseResponse();
        if (resp.isOk()) {
            secureSocket = (SSLSocket) sslSocketFactory.createSocket(socket,
                    socket.getInetAddress().getHostAddress(), socket.getPort(), true);
            if (rfcCheck) {
                // The manage sieve rfc says we should check that the name in the certificate
                // matches the hostname that we want.

                Principal p = secureSocket.getSession().getPeerPrincipal();
                if (p instanceof X500Principal) {
                    String serverName = getHostnameFromCert((X500Principal) p);
                    if (!hostname.equals(serverName)) {
                        throw new IOException("Secure connect failed: Server name " + serverName
                                + " doesn't match wanted " + hostname);
                    }
                } else {
                    log.warn("Unexpected principle: {}", p.getName());
                }
            }
            setupAfterConnect(secureSocket);
            return parseCapabilities();

        } else {
            return resp;
        }
    }

    /**
     * Authenticate against the remote server using SASL.
     *
     * The CallbackHandler should be setup appropriately, for example:
     * <pre>
     * <code>
     *
     * CallbackHandler cbh = new CallbackHandler() {
     *     public void handle(Callback[] clbcks) throws IOException,  UnsupportedCallbackException {
     *         for (Callback cb : clbcks) {
     *             if (cb instanceof NameCallback) {
     *                 NameCallback name = (NameCallback) cb;
     *                 name.setName("user");
     *             } else if (cb instanceof PasswordCallback) {
     *                 PasswordCallback passwd = (PasswordCallback) cb;
     *                 passwd.setPassword("secret".toCharArray());
     *             }
     *         }
     *     }
     * };
     * </code>
     * </pre>
     *
     * @param cbh CallbackHandler[] list of call backs that will be called by
     * the SASL code
     * @return ManageSieveResponse from the server, OK is authenticated, NO
     * means a problem
     * @throws SaslException
     * @throws IOException
     * @throws ParseException
     */
    public synchronized ManageSieveResponse authenticate(final CallbackHandler cbh)
            throws SaslException, IOException, ParseException {

        SaslClient sc = Sasl.createSaslClient(cap.getSASLMethods(), null, "sieve", hostname, null, cbh);

        String mechanism = escapeString(sc.getMechanismName());
        if (sc.hasInitialResponse()) {
            byte[] ir = sc.evaluateChallenge(new byte[0]);
            String ready = new String(Base64.encodeBase64(ir));
            ready = encodeString(ready.trim());
            sendCommand("AUTHENTICATE", mechanism, ready);
        } else {
            sendCommand("AUTHENTICATE", mechanism);
        }

        int token;
        ManageSieveResponse resp = null;
        do {
            token = in.nextToken();
            if (token == DQUOTE) {
                // String - so more data for the auth sequence
                in.pushBack();
                String msg = parseString();
                byte[] response = sc.evaluateChallenge(msg.getBytes());
                sendLine(encodeString(new String(response)));
            } else if (token == StreamTokenizer.TT_WORD) {
                in.pushBack();
                resp = parseResponse();
                break;
            } else {
                throw new ParseException(
                        "Expecting DQUOTE/WORD, got " + tokenToString(token) + " at line " + in.lineno());
            }
        } while (!sc.isComplete());

        // Complete
        sc.dispose();
        return resp;
    }

    /**
     * Authenticate against the remote server using SAS, using the given
     * username and password.
     *
     * @param username String username to authenticate with.
     * @param password String password to authenticate with.
     * @return OK on success, NO otherwise.
     */
    public synchronized ManageSieveResponse authenticate(final String username, final String password)
            throws SaslException, IOException, ParseException {
        CallbackHandler cbh = new CallbackHandler() {

            @Override
            public void handle(Callback[] clbcks) throws IOException, UnsupportedCallbackException {
                for (Callback cb : clbcks) {
                    if (cb instanceof NameCallback) {
                        NameCallback name = (NameCallback) cb;
                        name.setName(username);
                    } else if (cb instanceof PasswordCallback) {
                        PasswordCallback passwd = (PasswordCallback) cb;
                        passwd.setPassword(password.toCharArray());
                    }
                }
            }
        };
        return authenticate(cbh);
    }

    /**
     * "This command lists the scripts the user has on the server". The results
     * are stored into the @code{List<SieveScript>} passed in. Any existing
     * contents of this list will be lost. Up to one of the scripts listed will
     * be marked active.
     *
     * @param scripts @code{List<SieveScript>} non-null List of scripts. Will be
     * cleared if not zero length, even if there is a problem
     * @return ManageSieveResponse OK - list was fetched, NO - there was a
     * problem.
     * @throws IOException
     * @throws ParseException
     */
    public synchronized ManageSieveResponse listscripts(List<SieveScript> scripts)
            throws IOException, ParseException {
        if (!scripts.isEmpty()) {
            scripts.clear();
        }
        sendCommand("LISTSCRIPTS");
        while (true) {
            int token = in.nextToken();
            switch (token) {
            case DQUOTE:
            case LEFT_CURRLY_BRACE:
                in.pushBack();

                String scriptName = parseString();
                boolean isActive = false;
                token = in.nextToken();
                if (token == StreamTokenizer.TT_WORD) {
                    if (in.sval.equals("ACTIVE")) {
                        // active script;
                        isActive = true;
                    } else {
                        throw new ParseException("Unexpected word " + in.sval + " at line " + in.lineno());
                    }
                    token = in.nextToken();
                }

                if (token == StreamTokenizer.TT_EOL) {
                    scripts.add(new SieveScript(scriptName, null, isActive));
                } else {
                    throw new ParseException(
                            "Expected EOL, got  " + tokenToString(token) + " at line " + in.lineno());
                }
                break;
            case StreamTokenizer.TT_WORD:
                in.pushBack();
                return parseResponse();
            default:
                throw new ParseException("Unexpected token " + tokenToString(token) + " at line " + in.lineno());
            }
        }
    }

    /**
     * "The HAVESPACE command is used to query the server for available space".
     * Specify the name of a script, and the size (in bytes). The server will
     * check if creating a script with the given name and size will be within
     * the users quota. Note that an OK response doesn't guarantee that a
     * PUTSCRIPT command will work, since the server may be updated in another
     * thread in the meantime.
     *
     * @param name
     * @param size
     * @return
     * @throws IOException
     * @throws ParseException
     */
    public synchronized ManageSieveResponse havespace(final String name, final long size)
            throws IOException, ParseException {
        String escapedName = escapeString(name);
        String number = Long.toString(size, 10);
        sendCommand("HAVESPACE", escapedName, number);
        return parseResponse();
    }

    /**
     * "The PUTSCRIPT command is used by the client to submit a Sieve script to
     * the server". This will overwrite any existing script with the same name.
     * The active state of the named script is not changed (so replacing an
     * active script leaves that script active, otherwise the script will not be
     * active until SETACTIVE is used).
     * <p>
     * The server will check the script is valid before storing (and overwriting
     * if needed) the script, and will return parse errors in the "Human
     * readable" part of the response.
     * <p>
     * Even if the script is valid the response may contain WARNINGS.
     *
     *
     * @param name String name of the script
     * @param body String body of the script
     * @return OK if the script is added, NO on error
     * @throws IOException
     * @throws ParseException
     */
    public synchronized ManageSieveResponse putscript(final String name, final String body)
            throws IOException, ParseException {
        String encodedName = encodeString(name);
        String encodedBody = encodeString(body);
        sendCommand("PUTSCRIPT", encodedName, encodedBody);
        return parseResponse();
    }

    /**
     * "This command gets the contents of the specified script". The name of the
     * script is taken from the script parameter, and the body is stored in the
     * object
     *
     * @param script SieveScript to fetch/update
     * @return OK or NO response.
     */
    public synchronized ManageSieveResponse getScript(SieveScript script) throws IOException, ParseException {
        String encodedName = encodeString(script.getName());
        sendCommand("GETSCRIPT", encodedName);
        script.setBody(parseString());
        int token = in.nextToken();
        if (token != StreamTokenizer.TT_EOL) {
            throw new ParseException("Expecting EOL but got " + tokenToString(token) + " at line " + in.lineno());
        }
        return parseResponse();
    }

    /**
     * "This command is used to delete a user's Sieve script".
     *
     * @param name String name of the script to delete
     * @return OK if the script was deleted, NO otherwise
     * @throws IOException
     * @throws ParseException
     */
    public synchronized ManageSieveResponse deletescript(final String name) throws IOException, ParseException {
        String encodedName = encodeString(name);
        sendCommand("DELETESCRIPT", encodedName);
        return parseResponse();
    }

    /**
     * "This command sets a script active". The active script is the one used by
     * the MDA to filter incoming mail. It is not an error to have no scripts
     * active, or to set the same script active twice.
     * <p>
     * Use the empty string ("") to set no scripts active.
     *
     * @param name String name of the script to set active
     * @return
     * @throws IOException
     * @throws ParseException
     */
    public synchronized ManageSieveResponse setactive(final String name) throws IOException, ParseException {
        String encodedName = encodeString(name);
        sendCommand("SETACTIVE", encodedName);
        return parseResponse();
    }

    public synchronized ManageSieveResponse logout() throws IOException, ParseException {
        sendCommand("LOGOUT");
        return parseResponse();
    }

    public synchronized ManageSieveResponse renamescript(final String oldName, final String newName)
            throws IOException, ParseException {
        String encodedOldName = encodeString(oldName);
        String encodedNewName = encodeString(newName);
        sendCommand("RENAMESCRIPT", encodedOldName, encodedNewName);
        return parseResponse();
    }

    public synchronized ManageSieveResponse checkscript(final String script) throws IOException, ParseException {
        String encodedScript = encodeString(script);
        sendCommand("CHECKSCRIPT", encodedScript);
        return parseResponse();
    }

    public synchronized ManageSieveResponse noop(final String tag) throws IOException, ParseException {
        if (tag != null) {
            String encodedTag = encodeString(tag);
            sendCommand("NOOP", encodedTag);
        } else {
            sendCommand("NOOP");
        }
        return parseResponse();
    }

    public synchronized ManageSieveResponse capability() throws IOException, ParseException {
        sendCommand("CAPABILITY");
        parseCapabilities();
        return parseResponse();
    }

    private ManageSieveResponse parseCapabilities() throws IOException, ParseException {
        cap = new ServerCapabilities();

        while (true) {
            int token = in.nextToken();
            switch (token) {
            case StreamTokenizer.TT_WORD:
                // Unquoted word - end of capabilites
                in.pushBack();
                return parseResponse();
            case DQUOTE:
            case LEFT_CURRLY_BRACE:
                // Capabilities can be either literal or quoted
                in.pushBack();
                String word = parseString();
                if (word.equalsIgnoreCase("IMPLEMENTATION")) {
                    cap.setImplementationName(parseString());
                } else if (word.equalsIgnoreCase("SASL")) {
                    cap.setSASLMethods(parseString());
                } else if (word.equalsIgnoreCase("SIEVE")) {
                    cap.setSieveExtensions(parseString());
                } else if (word.equalsIgnoreCase("MAXREDIRECTS")) {
                    token = in.nextToken();
                    if (token == StreamTokenizer.TT_NUMBER) {
                        cap.setMaxRedirects((int) in.nval);
                    } else {
                        throw new ParseException(
                                "Expecting NUMBER got " + tokenToString(token) + " at " + in.lineno());
                    }
                } else if (word.equalsIgnoreCase("NOTIFY")) {
                    cap.setNotify(parseString());
                } else if (word.equalsIgnoreCase("STARTTLS")) {
                    cap.setHasTLS(true);
                } else if (word.equalsIgnoreCase("LANGUAGE")) {
                    cap.setLanguage(parseString());
                } else if (word.equalsIgnoreCase("VERSION")) {
                    cap.setVersion(parseString());
                } else if (word.equalsIgnoreCase("OWNER")) {
                    cap.setOwner(parseString());
                } else {
                    // Unknown capability, read until EOL
                    while (token != StreamTokenizer.TT_EOL) {
                        token = in.nextToken();
                    }
                    in.pushBack();
                }
                token = in.nextToken();
                if (token != StreamTokenizer.TT_EOL) {
                    throw new ParseException("Expecing EOL got " + tokenToString(token) + " at " + in.lineno());
                }
                break;

            default:
                throw new ParseException("Unexpected token " + token + " at " + in.lineno());
            }
        }
    }

    private ManageSieveResponse parseResponse() throws IOException, ParseException {
        ManageSieveResponse resp = new ManageSieveResponse();
        int token = in.nextToken();
        if (token == StreamTokenizer.TT_WORD) {
            // Get the type (OK NO BYTE)
            resp.setType(in.sval);
            token = in.nextToken();
            // Check for reason code
            if (token == LEFT_BRACKET) {
                token = in.nextToken();
                if (token == StreamTokenizer.TT_WORD) {
                    resp.setCode(in.sval);
                } else {
                    throw new ParseException(
                            "Expecting LEFT_BRACKET got " + tokenToString(token) + " at line " + in.lineno());
                }
                if (resp.getCode().hasParam()) {
                    resp.setParam(parseString());
                }
                token = in.nextToken();
                if (token != RIGHT_BRACKET) {
                    throw new ParseException(
                            "Expecting RIGHT_BRACKET got " + tokenToString(token) + " at line " + in.lineno());
                }
            } else {
                in.pushBack();
            }
            // Check for human readable message
            token = in.nextToken();
            if (token != StreamTokenizer.TT_EOL) {
                in.pushBack();
                resp.setMessage(parseString());
                token = in.nextToken();
            }

            // Done, end of line
            if (token != StreamTokenizer.TT_EOL) {
                throw new ParseException("Expecting EOL got " + tokenToString(token) + " at line " + in.lineno());
            }

        } else {
            throw new ParseException("Expecting WORD got " + tokenToString(token) + " at line " + in.lineno());
        }
        return resp;
    }

    private String parseString() throws IOException, ParseException {
        int token = in.nextToken();
        if (token == DQUOTE) {
            return in.sval;
        } else if (token == '{') {
            // "Literal" String - {<length>}CRLF<length bytes of string>
            token = in.nextToken();
            if (token != StreamTokenizer.TT_NUMBER) {
                throw new ParseException(
                        "Expecting NUMBER got " + tokenToString(token) + " at line " + in.lineno());
            }
            // Irritatingly, the tokenizer will parse a double here, even
            // if we only want an int. Sigh.
            int length = (int) in.nval;
            token = in.nextToken();
            if (token != '}') {
                throw new ParseException("Expecing } got " + tokenToString(token) + " at line " + in.lineno());
            }
            token = in.nextToken();
            if (token != StreamTokenizer.TT_EOL) {
                throw new ParseException("Expecting EOL got " + tokenToString(token) + " at line " + in.lineno());
            }
            // Drop out of the tokenizer to read the raw bytes...

            StringBuilder rawString = new StringBuilder();
            log.debug("Raw string: reading {} bytes", length);

            in.resetSyntax();
            int count = 0;
            while (count < length) {
                token = in.nextToken();
                if (token == StreamTokenizer.TT_WORD) {
                    // Tokenizer calls unicode "WORD" even in raw(ish) mode
                    rawString.append(in.sval);
                    count += in.sval.getBytes(UTF8).length;
                } else {
                    // Probably only ever one byte chars, however lets be
                    // careful out there.
                    char[] chars = Character.toChars(token);
                    rawString.append(chars);
                    count += chars.length;
                }
            }

            // Remember to reset the tokenizer now we're done
            setupTokenizer();

            return rawString.toString();
        } else {
            throw new ParseException(
                    "Expecing DQUOTE or {, got " + tokenToString(token) + " at line " + in.lineno());
        }
    }

    private String escapeString(final String raw) {
        StringBuilder result = new StringBuilder();
        result.append(DQUOTE);
        result.append(raw);
        result.append(DQUOTE);
        return result.toString();
    }

    /**
     * Turn a string into a {'length'+}.... form
     *
     * @param raw String to convert
     * @return converted String
     */
    private String encodeString(final String raw) {
        StringBuilder result = new StringBuilder();

        result.append("{");
        result.append(Integer.toString(raw.getBytes(UTF8).length));
        result.append("}");
        result.append(CRLF);
        result.append(raw);

        return result.toString();
    }

    private void sendCommand(final String command, String... param) throws IOException {
        StringBuilder line = new StringBuilder();
        line.append(command);
        if (param != null) {
            for (int i = 0; i < param.length; i++) {
                line.append(SP);
                line.append(param[i]);
            }
        }
        sendLine(line.toString());
    }

    private void sendLine(final String line) throws IOException {
        log.debug("Sending line: " + line);
        out.print(line);
        out.print(CRLF);
        out.flush();

        if (out.checkError()) {
            throw new IOException("Unknown error writing to server");
        }
    }

    private void setupAfterConnect(Socket sock) throws IOException {
        sock.setSoTimeout(socketTimeout);
        byteStream = new BufferedInputStream(sock.getInputStream());
        in = new StreamTokenizer(new InputStreamReader(byteStream, UTF8));
        setupTokenizer();
        out = new PrintWriter(new OutputStreamWriter(sock.getOutputStream()));
    }

    private void setupTokenizer() {
        in.resetSyntax();

        in.whitespaceChars(0x20, 0x20);
        in.whitespaceChars(0x0A, 0x0A);
        in.whitespaceChars(0x0D, 0x0D);

        in.wordChars(0x23, 0x27);
        in.wordChars(0x2A, 0x5B);
        in.wordChars(0x5D, 0x7A);
        in.wordChars(0x7C, 0x7C);
        in.wordChars(0x7E, 0x7E);

        in.quoteChar(DQUOTE);
        in.parseNumbers();
        in.eolIsSignificant(true);
    }

    private String tokenToString(final int c) {
        if (c > 0) {

            return new String(Character.toChars(c));
        } else {
            switch (c) {
            case StreamTokenizer.TT_EOF:
                return "EOF";
            case StreamTokenizer.TT_NUMBER:
                return "NUMBER";
            case StreamTokenizer.TT_EOL:
                return "EOL";
            case StreamTokenizer.TT_WORD:
                return ("WORD [" + in.sval + "]");
            default:
                return "UNKNOWN";
            }
        }
    }

    private String getHostnameFromCert(X500Principal principal) {
        String raw = principal.getName("CANONICAL");
        for (String phrase : raw.split(",")) {
            String[] parts = phrase.split("=");
            String key = parts[0];
            String value = parts[1];
            if (key.equals("cn")) {
                return value;
            }
        }
        return null;
    }
}