diff src/main/java/nl/cwi/monetdb/mcl/net/MapiSocket.java @ 281:b58c1b245ede

Correct and improve implementation of getChallengeResponse() for protocol 9 Add method getLogWriter() so when debug is enabled it can be used from other places (e.g. MonetConnection) Disabled unused method: debug(PrintStream out) Improved comments and documentation
author Martin van Dinther <martin.van.dinther@monetdbsolutions.com>
date Thu, 18 Jul 2019 16:44:25 +0200 (2019-07-18)
parents fab4e6165be9
children bb273e9c7e09
line wrap: on
line diff
--- a/src/main/java/nl/cwi/monetdb/mcl/net/MapiSocket.java
+++ b/src/main/java/nl/cwi/monetdb/mcl/net/MapiSocket.java
@@ -10,15 +10,12 @@ package nl.cwi.monetdb.mcl.net;
 
 import java.io.BufferedInputStream;
 import java.io.BufferedOutputStream;
-import java.io.EOFException;
 import java.io.FileWriter;
 import java.io.FilterInputStream;
 import java.io.FilterOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
-import java.io.PrintStream;
-import java.io.PrintWriter;
 import java.io.UnsupportedEncodingException;
 import java.io.Writer;
 import java.net.Socket;
@@ -32,7 +29,6 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashSet;
 import java.util.List;
-import java.util.Set;
 
 import nl.cwi.monetdb.mcl.MCLException;
 import nl.cwi.monetdb.mcl.io.BufferedMCLReader;
