diff --git a/changelog.md b/changelog.md index a63df439..4061eb64 100644 --- a/changelog.md +++ b/changelog.md @@ -2,6 +2,11 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## [0.4.0](https://github.com/shyiko/mysql-binlog-connector-java/compare/0.3.3...0.4.0) - 2016-08-15 + +### Added + - TLS support ([#70](https://github.com/shyiko/mysql-binlog-connector-java/issues/70)). + ## [0.3.3](https://github.com/shyiko/mysql-binlog-connector-java/compare/0.3.2...0.3.3) - 2016-08-08 ### Added diff --git a/src/main/java/com/github/shyiko/mysql/binlog/BinaryLogClient.java b/src/main/java/com/github/shyiko/mysql/binlog/BinaryLogClient.java index d0ddf164..6027a56e 100644 --- a/src/main/java/com/github/shyiko/mysql/binlog/BinaryLogClient.java +++ b/src/main/java/com/github/shyiko/mysql/binlog/BinaryLogClient.java @@ -31,8 +31,13 @@ import com.github.shyiko.mysql.binlog.io.ByteArrayInputStream; import com.github.shyiko.mysql.binlog.jmx.BinaryLogClientMXBean; import com.github.shyiko.mysql.binlog.network.AuthenticationException; +import com.github.shyiko.mysql.binlog.network.ClientCapabilities; +import com.github.shyiko.mysql.binlog.network.DefaultSSLSocketFactory; +import com.github.shyiko.mysql.binlog.network.SSLMode; +import com.github.shyiko.mysql.binlog.network.SSLSocketFactory; import com.github.shyiko.mysql.binlog.network.ServerException; import com.github.shyiko.mysql.binlog.network.SocketFactory; +import com.github.shyiko.mysql.binlog.network.TLSHostnameVerifier; import com.github.shyiko.mysql.binlog.network.protocol.ErrorPacket; import com.github.shyiko.mysql.binlog.network.protocol.GreetingPacket; import com.github.shyiko.mysql.binlog.network.protocol.Packet; @@ -44,12 +49,19 @@ import com.github.shyiko.mysql.binlog.network.protocol.command.DumpBinaryLogGtidCommand; import com.github.shyiko.mysql.binlog.network.protocol.command.PingCommand; import com.github.shyiko.mysql.binlog.network.protocol.command.QueryCommand; +import com.github.shyiko.mysql.binlog.network.protocol.command.SSLRequestCommand; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; import java.io.EOFException; import java.io.IOException; import java.net.InetSocketAddress; import java.net.Socket; import java.net.SocketException; +import java.security.GeneralSecurityException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; import java.util.Arrays; import java.util.Collections; import java.util.Iterator; @@ -74,6 +86,31 @@ */ public class BinaryLogClient implements BinaryLogClientMXBean { + private static final SSLSocketFactory DEFAULT_REQUIRED_SSL_MODE_SOCKET_FACTORY = new DefaultSSLSocketFactory() { + + @Override + protected void initSSLContext(SSLContext sc) throws GeneralSecurityException { + sc.init(null, new TrustManager[]{ + new X509TrustManager() { + + @Override + public void checkClientTrusted(X509Certificate[] x509Certificates, String s) + throws CertificateException { } + + @Override + public void checkServerTrusted(X509Certificate[] x509Certificates, String s) + throws CertificateException { } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + } + }, null); + } + }; + private static final SSLSocketFactory DEFAULT_VERIFY_CA_SSL_MODE_SOCKET_FACTORY = new DefaultSSLSocketFactory(); + // https://dev.mysql.com/doc/internals/en/sending-more-than-16mbyte.html private static final int MAX_PACKET_LENGTH = 16777215; @@ -90,6 +127,7 @@ public class BinaryLogClient implements BinaryLogClientMXBean { private volatile String binlogFilename; private volatile long binlogPosition = 4; private volatile long connectionId; + private SSLMode sslMode = SSLMode.DISABLED; private GtidSet gtidSet; private final Object gtidSetAccessLock = new Object(); @@ -100,6 +138,7 @@ public class BinaryLogClient implements BinaryLogClientMXBean { private final List lifecycleListeners = new LinkedList(); private SocketFactory socketFactory; + private SSLSocketFactory sslSocketFactory; private PacketChannel channel; private volatile boolean connected; @@ -166,6 +205,17 @@ public void setBlocking(boolean blocking) { this.blocking = blocking; } + public SSLMode getSSLMode() { + return sslMode; + } + + public void setSSLMode(SSLMode sslMode) { + if (sslMode == null) { + throw new IllegalArgumentException("SSL mode cannot be NULL"); + } + this.sslMode = sslMode; + } + /** * @return server id (65535 by default) * @see #setServerId(long) @@ -326,6 +376,13 @@ public void setSocketFactory(SocketFactory socketFactory) { this.socketFactory = socketFactory; } + /** + * @param sslSocketFactory custom ssl socket factory + */ + public void setSslSocketFactory(SSLSocketFactory sslSocketFactory) { + this.sslSocketFactory = sslSocketFactory; + } + /** * @param threadFactory custom thread factory. If not provided, threads will be created using simple "new Thread()". */ @@ -357,7 +414,7 @@ public void connect() throws IOException { ". Please make sure it's running.", e); } greetingPacket = receiveGreeting(); - authenticate(greetingPacket.getScramble(), greetingPacket.getServerCollation()); + authenticate(greetingPacket); if (binlogFilename == null) { fetchBinlogFilenameAndPosition(); } @@ -446,10 +503,30 @@ private void ensureEventDataDeserializer(EventType eventType, } } - private void authenticate(String salt, int collation) throws IOException { - AuthenticateCommand authenticateCommand = new AuthenticateCommand(schema, username, password, salt); + private void authenticate(GreetingPacket greetingPacket) throws IOException { + int collation = greetingPacket.getServerCollation(); + int packetNumber = 1; + if (sslMode != SSLMode.DISABLED) { + boolean serverSupportsSSL = (greetingPacket.getServerCapabilities() & ClientCapabilities.SSL) != 0; + if (!serverSupportsSSL && (sslMode == SSLMode.REQUIRED || sslMode == SSLMode.VERIFY_CA || + sslMode == SSLMode.VERIFY_IDENTITY)) { + throw new IOException("MySQL server does not support SSL"); + } + if (serverSupportsSSL) { + SSLRequestCommand sslRequestCommand = new SSLRequestCommand(); + sslRequestCommand.setCollation(collation); + channel.write(sslRequestCommand, packetNumber++); + SSLSocketFactory sslSocketFactory = this.sslSocketFactory != null ? this.sslSocketFactory : + sslMode == SSLMode.REQUIRED ? DEFAULT_REQUIRED_SSL_MODE_SOCKET_FACTORY : + DEFAULT_VERIFY_CA_SSL_MODE_SOCKET_FACTORY; + channel.upgradeToSSL(sslSocketFactory, + sslMode == SSLMode.VERIFY_IDENTITY ? new TLSHostnameVerifier() : null); + } + } + AuthenticateCommand authenticateCommand = new AuthenticateCommand(schema, username, password, + greetingPacket.getScramble()); authenticateCommand.setCollation(collation); - channel.write(authenticateCommand); + channel.write(authenticateCommand, packetNumber); byte[] authenticationResult = channel.read(); if (authenticationResult[0] != (byte) 0x00 /* ok */) { if (authenticationResult[0] == (byte) 0xFF /* error */) { diff --git a/src/main/java/com/github/shyiko/mysql/binlog/network/DefaultSSLSocketFactory.java b/src/main/java/com/github/shyiko/mysql/binlog/network/DefaultSSLSocketFactory.java new file mode 100644 index 00000000..0fabaa10 --- /dev/null +++ b/src/main/java/com/github/shyiko/mysql/binlog/network/DefaultSSLSocketFactory.java @@ -0,0 +1,64 @@ +/* + * Copyright 2016 Stanley Shyiko + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.shyiko.mysql.binlog.network; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocket; +import java.io.IOException; +import java.net.Socket; +import java.net.SocketException; +import java.security.GeneralSecurityException; + +/** + * @author Stanley Shyiko + */ +public class DefaultSSLSocketFactory implements SSLSocketFactory { + + private final String protocol; + + public DefaultSSLSocketFactory() { + this("TLSv1"); + } + + /** + * @param protocol TLSv1, TLSv1.1 or TLSv1.2 (the last two require JDK 7+) + */ + public DefaultSSLSocketFactory(String protocol) { + this.protocol = protocol; + } + + @Override + public SSLSocket createSocket(Socket socket) throws SocketException { + SSLContext sc; + try { + sc = SSLContext.getInstance(this.protocol); + initSSLContext(sc); + } catch (GeneralSecurityException e) { + throw new SocketException(e.getMessage()); + } + try { + return (SSLSocket) sc.getSocketFactory() + .createSocket(socket, socket.getInetAddress().getHostName(), socket.getPort(), true); + } catch (IOException e) { + throw new SocketException(e.getMessage()); + } + } + + protected void initSSLContext(SSLContext sc) throws GeneralSecurityException { + sc.init(null, null, null); + } + +} diff --git a/src/main/java/com/github/shyiko/mysql/binlog/network/IdentityVerificationException.java b/src/main/java/com/github/shyiko/mysql/binlog/network/IdentityVerificationException.java new file mode 100644 index 00000000..e1f145f2 --- /dev/null +++ b/src/main/java/com/github/shyiko/mysql/binlog/network/IdentityVerificationException.java @@ -0,0 +1,29 @@ +/* + * Copyright 2016 Stanley Shyiko + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.shyiko.mysql.binlog.network; + +import javax.net.ssl.SSLException; + +/** + * @author Stanley Shyiko + */ +public class IdentityVerificationException extends SSLException { + + public IdentityVerificationException(String message) { + super(message); + } + +} diff --git a/src/main/java/com/github/shyiko/mysql/binlog/network/SSLMode.java b/src/main/java/com/github/shyiko/mysql/binlog/network/SSLMode.java new file mode 100644 index 00000000..a5ce7f42 --- /dev/null +++ b/src/main/java/com/github/shyiko/mysql/binlog/network/SSLMode.java @@ -0,0 +1,50 @@ +/* + * Copyright 2016 Stanley Shyiko + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.shyiko.mysql.binlog.network; + +/** + * @see Stanley Shyiko + */ +public enum SSLMode { + + /** + * Establish a secure (encrypted) connection if the server supports secure connections. + * Fall back to an unencrypted connection otherwise. + */ + PREFERRED, + /** + * Establish an unencrypted connection. + */ + DISABLED, + /** + * Establish a secure connection if the server supports secure connections. + * The connection attempt fails if a secure connection cannot be established. + */ + REQUIRED, + /** + * Like REQUIRED, but additionally verify the server TLS certificate against the configured Certificate Authority + * (CA) certificates. The connection attempt fails if no valid matching CA certificates are found. + */ + VERIFY_CA, + /** + * Like VERIFY_CA, but additionally verify that the server certificate matches the host to which the connection is + * attempted. + */ + VERIFY_IDENTITY + +} diff --git a/src/main/java/com/github/shyiko/mysql/binlog/network/SSLSocketFactory.java b/src/main/java/com/github/shyiko/mysql/binlog/network/SSLSocketFactory.java new file mode 100644 index 00000000..9a5afbdf --- /dev/null +++ b/src/main/java/com/github/shyiko/mysql/binlog/network/SSLSocketFactory.java @@ -0,0 +1,28 @@ +/* + * Copyright 2016 Stanley Shyiko + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.shyiko.mysql.binlog.network; + +import javax.net.ssl.SSLSocket; +import java.net.Socket; +import java.net.SocketException; + +/** + * @author Stanley Shyiko + */ +public interface SSLSocketFactory { + + SSLSocket createSocket(Socket socket) throws SocketException; +} diff --git a/src/main/java/com/github/shyiko/mysql/binlog/network/TLSHostnameVerifier.java b/src/main/java/com/github/shyiko/mysql/binlog/network/TLSHostnameVerifier.java new file mode 100644 index 00000000..28b106d6 --- /dev/null +++ b/src/main/java/com/github/shyiko/mysql/binlog/network/TLSHostnameVerifier.java @@ -0,0 +1,49 @@ +/* + * Copyright 2016 Stanley Shyiko + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.shyiko.mysql.binlog.network; + +import sun.security.util.HostnameChecker; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +/** + * @author Stanley Shyiko + */ +public class TLSHostnameVerifier implements HostnameVerifier { + + public boolean verify(String hostname, SSLSession session) { + HostnameChecker checker = HostnameChecker.getInstance(HostnameChecker.TYPE_TLS); + try { + Certificate[] peerCertificates = session.getPeerCertificates(); + if (peerCertificates.length > 0 && peerCertificates[0] instanceof X509Certificate) { + X509Certificate peerCertificate = (X509Certificate) peerCertificates[0]; + try { + checker.match(hostname, peerCertificate); + return true; + } catch (CertificateException ignored) { + } + } + } catch (SSLPeerUnverifiedException ignored) { + } + return false; + } + +} diff --git a/src/main/java/com/github/shyiko/mysql/binlog/network/protocol/PacketChannel.java b/src/main/java/com/github/shyiko/mysql/binlog/network/protocol/PacketChannel.java index 87c32055..0ada6542 100644 --- a/src/main/java/com/github/shyiko/mysql/binlog/network/protocol/PacketChannel.java +++ b/src/main/java/com/github/shyiko/mysql/binlog/network/protocol/PacketChannel.java @@ -18,9 +18,12 @@ import com.github.shyiko.mysql.binlog.io.BufferedSocketInputStream; import com.github.shyiko.mysql.binlog.io.ByteArrayInputStream; import com.github.shyiko.mysql.binlog.io.ByteArrayOutputStream; -import com.github.shyiko.mysql.binlog.network.protocol.command.AuthenticateCommand; +import com.github.shyiko.mysql.binlog.network.IdentityVerificationException; +import com.github.shyiko.mysql.binlog.network.SSLSocketFactory; import com.github.shyiko.mysql.binlog.network.protocol.command.Command; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLSocket; import java.io.IOException; import java.net.Socket; import java.nio.channels.Channel; @@ -58,16 +61,33 @@ public byte[] read() throws IOException { return inputStream.read(length); } - public void write(Command command) throws IOException { + public void write(Command command, int packetNumber) throws IOException { byte[] body = command.toByteArray(); outputStream.writeInteger(body.length, 3); // packet length - outputStream.writeInteger(command instanceof AuthenticateCommand ? 1 : 0, 1); // packet number + outputStream.writeInteger(packetNumber, 1); outputStream.write(body, 0, body.length); // though it has no effect in case of default (underlying) output stream (SocketOutputStream), // it may be necessary in case of non-default one outputStream.flush(); } + public void write(Command command) throws IOException { + write(command, 0); + } + + public void upgradeToSSL(SSLSocketFactory sslSocketFactory, HostnameVerifier hostnameVerifier) throws IOException { + SSLSocket sslSocket = sslSocketFactory.createSocket(this.socket); + sslSocket.startHandshake(); + socket = sslSocket; + inputStream = new ByteArrayInputStream(sslSocket.getInputStream()); + outputStream = new ByteArrayOutputStream(sslSocket.getOutputStream()); + if (hostnameVerifier != null && !hostnameVerifier.verify(sslSocket.getInetAddress().getHostName(), + sslSocket.getSession())) { + throw new IdentityVerificationException("\"" + sslSocket.getInetAddress().getHostName() + + "\" identity was not confirmed"); + } + } + @Override public boolean isOpen() { return !socket.isClosed(); diff --git a/src/main/java/com/github/shyiko/mysql/binlog/network/protocol/command/SSLRequestCommand.java b/src/main/java/com/github/shyiko/mysql/binlog/network/protocol/command/SSLRequestCommand.java new file mode 100644 index 00000000..ea748104 --- /dev/null +++ b/src/main/java/com/github/shyiko/mysql/binlog/network/protocol/command/SSLRequestCommand.java @@ -0,0 +1,57 @@ +/* + * Copyright 2016 Stanley Shyiko + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.shyiko.mysql.binlog.network.protocol.command; + +import com.github.shyiko.mysql.binlog.io.ByteArrayOutputStream; +import com.github.shyiko.mysql.binlog.network.ClientCapabilities; + +import java.io.IOException; + +/** + * @author Stanley Shyiko + */ +public class SSLRequestCommand implements Command { + + private int clientCapabilities; + private int collation; + + public void setClientCapabilities(int clientCapabilities) { + this.clientCapabilities = clientCapabilities; + } + + public void setCollation(int collation) { + this.collation = collation; + } + + @Override + public byte[] toByteArray() throws IOException { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + int clientCapabilities = this.clientCapabilities; + if (clientCapabilities == 0) { + clientCapabilities = ClientCapabilities.LONG_FLAG | + ClientCapabilities.PROTOCOL_41 | ClientCapabilities.SECURE_CONNECTION; + } + clientCapabilities |= ClientCapabilities.SSL; + buffer.writeInteger(clientCapabilities, 4); + buffer.writeInteger(0, 4); // maximum packet length + buffer.writeInteger(collation, 1); + for (int i = 0; i < 23; i++) { + buffer.write(0); + } + return buffer.toByteArray(); + } + +} diff --git a/supplement/codequality/checkstyle.xml b/supplement/codequality/checkstyle.xml index f8800a0e..60119283 100644 --- a/supplement/codequality/checkstyle.xml +++ b/supplement/codequality/checkstyle.xml @@ -61,7 +61,8 @@ - + +