Mercurial > hg > monetdb-java
changeset 834:5aa19bbed0d6 monetdbs
Comments and formatting
author | Joeri van Ruth <joeri.van.ruth@monetdbsolutions.com> |
---|---|
date | Wed, 13 Dec 2023 15:39:47 +0100 (16 months ago) |
parents | a71afa48f269 |
children | 071be9a628e8 |
files | src/main/java/org/monetdb/client/JdbcClient.java src/main/java/org/monetdb/jdbc/MonetConnection.java src/main/java/org/monetdb/jdbc/MonetDriver.java src/main/java/org/monetdb/mcl/net/MapiSocket.java src/main/java/org/monetdb/mcl/net/MonetUrlParser.java src/main/java/org/monetdb/mcl/net/Parameter.java src/main/java/org/monetdb/mcl/net/ParameterType.java src/main/java/org/monetdb/mcl/net/SecureSocket.java src/main/java/org/monetdb/mcl/net/Target.java src/main/java/org/monetdb/mcl/net/ValidationError.java tests/TLSTester.java tests/UrlTester.java |
diffstat | 12 files changed, 2054 insertions(+), 1839 deletions(-) [+] |
line wrap: on
line diff
--- a/src/main/java/org/monetdb/client/JdbcClient.java +++ b/src/main/java/org/monetdb/client/JdbcClient.java @@ -192,9 +192,6 @@ public final class JdbcClient { "statements read. Batching can greatly speedup the " + "process of restoring a database dump."); -// This file can contain defaults for the flags user, password, language, -// database, save_history, format, host, port, and width. For example, an - copts.addIgnored("save_history"); copts.addIgnored("format"); copts.addIgnored("width");
--- a/src/main/java/org/monetdb/jdbc/MonetConnection.java +++ b/src/main/java/org/monetdb/jdbc/MonetConnection.java @@ -75,7 +75,7 @@ public class MonetConnection extends MonetWrapper implements Connection, AutoCloseable { - /** All connection parameters */ + /* All connection parameters */ Target target; /** A connection to mserver5 using a TCP socket */ private final MapiSocket server; @@ -139,13 +139,11 @@ public class MonetConnection private DatabaseMetaData dbmd; /** - * Constructor of a Connection for MonetDB. At this moment the - * current implementation limits itself to storing the given host, - * database, username and password for later use by the - * createStatement() call. This constructor is only accessible to + * Constructor of a Connection for MonetDB. + * This constructor is only accessible to * classes from the jdbc package. * - * @param target a Target object holding the connection parameters + * @param target a {@link Target} object containing all connection parameters * @throws SQLException if a database error occurs * @throws IllegalArgumentException is one of the arguments is null or empty */ @@ -184,20 +182,20 @@ public class MonetConnection switch (target.getLanguage()) { case "sql": lang = LANG_SQL; - queryTempl[0] = "s"; // pre - queryTempl[1] = "\n;"; // post - queryTempl[2] = "\n;\n"; // separator - commandTempl[0] = "X"; // pre - commandTempl[1] = ""; // post + queryTempl[0] = "s"; // pre + queryTempl[1] = "\n;"; // post + queryTempl[2] = "\n;\n"; // separator + commandTempl[0] = "X"; // pre + commandTempl[1] = ""; // post callback = new SqlOptionsCallback(); break; case "mal": lang = LANG_MAL; - queryTempl[0] = ""; // pre - queryTempl[1] = ";\n"; // post - queryTempl[2] = ";\n"; // separator - commandTempl[0] = ""; // pre - commandTempl[1] = ""; // post + queryTempl[0] = ""; // pre + queryTempl[1] = ";\n"; // post + queryTempl[2] = ";\n"; // separator + commandTempl[0] = ""; // pre + commandTempl[1] = ""; // post break; default: lang = LANG_UNKNOWN; @@ -1248,6 +1246,15 @@ public class MonetConnection return isValid; } + /** + * Construct a Properties object holding all connection parameters such + * as host, port, TLS configuration, autocommit, etc. + * Passing this to {@link DriverManager.getConnection()} together + * with the URL "jdbc:monetdb:" will create a new connection identical to + * the current one. + * @return + */ + public Properties getConnectionProperties() { return target.getProperties(); } @@ -3747,7 +3754,8 @@ public class MonetConnection } } - public static enum SqlOption { + /* encode knowledge of currently available handshake options as an enum. */ + enum SqlOption { Autocommit(1, "auto_commit"), ReplySize(2, "reply_size"), SizeHeader(3, "size_header"), @@ -3766,6 +3774,7 @@ public class MonetConnection private class SqlOptionsCallback extends MapiSocket.OptionsCallback { private int level; + @Override public void addOptions(String lang, int level) { if (!lang.equals("sql")) @@ -3784,7 +3793,7 @@ public class MonetConnection } private boolean contribute(SqlOption opt, int value) { - if (this.level <= opt.level) + if (opt.level >= this.level) return false; contribute(opt.field, value); return true;
--- a/src/main/java/org/monetdb/jdbc/MonetDriver.java +++ b/src/main/java/org/monetdb/jdbc/MonetDriver.java @@ -8,19 +8,11 @@ package org.monetdb.jdbc; -import org.monetdb.mcl.net.MonetUrlParser; -import org.monetdb.mcl.net.Parameter; import org.monetdb.mcl.net.Target; import org.monetdb.mcl.net.ValidationError; import java.net.URISyntaxException; -import java.sql.Connection; -import java.sql.Driver; -import java.sql.DriverManager; -import java.sql.DriverPropertyInfo; -import java.sql.SQLException; -import java.sql.SQLFeatureNotSupportedException; -import java.sql.Types; +import java.sql.*; import java.util.Map.Entry; import java.util.Properties; @@ -70,11 +62,9 @@ public final class MonetDriver implement */ @Override public boolean acceptsURL(final String url) { - if (url == null) + if (url == null) return false; - if (url.startsWith("jdbc:monetdb:") || url.startsWith("jdbc:monetdbs:")) - return true; - return false; + return url.startsWith("jdbc:monetdb:") || url.startsWith("jdbc:monetdbs:"); } /**
--- a/src/main/java/org/monetdb/mcl/net/MapiSocket.java +++ b/src/main/java/org/monetdb/mcl/net/MapiSocket.java @@ -22,7 +22,6 @@ import java.nio.charset.StandardCharsets import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.*; -import java.util.stream.Collectors; import org.monetdb.mcl.MCLException; import org.monetdb.mcl.io.BufferedMCLReader; @@ -81,7 +80,12 @@ import org.monetdb.mcl.parser.MCLParseEx * @see org.monetdb.mcl.io.BufferedMCLWriter */ public final class MapiSocket { - public static final byte[] NUL_BYTES = new byte[]{ 0, 0, 0, 0, 0, 0, 0, 0, }; + /* an even number of NUL bytes used during the handshake */ + private static final byte[] NUL_BYTES = new byte[]{ 0, 0, 0, 0, 0, 0, 0, 0, }; + + /* A mapping between hash algorithm names as used in the MAPI + * protocol, and the names by which the Java runtime knows them. + */ private static final String[][] KNOWN_ALGORITHMS = new String[][] { {"SHA512", "SHA-512"}, {"SHA384", "SHA-384"}, @@ -262,6 +266,26 @@ public final class MapiSocket { return connect(new Target(url, props), null); } + /** + * Connect according to the settings in the 'target' parameter. + * If followRedirect is false, a RedirectionException is + * thrown when a redirect is encountered. + * + * Some settings, such as the initial reply size, can already be configured + * during the handshake, saving a command round-trip later on. + * To do so, create and pass a subclass of {@link MapiSocket.OptionsCallback}. + * + * @param target the connection settings + * @param callback will be called if the server allows options to be set during the + * initial handshake + * @return A List with informational (warning) messages. If this + * list is empty; then there are no warnings. + * @throws IOException if an I/O error occurs when creating the socket + * @throws SocketException - if there is an error in the underlying protocol, such as a TCP error. + * @throws UnknownHostException if the IP address of the host could not be determined + * @throws MCLParseException if bogus data is received + * @throws MCLException if an MCL related error occurs + */ public List<String> connect(Target target, OptionsCallback callback) throws MCLException, MCLParseException, IOException { // get rid of any earlier connection state, including the existing target close(); @@ -521,7 +545,7 @@ public final class MapiSocket { } return digest; } - String algoNames = algos.stream().collect(Collectors.joining()); + String algoNames = String.join(",", algos); throw new MCLException("No supported hash algorithm: " + algoNames); } @@ -1448,9 +1472,35 @@ public final class MapiSocket { } } + /** + * Callback used during the initial MAPI handshake. + * + * Newer MonetDB versions allow setting some options during the handshake. + * The options are language-specific and each has a 'level'. The server + * advertises up to which level options are supported for a given language + * and for each language/option combination, {@link addOptions} will be invoked. + * It should call {@link contribute} for each option it wants to set. + * + * At the time of writing, only the 'sql' language supports options, + * they are listed in enum mapi_handshake_options_levels in mapi.h. + */ public static abstract class OptionsCallback { private StringBuilder buffer; + /** + * Callback called for each language/level combination supported by the + * server. May call {@link contribute} for options with a level STRICTLY + * LOWER than the level passed as a parameter. + * @param lang language advertised by the server + * @param level one higher than the maximum supported option + */ + public abstract void addOptions(String lang, int level); + + /** + * Pass option=value during the handshake + * @param field + * @param value + */ protected void contribute(String field, int value) { if (buffer.length() > 0) buffer.append(','); @@ -1459,7 +1509,6 @@ public final class MapiSocket { buffer.append(value); } - public abstract void addOptions(String lang, int level); void setBuffer(StringBuilder buf) { buffer = buf;
--- a/src/main/java/org/monetdb/mcl/net/MonetUrlParser.java +++ b/src/main/java/org/monetdb/mcl/net/MonetUrlParser.java @@ -6,270 +6,275 @@ import java.net.URISyntaxException; import java.net.URLDecoder; import java.net.URLEncoder; +/** + * Helper class to keep the URL parsing code separate from the rest of + * the {@link Target} class. + */ public class MonetUrlParser { - private final Target target; - private final String urlText; - private final URI url; + private final Target target; + private final String urlText; + private final URI url; - public MonetUrlParser(Target target, String url) throws URISyntaxException { - this.target = target; - this.urlText = url; - // we want to accept monetdb:// but the Java URI parser rejects that. - switch (url) { - case "monetdb:-": - case "monetdbs:-": - throw new URISyntaxException(url, "invalid MonetDB URL"); - case "monetdb://": - case "monetdbs://": - url += "-"; - break; - } - this.url = new URI(url); - } + private MonetUrlParser(Target target, String url) throws URISyntaxException { + this.target = target; + this.urlText = url; + // we want to accept monetdb:// but the Java URI parser rejects that. + switch (url) { + case "monetdb:-": + case "monetdbs:-": + throw new URISyntaxException(url, "invalid MonetDB URL"); + case "monetdb://": + case "monetdbs://": + url += "-"; + break; + } + this.url = new URI(url); + } - public static void parse(Target target, String url) throws URISyntaxException, ValidationError { - if (url.equals("monetdb://")) { - // deal with peculiarity of Java's URI parser - url = "monetdb:///"; - } + public static void parse(Target target, String url) throws URISyntaxException, ValidationError { + if (url.equals("monetdb://")) { + // deal with peculiarity of Java's URI parser + url = "monetdb:///"; + } - target.barrier(); - if (url.startsWith("mapi:")) { - try { - MonetUrlParser parser = new MonetUrlParser(target, url.substring(5)); - parser.parseClassic(); - } catch (URISyntaxException e) { - URISyntaxException exc = new URISyntaxException(e.getInput(), e.getReason(), -1); - exc.setStackTrace(e.getStackTrace()); - throw exc; - } - } else { - MonetUrlParser parser = new MonetUrlParser(target, url); - parser.parseModern(); - } - target.barrier(); - } + target.barrier(); + if (url.startsWith("mapi:")) { + try { + MonetUrlParser parser = new MonetUrlParser(target, url.substring(5)); + parser.parseClassic(); + } catch (URISyntaxException e) { + URISyntaxException exc = new URISyntaxException(e.getInput(), e.getReason(), -1); + exc.setStackTrace(e.getStackTrace()); + throw exc; + } + } else { + MonetUrlParser parser = new MonetUrlParser(target, url); + parser.parseModern(); + } + target.barrier(); + } - public static String percentDecode(String context, String text) throws URISyntaxException { - try { - return URLDecoder.decode(text, "UTF-8"); - } catch (UnsupportedEncodingException e) { - throw new IllegalStateException("should be unreachable: UTF-8 unknown??", e); - } catch (IllegalArgumentException e) { - throw new URISyntaxException(text, context + ": invalid percent escape"); - } - } + public static String percentDecode(String context, String text) throws URISyntaxException { + try { + return URLDecoder.decode(text, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new IllegalStateException("should be unreachable: UTF-8 unknown??", e); + } catch (IllegalArgumentException e) { + throw new URISyntaxException(text, context + ": invalid percent escape"); + } + } - public static String percentEncode(String text) { - try { - return URLEncoder.encode(text, "UTF-8"); - } catch (UnsupportedEncodingException e) { - throw new RuntimeException(e); - } - } + public static String percentEncode(String text) { + try { + return URLEncoder.encode(text, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } - private void parseModern() throws URISyntaxException, ValidationError { - clearBasic(); + private void parseModern() throws URISyntaxException, ValidationError { + clearBasic(); - String scheme = url.getScheme(); - if (scheme == null) throw new URISyntaxException(urlText, "URL scheme must be monetdb:// or monetdbs://"); - switch (scheme) { - case "monetdb": - target.setTls(false); - break; - case "monetdbs": - target.setTls(true); - break; - default: - throw new URISyntaxException(urlText, "URL scheme must be monetdb:// or monetdbs://"); - } + String scheme = url.getScheme(); + if (scheme == null) + throw new URISyntaxException(urlText, "URL scheme must be monetdb:// or monetdbs://"); + switch (scheme) { + case "monetdb": + target.setTls(false); + break; + case "monetdbs": + target.setTls(true); + break; + default: + throw new URISyntaxException(urlText, "URL scheme must be monetdb:// or monetdbs://"); + } - // The built-in getHost and getPort methods do strange things - // in edge cases such as percent-encoded host names and - // invalid port numbers - String authority = url.getAuthority(); - String host; - String remainder; - int pos; - if (authority == null) { - if (!url.getRawSchemeSpecificPart().startsWith("//")) { - throw new URISyntaxException(urlText, "expected //"); - } - host = ""; - remainder = ""; - } else if (authority.equals("-")) { - host = ""; - remainder = ""; - } else { - if (authority.startsWith("[")) { - // IPv6 - pos = authority.indexOf(']'); - if (pos < 0) - throw new URISyntaxException(urlText, "unmatched '['"); - host = authority.substring(1, pos); - remainder = authority.substring(pos + 1); - } else if ((pos = authority.indexOf(':')) >= 0) { - host = authority.substring(0, pos); - remainder = authority.substring(pos); - } else { - host = authority; - remainder = ""; - } - } - host = Target.unpackHost(host); - target.setHost(host); + // The built-in getHost and getPort methods do strange things + // in edge cases such as percent-encoded host names and + // invalid port numbers + String authority = url.getAuthority(); + String host; + String remainder; + int pos; + if (authority == null) { + if (!url.getRawSchemeSpecificPart().startsWith("//")) { + throw new URISyntaxException(urlText, "expected //"); + } + host = ""; + remainder = ""; + } else if (authority.equals("-")) { + host = ""; + remainder = ""; + } else { + if (authority.startsWith("[")) { + // IPv6 + pos = authority.indexOf(']'); + if (pos < 0) + throw new URISyntaxException(urlText, "unmatched '['"); + host = authority.substring(1, pos); + remainder = authority.substring(pos + 1); + } else if ((pos = authority.indexOf(':')) >= 0) { + host = authority.substring(0, pos); + remainder = authority.substring(pos); + } else { + host = authority; + remainder = ""; + } + } + host = Target.unpackHost(host); + target.setHost(host); - if (remainder.isEmpty()) { - // do nothing - } else if (remainder.startsWith(":")) { - String portStr = remainder.substring(1); - try { - int port = Integer.parseInt(portStr); - if (port <= 0 || port > 65535) - portStr = null; - } catch (NumberFormatException e) { - portStr = null; - } - if (portStr == null) - throw new ValidationError(urlText, "invalid port number"); - target.setString(Parameter.PORT, portStr); - } + if (remainder.isEmpty()) { + // do nothing + } else if (remainder.startsWith(":")) { + String portStr = remainder.substring(1); + try { + int port = Integer.parseInt(portStr); + if (port <= 0 || port > 65535) + portStr = null; + } catch (NumberFormatException e) { + portStr = null; + } + if (portStr == null) + throw new ValidationError(urlText, "invalid port number"); + target.setString(Parameter.PORT, portStr); + } - String path = url.getRawPath(); - String[] parts = path.split("/", 4); - // <0: empty before leading slash> / <1: database> / <2: tableschema> / <3: table> / <4: should not exist> - switch (parts.length) { - case 4: - target.setString(Parameter.TABLE, percentDecode(Parameter.TABLE.name, parts[3])); - // fallthrough - case 3: - target.setString(Parameter.TABLESCHEMA, percentDecode(Parameter.TABLESCHEMA.name, parts[2])); - // fallthrough - case 2: - target.setString(Parameter.DATABASE, percentDecode(Parameter.DATABASE.name, parts[1])); - case 1: - case 0: - // fallthrough - break; - } + String path = url.getRawPath(); + String[] parts = path.split("/", 4); + // <0: empty before leading slash> / <1: database> / <2: tableschema> / <3: table> / <4: should not exist> + switch (parts.length) { + case 4: + target.setString(Parameter.TABLE, percentDecode(Parameter.TABLE.name, parts[3])); + // fallthrough + case 3: + target.setString(Parameter.TABLESCHEMA, percentDecode(Parameter.TABLESCHEMA.name, parts[2])); + // fallthrough + case 2: + target.setString(Parameter.DATABASE, percentDecode(Parameter.DATABASE.name, parts[1])); + case 1: + case 0: + // fallthrough + break; + } - final String query = url.getRawQuery(); - if (query != null) { - final String args[] = query.split("&"); - for (int i = 0; i < args.length; i++) { - pos = args[i].indexOf('='); - if (pos <= 0) { - throw new URISyntaxException(args[i], "invalid key=value pair"); - } - String key = args[i].substring(0, pos); - key = percentDecode(key, key); - Parameter parm = Parameter.forName(key); - if (parm != null && parm.isCore) - throw new URISyntaxException(key, key + "= is not allowed as a query parameter"); + final String query = url.getRawQuery(); + if (query != null) { + final String[] args = query.split("&"); + for (int i = 0; i < args.length; i++) { + pos = args[i].indexOf('='); + if (pos <= 0) { + throw new URISyntaxException(args[i], "invalid key=value pair"); + } + String key = args[i].substring(0, pos); + key = percentDecode(key, key); + Parameter parm = Parameter.forName(key); + if (parm != null && parm.isCore) + throw new URISyntaxException(key, key + "= is not allowed as a query parameter"); - String value = args[i].substring(pos + 1); - target.setString(key, percentDecode(key, value)); - } - } - } + String value = args[i].substring(pos + 1); + target.setString(key, percentDecode(key, value)); + } + } + } - private void parseClassic() throws URISyntaxException, ValidationError { - if (!url.getRawSchemeSpecificPart().startsWith("//")) { - throw new URISyntaxException(urlText, "expected //"); - } + private void parseClassic() throws URISyntaxException, ValidationError { + if (!url.getRawSchemeSpecificPart().startsWith("//")) { + throw new URISyntaxException(urlText, "expected //"); + } - String scheme = url.getScheme(); - if (scheme == null) - scheme = ""; - switch (scheme) { - case "monetdb": - parseClassicAuthorityAndPath(); - break; - case "merovingian": - String authority = url.getRawAuthority(); - // authority must be "proxy" ignore authority and path - boolean valid = urlText.startsWith("merovingian://proxy?") || urlText.equals("merovingian://proxy"); - if (!valid) - throw new URISyntaxException(urlText, "with mapi:merovingian:, only //proxy is supported"); - break; - default: - throw new URISyntaxException(urlText, "URL scheme must be mapi:monetdb:// or mapi:merovingian://"); - } + String scheme = url.getScheme(); + if (scheme == null) + scheme = ""; + switch (scheme) { + case "monetdb": + parseClassicAuthorityAndPath(); + break; + case "merovingian": + String authority = url.getRawAuthority(); + // authority must be "proxy" ignore authority and path + boolean valid = urlText.startsWith("merovingian://proxy?") || urlText.equals("merovingian://proxy"); + if (!valid) + throw new URISyntaxException(urlText, "with mapi:merovingian:, only //proxy is supported"); + break; + default: + throw new URISyntaxException(urlText, "URL scheme must be mapi:monetdb:// or mapi:merovingian://"); + } - final String query = url.getRawQuery(); - if (query != null) { - final String args[] = query.split("&"); - for (int i = 0; i < args.length; i++) { - String arg = args[i]; - if (arg.startsWith("language=")) { - String language = arg.substring(9); - target.setString(Parameter.LANGUAGE, language); - } else if (arg.startsWith("database=")) { - String database = arg.substring(9); - target.setString(Parameter.DATABASE, database); - } else { - // ignore - } - } - } - } + final String query = url.getRawQuery(); + if (query != null) { + final String[] args = query.split("&"); + for (int i = 0; i < args.length; i++) { + String arg = args[i]; + if (arg.startsWith("language=")) { + String language = arg.substring(9); + target.setString(Parameter.LANGUAGE, language); + } else if (arg.startsWith("database=")) { + String database = arg.substring(9); + target.setString(Parameter.DATABASE, database); + } else { + // ignore + } + } + } + } - private void parseClassicAuthorityAndPath() throws URISyntaxException, ValidationError { - clearBasic(); - String authority = url.getRawAuthority(); - String host; - String portStr; - int pos; - if (authority == null) { - host = ""; - portStr = ""; - } else if (authority.indexOf('@') >= 0) { - throw new URISyntaxException(urlText, "user@host syntax is not allowed"); - } else if ((pos = authority.indexOf(':')) >= 0) { - host = authority.substring(0, pos); - portStr = authority.substring(pos + 1); - } else { - host = authority; - portStr = ""; - } + private void parseClassicAuthorityAndPath() throws URISyntaxException, ValidationError { + clearBasic(); + String authority = url.getRawAuthority(); + String host; + String portStr; + int pos; + if (authority == null) { + host = ""; + portStr = ""; + } else if (authority.indexOf('@') >= 0) { + throw new URISyntaxException(urlText, "user@host syntax is not allowed"); + } else if ((pos = authority.indexOf(':')) >= 0) { + host = authority.substring(0, pos); + portStr = authority.substring(pos + 1); + } else { + host = authority; + portStr = ""; + } - if (!portStr.isEmpty()) { - int port; - try { - port = Integer.parseInt(portStr); - } catch (NumberFormatException e) { - port = -1; - } - if (port <= 0) { - throw new ValidationError(urlText, "invalid port number"); - } - target.setString(Parameter.PORT, portStr); - } + if (!portStr.isEmpty()) { + int port; + try { + port = Integer.parseInt(portStr); + } catch (NumberFormatException e) { + port = -1; + } + if (port <= 0) { + throw new ValidationError(urlText, "invalid port number"); + } + target.setString(Parameter.PORT, portStr); + } - String path = url.getRawPath(); - if (host.isEmpty() && portStr.isEmpty()) { - // socket - target.clear(Parameter.HOST); - target.setString(Parameter.SOCK, path != null ? path : ""); - } else { - // tcp - target.clear(Parameter.SOCK); - target.setString(Parameter.HOST, host); - if (path == null || path.isEmpty()) { - // do nothing - } else if (!path.startsWith("/")) { - throw new URISyntaxException(urlText, "expect path to start with /"); - } else { - String database = path.substring(1); - target.setString(Parameter.DATABASE, database); - } - } - } + String path = url.getRawPath(); + if (host.isEmpty() && portStr.isEmpty()) { + // socket + target.clear(Parameter.HOST); + target.setString(Parameter.SOCK, path != null ? path : ""); + } else { + // tcp + target.clear(Parameter.SOCK); + target.setString(Parameter.HOST, host); + if (path == null || path.isEmpty()) { + // do nothing + } else if (!path.startsWith("/")) { + throw new URISyntaxException(urlText, "expect path to start with /"); + } else { + String database = path.substring(1); + target.setString(Parameter.DATABASE, database); + } + } + } - private void clearBasic() { - target.clear(Parameter.TLS); - target.clear(Parameter.HOST); - target.clear(Parameter.PORT); - target.clear(Parameter.DATABASE); - } + private void clearBasic() { + target.clear(Parameter.TLS); + target.clear(Parameter.HOST); + target.clear(Parameter.PORT); + target.clear(Parameter.DATABASE); + } }
--- a/src/main/java/org/monetdb/mcl/net/Parameter.java +++ b/src/main/java/org/monetdb/mcl/net/Parameter.java @@ -3,99 +3,142 @@ package org.monetdb.mcl.net; import java.util.Calendar; +/** + * Enumerates things that can be configured on a connection to MonetDB. + */ public enum Parameter { - TLS("tls", ParameterType.Bool, false, "secure the connection using TLS", true), - HOST("host", ParameterType.Str, "", "IP number, domain name or one of the special values `localhost` and `localhost.`", true), - PORT("port", ParameterType.Int, -1, "Port to connect to, 1..65535 or -1 for 'not set'", true), - DATABASE("database", ParameterType.Str, "", "name of database to connect to", true), - TABLESCHEMA("tableschema", ParameterType.Str, "", "only used for REMOTE TABLE, otherwise unused", true), - TABLE("table", ParameterType.Str, "", "only used for REMOTE TABLE, otherwise unused", true), - SOCK("sock", ParameterType.Path, "", "path to Unix domain socket to connect to", false), - SOCKDIR("sockdir", ParameterType.Path, "/tmp", "Directory for implicit Unix domain sockets (.s.monetdb.PORT)", false), - CERT("cert", ParameterType.Path, "", "path to TLS certificate to authenticate server with", false), - CERTHASH("certhash", ParameterType.Str, "", "hash of server TLS certificate must start with these hex digits; overrides cert", false), - CLIENTKEY("clientkey", ParameterType.Path, "", "path to TLS key (+certs) to authenticate with as client", false), - CLIENTCERT("clientcert", ParameterType.Path, "", "path to TLS certs for 'clientkey', if not included there", false), - USER("user", ParameterType.Str, "", "user name to authenticate as", false), - PASSWORD("password", ParameterType.Str, "", "password to authenticate with", false), - LANGUAGE("language", ParameterType.Str, "sql", "for example, \"sql\", \"mal\", \"msql\", \"profiler\"", false), - AUTOCOMMIT("autocommit", ParameterType.Bool, true, "initial value of autocommit", false), - SCHEMA("schema", ParameterType.Str, "", "initial schema", false), - TIMEZONE("timezone", ParameterType.Int, null, "client time zone as minutes east of UTC", false), - BINARY("binary", ParameterType.Str, "on", "whether to use binary result set format (number or bool)", false), - REPLYSIZE("replysize", ParameterType.Int, 250, "rows beyond this limit are retrieved on demand, <1 means unlimited", false), - FETCHSIZE("fetchsize", ParameterType.Int, null, "alias for replysize, specific to jdbc", false), - HASH("hash", ParameterType.Str, "", "specific to jdbc", false), - DEBUG("debug", ParameterType.Bool, false, "specific to jdbc", false), - LOGFILE("logfile", ParameterType.Str, "", "specific to jdbc", false), + TLS("tls", ParameterType.Bool, false, "secure the connection using TLS", true), + HOST("host", ParameterType.Str, "", "IP number, domain name or one of the special values `localhost` and `localhost.`", true), + PORT("port", ParameterType.Int, -1, "Port to connect to, 1..65535 or -1 for 'not set'", true), + DATABASE("database", ParameterType.Str, "", "name of database to connect to", true), + TABLESCHEMA("tableschema", ParameterType.Str, "", "only used for REMOTE TABLE, otherwise unused", true), + TABLE("table", ParameterType.Str, "", "only used for REMOTE TABLE, otherwise unused", true), + SOCK("sock", ParameterType.Path, "", "path to Unix domain socket to connect to", false), + SOCKDIR("sockdir", ParameterType.Path, "/tmp", "Directory for implicit Unix domain sockets (.s.monetdb.PORT)", false), + CERT("cert", ParameterType.Path, "", "path to TLS certificate to authenticate server with", false), + CERTHASH("certhash", ParameterType.Str, "", "hash of server TLS certificate must start with these hex digits; overrides cert", false), + CLIENTKEY("clientkey", ParameterType.Path, "", "path to TLS key (+certs) to authenticate with as client", false), + CLIENTCERT("clientcert", ParameterType.Path, "", "path to TLS certs for 'clientkey', if not included there", false), + USER("user", ParameterType.Str, "", "user name to authenticate as", false), + PASSWORD("password", ParameterType.Str, "", "password to authenticate with", false), + LANGUAGE("language", ParameterType.Str, "sql", "for example, \"sql\", \"mal\", \"msql\", \"profiler\"", false), + AUTOCOMMIT("autocommit", ParameterType.Bool, true, "initial value of autocommit", false), + SCHEMA("schema", ParameterType.Str, "", "initial schema", false), + TIMEZONE("timezone", ParameterType.Int, null, "client time zone as minutes east of UTC", false), + BINARY("binary", ParameterType.Str, "on", "whether to use binary result set format (number or bool)", false), + REPLYSIZE("replysize", ParameterType.Int, 250, "rows beyond this limit are retrieved on demand, <1 means unlimited", false), + FETCHSIZE("fetchsize", ParameterType.Int, null, "alias for replysize, specific to jdbc", false), + HASH("hash", ParameterType.Str, "", "specific to jdbc", false), + DEBUG("debug", ParameterType.Bool, false, "specific to jdbc", false), + LOGFILE("logfile", ParameterType.Str, "", "specific to jdbc", false), - SO_TIMEOUT("so_timeout", ParameterType.Int, 0, "abort if network I/O does not complete in this many milliseconds", false), - CLOB_AS_VARCHAR("treat_clob_as_varchar", ParameterType.Bool, true, "return CLOB/TEXT data as type VARCHAR instead of type CLOB", false), - BLOB_AS_BINARY("treat_blob_as_binary", ParameterType.Bool, true, "return BLOB data as type BINARY instead of type BLOB", false), - ; + SO_TIMEOUT("so_timeout", ParameterType.Int, 0, "abort if network I/O does not complete in this many milliseconds", false), CLOB_AS_VARCHAR("treat_clob_as_varchar", ParameterType.Bool, true, "return CLOB/TEXT data as type VARCHAR instead of type CLOB", false), BLOB_AS_BINARY("treat_blob_as_binary", ParameterType.Bool, true, "return BLOB data as type BINARY instead of type BLOB", false), + ; - public final String name; - public final ParameterType type; - private final Object defaultValue; - public final String description; - public final boolean isCore; + public final String name; + public final ParameterType type; + public final String description; + public final boolean isCore; + private final Object defaultValue; - Parameter(String name, ParameterType type, Object defaultValue, String description, boolean isCore) { - this.name = name; - this.type = type; - this.isCore = isCore; - this.defaultValue = defaultValue; - this.description = description; - } + Parameter(String name, ParameterType type, Object defaultValue, String description, boolean isCore) { + this.name = name; + this.type = type; + this.isCore = isCore; + this.defaultValue = defaultValue; + this.description = description; + } - public static Parameter forName(String name) { - switch (name) { - case "tls": return TLS; - case "host": return HOST; - case "port": return PORT; - case "database": return DATABASE; - case "tableschema": return TABLESCHEMA; - case "table": return TABLE; - case "sock": return SOCK; - case "sockdir": return SOCKDIR; - case "cert": return CERT; - case "certhash": return CERTHASH; - case "clientkey": return CLIENTKEY; - case "clientcert": return CLIENTCERT; - case "user": return USER; - case "password": return PASSWORD; - case "language": return LANGUAGE; - case "autocommit": return AUTOCOMMIT; - case "schema": return SCHEMA; - case "timezone": return TIMEZONE; - case "binary": return BINARY; - case "replysize": return REPLYSIZE; - case "fetchsize": return FETCHSIZE; - case "hash": return HASH; - case "debug": return DEBUG; - case "logfile": return LOGFILE; - case "so_timeout": return SO_TIMEOUT; - case "treat_clob_as_varchar": return CLOB_AS_VARCHAR; - case "treat_blob_as_binary": return BLOB_AS_BINARY; - default: return null; - } - } + public static Parameter forName(String name) { + switch (name) { + case "tls": + return TLS; + case "host": + return HOST; + case "port": + return PORT; + case "database": + return DATABASE; + case "tableschema": + return TABLESCHEMA; + case "table": + return TABLE; + case "sock": + return SOCK; + case "sockdir": + return SOCKDIR; + case "cert": + return CERT; + case "certhash": + return CERTHASH; + case "clientkey": + return CLIENTKEY; + case "clientcert": + return CLIENTCERT; + case "user": + return USER; + case "password": + return PASSWORD; + case "language": + return LANGUAGE; + case "autocommit": + return AUTOCOMMIT; + case "schema": + return SCHEMA; + case "timezone": + return TIMEZONE; + case "binary": + return BINARY; + case "replysize": + return REPLYSIZE; + case "fetchsize": + return FETCHSIZE; + case "hash": + return HASH; + case "debug": + return DEBUG; + case "logfile": + return LOGFILE; + case "so_timeout": + return SO_TIMEOUT; + case "treat_clob_as_varchar": + return CLOB_AS_VARCHAR; + case "treat_blob_as_binary": + return BLOB_AS_BINARY; + default: + return null; + } + } - public static boolean isIgnored(String name) { - if (Parameter.forName(name) != null) - return false; - return name.contains("_"); - } + /** + * Determine if a given setting can safely be ignored. + * The ground rule is that if we encounter an unknown setting + * without an underscore in the name, it is an error. If it has + * an underscore in its name, it can be ignored. + * @param name the name of the setting to check + * @return true if it can safely be ignored + */ + public static boolean isIgnored(String name) { + if (Parameter.forName(name) != null) + return false; + return name.contains("_"); + } - public Object getDefault() { - switch (this) { - case TIMEZONE: - Calendar cal = Calendar.getInstance(); - int offsetMillis = cal.get(Calendar.ZONE_OFFSET) + cal.get(Calendar.DST_OFFSET); - int offsetSeconds = offsetMillis / 1000; - return (Integer)offsetSeconds; - default: - return defaultValue; - } - } + /** + * Return a default value for the given setting, as an Object of the appropriate type. + * Note that the value returned for TIMEZONE may change if the system time zone + * is changed or if Daylight Saving Time starts or ends. + * @return + */ + public Object getDefault() { + switch (this) { + case TIMEZONE: + Calendar cal = Calendar.getInstance(); + int offsetMillis = cal.get(Calendar.ZONE_OFFSET) + cal.get(Calendar.DST_OFFSET); + int offsetSeconds = offsetMillis / 1000; + return offsetSeconds; + default: + return defaultValue; + } + } }
--- a/src/main/java/org/monetdb/mcl/net/ParameterType.java +++ b/src/main/java/org/monetdb/mcl/net/ParameterType.java @@ -1,68 +1,103 @@ package org.monetdb.mcl.net; +/** + * Enumeration of the types a {@link Parameter} may have. + */ public enum ParameterType { - Str, - Int, - Bool, - Path; - - public Object parse(String name, String value) throws ValidationError { - if (value == null) - return null; - - try { - switch (this) { - case Bool: - return parseBool(value); - case Int: - return Integer.parseInt(value); - case Str: - case Path: - return value; - default: - throw new IllegalStateException("unreachable"); - } - } catch (IllegalArgumentException e) { - String message = e.toString(); - throw new ValidationError(name, message); - } - } + /** + * The Parameter is an arbitrary string + */ + Str, + /** The Parameter can be interpreted as an {@link Integer} */ + Int, + /** The Parameter is a {@link Boolean} and can be + * written "true", "false", "on", "off", "yes" or "no". + * Uppercase letters are also accepted + */ + Bool, + /** + * Functionally the same as {@link ParameterType.Str } but + * indicates the value is to be interpreted as a path on the + * client's file system. + */ + Path; - public String format(Object value) { - switch (this) { - case Bool: - return (Boolean)value ? "true": "false"; - case Int: - return Integer.toString((Integer)value); - case Str: - case Path: - return (String) value; - default: - throw new IllegalStateException("unreachable"); - } - } + /** + * Convert a string to a boolean, accepting true/false/yes/no/on/off. + * + * Uppercase is also accepted. + * @param value text to be parsed + * @return boolean interpretation of the text + */ + public static boolean parseBool(String value) { + boolean lowered = false; + String original = value; + while (true) { + switch (value) { + case "true": + case "yes": + case "on": + return true; + case "false": + case "no": + case "off": + return false; + default: + if (!lowered) { + value = value.toLowerCase(); + lowered = true; + continue; + } + throw new IllegalArgumentException("invalid boolean value: " + original); + } + } + } - public static boolean parseBool(String value) { - boolean lowered = false; - String original = value; - while (true) { - switch (value) { - case "true": - case "yes": - case "on": - return true; - case "false": - case "no": - case "off": - return false; - default: - if (!lowered) { - value = value.toLowerCase(); - lowered = true; - continue; - } - throw new IllegalArgumentException("invalid boolean value: " + original); - } - } - } + /** + * Convert text into an Object of the appropriate type + * @param name name of the setting for use in error messages + * @param value text to be converted + * @return Object representation of the text + * @throws ValidationError if the text cannot be converted + */ + public Object parse(String name, String value) throws ValidationError { + if (value == null) + return null; + + try { + switch (this) { + case Bool: + return parseBool(value); + case Int: + return Integer.parseInt(value); + case Str: + case Path: + return value; + default: + throw new IllegalStateException("unreachable"); + } + } catch (IllegalArgumentException e) { + String message = e.toString(); + throw new ValidationError(name, message); + } + } + + /** + * Represent the object as a string. + * @param value, must be of the appropriate type + * @return textual representation + */ + public String format(Object value) { + switch (this) { + case Bool: + return (Boolean) value ? "true" : "false"; + case Int: + return Integer.toString((Integer) value); + case Str: + case Path: + return (String) value; + default: + throw new IllegalStateException("unreachable"); + } + } }
--- a/src/main/java/org/monetdb/mcl/net/SecureSocket.java +++ b/src/main/java/org/monetdb/mcl/net/SecureSocket.java @@ -13,171 +13,172 @@ import java.security.cert.X509Certificat import java.util.Collections; public class SecureSocket { - private static final String[] ENABLED_PROTOCOLS = {"TLSv1.3"}; - private static final String[] APPLICATION_PROTOCOLS = {"mapi/9"}; - - public static Socket wrap(Target.Validated validated, Socket inner) throws IOException { - Target.Verify verify = validated.connectVerify(); - SSLSocketFactory socketFactory; - boolean checkName = true; - try { - switch (verify) { - case System: - socketFactory = (SSLSocketFactory) SSLSocketFactory.getDefault(); - break; - case Cert: - KeyStore keyStore = keyStoreForCert(validated.getCert()); - socketFactory = certBasedSocketFactory(keyStore); - break; - case Hash: - socketFactory = hashBasedSocketFactory(validated.connectCertHashDigits()); - checkName = false; - break; - default: - throw new RuntimeException("unreachable: unexpected verification strategy " + verify.name()); - } - return wrapSocket(inner, validated, socketFactory, checkName); - } catch (CertificateException e) { - throw new SSLException(e.getMessage(), e); - } - } + private static final String[] ENABLED_PROTOCOLS = {"TLSv1.3"}; + private static final String[] APPLICATION_PROTOCOLS = {"mapi/9"}; - private static SSLSocket wrapSocket(Socket inner, Target.Validated validated, SSLSocketFactory socketFactory, boolean checkName) throws IOException { - SSLSocket sock = (SSLSocket) socketFactory.createSocket(inner, validated.connectTcp(), validated.connectPort(), true); - sock.setUseClientMode(true); - SSLParameters parameters = sock.getSSLParameters(); - - parameters.setProtocols(ENABLED_PROTOCOLS); - - parameters.setServerNames(Collections.singletonList(new SNIHostName(validated.connectTcp()))); - - if (checkName) { - parameters.setEndpointIdentificationAlgorithm("HTTPS"); - } + public static Socket wrap(Target.Validated validated, Socket inner) throws IOException { + Target.Verify verify = validated.connectVerify(); + SSLSocketFactory socketFactory; + boolean checkName = true; + try { + switch (verify) { + case System: + socketFactory = (SSLSocketFactory) SSLSocketFactory.getDefault(); + break; + case Cert: + KeyStore keyStore = keyStoreForCert(validated.getCert()); + socketFactory = certBasedSocketFactory(keyStore); + break; + case Hash: + socketFactory = hashBasedSocketFactory(validated.connectCertHashDigits()); + checkName = false; + break; + default: + throw new RuntimeException("unreachable: unexpected verification strategy " + verify.name()); + } + return wrapSocket(inner, validated, socketFactory, checkName); + } catch (CertificateException e) { + throw new SSLException(e.getMessage(), e); + } + } - // Unfortunately, SSLParameters.setApplicationProtocols is only available - // since language level 9 and currently we're on 8. - // Still call it if it happens to be available. - try { - Method setApplicationProtocols = SSLParameters.class.getMethod("setApplicationProtocols", String[].class); - setApplicationProtocols.invoke(parameters, (Object) APPLICATION_PROTOCOLS); - } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException ignored) {} + private static SSLSocket wrapSocket(Socket inner, Target.Validated validated, SSLSocketFactory socketFactory, boolean checkName) throws IOException { + SSLSocket sock = (SSLSocket) socketFactory.createSocket(inner, validated.connectTcp(), validated.connectPort(), true); + sock.setUseClientMode(true); + SSLParameters parameters = sock.getSSLParameters(); + + parameters.setProtocols(ENABLED_PROTOCOLS); + + parameters.setServerNames(Collections.singletonList(new SNIHostName(validated.connectTcp()))); + + if (checkName) { + parameters.setEndpointIdentificationAlgorithm("HTTPS"); + } - sock.setSSLParameters(parameters); - sock.startHandshake(); - return sock; - } + // Unfortunately, SSLParameters.setApplicationProtocols is only available + // since language level 9 and currently we're on 8. + // Still call it if it happens to be available. + try { + Method setApplicationProtocols = SSLParameters.class.getMethod("setApplicationProtocols", String[].class); + setApplicationProtocols.invoke(parameters, (Object) APPLICATION_PROTOCOLS); + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException ignored) { + } - private static X509Certificate loadCertificate(String path) throws CertificateException, IOException { - CertificateFactory factory = CertificateFactory.getInstance("X509"); - try (FileInputStream s = new FileInputStream(path)) { - return (X509Certificate) factory.generateCertificate(s); - } - } + sock.setSSLParameters(parameters); + sock.startHandshake(); + return sock; + } - private static SSLSocketFactory certBasedSocketFactory(KeyStore store) throws IOException, CertificateException { - TrustManagerFactory trustManagerFactory; - try { - trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - trustManagerFactory.init(store); - } catch (NoSuchAlgorithmException | KeyStoreException e) { - throw new RuntimeException("Could not create TrustManagerFactory", e); - } + private static X509Certificate loadCertificate(String path) throws CertificateException, IOException { + CertificateFactory factory = CertificateFactory.getInstance("X509"); + try (FileInputStream s = new FileInputStream(path)) { + return (X509Certificate) factory.generateCertificate(s); + } + } - SSLContext context; - try { - context = SSLContext.getInstance("TLS"); - context.init(null, trustManagerFactory.getTrustManagers(), null); - } catch (NoSuchAlgorithmException | KeyManagementException e) { - throw new RuntimeException("Could not create SSLContext", e); - } - - return context.getSocketFactory(); - } + private static SSLSocketFactory certBasedSocketFactory(KeyStore store) throws IOException, CertificateException { + TrustManagerFactory trustManagerFactory; + try { + trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(store); + } catch (NoSuchAlgorithmException | KeyStoreException e) { + throw new RuntimeException("Could not create TrustManagerFactory", e); + } - private static KeyStore keyStoreForCert(String path) throws IOException, CertificateException { - try { - X509Certificate cert = loadCertificate(path); - KeyStore store = emptyKeyStore(); - store.setCertificateEntry("root", cert); - return store; - } catch (KeyStoreException e) { - throw new RuntimeException("Could not create KeyStore for certificate", e); - } - } + SSLContext context; + try { + context = SSLContext.getInstance("TLS"); + context.init(null, trustManagerFactory.getTrustManagers(), null); + } catch (NoSuchAlgorithmException | KeyManagementException e) { + throw new RuntimeException("Could not create SSLContext", e); + } + + return context.getSocketFactory(); + } - private static KeyStore emptyKeyStore() throws IOException, CertificateException { - KeyStore store; - try { - store = KeyStore.getInstance("PKCS12"); - store.load(null, null); - return store; - } catch (KeyStoreException | NoSuchAlgorithmException e) { - throw new RuntimeException("Could not create KeyStore for certificate", e); - } - } + private static KeyStore keyStoreForCert(String path) throws IOException, CertificateException { + try { + X509Certificate cert = loadCertificate(path); + KeyStore store = emptyKeyStore(); + store.setCertificateEntry("root", cert); + return store; + } catch (KeyStoreException e) { + throw new RuntimeException("Could not create KeyStore for certificate", e); + } + } - private static SSLSocketFactory hashBasedSocketFactory(String hashDigits) { - TrustManager trustManager = new HashBasedTrustManager(hashDigits); - try { - SSLContext context = SSLContext.getInstance("TLS"); - context.init(null, new TrustManager[]{ trustManager}, null); - return context.getSocketFactory(); - } catch (NoSuchAlgorithmException | KeyManagementException e) { - throw new RuntimeException("Could not create SSLContext", e); - } + private static KeyStore emptyKeyStore() throws IOException, CertificateException { + KeyStore store; + try { + store = KeyStore.getInstance("PKCS12"); + store.load(null, null); + return store; + } catch (KeyStoreException | NoSuchAlgorithmException e) { + throw new RuntimeException("Could not create KeyStore for certificate", e); + } + } - } + private static SSLSocketFactory hashBasedSocketFactory(String hashDigits) { + TrustManager trustManager = new HashBasedTrustManager(hashDigits); + try { + SSLContext context = SSLContext.getInstance("TLS"); + context.init(null, new TrustManager[]{trustManager}, null); + return context.getSocketFactory(); + } catch (NoSuchAlgorithmException | KeyManagementException e) { + throw new RuntimeException("Could not create SSLContext", e); + } - private static class HashBasedTrustManager implements X509TrustManager { - private static final char[] HEXDIGITS = "0123456789abcdef".toCharArray(); - private final String hashDigits; + } - public HashBasedTrustManager(String hashDigits) { - this.hashDigits = hashDigits; - } + private static class HashBasedTrustManager implements X509TrustManager { + private static final char[] HEXDIGITS = "0123456789abcdef".toCharArray(); + private final String hashDigits; + + public HashBasedTrustManager(String hashDigits) { + this.hashDigits = hashDigits; + } - @Override - public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { - throw new RuntimeException("this TrustManager is only suitable for client side connections"); - } + @Override + public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { + throw new RuntimeException("this TrustManager is only suitable for client side connections"); + } - @Override - public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { - X509Certificate cert = x509Certificates[0]; - byte[] certBytes = cert.getEncoded(); + @Override + public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { + X509Certificate cert = x509Certificates[0]; + byte[] certBytes = cert.getEncoded(); - // for now it's always SHA256. - byte[] hashBytes; - try { - MessageDigest hasher = MessageDigest.getInstance("SHA-256"); - hasher.update(certBytes); - hashBytes = hasher.digest(); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException("failed to instantiate hash digest"); - } + // for now it's always SHA256. + byte[] hashBytes; + try { + MessageDigest hasher = MessageDigest.getInstance("SHA-256"); + hasher.update(certBytes); + hashBytes = hasher.digest(); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("failed to instantiate hash digest"); + } - // convert to hex digits - StringBuilder buffer = new StringBuilder(2 * hashBytes.length); - for (byte b: hashBytes) { - int hi = (b & 0xF0) >> 4; - int lo = b & 0x0F; - buffer.append(HEXDIGITS[hi]); - buffer.append(HEXDIGITS[lo]); - } - String certDigits = buffer.toString(); + // convert to hex digits + StringBuilder buffer = new StringBuilder(2 * hashBytes.length); + for (byte b : hashBytes) { + int hi = (b & 0xF0) >> 4; + int lo = b & 0x0F; + buffer.append(HEXDIGITS[hi]); + buffer.append(HEXDIGITS[lo]); + } + String certDigits = buffer.toString(); - if (!certDigits.startsWith(hashDigits)) { - throw new CertificateException("Certificate hash does not start with '" + hashDigits + "': " + certDigits); - } + if (!certDigits.startsWith(hashDigits)) { + throw new CertificateException("Certificate hash does not start with '" + hashDigits + "': " + certDigits); + } - } + } - @Override - public X509Certificate[] getAcceptedIssuers() { - return new X509Certificate[0]; - } - } + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + } }
--- a/src/main/java/org/monetdb/mcl/net/Target.java +++ b/src/main/java/org/monetdb/mcl/net/Target.java @@ -5,731 +5,820 @@ import java.util.Properties; import java.util.regex.Pattern; public class Target { - private boolean tls = false; - private String host = ""; - private int port = -1; - private String database = ""; - private String tableschema = ""; - private String table = ""; - private String sock = ""; - private String sockdir = "/tmp"; - private String cert = ""; - private String certhash = ""; - private String clientkey = ""; - private String clientcert = ""; - private String user = ""; - private String password = ""; - private String language = "sql"; - private boolean autocommit = true; - private String schema = ""; - private int timezone; - private String binary = "on"; - private int replySize = 250; - private String hash = ""; - private boolean debug = false; - private String logfile = ""; - private int soTimeout = 0; - private boolean treatClobAsVarchar = true; - private boolean treatBlobAsBinary = true; + protected static final Target defaults = new Target(); + private static final Pattern namePattern = Pattern.compile("^[a-zzA-Z_][-a-zA-Z0-9_.]*$"); + private static final Pattern hashPattern = Pattern.compile("^sha256:[0-9a-fA-F:]*$"); + private boolean tls = false; + private String host = ""; + private int port = -1; + private String database = ""; + private String tableschema = ""; + private String table = ""; + private String sock = ""; + private String sockdir = "/tmp"; + private String cert = ""; + private String certhash = ""; + private String clientkey = ""; + private String clientcert = ""; + private String user = ""; + private String password = ""; + private String language = "sql"; + private boolean autocommit = true; + private String schema = ""; + private int timezone; + private String binary = "on"; + private int replySize = 250; + private String hash = ""; + private boolean debug = false; + private String logfile = ""; + private int soTimeout = 0; + private boolean treatClobAsVarchar = true; + private boolean treatBlobAsBinary = true; + private boolean userWasSet = false; + private boolean passwordWasSet = false; + private Validated validated = null; - private boolean userWasSet = false; - private boolean passwordWasSet = false; - protected static final Target defaults = new Target(); - private Validated validated = null; - - private static Pattern namePattern = Pattern.compile("^[a-zzA-Z_][-a-zA-Z0-9_.]*$"); - private static Pattern hashPattern = Pattern.compile("^sha256:[0-9a-fA-F:]*$"); + public Target() { + this.timezone = (int) Parameter.TIMEZONE.getDefault(); + } - public Target() { - this.timezone = (int)Parameter.TIMEZONE.getDefault(); - } - - public Target(String url, Properties props) throws URISyntaxException, ValidationError { - this(); - setProperties(props); - parseUrl(url); - } + public Target(String url, Properties props) throws URISyntaxException, ValidationError { + this(); + setProperties(props); + parseUrl(url); + } - public void barrier() { - if (userWasSet && !passwordWasSet) - password = ""; - userWasSet = false; - passwordWasSet = false; - } + public static String packHost(String host) { + switch (host) { + case "localhost": + return "localhost."; + case "": + return "localhost"; + default: + return host; + } + } - public static String packHost(String host) { - switch (host) { - case "localhost": - return "localhost."; - case "": - return "localhost"; - default: - return host; - } - } + public static String unpackHost(String host) { + switch (host) { + case "localhost.": + return "localhost"; + case "localhost": + return ""; + default: + return host; + } + } + + public void barrier() { + if (userWasSet && !passwordWasSet) + password = ""; + userWasSet = false; + passwordWasSet = false; + } - public static String unpackHost(String host) { - switch (host) { - case "localhost.": - return "localhost"; - case "localhost": - return ""; - default: - return host; - } - } + public void setString(String key, String value) throws ValidationError { + Parameter parm = Parameter.forName(key); + if (parm != null) + setString(parm, value); + else if (!Parameter.isIgnored(key)) + throw new ValidationError(key, "unknown parameter"); + } + + public void setString(Parameter parm, String value) throws ValidationError { + if (value == null) + throw new NullPointerException("'value' must not be null"); + assign(parm, parm.type.parse(parm.name, value)); + } - public void setString(String key, String value) throws ValidationError { - Parameter parm = Parameter.forName(key); - if (parm != null) - setString(parm, value); - else if (!Parameter.isIgnored(key)) - throw new ValidationError(key, "unknown parameter"); - } + public void clear(Parameter parm) { + assign(parm, parm.getDefault()); + } - public void setString(Parameter parm, String value) throws ValidationError { - if (value == null) - throw new NullPointerException("'value' must not be null"); - assign(parm, parm.type.parse(parm.name, value)); - } - - public void clear(Parameter parm) { - assign(parm, parm.getDefault()); - } + public void parseUrl(String url) throws URISyntaxException, ValidationError { + if (url == null) + return; + if (url.startsWith("jdbc:")) + url = url.substring(5); + if (url.equals("monetdb:")) { + return; + } + MonetUrlParser.parse(this, url); + } - public void setProperties(Properties props) throws ValidationError { - if (props != null) { - for (String key : props.stringPropertyNames()) { - String value = props.getProperty(key); - if (key.equals(Parameter.HOST.name)) - value = Target.unpackHost(value); - setString(key, value); - } - } - } - - public void parseUrl(String url) throws URISyntaxException, ValidationError { - if (url == null) - return; - if (url.startsWith("jdbc:")) - url = url.substring(5); - if (url.equals("monetdb:")) { - return; - } - MonetUrlParser.parse(this, url); - } + private void assign(Parameter parm, Object value) { + switch (parm) { + case TLS: + setTls((boolean) value); + break; + case HOST: + setHost((String) value); + break; + case PORT: + setPort((int) value); + break; + case DATABASE: + setDatabase((String) value); + break; + case TABLESCHEMA: + setTableschema((String) value); + break; + case TABLE: + setTable((String) value); + break; + case SOCK: + setSock((String) value); + break; + case SOCKDIR: + setSockdir((String) value); + break; + case CERT: + setCert((String) value); + break; + case CERTHASH: + setCerthash((String) value); + break; + case CLIENTKEY: + setClientkey((String) value); + break; + case CLIENTCERT: + setClientcert((String) value); + break; + case USER: + setUser((String) value); + break; + case PASSWORD: + setPassword((String) value); + break; + case LANGUAGE: + setLanguage((String) value); + break; + case AUTOCOMMIT: + setAutocommit((boolean) value); + break; + case SCHEMA: + setSchema((String) value); + break; + case TIMEZONE: + setTimezone((int) value); + break; + case BINARY: + setBinary((String) value); + break; + case REPLYSIZE: + setReplySize((int) value); + break; + case FETCHSIZE: + setReplySize((int) value); + break; + case HASH: + setHash((String) value); + break; + case DEBUG: + setDebug((boolean) value); + break; + case LOGFILE: + setLogfile((String) value); + break; - private void assign(Parameter parm, Object value) { - switch (parm) { - case TLS: setTls((boolean)value); break; - case HOST: setHost((String)value); break; - case PORT: setPort((int)value); break; - case DATABASE: setDatabase((String)value); break; - case TABLESCHEMA: setTableschema((String)value); break; - case TABLE: setTable((String)value); break; - case SOCK: setSock((String)value); break; - case SOCKDIR: setSockdir((String)value); break; - case CERT: setCert((String)value); break; - case CERTHASH: setCerthash((String)value); break; - case CLIENTKEY: setClientkey((String)value); break; - case CLIENTCERT: setClientcert((String)value); break; - case USER: setUser((String)value); break; - case PASSWORD: setPassword((String)value); break; - case LANGUAGE: setLanguage((String)value); break; - case AUTOCOMMIT: setAutocommit((boolean)value); break; - case SCHEMA: setSchema((String)value); break; - case TIMEZONE: setTimezone((int)value); break; - case BINARY: setBinary((String)value); break; - case REPLYSIZE: setReplySize((int)value); break; - case FETCHSIZE: setReplySize((int)value); break; - case HASH: setHash((String)value); break; - case DEBUG: setDebug((boolean)value); break; - case LOGFILE: setLogfile((String)value); break; + case SO_TIMEOUT: + setSoTimeout((int) value); + break; + case CLOB_AS_VARCHAR: + setTreatClobAsVarchar((boolean) value); + break; + case BLOB_AS_BINARY: + setTreatBlobAsBinary((boolean) value); + break; - case SO_TIMEOUT: setSoTimeout((int)value); break; - case CLOB_AS_VARCHAR: setTreatClobAsVarchar((boolean)value); break; - case BLOB_AS_BINARY: setTreatBlobAsBinary((boolean)value); break; + default: + throw new IllegalStateException("unreachable -- missing case: " + parm.name); + } + } - default: - throw new IllegalStateException("unreachable -- missing case: " + parm.name); - } - } - - public String getString(Parameter parm) { - Object value = getObject(parm); - return parm.type.format(value); - } + public String getString(Parameter parm) { + Object value = getObject(parm); + return parm.type.format(value); + } - public Object getObject(Parameter parm) { - switch (parm) { - case TLS: return tls; - case HOST: return host; - case PORT: return port; - case DATABASE: return database; - case TABLESCHEMA: return tableschema; - case TABLE: return table; - case SOCK: return sock; - case SOCKDIR: return sockdir; - case CERT: return cert; - case CERTHASH: return certhash; - case CLIENTKEY: return clientkey; - case CLIENTCERT: return clientcert; - case USER: return user; - case PASSWORD: return password; - case LANGUAGE: return language; - case AUTOCOMMIT: return autocommit; - case SCHEMA: return schema; - case TIMEZONE: return timezone; - case BINARY: return binary; - case REPLYSIZE: return replySize; - case FETCHSIZE: return replySize; - case HASH: return hash; - case DEBUG: return debug; - case LOGFILE: return logfile; - case SO_TIMEOUT: return soTimeout; - case CLOB_AS_VARCHAR: return treatClobAsVarchar; - case BLOB_AS_BINARY: return treatBlobAsBinary; - default: - throw new IllegalStateException("unreachable -- missing case"); - } - } + public Object getObject(Parameter parm) { + switch (parm) { + case TLS: + return tls; + case HOST: + return host; + case PORT: + return port; + case DATABASE: + return database; + case TABLESCHEMA: + return tableschema; + case TABLE: + return table; + case SOCK: + return sock; + case SOCKDIR: + return sockdir; + case CERT: + return cert; + case CERTHASH: + return certhash; + case CLIENTKEY: + return clientkey; + case CLIENTCERT: + return clientcert; + case USER: + return user; + case PASSWORD: + return password; + case LANGUAGE: + return language; + case AUTOCOMMIT: + return autocommit; + case SCHEMA: + return schema; + case TIMEZONE: + return timezone; + case BINARY: + return binary; + case REPLYSIZE: + return replySize; + case FETCHSIZE: + return replySize; + case HASH: + return hash; + case DEBUG: + return debug; + case LOGFILE: + return logfile; + case SO_TIMEOUT: + return soTimeout; + case CLOB_AS_VARCHAR: + return treatClobAsVarchar; + case BLOB_AS_BINARY: + return treatBlobAsBinary; + default: + throw new IllegalStateException("unreachable -- missing case"); + } + } - public boolean isTls() { - return tls; - } + public boolean isTls() { + return tls; + } + + public void setTls(boolean tls) { + this.tls = tls; + validated = null; + } + + public String getHost() { + return host; + } - public void setTls(boolean tls) { - this.tls = tls; - validated = null; - } + public void setHost(String host) { + this.host = host; + validated = null; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + validated = null; + } + + public String getDatabase() { + return database; + } - public String getHost() { - return host; - } + public void setDatabase(String database) { + this.database = database; + validated = null; + } + + public String getTableschema() { + return tableschema; + } - public void setHost(String host) { - this.host = host; - validated = null; - } + public void setTableschema(String tableschema) { + this.tableschema = tableschema; + validated = null; + } + + public String getTable() { + return table; + } - public int getPort() { - return port; - } + public void setTable(String table) { + this.table = table; + validated = null; + } + + public String getSock() { + return sock; + } - public void setPort(int port) { - this.port = port; - validated = null; - } + public void setSock(String sock) { + this.sock = sock; + validated = null; + } - public String getDatabase() { - return database; - } + public String getSockdir() { + return sockdir; + } - public void setDatabase(String database) { - this.database = database; - validated = null; - } + public void setSockdir(String sockdir) { + this.sockdir = sockdir; + validated = null; + } + + public String getCert() { + return cert; + } - public String getTableschema() { - return tableschema; - } + public void setCert(String cert) { + this.cert = cert; + validated = null; + } + + public String getCerthash() { + return certhash; + } - public void setTableschema(String tableschema) { - this.tableschema = tableschema; - validated = null; - } + public void setCerthash(String certhash) { + this.certhash = certhash; + validated = null; + } - public String getTable() { - return table; - } + public String getClientkey() { + return clientkey; + } - public void setTable(String table) { - this.table = table; - validated = null; - } + public void setClientkey(String clientkey) { + this.clientkey = clientkey; + validated = null; + } - public String getSock() { - return sock; - } + public String getClientcert() { + return clientcert; + } - public void setSock(String sock) { - this.sock = sock; - validated = null; - } + public void setClientcert(String clientcert) { + this.clientcert = clientcert; + validated = null; + } - public String getSockdir() { - return sockdir; - } + public String getUser() { + return user; + } + + public void setUser(String user) { + this.user = user; + this.userWasSet = true; + validated = null; + } - public void setSockdir(String sockdir) { - this.sockdir = sockdir; - validated = null; - } + public String getPassword() { + return password; + } - public String getCert() { - return cert; - } + public void setPassword(String password) { + this.password = password; + this.passwordWasSet = true; + validated = null; + } - public void setCert(String cert) { - this.cert = cert; - validated = null; - } + public String getLanguage() { + return language; + } + + public void setLanguage(String language) { + this.language = language; + validated = null; + } - public String getCerthash() { - return certhash; - } + public boolean isAutocommit() { + return autocommit; + } + + public void setAutocommit(boolean autocommit) { + this.autocommit = autocommit; + validated = null; + } - public void setCerthash(String certhash) { - this.certhash = certhash; - validated = null; - } + public String getSchema() { + return schema; + } - public String getClientkey() { - return clientkey; - } + public void setSchema(String schema) { + this.schema = schema; + validated = null; + } - public void setClientkey(String clientkey) { - this.clientkey = clientkey; - validated = null; - } + public int getTimezone() { + return timezone; + } + + public void setTimezone(int timezone) { + this.timezone = timezone; + validated = null; + } - public String getClientcert() { - return clientcert; - } + public String getBinary() { + return binary; + } - public void setClientcert(String clientcert) { - this.clientcert = clientcert; - validated = null; - } + public void setBinary(String binary) { + this.binary = binary; + validated = null; + } + + public int getReplySize() { + return replySize; + } - public String getUser() { - return user; - } + public void setReplySize(int replySize) { + this.replySize = replySize; + validated = null; + } - public void setUser(String user) { - this.user = user; - this.userWasSet = true; - validated = null; - } + public String getHash() { + return hash; + } + + public void setHash(String hash) { + this.hash = hash; + validated = null; + } - public String getPassword() { - return password; - } + public boolean isDebug() { + return debug; + } + + public void setDebug(boolean debug) { + this.debug = debug; + validated = null; + } - public void setPassword(String password) { - this.password = password; - this.passwordWasSet = true; - validated = null; - } + public String getLogfile() { + return logfile; + } - public String getLanguage() { - return language; - } + public void setLogfile(String logfile) { + this.logfile = logfile; + validated = null; + } - public void setLanguage(String language) { - this.language = language; - validated = null; - } + public int getSoTimeout() { + return soTimeout; + } - public boolean isAutocommit() { - return autocommit; - } + public void setSoTimeout(int soTimeout) { + this.soTimeout = soTimeout; + validated = null; + } + + public boolean isTreatClobAsVarchar() { + return treatClobAsVarchar; + } - public void setAutocommit(boolean autocommit) { - this.autocommit = autocommit; - validated = null; - } + public void setTreatClobAsVarchar(boolean treatClobAsVarchar) { + this.treatClobAsVarchar = treatClobAsVarchar; + validated = null; + } - public String getSchema() { - return schema; - } + public boolean isTreatBlobAsBinary() { + return treatBlobAsBinary; + } - public void setSchema(String schema) { - this.schema = schema; - validated = null; - } + public void setTreatBlobAsBinary(boolean treatBlobAsBinary) { + this.treatBlobAsBinary = treatBlobAsBinary; + validated = null; + } - public int getTimezone() { - return timezone; - } + public Validated validate() throws ValidationError { + if (validated == null) + validated = new Validated(); + return validated; + } - public void setTimezone(int timezone) { - this.timezone = timezone; - validated = null; - } - - public String getBinary() { - return binary; - } + public String buildUrl() { + final StringBuilder sb = new StringBuilder(128); + sb.append("jdbc:"); + sb.append(tls ? "monetdbs" : "monetdb"); + sb.append("://"); + sb.append(packHost(host)); + if (!Parameter.PORT.getDefault().equals(port)) { + sb.append(':'); + sb.append(port); + } + sb.append('/').append(database); + String sep = "?"; + for (Parameter parm : Parameter.values()) { + if (parm.isCore || parm == Parameter.USER || parm == Parameter.PASSWORD) + continue; + Object defaultValue = parm.getDefault(); + if (defaultValue == null) + continue; + Object value = getObject(parm); + if (value.equals(defaultValue)) + continue; + sb.append(sep).append(parm.name).append('='); + String raw = getString(parm); + String encoded = MonetUrlParser.percentEncode(raw); + sb.append(encoded); + sep = "&"; + } + return sb.toString(); + } - public void setBinary(String binary) { - this.binary = binary; - validated = null; - } - - public int getReplySize() { - return replySize; - } - - public void setReplySize(int replySize) { - this.replySize = replySize; - validated = null; - } - - public String getHash() { - return hash; - } + public Properties getProperties() { + Properties props = new Properties(); + for (Parameter parm : Parameter.values()) { + Object defaultValue = parm.getDefault(); + if (defaultValue == null || defaultValue.equals(getObject(parm))) + continue; + String value = getString(parm); + if (parm == Parameter.HOST) + value = packHost(host); + props.setProperty(parm.name, value); + } - public void setHash(String hash) { - this.hash = hash; - validated = null; - } + return props; + } - public boolean isDebug() { - return debug; - } + public void setProperties(Properties props) throws ValidationError { + if (props != null) { + for (String key : props.stringPropertyNames()) { + String value = props.getProperty(key); + if (key.equals(Parameter.HOST.name)) + value = Target.unpackHost(value); + setString(key, value); + } + } + } + + public enum Verify { + None, Cert, Hash, System + } + + public class Validated { - public void setDebug(boolean debug) { - this.debug = debug; - validated = null; - } + private final int nbinary; - public String getLogfile() { - return logfile; - } + Validated() throws ValidationError { + + // 1. The parameters have the types listed in the table in [Section + // Parameters](#parameters). - public void setLogfile(String logfile) { - this.logfile = logfile; - validated = null; - } - - public int getSoTimeout() { - return soTimeout; - } + String binaryString = binary; + int binaryInt; + try { + binaryInt = (int) ParameterType.Int.parse(Parameter.BINARY.name, binaryString); + } catch (ValidationError e) { + try { + boolean b = (boolean) ParameterType.Bool.parse(Parameter.BINARY.name, binaryString); + binaryInt = b ? 65535 : 0; + } catch (ValidationError ee) { + throw new ValidationError("binary= must be either a number or true/yes/on/false/no/off"); + } + } + if (binaryInt < 0) + throw new ValidationError("binary= cannot be negative"); + nbinary = binaryInt; - public void setSoTimeout(int soTimeout) { - this.soTimeout = soTimeout; - validated = null; - } - - public void setTreatClobAsVarchar(boolean treatClobAsVarchar) { - this.treatClobAsVarchar = treatClobAsVarchar; - validated = null; - } - - public boolean isTreatClobAsVarchar() { - return treatClobAsVarchar; - } - - public boolean isTreatBlobAsBinary() { - return treatBlobAsBinary; - } - - public void setTreatBlobAsBinary(boolean treatBlobAsBinary) { - this.treatBlobAsBinary = treatBlobAsBinary; - validated = null; - } - - public Validated validate() throws ValidationError { - if (validated == null) - validated = new Validated(); - return validated; - } + // 2. At least one of **sock** and **host** must be empty. + if (!sock.isEmpty() && !host.isEmpty()) + throw new ValidationError("sock=" + sock + " cannot be combined with host=" + host); - public String buildUrl() { - final StringBuilder sb = new StringBuilder(128); - sb.append("jdbc:"); - sb.append(tls ? "monetdbs": "monetdb"); - sb.append("://"); - sb.append(packHost(host)); - if (!Parameter.PORT.getDefault().equals(port)) { - sb.append(':'); - sb.append(port); - } - sb.append('/').append(database); - String sep = "?"; - for (Parameter parm: Parameter.values()) { - if (parm.isCore || parm == Parameter.USER || parm == Parameter.PASSWORD) - continue; - Object defaultValue = parm.getDefault(); - if (defaultValue == null) - continue; - Object value = getObject(parm); - if (value.equals(defaultValue)) - continue; - sb.append(sep).append(parm.name).append('='); - String raw = getString(parm); - String encoded = MonetUrlParser.percentEncode(raw); - sb.append(encoded); - sep = "&"; - } - return sb.toString(); - } + // 3. The string parameter **binary** must either parse as a boolean or as a + // non-negative integer. + // + // (checked above) - public Properties getProperties() { - Properties props = new Properties(); - for (Parameter parm: Parameter.values()) { - Object defaultValue = parm.getDefault(); - if (defaultValue == null || defaultValue.equals(getObject(parm))) - continue; - String value = getString(parm); - if (parm == Parameter.HOST) - value = packHost(host); - props.setProperty(parm.name, value); - } + // 4. If **sock** is not empty, **tls** must be 'off'. + if (!sock.isEmpty() && tls) + throw new ValidationError("monetdbs:// cannot be combined with sock="); - return props; - } - - public class Validated { - - private final int nbinary; - - Validated() throws ValidationError { - - // 1. The parameters have the types listed in the table in [Section - // Parameters](#parameters). + // 5. If **certhash** is not empty, it must be of the form `{sha256}hexdigits` + // where hexdigits is a non-empty sequence of 0-9, a-f, A-F and colons. + // TODO + if (!certhash.isEmpty()) { + if (!certhash.toLowerCase().startsWith("sha256:")) + throw new ValidationError("certificate hash must start with 'sha256:'"); + if (!hashPattern.matcher(certhash).matches()) + throw new ValidationError("invalid certificate hash"); + } - String binaryString = binary; - int binaryInt; - try { - binaryInt = (int) ParameterType.Int.parse(Parameter.BINARY.name, binaryString); - } catch (ValidationError e) { - try { - boolean b = (boolean) ParameterType.Bool.parse(Parameter.BINARY.name, binaryString); - binaryInt = b ? 65535 : 0; - } catch (ValidationError ee) { - throw new ValidationError("binary= must be either a number or true/yes/on/false/no/off"); - } - } - if (binaryInt < 0) - throw new ValidationError("binary= cannot be negative"); - nbinary = binaryInt; - - - // 2. At least one of **sock** and **host** must be empty. - if (!sock.isEmpty() && !host.isEmpty()) - throw new ValidationError("sock=" + sock + " cannot be combined with host=" + host); + // 6. If **tls** is 'off', **cert** and **certhash** must be 'off' as well. + if (!tls) { + if (!cert.isEmpty() || !certhash.isEmpty()) + throw new ValidationError("cert= and certhash= are only allowed in combination with monetdbs://"); + } - // 3. The string parameter **binary** must either parse as a boolean or as a - // non-negative integer. - // - // (checked above) - - // 4. If **sock** is not empty, **tls** must be 'off'. - if (!sock.isEmpty() && tls) throw new ValidationError("monetdbs:// cannot be combined with sock="); - - // 5. If **certhash** is not empty, it must be of the form `{sha256}hexdigits` - // where hexdigits is a non-empty sequence of 0-9, a-f, A-F and colons. - // TODO - if (!certhash.isEmpty()) { - if (!certhash.toLowerCase().startsWith("sha256:")) - throw new ValidationError("certificate hash must start with 'sha256:'"); - if (!hashPattern.matcher(certhash).matches()) - throw new ValidationError("invalid certificate hash"); - } - - // 6. If **tls** is 'off', **cert** and **certhash** must be 'off' as well. - if (!tls) { - if (!cert.isEmpty() || !certhash.isEmpty()) - throw new ValidationError("cert= and certhash= are only allowed in combination with monetdbs://"); - } - - // 7. Parameters **database**, **tableschema** and **table** must consist only of - // upper- and lowercase letters, digits, periods, dashes and underscores. They must not - // start with a dash. - // If **table** is not empty, **tableschema** must also not be empty. - // If **tableschema** is not empty, **database** must also not be empty. - if (database.isEmpty() && !tableschema.isEmpty()) - throw new ValidationError("table schema cannot be set without database"); - if (tableschema.isEmpty() && !table.isEmpty()) - throw new ValidationError("table cannot be set without schema"); - if (!database.isEmpty() && !namePattern.matcher(database).matches()) - throw new ValidationError("invalid database name"); - if (!tableschema.isEmpty() && !namePattern.matcher(tableschema).matches()) - throw new ValidationError("invalid table schema name"); - if (!table.isEmpty() && !namePattern.matcher(table).matches()) - throw new ValidationError("invalid table name"); + // 7. Parameters **database**, **tableschema** and **table** must consist only of + // upper- and lowercase letters, digits, periods, dashes and underscores. They must not + // start with a dash. + // If **table** is not empty, **tableschema** must also not be empty. + // If **tableschema** is not empty, **database** must also not be empty. + if (database.isEmpty() && !tableschema.isEmpty()) + throw new ValidationError("table schema cannot be set without database"); + if (tableschema.isEmpty() && !table.isEmpty()) + throw new ValidationError("table cannot be set without schema"); + if (!database.isEmpty() && !namePattern.matcher(database).matches()) + throw new ValidationError("invalid database name"); + if (!tableschema.isEmpty() && !namePattern.matcher(tableschema).matches()) + throw new ValidationError("invalid table schema name"); + if (!table.isEmpty() && !namePattern.matcher(table).matches()) + throw new ValidationError("invalid table name"); - // 8. Parameter **port** must be -1 or in the range 1-65535. - if (port < -1 || port == 0 || port > 65535) throw new ValidationError("invalid port number " + port); + // 8. Parameter **port** must be -1 or in the range 1-65535. + if (port < -1 || port == 0 || port > 65535) + throw new ValidationError("invalid port number " + port); - // 9. If **clientcert** is set, **clientkey** must also be set. - if (!clientcert.isEmpty() && clientkey.isEmpty()) - throw new ValidationError("clientcert= is only valid in combination with clientkey="); + // 9. If **clientcert** is set, **clientkey** must also be set. + if (!clientcert.isEmpty() && clientkey.isEmpty()) + throw new ValidationError("clientcert= is only valid in combination with clientkey="); - // JDBC specific - if (soTimeout < 0) - throw new ValidationError("so_timeout= must not be negative"); - } + // JDBC specific + if (soTimeout < 0) + throw new ValidationError("so_timeout= must not be negative"); + } - public boolean getTls() { - return tls; - } + public boolean getTls() { + return tls; + } - // Getter is private because you probably want connectTcp() instead - private String getHost() { - return host; - } + // Getter is private because you probably want connectTcp() instead + private String getHost() { + return host; + } - // Getter is private because you probably want connectPort() instead - private int getPort() { - return port; - } + // Getter is private because you probably want connectPort() instead + private int getPort() { + return port; + } - public String getDatabase() { - return database; - } + public String getDatabase() { + return database; + } - public String getTableschema() { - return tableschema; - } + public String getTableschema() { + return tableschema; + } - public String getTable() { - return table; - } + public String getTable() { + return table; + } - // Getter is private because you probably want connectUnix() instead - private String getSock() { - return sock; - } + // Getter is private because you probably want connectUnix() instead + private String getSock() { + return sock; + } - public String getSockdir() { - return sockdir; - } + public String getSockdir() { + return sockdir; + } - public String getCert() { - return cert; - } + public String getCert() { + return cert; + } + + public String getCerthash() { + return certhash; + } - public String getCerthash() { - return certhash; - } + public String getClientkey() { + return clientkey; + } - public String getClientkey() { - return clientkey; - } + public String getClientcert() { + return clientcert; + } - public String getClientcert() { - return clientcert; - } + public String getUser() { + return user; + } - public String getUser() { - return user; - } + public String getPassword() { + return password; + } - public String getPassword() { - return password; - } + public String getLanguage() { + return language; + } - public String getLanguage() { - return language; - } + public boolean getAutocommit() { + return autocommit; + } - public boolean getAutocommit() { - return autocommit; - } + public String getSchema() { + return schema; + } - public String getSchema() { - return schema; - } + public int getTimezone() { + return timezone; + } - public int getTimezone() { - return timezone; - } + // Getter is private because you probably want connectBinary() instead + public int getBinary() { + return nbinary; + } - // Getter is private because you probably want connectBinary() instead - public int getBinary() { - return nbinary; - } + public int getReplySize() { + return replySize; + } - public int getReplySize() { - return replySize; - } + public String getHash() { + return hash; + } - public String getHash() { - return hash; - } + public boolean getDebug() { + return debug; + } - public boolean getDebug() { - return debug; - } + public String getLogfile() { + return logfile; + } - public String getLogfile() { - return logfile; - } + public int getSoTimeout() { + return soTimeout; + } - public int getSoTimeout() { - return soTimeout; - } + public boolean isTreatClobAsVarchar() { + return treatClobAsVarchar; + } - public boolean isTreatClobAsVarchar() { - return treatClobAsVarchar; - } + public boolean isTreatBlobAsBinary() { + return treatBlobAsBinary; + } - public boolean isTreatBlobAsBinary() { - return treatBlobAsBinary; - } + public boolean connectScan() { + if (database.isEmpty()) + return false; + if (!sock.isEmpty() || !host.isEmpty() || port != -1) + return false; + return !tls; + } - public boolean connectScan() { - if (database.isEmpty()) return false; - if (!sock.isEmpty() || !host.isEmpty() || port != -1) return false; - return !tls; - } - - public int connectPort() { - return port == -1 ? 50000 : port; - } + public int connectPort() { + return port == -1 ? 50000 : port; + } - public String connectUnix() { - if (!sock.isEmpty()) return sock; - if (tls) return ""; - if (host.isEmpty()) return sockdir + "/.s.monetdb." + connectPort(); - return ""; - } + public String connectUnix() { + if (!sock.isEmpty()) + return sock; + if (tls) + return ""; + if (host.isEmpty()) + return sockdir + "/.s.monetdb." + connectPort(); + return ""; + } - public String connectTcp() { - if (!sock.isEmpty()) return ""; - if (host.isEmpty()) return "localhost"; - return host; - } - - public Verify connectVerify() { - if (!tls) return Verify.None; - if (!certhash.isEmpty()) return Verify.Hash; - if (!cert.isEmpty()) return Verify.Cert; - return Verify.System; - } + public String connectTcp() { + if (!sock.isEmpty()) + return ""; + if (host.isEmpty()) + return "localhost"; + return host; + } - public String connectCertHashDigits() { - if (!tls) return null; - StringBuilder builder = new StringBuilder(certhash.length()); - for (int i = "sha256:".length(); i < certhash.length(); i++) { - char c = certhash.charAt(i); - if (Character.digit(c, 16) >= 0) builder.append(Character.toLowerCase(c)); - } - return builder.toString(); - } - - public int connectBinary() { - return nbinary; - } + public Verify connectVerify() { + if (!tls) + return Verify.None; + if (!certhash.isEmpty()) + return Verify.Hash; + if (!cert.isEmpty()) + return Verify.Cert; + return Verify.System; + } - public String connectClientKey() { - return clientkey; - } + public String connectCertHashDigits() { + if (!tls) + return null; + StringBuilder builder = new StringBuilder(certhash.length()); + for (int i = "sha256:".length(); i < certhash.length(); i++) { + char c = certhash.charAt(i); + if (Character.digit(c, 16) >= 0) + builder.append(Character.toLowerCase(c)); + } + return builder.toString(); + } - public String connectClientCert() { - return clientcert.isEmpty() ? clientkey : clientcert; - } - } + public int connectBinary() { + return nbinary; + } - public enum Verify { - None, - Cert, - Hash, - System; - } + public String connectClientKey() { + return clientkey; + } + + public String connectClientCert() { + return clientcert.isEmpty() ? clientkey : clientcert; + } + } }
--- a/src/main/java/org/monetdb/mcl/net/ValidationError.java +++ b/src/main/java/org/monetdb/mcl/net/ValidationError.java @@ -1,11 +1,11 @@ package org.monetdb.mcl.net; public class ValidationError extends Exception { - public ValidationError(String parameter, String message) { - super(parameter + ": " + message); - } + public ValidationError(String parameter, String message) { + super(parameter + ": " + message); + } - public ValidationError(String message) { - super(message); - } + public ValidationError(String message) { + super(message); + } }
--- a/tests/TLSTester.java +++ b/tests/TLSTester.java @@ -11,307 +11,304 @@ import java.sql.SQLException; import java.util.HashMap; import java.util.HashSet; import java.util.Properties; -import java.util.stream.Collectors; public class TLSTester { - int verbose = 0; - String serverHost = null; - String altHost = null; - int serverPort = -1; - boolean enableTrusted = false; - File tempDir = null; - final HashMap<String, File> fileCache = new HashMap<>(); - private HashSet<String> preparedButNotRun = new HashSet<>(); + final HashMap<String, File> fileCache = new HashMap<>(); + int verbose = 0; + String serverHost = null; + String altHost = null; + int serverPort = -1; + boolean enableTrusted = false; + File tempDir = null; + private final HashSet<String> preparedButNotRun = new HashSet<>(); - public TLSTester(String[] args) { - for (int i = 0; i < args.length; i++) { - String arg = args[i]; - if (arg.equals("-v")) { - verbose = 1; - } else if (arg.equals("-a")) { - altHost = args[++i]; - } else if (arg.equals("-t")) { - enableTrusted = true; - } else if (!arg.startsWith("-") && serverHost == null) { - int idx = arg.indexOf(':'); - if (idx > 0) { - serverHost = arg.substring(0, idx); - try { - serverPort = Integer.parseInt(arg.substring(idx + 1)); - if (serverPort > 0 && serverPort < 65536) - continue; - } catch (NumberFormatException ignored) { - } - } - // if we get here it wasn't very valid - throw new IllegalArgumentException("Invalid argument: " + arg); - } else { - throw new IllegalArgumentException("Unexpected argument: " + arg); - } - } - } + public TLSTester(String[] args) { + for (int i = 0; i < args.length; i++) { + String arg = args[i]; + if (arg.equals("-v")) { + verbose = 1; + } else if (arg.equals("-a")) { + altHost = args[++i]; + } else if (arg.equals("-t")) { + enableTrusted = true; + } else if (!arg.startsWith("-") && serverHost == null) { + int idx = arg.indexOf(':'); + if (idx > 0) { + serverHost = arg.substring(0, idx); + try { + serverPort = Integer.parseInt(arg.substring(idx + 1)); + if (serverPort > 0 && serverPort < 65536) + continue; + } catch (NumberFormatException ignored) { + } + } + // if we get here it wasn't very valid + throw new IllegalArgumentException("Invalid argument: " + arg); + } else { + throw new IllegalArgumentException("Unexpected argument: " + arg); + } + } + } - public static void main(String[] args) throws IOException, SQLException, ClassNotFoundException { - Class.forName("org.monetdb.jdbc.MonetDriver"); - TLSTester main = new TLSTester(args); - main.run(); - } + public static void main(String[] args) throws IOException, SQLException, ClassNotFoundException { + Class.forName("org.monetdb.jdbc.MonetDriver"); + TLSTester main = new TLSTester(args); + main.run(); + } - private HashMap<String,Integer> loadPortMap(String testName) throws IOException { - HashMap<String,Integer> portMap = new HashMap<>(); - InputStream in = fetchData("/?test=" + testName); - BufferedReader br = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)); - for (String line = br.readLine(); line != null; line = br.readLine()) { - int idx = line.indexOf(':'); - String service = line.substring(0, idx); - int port; - try { - port = Integer.parseInt(line.substring(idx + 1)); - } catch (NumberFormatException e) { - throw new RuntimeException("Invalid port map line: " + line); - } - portMap.put(service, port); - } - return portMap; - } + private HashMap<String, Integer> loadPortMap(String testName) throws IOException { + HashMap<String, Integer> portMap = new HashMap<>(); + InputStream in = fetchData("/?test=" + testName); + BufferedReader br = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)); + for (String line = br.readLine(); line != null; line = br.readLine()) { + int idx = line.indexOf(':'); + String service = line.substring(0, idx); + int port; + try { + port = Integer.parseInt(line.substring(idx + 1)); + } catch (NumberFormatException e) { + throw new RuntimeException("Invalid port map line: " + line); + } + portMap.put(service, port); + } + return portMap; + } - private File resource(String resource) throws IOException { - if (!fileCache.containsKey(resource)) - fetchResource(resource); - return fileCache.get(resource); - } + private File resource(String resource) throws IOException { + if (!fileCache.containsKey(resource)) + fetchResource(resource); + return fileCache.get(resource); + } - private void fetchResource(String resource) throws IOException { - if (!resource.startsWith("/")) { - throw new IllegalArgumentException("Resource must start with slash: " + resource); - } - if (tempDir == null) { - tempDir = Files.createTempDirectory("tlstest").toFile(); - tempDir.deleteOnExit(); - } - File outPath = new File(tempDir, resource.substring(1)); - try (InputStream in = fetchData(resource); FileOutputStream out = new FileOutputStream(outPath)) { - byte[] buffer = new byte[12]; - while (true) { - int n = in.read(buffer); - if (n <= 0) - break; - out.write(buffer, 0, n); - } - } - fileCache.put(resource, outPath); - } + private void fetchResource(String resource) throws IOException { + if (!resource.startsWith("/")) { + throw new IllegalArgumentException("Resource must start with slash: " + resource); + } + if (tempDir == null) { + tempDir = Files.createTempDirectory("tlstest").toFile(); + tempDir.deleteOnExit(); + } + File outPath = new File(tempDir, resource.substring(1)); + try (InputStream in = fetchData(resource); FileOutputStream out = new FileOutputStream(outPath)) { + byte[] buffer = new byte[12]; + while (true) { + int n = in.read(buffer); + if (n <= 0) + break; + out.write(buffer, 0, n); + } + } + fileCache.put(resource, outPath); + } - private byte[] fetchBytes(String resource) throws IOException { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - try (InputStream in = fetchData(resource)) { - byte[] buffer = new byte[22]; - while (true) { - int nread = in.read(buffer); - if (nread <= 0) - break; - out.write(buffer, 0, nread); - } - return out.toByteArray(); - } - } + private byte[] fetchBytes(String resource) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try (InputStream in = fetchData(resource)) { + byte[] buffer = new byte[22]; + while (true) { + int nread = in.read(buffer); + if (nread <= 0) + break; + out.write(buffer, 0, nread); + } + return out.toByteArray(); + } + } - private InputStream fetchData(String resource) throws IOException { - URL url = new URL("http://" + serverHost + ":" + serverPort + resource); - URLConnection conn = url.openConnection(); - conn.connect(); - return conn.getInputStream(); - } + private InputStream fetchData(String resource) throws IOException { + URL url = new URL("http://" + serverHost + ":" + serverPort + resource); + URLConnection conn = url.openConnection(); + conn.connect(); + return conn.getInputStream(); + } - private void run() throws IOException, SQLException { - test_connect_plain(); - test_connect_tls(); - test_refuse_no_cert(); - test_refuse_wrong_cert(); - test_refuse_wrong_host(); - test_refuse_tlsv12(); - test_refuse_expired(); + private void run() throws IOException, SQLException { + test_connect_plain(); + test_connect_tls(); + test_refuse_no_cert(); + test_refuse_wrong_cert(); + test_refuse_wrong_host(); + test_refuse_tlsv12(); + test_refuse_expired(); // test_connect_client_auth1(); // test_connect_client_auth2(); - test_fail_tls_to_plain(); - test_fail_plain_to_tls(); - test_connect_server_name(); - test_connect_alpn_mapi9(); - test_connect_trusted(); - test_refuse_trusted_wrong_host(); + test_fail_tls_to_plain(); + test_fail_plain_to_tls(); + test_connect_server_name(); + test_connect_alpn_mapi9(); + test_connect_trusted(); + test_refuse_trusted_wrong_host(); - // did we forget to call expectSucceed and expectFailure somewhere? - if (!preparedButNotRun.isEmpty()) { - String names = String.join(", ", preparedButNotRun); - throw new RuntimeException("Not all tests called expectSuccess/expectFailure: " + names); - } - } + // did we forget to call expectSucceed and expectFailure somewhere? + if (!preparedButNotRun.isEmpty()) { + String names = String.join(", ", preparedButNotRun); + throw new RuntimeException("Not all tests called expectSuccess/expectFailure: " + names); + } + } - private void test_connect_plain() throws IOException, SQLException { - attempt("connect_plain", "plain").with(Parameter.TLS, false).expectSuccess(); - } + private void test_connect_plain() throws IOException, SQLException { + attempt("connect_plain", "plain").with(Parameter.TLS, false).expectSuccess(); + } - private void test_connect_tls() throws IOException, SQLException { - Attempt attempt = attempt("connect_tls", "server1"); - attempt.withFile(Parameter.CERT, "/ca1.crt").expectSuccess(); - } + private void test_connect_tls() throws IOException, SQLException { + Attempt attempt = attempt("connect_tls", "server1"); + attempt.withFile(Parameter.CERT, "/ca1.crt").expectSuccess(); + } - private void test_refuse_no_cert() throws IOException, SQLException { - attempt("refuse_no_cert", "server1").expectFailure("PKIX path building failed"); - } + private void test_refuse_no_cert() throws IOException, SQLException { + attempt("refuse_no_cert", "server1").expectFailure("PKIX path building failed"); + } - private void test_refuse_wrong_cert() throws IOException, SQLException { - Attempt attempt = attempt("refuse_wrong_cert", "server1"); - attempt.withFile(Parameter.CERT, "/ca2.crt").expectFailure("PKIX path building failed"); - } + private void test_refuse_wrong_cert() throws IOException, SQLException { + Attempt attempt = attempt("refuse_wrong_cert", "server1"); + attempt.withFile(Parameter.CERT, "/ca2.crt").expectFailure("PKIX path building failed"); + } - private void test_refuse_wrong_host() throws IOException, SQLException { - Attempt attempt = attempt("refuse_wrong_host", "server1").with(Parameter.HOST, altHost); - attempt.withFile(Parameter.CERT, "/ca1.crt").expectFailure("No subject alternative DNS name"); - } + private void test_refuse_wrong_host() throws IOException, SQLException { + Attempt attempt = attempt("refuse_wrong_host", "server1").with(Parameter.HOST, altHost); + attempt.withFile(Parameter.CERT, "/ca1.crt").expectFailure("No subject alternative DNS name"); + } - private void test_refuse_tlsv12() throws IOException, SQLException { - Attempt attempt = attempt("refuse_tlsv12", "tls12"); - attempt.withFile(Parameter.CERT, "/ca1.crt").expectFailure("protocol_version"); - } + private void test_refuse_tlsv12() throws IOException, SQLException { + Attempt attempt = attempt("refuse_tlsv12", "tls12"); + attempt.withFile(Parameter.CERT, "/ca1.crt").expectFailure("protocol_version"); + } - private void test_refuse_expired() throws IOException, SQLException { - Attempt attempt = attempt("refuse_expired", "expiredcert"); - attempt.withFile(Parameter.CERT, "/ca1.crt").expectFailure("PKIX path validation failed"); - } - - private void test_connect_client_auth1() throws IOException, SQLException { - attempt("connect_client_auth1", "clientauth") - .withFile(Parameter.CERT, "/ca1.crt") - .withFile(Parameter.CLIENTKEY, "/client2.keycrt") - .expectSuccess(); - } + private void test_refuse_expired() throws IOException, SQLException { + Attempt attempt = attempt("refuse_expired", "expiredcert"); + attempt.withFile(Parameter.CERT, "/ca1.crt").expectFailure("PKIX path validation failed"); + } - private void test_connect_client_auth2() throws IOException, SQLException { - attempt("connect_client_auth2", "clientauth") - .withFile(Parameter.CERT, "/ca1.crt") - .withFile(Parameter.CLIENTKEY, "/client2.key") - .withFile(Parameter.CLIENTCERT, "/client2.crt") - .expectSuccess(); - } + private void test_connect_client_auth1() throws IOException, SQLException { + attempt("connect_client_auth1", "clientauth") + .withFile(Parameter.CERT, "/ca1.crt") + .withFile(Parameter.CLIENTKEY, "/client2.keycrt") + .expectSuccess(); + } - private void test_fail_tls_to_plain() throws IOException, SQLException { - Attempt attempt = attempt("fail_tls_to_plain", "plain"); - attempt.withFile(Parameter.CERT, "/ca1.crt").expectFailure(""); + private void test_connect_client_auth2() throws IOException, SQLException { + attempt("connect_client_auth2", "clientauth") + .withFile(Parameter.CERT, "/ca1.crt") + .withFile(Parameter.CLIENTKEY, "/client2.key") + .withFile(Parameter.CLIENTCERT, "/client2.crt") + .expectSuccess(); + } - } + private void test_fail_tls_to_plain() throws IOException, SQLException { + Attempt attempt = attempt("fail_tls_to_plain", "plain"); + attempt.withFile(Parameter.CERT, "/ca1.crt").expectFailure(""); - private void test_fail_plain_to_tls() throws IOException, SQLException { - attempt("fail_plain_to_tls", "server1").with(Parameter.TLS, false).expectFailure("Cannot connect"); - } + } - private void test_connect_server_name() throws IOException, SQLException { - Attempt attempt = attempt("connect_server_name", "sni"); - attempt.withFile(Parameter.CERT, "/ca1.crt").expectSuccess(); - } + private void test_fail_plain_to_tls() throws IOException, SQLException { + attempt("fail_plain_to_tls", "server1").with(Parameter.TLS, false).expectFailure("Cannot connect"); + } - private void test_connect_alpn_mapi9() throws IOException, SQLException { - attempt("connect_alpn_mapi9", "alpn_mapi9") - .withFile(Parameter.CERT, "/ca1.crt") - .expectSuccess(); - } + private void test_connect_server_name() throws IOException, SQLException { + Attempt attempt = attempt("connect_server_name", "sni"); + attempt.withFile(Parameter.CERT, "/ca1.crt").expectSuccess(); + } - private void test_connect_trusted() throws IOException, SQLException { - attempt("connect_trusted", null) - .with(Parameter.HOST, "monetdb.ergates.nl") - .with(Parameter.PORT, 50000) - .expectSuccess(); - } + private void test_connect_alpn_mapi9() throws IOException, SQLException { + attempt("connect_alpn_mapi9", "alpn_mapi9").withFile(Parameter.CERT, "/ca1.crt").expectSuccess(); + } + + private void test_connect_trusted() throws IOException, SQLException { + attempt("connect_trusted", null) + .with(Parameter.HOST, "monetdb.ergates.nl") + .with(Parameter.PORT, 50000) + .expectSuccess(); + } - private void test_refuse_trusted_wrong_host() throws IOException, SQLException { - attempt("test_refuse_trusted_wrong_host", null) - .with(Parameter.HOST, "monetdbxyz.ergates.nl") - .with(Parameter.PORT, 50000) - .expectFailure("No subject alternative DNS name"); - } + private void test_refuse_trusted_wrong_host() throws IOException, SQLException { + attempt("test_refuse_trusted_wrong_host", null) + .with(Parameter.HOST, "monetdbxyz.ergates.nl") + .with(Parameter.PORT, 50000) + .expectFailure("No subject alternative DNS name"); + } - private Attempt attempt(String testName, String portName) throws IOException { - preparedButNotRun.add(testName); - return new Attempt(testName, portName); - } + private Attempt attempt(String testName, String portName) throws IOException { + preparedButNotRun.add(testName); + return new Attempt(testName, portName); + } - private class Attempt { - private final String testName; - private final Properties props = new Properties(); - boolean disabled = false; + private class Attempt { + private final String testName; + private final Properties props = new Properties(); + boolean disabled = false; - public Attempt(String testName, String portName) throws IOException { - HashMap<String, Integer> portMap = loadPortMap(testName); + public Attempt(String testName, String portName) throws IOException { + HashMap<String, Integer> portMap = loadPortMap(testName); - this.testName = testName; - with(Parameter.TLS, true); - with(Parameter.HOST, serverHost); - with(Parameter.SO_TIMEOUT, 3000); - if (portName != null) { - Integer port = portMap.get(portName); - if (port != null) { - with(Parameter.PORT, port); - } else { - throw new RuntimeException("Unknown port name: " + portName); - } - } - } + this.testName = testName; + with(Parameter.TLS, true); + with(Parameter.HOST, serverHost); + with(Parameter.SO_TIMEOUT, 3000); + if (portName != null) { + Integer port = portMap.get(portName); + if (port != null) { + with(Parameter.PORT, port); + } else { + throw new RuntimeException("Unknown port name: " + portName); + } + } + } - private Attempt with(Parameter parm, String value) { - props.setProperty(parm.name, value); - return this; - } + private Attempt with(Parameter parm, String value) { + props.setProperty(parm.name, value); + return this; + } - private Attempt with(Parameter parm, int value) { - props.setProperty(parm.name, Integer.toString(value)); - return this; - } + private Attempt with(Parameter parm, int value) { + props.setProperty(parm.name, Integer.toString(value)); + return this; + } - private Attempt with(Parameter parm, boolean value) { - props.setProperty(parm.name, value ? "true" : "false"); - return this; - } + private Attempt with(Parameter parm, boolean value) { + props.setProperty(parm.name, value ? "true" : "false"); + return this; + } - private Attempt withFile(Parameter parm, String certResource) throws IOException { - File certFile = resource(certResource); - String path = certFile.getPath(); - with(parm, path); - return this; - } + private Attempt withFile(Parameter parm, String certResource) throws IOException { + File certFile = resource(certResource); + String path = certFile.getPath(); + with(parm, path); + return this; + } - public void expectSuccess() throws SQLException { - preparedButNotRun.remove(testName); - if (disabled) - return; - try { - Connection conn = DriverManager.getConnection("jdbc:monetdb:", props); - conn.close(); - } catch (SQLException e) { - if (e.getMessage().startsWith("Sorry, this is not a real MonetDB instance")) { - // it looks like a failure but this is actually our success scenario - // because this is what the TLS Tester does when the connection succeeds. - return; - } - // other exceptions ARE errors and should be reported. - throw e; - } - } + public void expectSuccess() throws SQLException { + preparedButNotRun.remove(testName); + if (disabled) + return; + try { + Connection conn = DriverManager.getConnection("jdbc:monetdb:", props); + conn.close(); + } catch (SQLException e) { + if (e.getMessage().startsWith("Sorry, this is not a real MonetDB instance")) { + // it looks like a failure but this is actually our success scenario + // because this is what the TLS Tester does when the connection succeeds. + return; + } + // other exceptions ARE errors and should be reported. + throw e; + } + } - public void expectFailure(String... expectedMessages) throws SQLException { - if (disabled) - return; - try { - expectSuccess(); - throw new RuntimeException("Expected test " + testName + " to throw an exception but it didn't"); - } catch (SQLException e) { - for (String expected : expectedMessages) - if (e.getMessage().contains(expected)) - return; - String message = "Test " + testName + " threw the wrong exception: " + e.getMessage() + '\n' + "Expected:\n <" + String.join(">\n <", expectedMessages) + ">"; - throw new RuntimeException(message); + public void expectFailure(String... expectedMessages) throws SQLException { + if (disabled) + return; + try { + expectSuccess(); + throw new RuntimeException("Expected test " + testName + " to throw an exception but it didn't"); + } catch (SQLException e) { + for (String expected : expectedMessages) + if (e.getMessage().contains(expected)) + return; + String message = "Test " + testName + " threw the wrong exception: " + e.getMessage() + '\n' + "Expected:\n <" + String.join(">\n <", expectedMessages) + ">"; + throw new RuntimeException(message); - } - } + } + } - } + } }
--- a/tests/UrlTester.java +++ b/tests/UrlTester.java @@ -5,410 +5,410 @@ import java.net.URISyntaxException; import java.util.ArrayList; public class UrlTester { - final String filename; - final int verbose; - final BufferedReader reader; - int lineno = 0; - int testCount = 0; - Target target = null; - Target.Validated validated = null; + final String filename; + final int verbose; + final BufferedReader reader; + int lineno = 0; + int testCount = 0; + Target target = null; + Target.Validated validated = null; - public UrlTester(String filename, BufferedReader reader, int verbose) { - this.filename = filename; - this.verbose = verbose; - this.reader = reader; - } + public UrlTester(String filename, BufferedReader reader, int verbose) { + this.filename = filename; + this.verbose = verbose; + this.reader = reader; + } - public UrlTester(String filename, int verbose) throws IOException { - this.filename = filename; - this.verbose = verbose; - this.reader = new BufferedReader(new FileReader(filename)); - } + public UrlTester(String filename, int verbose) throws IOException { + this.filename = filename; + this.verbose = verbose; + this.reader = new BufferedReader(new FileReader(filename)); + } - public static void main(String[] args) throws IOException { - ArrayList<String> filenames = new ArrayList<>(); - int verbose = 0; - for (String arg : args) { - switch (arg) { - case "-vvv": - verbose++; - case "-vv": - verbose++; - case "-v": - verbose++; - break; - case "-h": - case "--help": - exitUsage(null); - break; - default: - if (!arg.startsWith("-")) { - filenames.add(arg); - } else { - exitUsage("Unexpected argument: " + arg); - } - break; - } - } + public static void main(String[] args) throws IOException { + ArrayList<String> filenames = new ArrayList<>(); + int verbose = 0; + for (String arg : args) { + switch (arg) { + case "-vvv": + verbose++; + case "-vv": + verbose++; + case "-v": + verbose++; + break; + case "-h": + case "--help": + exitUsage(null); + break; + default: + if (!arg.startsWith("-")) { + filenames.add(arg); + } else { + exitUsage("Unexpected argument: " + arg); + } + break; + } + } - runUnitTests(); + runUnitTests(); - try { - if (filenames.isEmpty()) { - runAllTests(); - } else { - for (String filename : filenames) { - new UrlTester(filename, verbose).run(); - } - } - } catch (Failure e) { - System.err.println("Test failed: " + e.getMessage()); - System.exit(1); - } - } + try { + if (filenames.isEmpty()) { + runAllTests(); + } else { + for (String filename : filenames) { + new UrlTester(filename, verbose).run(); + } + } + } catch (Failure e) { + System.err.println("Test failed: " + e.getMessage()); + System.exit(1); + } + } - private static void exitUsage(String message) { - if (message != null) { - System.err.println(message); - } - System.err.println("Usage: UrlTester OPTIONS [FILENAME..]"); - System.err.println("Options:"); - System.err.println(" -v Be more verbose"); - System.err.println(" -h --help Show this help"); - int status = message == null ? 0 : 1; - System.exit(status); - } + private static void exitUsage(String message) { + if (message != null) { + System.err.println(message); + } + System.err.println("Usage: UrlTester OPTIONS [FILENAME..]"); + System.err.println("Options:"); + System.err.println(" -v Be more verbose"); + System.err.println(" -h --help Show this help"); + int status = message == null ? 0 : 1; + System.exit(status); + } - public static UrlTester forResource(String resourceName, int verbose) throws FileNotFoundException { - InputStream stream = UrlTester.class.getResourceAsStream(resourceName); - if (stream == null) { - throw new FileNotFoundException("Resource " + resourceName); - } - BufferedReader reader = new BufferedReader(new InputStreamReader(stream)); - return new UrlTester(resourceName, reader, verbose); - } + public static UrlTester forResource(String resourceName, int verbose) throws FileNotFoundException { + InputStream stream = UrlTester.class.getResourceAsStream(resourceName); + if (stream == null) { + throw new FileNotFoundException("Resource " + resourceName); + } + BufferedReader reader = new BufferedReader(new InputStreamReader(stream)); + return new UrlTester(resourceName, reader, verbose); + } - public static void runAllTests() throws IOException, Failure { - runUnitTests(); - UrlTester.forResource("/tests.md", 0).run(); - UrlTester.forResource("/javaspecific.md", 0).run(); - } + public static void runAllTests() throws IOException, Failure { + runUnitTests(); + UrlTester.forResource("/tests.md", 0).run(); + UrlTester.forResource("/javaspecific.md", 0).run(); + } - public static void runUnitTests() { - testDefaults(); - testParameters(); - } + public static void runUnitTests() { + testDefaults(); + testParameters(); + } - private static void testDefaults() { - Target target = new Target(); + private static void testDefaults() { + Target target = new Target(); - for (Parameter parm : Parameter.values()) { - Object expected = parm.getDefault(); - if (expected == null) - continue; - Object actual = target.getObject(parm); - if (!expected.equals(actual)) { - throw new RuntimeException("Default for " + parm.name + " expected to be <" + expected + "> but is <" + actual + ">"); - } - } - } + for (Parameter parm : Parameter.values()) { + Object expected = parm.getDefault(); + if (expected == null) + continue; + Object actual = target.getObject(parm); + if (!expected.equals(actual)) { + throw new RuntimeException("Default for " + parm.name + " expected to be <" + expected + "> but is <" + actual + ">"); + } + } + } - private static void testParameters() { - for (Parameter parm : Parameter.values()) { - Parameter found = Parameter.forName(parm.name); - if (parm != found) { - String foundStr = found != null ? found.name : "null"; - throw new RuntimeException("Looking up <" + parm.name + ">, found <" + foundStr); - } - } - } + private static void testParameters() { + for (Parameter parm : Parameter.values()) { + Parameter found = Parameter.forName(parm.name); + if (parm != found) { + String foundStr = found != null ? found.name : "null"; + throw new RuntimeException("Looking up <" + parm.name + ">, found <" + foundStr); + } + } + } - public void run() throws Failure, IOException { - try { - processFile(); - } catch (Failure e) { - if (e.getFilename() == null) { - e.setFilename(filename); - e.setLineno(lineno); - throw e; - } - } - } + public void run() throws Failure, IOException { + try { + processFile(); + } catch (Failure e) { + if (e.getFilename() == null) { + e.setFilename(filename); + e.setLineno(lineno); + throw e; + } + } + } - private void processFile() throws IOException, Failure { - while (true) { - String line = reader.readLine(); - if (line == null) - break; - lineno++; - processLine(line); - } - if (verbose >= 1) { - System.out.println(); - System.out.println("Ran " + testCount + " tests in " + lineno + " lines"); - } - } + private void processFile() throws IOException, Failure { + while (true) { + String line = reader.readLine(); + if (line == null) + break; + lineno++; + processLine(line); + } + if (verbose >= 1) { + System.out.println(); + System.out.println("Ran " + testCount + " tests in " + lineno + " lines"); + } + } - private void processLine(String line) throws Failure { - line = line.replaceFirst("\\s+$", ""); // remove trailing - if (target == null && line.equals("```test")) { - if (verbose >= 2) { - if (testCount > 0) { - System.out.println(); - } - System.out.println("\u25B6 " + filename + ":" + lineno); - } - target = new Target(); - testCount++; - return; - } - if (target != null) { - if (line.equals("```")) { - stopProcessing(); - return; - } - handleCommand(line); - } - } + private void processLine(String line) throws Failure { + line = line.replaceFirst("\\s+$", ""); // remove trailing + if (target == null && line.equals("```test")) { + if (verbose >= 2) { + if (testCount > 0) { + System.out.println(); + } + System.out.println("\u25B6 " + filename + ":" + lineno); + } + target = new Target(); + testCount++; + return; + } + if (target != null) { + if (line.equals("```")) { + stopProcessing(); + return; + } + handleCommand(line); + } + } - private void stopProcessing() { - target = null; - validated = null; - } + private void stopProcessing() { + target = null; + validated = null; + } - private void handleCommand(String line) throws Failure { - if (verbose >= 3) { - System.out.println(line); - } - if (line.isEmpty()) - return; + private void handleCommand(String line) throws Failure { + if (verbose >= 3) { + System.out.println(line); + } + if (line.isEmpty()) + return; - String[] parts = line.split("\\s+", 2); - String command = parts[0]; - switch (command.toUpperCase()) { - case "ONLY": - handleOnly(true, parts[1]); - return; - case "NOT": - handleOnly(false, parts[1]); - return; - case "PARSE": - handleParse(parts[1], null); - return; - case "ACCEPT": - handleParse(parts[1], true); - return; - case "REJECT": - handleParse(parts[1], false); - return; - case "SET": - handleSet(parts[1]); - return; - case "EXPECT": - handleExpect(parts[1]); - return; - default: - throw new Failure("Unexpected command: " + command); - } + String[] parts = line.split("\\s+", 2); + String command = parts[0]; + switch (command.toUpperCase()) { + case "ONLY": + handleOnly(true, parts[1]); + return; + case "NOT": + handleOnly(false, parts[1]); + return; + case "PARSE": + handleParse(parts[1], null); + return; + case "ACCEPT": + handleParse(parts[1], true); + return; + case "REJECT": + handleParse(parts[1], false); + return; + case "SET": + handleSet(parts[1]); + return; + case "EXPECT": + handleExpect(parts[1]); + return; + default: + throw new Failure("Unexpected command: " + command); + } - } + } - private void handleOnly(boolean mustBePresent, String rest) throws Failure { - boolean found = false; - for (String part : rest.split("\\s+")) { - if (part.equals("jdbc")) { - found = true; - break; - } - } - if (found != mustBePresent) { - // do not further process this block - stopProcessing(); - } - } + private void handleOnly(boolean mustBePresent, String rest) throws Failure { + boolean found = false; + for (String part : rest.split("\\s+")) { + if (part.equals("jdbc")) { + found = true; + break; + } + } + if (found != mustBePresent) { + // do not further process this block + stopProcessing(); + } + } - private int findEqualSign(String rest) throws Failure { - int index = rest.indexOf('='); - if (index < -1) - throw new Failure("Expected to find a '='"); - return index; - } + private int findEqualSign(String rest) throws Failure { + int index = rest.indexOf('='); + if (index < -1) + throw new Failure("Expected to find a '='"); + return index; + } - private String splitKey(String rest) throws Failure { - int index = findEqualSign(rest); - return rest.substring(0, index); - } + private String splitKey(String rest) throws Failure { + int index = findEqualSign(rest); + return rest.substring(0, index); + } - private String splitValue(String rest) throws Failure { - int index = findEqualSign(rest); - return rest.substring(index + 1); - } + private String splitValue(String rest) throws Failure { + int index = findEqualSign(rest); + return rest.substring(index + 1); + } - private void handleSet(String rest) throws Failure { - validated = null; - String key = splitKey(rest); - String value = splitValue(rest); + private void handleSet(String rest) throws Failure { + validated = null; + String key = splitKey(rest); + String value = splitValue(rest); - try { - target.setString(key, value); - } catch (ValidationError e) { - throw new Failure(e.getMessage()); - } - } + try { + target.setString(key, value); + } catch (ValidationError e) { + throw new Failure(e.getMessage()); + } + } - private void handleParse(String rest, Boolean shouldSucceed) throws Failure { - URISyntaxException parseError = null; - ValidationError validationError = null; + private void handleParse(String rest, Boolean shouldSucceed) throws Failure { + URISyntaxException parseError = null; + ValidationError validationError = null; - validated = null; - try { - target.barrier(); - MonetUrlParser.parse(target, rest); - } catch (URISyntaxException e) { - parseError = e; - } catch (ValidationError e) { - validationError = e; - } + validated = null; + try { + target.barrier(); + MonetUrlParser.parse(target, rest); + } catch (URISyntaxException e) { + parseError = e; + } catch (ValidationError e) { + validationError = e; + } - if (parseError == null && validationError == null) { - try { - tryValidate(); - } catch (ValidationError e) { - validationError = e; - } - } + if (parseError == null && validationError == null) { + try { + tryValidate(); + } catch (ValidationError e) { + validationError = e; + } + } - if (shouldSucceed == Boolean.FALSE) { - if (parseError != null || validationError != null) - return; // happy - else - throw new Failure("URL unexpectedly parsed and validated"); - } + if (shouldSucceed == Boolean.FALSE) { + if (parseError != null || validationError != null) + return; // happy + else + throw new Failure("URL unexpectedly parsed and validated"); + } - if (parseError != null) - throw new Failure("Parse error: " + parseError); - if (validationError != null && shouldSucceed == Boolean.TRUE) - throw new Failure("Validation error: " + validationError); - } + if (parseError != null) + throw new Failure("Parse error: " + parseError); + if (validationError != null && shouldSucceed == Boolean.TRUE) + throw new Failure("Validation error: " + validationError); + } - private void handleExpect(String rest) throws Failure { - String key = splitKey(rest); - String expectedString = splitValue(rest); + private void handleExpect(String rest) throws Failure { + String key = splitKey(rest); + String expectedString = splitValue(rest); - Object actual = null; - try { - actual = extract(key); - } catch (ValidationError e) { - throw new Failure(e.getMessage()); - } + Object actual = null; + try { + actual = extract(key); + } catch (ValidationError e) { + throw new Failure(e.getMessage()); + } - Object expected; - try { - if (actual instanceof Boolean) - expected = ParameterType.Bool.parse(key, expectedString); - else if (actual instanceof Integer) - expected = ParameterType.Int.parse(key, expectedString); - else - expected = expectedString; - } catch (ValidationError e) { - String typ = actual.getClass().getName(); - throw new Failure("Cannot convert expected value <" + expectedString + "> to " + typ + ": " + e.getMessage()); - } + Object expected; + try { + if (actual instanceof Boolean) + expected = ParameterType.Bool.parse(key, expectedString); + else if (actual instanceof Integer) + expected = ParameterType.Int.parse(key, expectedString); + else + expected = expectedString; + } catch (ValidationError e) { + String typ = actual.getClass().getName(); + throw new Failure("Cannot convert expected value <" + expectedString + "> to " + typ + ": " + e.getMessage()); + } - if (actual.equals(expected)) - return; - throw new Failure("Expected " + key + "=<" + expectedString + ">, found <" + actual + ">"); - } + if (actual.equals(expected)) + return; + throw new Failure("Expected " + key + "=<" + expectedString + ">, found <" + actual + ">"); + } - private Target.Validated tryValidate() throws ValidationError { - if (validated == null) - validated = target.validate(); - return validated; - } + private Target.Validated tryValidate() throws ValidationError { + if (validated == null) + validated = target.validate(); + return validated; + } - private Object extract(String key) throws ValidationError, Failure { - switch (key) { - case "valid": - try { - tryValidate(); - } catch (ValidationError e) { - return Boolean.FALSE; - } - return Boolean.TRUE; + private Object extract(String key) throws ValidationError, Failure { + switch (key) { + case "valid": + try { + tryValidate(); + } catch (ValidationError e) { + return Boolean.FALSE; + } + return Boolean.TRUE; - case "connect_scan": - return tryValidate().connectScan(); - case "connect_port": - return tryValidate().connectPort(); - case "connect_unix": - return tryValidate().connectUnix(); - case "connect_tcp": - return tryValidate().connectTcp(); - case "connect_tls_verify": - switch (tryValidate().connectVerify()) { - case None: - return ""; - case Cert: - return "cert"; - case Hash: - return "hash"; - case System: - return "system"; - default: - throw new IllegalStateException("unreachable"); - } - case "connect_certhash_digits": - return tryValidate().connectCertHashDigits(); - case "connect_binary": - return tryValidate().connectBinary(); - case "connect_clientkey": - return tryValidate().connectClientKey(); - case "connect_clientcert": - return tryValidate().connectClientCert(); + case "connect_scan": + return tryValidate().connectScan(); + case "connect_port": + return tryValidate().connectPort(); + case "connect_unix": + return tryValidate().connectUnix(); + case "connect_tcp": + return tryValidate().connectTcp(); + case "connect_tls_verify": + switch (tryValidate().connectVerify()) { + case None: + return ""; + case Cert: + return "cert"; + case Hash: + return "hash"; + case System: + return "system"; + default: + throw new IllegalStateException("unreachable"); + } + case "connect_certhash_digits": + return tryValidate().connectCertHashDigits(); + case "connect_binary": + return tryValidate().connectBinary(); + case "connect_clientkey": + return tryValidate().connectClientKey(); + case "connect_clientcert": + return tryValidate().connectClientCert(); - default: - Parameter parm = Parameter.forName(key); - if (parm != null) - return target.getObject(parm); - else - throw new Failure("Unknown attribute: " + key); - } - } + default: + Parameter parm = Parameter.forName(key); + if (parm != null) + return target.getObject(parm); + else + throw new Failure("Unknown attribute: " + key); + } + } - public static class Failure extends Exception { - private String filename = null; - private int lineno = -1; + public static class Failure extends Exception { + private String filename = null; + private int lineno = -1; - public Failure(String message) { - super(message); - } + public Failure(String message) { + super(message); + } - @Override - public String getMessage() { - StringBuilder buffer = new StringBuilder(); - if (filename != null) { - buffer.append(filename).append(":"); - if (lineno >= 0) - buffer.append(lineno).append(":"); - } - buffer.append(super.getMessage()); - return buffer.toString(); - } + @Override + public String getMessage() { + StringBuilder buffer = new StringBuilder(); + if (filename != null) { + buffer.append(filename).append(":"); + if (lineno >= 0) + buffer.append(lineno).append(":"); + } + buffer.append(super.getMessage()); + return buffer.toString(); + } - public String getFilename() { - return filename; - } + public String getFilename() { + return filename; + } - public void setFilename(String filename) { - this.filename = filename; - } + public void setFilename(String filename) { + this.filename = filename; + } - public int getLineno() { - return lineno; - } + public int getLineno() { + return lineno; + } - public void setLineno(int lineno) { - this.lineno = lineno; - } - } + public void setLineno(int lineno) { + this.lineno = lineno; + } + } }