@@ -111,16 +107,18 @@ public final class MapiSocket {
 	private String language = "sql";
 	/** The hash methods to use (null = default) */
 	private String hash = null;
+
 	/** Whether we should follow redirects */
 	private boolean followRedirects = true;
 	/** How many redirections do we follow until we're fed up with it? */
 	private int ttl = 10;
+
 	/** Whether we are debugging or not */
 	private boolean debug = false;
 	/** The Writer for the debug log-file */
 	private Writer log;
 
-	/** The blocksize (hardcoded in compliance with stream.mx) */
+	/** The blocksize (hardcoded in compliance with MonetDB common/stream/stream.h) */
 	public final static int BLOCK = 8 * 1024 - 2;
 
 	/** A short in two bytes for holding the block size in bytes */
@@ -201,7 +199,7 @@ public final class MapiSocket {
 	 * operation to have effect.
 	 *
 	 * @param s The specified timeout, in milliseconds.  A timeout
-	 *        of zero is interpreted as an infinite timeout.
+	 *        of zero will disable timeout (i.e., timeout of infinity).
 	 * @throws SocketException Issue with the socket
 	 */
 	public void setSoTimeout(int s) throws SocketException {
@@ -209,7 +207,7 @@ public final class MapiSocket {
 			throw new IllegalArgumentException("timeout can't be negative");
 		}
 		this.soTimeout = s;
-		// limit time to wait on blocking operations (0 = indefinite)
+		// limit time to wait on blocking operations
 		if (con != null) {
 			con.setSoTimeout(s);
 		}
@@ -229,7 +227,7 @@ public final class MapiSocket {
 	}
 
 	/**
-	 * Enables/disables debug
+	 * Enables/disables debug mode with logging to file
 	 *
 	 * @param debug Value to set
 	 */
@@ -266,7 +264,7 @@ public final class MapiSocket {
 		throws IOException, UnknownHostException, SocketException, MCLParseException, MCLException
 	{
 		if (ttl-- <= 0)
-			throw new MCLException("Maximum number of redirects reached, aborting connection attempt. Sorry.");
+			throw new MCLException("Maximum number of redirects reached, aborting connection attempt.");
 
 		if (makeConnection) {
 			con = new Socket(host, port);
@@ -288,28 +286,20 @@ public final class MapiSocket {
 
 		String c = reader.readLine();
 		reader.waitForPrompt();
-		writer.writeLine(
-				getChallengeResponse(
-					c,
-					user,
-					pass,
-					language,
-					database,
-					hash
-					)
-				);
-
-		// read monet response till prompt
+		writer.writeLine(getChallengeResponse(c, user, pass, language, database, hash));
+		// read monetdb mserver response till prompt
 		List<String> redirects = new ArrayList<String>();
 		List<String> warns = new ArrayList<String>();
 		String err = "", tmp;
 		int lineType;
 		do {
-			if ((tmp = reader.readLine()) == null)
+			tmp = reader.readLine();
+			if (tmp == null)
 				throw new IOException("Read from " +
 						con.getInetAddress().getHostName() + ":" +
 						con.getPort() + ": End of stream reached");
-			if ((lineType = reader.getLineType()) == BufferedMCLReader.ERROR) {
+			lineType = reader.getLineType();
+			if (lineType == BufferedMCLReader.ERROR) {
 				err += "\n" + tmp.substring(7);
 			} else if (lineType == BufferedMCLReader.INFO) {
 				warns.add(tmp.substring(1));
@@ -317,10 +307,12 @@ public final class MapiSocket {
 				redirects.add(tmp.substring(1));
 			}
 		} while (lineType != BufferedMCLReader.PROMPT);
+
 		if (err.length() > 0) {
 			close();
-			throw new MCLException(err.trim());
+			throw new MCLException(err);
 		}
+
 		if (!redirects.isEmpty()) {
 			if (followRedirects) {
 				// Ok, server wants us to go somewhere else.  The list
@@ -353,8 +345,7 @@ public final class MapiSocket {
 							if (tmp.equals("database")) {
 								tmp = args[i].substring(pos + 1);
 								if (!tmp.equals(database)) {
-									warns.add("redirect points to different " +
-											"database: " + tmp);
+									warns.add("redirect points to different database: " + tmp);
 									setDatabase(tmp);
 								}
 							} else if (tmp.equals("language")) {
@@ -393,7 +384,7 @@ public final class MapiSocket {
 					if (tmp != null && tmp.length() > 0) {
 						tmp = tmp.substring(1).trim();
 						if (!tmp.isEmpty() && !tmp.equals(database)) {
-							warns.add("redirect points to different " + "database: " + tmp);
+							warns.add("redirect points to different database: " + tmp);
 							setDatabase(tmp);
 						}
 					}
@@ -424,6 +415,7 @@ public final class MapiSocket {
 	 * string is null, a challengeless response is returned.
 	 *
 	 * @param chalstr the challenge string
+	 *	for example: H8sRMhtevGd:mserver:9:PROT10,RIPEMD160,SHA256,SHA1,MD5,COMPRESSION_SNAPPY,COMPRESSION_LZ4:LIT:SHA512:
 	 * @param username the username to use
 	 * @param password the password to use
 	 * @param language the language to use
@@ -438,80 +430,76 @@ public final class MapiSocket {
 			String database,
 			String hash
 	) throws MCLParseException, MCLException, IOException {
-		String response;
-		String algo;
-
 		// parse the challenge string, split it on ':'
 		String[] chaltok = chalstr.split(":");
-		if (chaltok.length <= 4)
-			throw new MCLParseException("Server challenge string unusable!  Challenge contains too few tokens: " + chalstr);
+		if (chaltok.length <= 5)
+			throw new MCLParseException("Server challenge string unusable! It contains too few (" + chaltok.length + ") tokens: " + chalstr);
 
 		// challenge string to use as salt/key
 		String challenge = chaltok[0];
-		String servert = chaltok[1];
 		try {
-			version = Integer.parseInt(chaltok[2].trim());	// protocol version
+			version = Integer.parseInt(chaltok[2]);	// protocol version
 		} catch (NumberFormatException e) {
-			throw new MCLParseException("Protocol version unparseable: " + chaltok[3]);
+			throw new MCLParseException("Protocol version (" + chaltok[2] + ") unparseable as integer.");
 		}
 
 		// handle the challenge according to the version it is
 		switch (version) {
-			default:
-				throw new MCLException("Unsupported protocol version: " + version);
 			case 9:
-				// proto 9 is like 8, but uses a hash instead of the
-				// plain password, the server tells us which hash in the
-				// challenge after the byte-order
+				// proto 9 is like 8, but uses a hash instead of the plain password
+				// the server tells us (in 6th token) which hash in the
+				// challenge after the byte-order token
 
+				String algo;
+				String pwhash = chaltok[5];
 				/* NOTE: Java doesn't support RIPEMD160 :( */
-				if (chaltok[5].equals("SHA512")) {
+				if (pwhash.equals("SHA512")) {
 					algo = "SHA-512";
-				} else if (chaltok[5].equals("SHA384")) {
+				} else if (pwhash.equals("SHA384")) {
 					algo = "SHA-384";
-				} else if (chaltok[5].equals("SHA256")) {
+				} else if (pwhash.equals("SHA256")) {
 					algo = "SHA-256";
 				/* NOTE: Java doesn't support SHA-224 */
-				} else if (chaltok[5].equals("SHA1")) {
+				} else if (pwhash.equals("SHA1")) {
 					algo = "SHA-1";
-				} else if (chaltok[5].equals("MD5")) {
+				} else if (pwhash.equals("MD5")) {
 					algo = "MD5";
 				} else {
-					throw new MCLException("Unsupported password hash: " + chaltok[5]);
+					throw new MCLException("Unsupported password hash: " + pwhash);
 				}
-
 				try {
 					MessageDigest md = MessageDigest.getInstance(algo);
 					md.update(password.getBytes("UTF-8"));
 					byte[] digest = md.digest();
 					password = toHex(digest);
 				} catch (NoSuchAlgorithmException e) {
-					throw new AssertionError("internal error: " + e.toString());
+					throw new MCLException("This JVM does not support password hash: " + pwhash + "\n" + e.toString());
 				} catch (UnsupportedEncodingException e) {
-					throw new AssertionError("internal error: " + e.toString());
+					throw new MCLException("This JVM does not support UTF-8 encoding\n" + e.toString());
 				}
 
 				// proto 7 (finally) used the challenge and works with a
 				// password hash.  The supported implementations come
 				// from the server challenge.  We chose the best hash
-				// we can find, in the order SHA1, MD5, plain.  Also,
-				// the byte-order is reported in the challenge string,
+				// we can find, in the order SHA512, SHA1, MD5, plain.
+				// Also the byte-order is reported in the challenge string,
 				// which makes sense, since only blockmode is supported.
 				// proto 8 made this obsolete, but retained the
-				// byte-order report for future "binary" transports.  In
-				// proto 8, the byte-order of the blocks is always little
+				// byte-order report for future "binary" transports.
+				// In proto 8, the byte-order of the blocks is always little
 				// endian because most machines today are.
-				String hashes = (hash == null ? chaltok[3] : hash);
-				Set<String> hashesSet = new HashSet<String>(Arrays.asList(hashes.toUpperCase().split("[, ]")));
+				String hashes = (hash == null || hash.isEmpty()) ? chaltok[3] : hash;
+				HashSet<String> hashesSet = new HashSet<String>(Arrays.asList(hashes.toUpperCase().split("[, ]")));	// split on comma or space
 
 				// if we deal with merovingian, mask our credentials
-				if (servert.equals("merovingian") && !language.equals("control")) {
+				if (chaltok[1].equals("merovingian") && !language.equals("control")) {
 					username = "merovingian";
 					password = "merovingian";
 				}
-				String pwhash;
+
+				// reuse variables algo and pwhash
 				algo = null;
-
+				pwhash = null;
 				if (hashesSet.contains("SHA512")) {
 					algo = "SHA-512";
 					pwhash = "{SHA512}";
@@ -528,46 +516,41 @@ public final class MapiSocket {
 					algo = "MD5";
 					pwhash = "{MD5}";
 				} else {
-					throw new MCLException("no supported password hashes in " + hashes);
+					throw new MCLException("no supported hash algorithms found in " + hashes);
 				}
-				if (algo != null) {
-					try {
-						MessageDigest md = MessageDigest.getInstance(algo);
-						md.update(password.getBytes("UTF-8"));
-						md.update(challenge.getBytes("UTF-8"));
-						byte[] digest = md.digest();
-						pwhash += toHex(digest);
-					} catch (NoSuchAlgorithmException e) {
-						throw new AssertionError("internal error: " + e.toString());
-					} catch (UnsupportedEncodingException e) {
-						throw new AssertionError("internal error: " + e.toString());
-					}
+				try {
+					MessageDigest md = MessageDigest.getInstance(algo);
+					md.update(password.getBytes("UTF-8"));
+					md.update(challenge.getBytes("UTF-8"));
+					byte[] digest = md.digest();
+					pwhash += toHex(digest);
+				} catch (NoSuchAlgorithmException e) {
+					throw new MCLException("This JVM does not support password hash: " + pwhash + "\n" + e.toString());
+				} catch (UnsupportedEncodingException e) {
+					throw new MCLException("This JVM does not support UTF-8 encoding\n" + e.toString());
 				}
-				// TODO: some day when we need this, we should store
-				// this
+
+				// TODO: some day when we need this, we should store this
 				if (chaltok[4].equals("BIG")) {
 					// byte-order of server is big-endian
 				} else if (chaltok[4].equals("LIT")) {
 					// byte-order of server is little-endian
 				} else {
-					throw new MCLParseException("Invalid byte-order: " + chaltok[5]);
+					throw new MCLParseException("Invalid byte-order: " + chaltok[4]);
 				}
 
 				// generate response
-				response = "BIG:"	// JVM byte-order is big-endian
-					+ username + ":" + pwhash + ":" + language
-					+ ":" + (database == null ? "" : database) + ":";
-
+				String response = "BIG:"	// JVM byte-order is big-endian
+					+ username + ":"
+					+ pwhash + ":"
+					+ language + ":"
+					+ (database == null ? "" : database) + ":";
 				return response;
+			default:
+				throw new MCLException("Unsupported protocol version: " + version);
 		}
 	}
 
-	private static char hexChar(int n) {
-		return (n > 9)
-			? (char) ('a' + (n - 10))
-			: (char) ('0' + n);
-	}
-
 	/**
 	 * Small helper method to convert a byte string to a hexadecimal
 	 * string representation.
@@ -585,6 +568,13 @@ public final class MapiSocket {
 		return new String(result);
 	}
 
+	private static char hexChar(int n) {
+		return (n > 9)
+			? (char) ('a' + (n - 10))
+			: (char) ('0' + n);
+	}
+
+
 	/**
 	 * Returns an InputStream that reads from this open connection on
 	 * the MapiSocket.
@@ -656,12 +646,13 @@ public final class MapiSocket {
 	 * encouraged to start debugging before actually connecting the
 	 * socket.
 	 *
-	 * @param out to write the log to
+	 * @param out to write the log to a print stream
 	 * @throws IOException if the file could not be opened for writing
 	 */
-	public void debug(PrintStream out) throws IOException {
-		debug(new PrintWriter(out));
-	}
+// disabled as it is not used by JDBC driver code
+//	public void debug(PrintStream out) throws IOException {
+//		debug(new PrintWriter(out));
+//	}
 
 	/**
 	 * Enables logging to a stream what is read and written from and to
@@ -670,14 +661,22 @@ public final class MapiSocket {
 	 * socket.
 	 *
 	 * @param out to write the log to
-	 * @throws IOException if the file could not be opened for writing
 	 */
-	public void debug(Writer out) throws IOException {
+	public void debug(Writer out) {
 		log = out;
 		debug = true;
 	}
 
 	/**
+	 * Get the log Writer.
+	 *
+	 * @return the log writer
+	 */
+	public Writer getLogWriter() {
+		return log;
+	}
+
+	/**
 	 * Inner class that is used to write data on a normal stream as a
 	 * blocked stream.  A call to the flush() method will write a
 	 * "final" block to the underlying stream.  Non-final blocks are
@@ -1024,8 +1023,9 @@ public final class MapiSocket {
 	}
 
 	/**
-	 * Closes the streams and socket connected to the server if
-	 * possible.  If an error occurs during disconnecting it is ignored.
+	 * Closes the streams and socket connected to the server if possible.
+	 * If an error occurs at closing a resource, it is ignored so as many
+	 * resources as possible are closed.
 	 */
 	public synchronized void close() {
 		if (writer != null) {
@@ -1054,7 +1054,7 @@ public final class MapiSocket {
 		}
 		if (con != null) {
 			try {
-				con.close();
+				con.close();	// close the socket
 				con = null;
 			} catch (IOException e) { /* ignore it */ }
 		}