Skip to content

Commit 3887ff2

Browse files
committed
shyiko#70 - Secure binlog connection with SSL
1 parent 78359db commit 3887ff2

11 files changed

+430
-59
lines changed

readme.md

+32-33
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ but ended up as a complete rewrite. Key differences/features:
88
- automatic binlog filename/position | GTID resolution
99
- resumable disconnects
1010
- plugable failover strategies
11-
- JMX exposure (optionally with statistics)
11+
- binlog_checksum=CRC32 support (for MySQL 5.6.2+ users)
12+
- secure communication over the TLS
13+
- JMX-friendly
14+
- real-time stats
1215
- availability in Maven Central
1316
- no third-party dependencies
14-
- binlog_checksum=CRC32 support (for MySQL 5.6.2+ users)
1517
- test suite over different versions of MySQL releases
1618

1719
> If you are looking for something similar in other languages - check out
@@ -30,32 +32,7 @@ Get the latest JAR(s) from [here](http://search.maven.org/#search%7Cga%7C1%7Cg%3
3032
</dependency>
3133
```
3234

33-
The latest development version always available through Sonatype Snapshots repository (as shown below).
34-
35-
```xml
36-
<dependencies>
37-
<dependency>
38-
<groupId>com.github.shyiko</groupId>
39-
<artifactId>mysql-binlog-connector-java</artifactId>
40-
<version>0.3.2-SNAPSHOT</version>
41-
</dependency>
42-
</dependencies>
43-
44-
<repositories>
45-
<repository>
46-
<id>sonatype-snapshots</id>
47-
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
48-
<snapshots>
49-
<enabled>true</enabled>
50-
</snapshots>
51-
<releases>
52-
<enabled>false</enabled>
53-
</releases>
54-
</repository>
55-
</repositories>
56-
```
57-
58-
### Reading binary log file
35+
#### Reading binary log file
5936

6037
```java
6138
File binlogFile = ...
@@ -69,7 +46,7 @@ try {
6946
}
7047
```
7148

72-
### Tapping into MySQL replication stream
49+
#### Tapping into MySQL replication stream
7350

7451
> PREREQUISITES: Whichever user you plan to use for the BinaryLogClient, he MUST have [REPLICATION SLAVE](http://dev.mysql.com/doc/refman/5.5/en/privileges-provided.html#priv_replication-slave) privilege. Unless you specify binlogFilename/binlogPosition yourself (in which case automatic resolution won't kick in), you'll need [REPLICATION CLIENT](http://dev.mysql.com/doc/refman/5.5/en/privileges-provided.html#priv_replication-client) granted as well.
7552
@@ -91,7 +68,7 @@ kick off from a specific filename or position, use `client.setBinlogFilename(fil
9168
> `client.connect()` is blocking (meaning that client will listen for events in the current thread).
9269
`client.connect(timeout)`, on the other hand, spawns a separate thread.
9370

94-
### Controlling event deserialization
71+
#### Controlling event deserialization
9572

9673
> You might need it for several reasons:
9774
you don't want to waste time deserializing events you won't need;
@@ -120,7 +97,7 @@ BinaryLogClient client = ...
12097
client.setEventDeserializer(eventDeserializer);
12198
```
12299

123-
### Exposing BinaryLogClient with JMX
100+
#### Exposing BinaryLogClient through JMX
124101

125102
```java
126103
MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer();
@@ -136,10 +113,32 @@ ObjectName statsObjectName = new ObjectName("mysql.binlog:type=BinaryLogClientSt
136113
mBeanServer.registerMBean(stats, statsObjectName);
137114
```
138115

116+
#### Using SSL
117+
118+
> Introduced in 1.0.0.
119+
120+
TLSv1.1 & TLSv1.2 require [JDK 7](http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6916074)+.
121+
Prior to MySQL 5.7.10, MySQL supported only TLSv1
122+
(see [Secure Connection Protocols and Ciphers](http://dev.mysql.com/doc/refman/5.7/en/secure-connection-protocols-ciphers.html)).
123+
124+
> To check that MySQL server is [properly configured with SSL support](http://dev.mysql.com/doc/refman/5.7/en/using-secure-connections.html) -
125+
`mysql -h host -u root -ptypeyourpasswordmaybe -e "show global variables like 'have_%ssl';"` ("Value"
126+
should be "YES"). State of the current session can be determined using `\s` ("SSL" should not be blank).
127+
128+
```java
129+
System.setProperty("javax.net.ssl.keyStore", "/path/to/keystore.jks");
130+
System.setProperty("javax.net.ssl.keyStorePassword", "keystore.password");
131+
System.setProperty("javax.net.ssl.trustStore", "/path/to/truststore.jks");
132+
System.setProperty("javax.net.ssl.trustStorePassword","truststore.password");
133+
134+
BinaryLogClient client = ...
135+
client.setSSLMode(SSLMode.REQUIRED);
136+
```
137+
139138
## Implementation notes
140139

141-
- data of numeric types (tinyint, etc) always returned signed(!) regardless of whether column definition includes "unsigned" keyword or not
142-
- data of var\*/\*text/\*blob types always returned as a byte array (for var\* this is true starting from mysql-binlog-connector-java@1.0.0).
140+
- data of numeric types (tinyint, etc) always returned signed(!) regardless of whether column definition includes "unsigned" keyword or not.
141+
- data of var\*/\*text/\*blob types always returned as a byte array (for var\* this is true starting from 1.0.0).
143142

144143
## Frequently Asked Questions
145144

src/main/java/com/github/shyiko/mysql/binlog/BinaryLogClient.java

+52-22
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,17 @@
3030
import com.github.shyiko.mysql.binlog.event.deserialization.GtidEventDataDeserializer;
3131
import com.github.shyiko.mysql.binlog.event.deserialization.QueryEventDataDeserializer;
3232
import com.github.shyiko.mysql.binlog.event.deserialization.RotateEventDataDeserializer;
33-
import com.github.shyiko.mysql.binlog.io.BufferedSocketInputStream;
3433
import com.github.shyiko.mysql.binlog.io.ByteArrayInputStream;
3534
import com.github.shyiko.mysql.binlog.jmx.BinaryLogClientMXBean;
3635
import com.github.shyiko.mysql.binlog.network.AuthenticationException;
36+
import com.github.shyiko.mysql.binlog.network.ClientCapabilities;
37+
import com.github.shyiko.mysql.binlog.network.DefaultSSLSocketFactory;
38+
import com.github.shyiko.mysql.binlog.network.DefaultSocketFactory;
39+
import com.github.shyiko.mysql.binlog.network.SSLMode;
40+
import com.github.shyiko.mysql.binlog.network.SSLSocketFactory;
3741
import com.github.shyiko.mysql.binlog.network.ServerException;
3842
import com.github.shyiko.mysql.binlog.network.SocketFactory;
43+
import com.github.shyiko.mysql.binlog.network.TLSHostnameVerifier;
3944
import com.github.shyiko.mysql.binlog.network.protocol.ErrorPacket;
4045
import com.github.shyiko.mysql.binlog.network.protocol.GreetingPacket;
4146
import com.github.shyiko.mysql.binlog.network.protocol.Packet;
@@ -47,10 +52,10 @@
4752
import com.github.shyiko.mysql.binlog.network.protocol.command.DumpBinaryLogGtidCommand;
4853
import com.github.shyiko.mysql.binlog.network.protocol.command.PingCommand;
4954
import com.github.shyiko.mysql.binlog.network.protocol.command.QueryCommand;
55+
import com.github.shyiko.mysql.binlog.network.protocol.command.SSLRequestCommand;
5056

5157
import java.io.EOFException;
5258
import java.io.IOException;
53-
import java.io.InputStream;
5459
import java.net.InetSocketAddress;
5560
import java.net.Socket;
5661
import java.net.SocketException;
@@ -78,22 +83,8 @@
7883
*/
7984
public class BinaryLogClient implements BinaryLogClientMXBean {
8085

81-
private static final SocketFactory DEFAULT_SOCKET_FACTORY = new SocketFactory() {
82-
83-
@Override
84-
public Socket createSocket() throws SocketException {
85-
return new Socket() {
86-
87-
private InputStream inputStream;
88-
89-
@Override
90-
public synchronized InputStream getInputStream() throws IOException {
91-
return inputStream != null ? inputStream :
92-
(inputStream = new BufferedSocketInputStream(super.getInputStream()));
93-
}
94-
};
95-
}
96-
};
86+
private static final SocketFactory DEFAULT_SOCKET_FACTORY = new DefaultSocketFactory();
87+
private static final SSLSocketFactory DEFAULT_SSL_SOCKET_FACTORY = new DefaultSSLSocketFactory();
9788

9889
private final Logger logger = Logger.getLogger(getClass().getName());
9990

@@ -108,6 +99,7 @@ public synchronized InputStream getInputStream() throws IOException {
10899
private volatile String binlogFilename;
109100
private volatile long binlogPosition = 4;
110101
private volatile long connectionId;
102+
private SSLMode sslMode = SSLMode.DISABLED;
111103

112104
private volatile GtidSet gtidSet;
113105
private final Object gtidSetAccessLock = new Object();
@@ -118,6 +110,7 @@ public synchronized InputStream getInputStream() throws IOException {
118110
private final List<LifecycleListener> lifecycleListeners = new LinkedList<LifecycleListener>();
119111

120112
private SocketFactory socketFactory;
113+
private SSLSocketFactory sslSocketFactory;
121114

122115
private PacketChannel channel;
123116
private volatile boolean connected;
@@ -187,6 +180,17 @@ public void setBlocking(boolean blocking) {
187180
this.blocking = blocking;
188181
}
189182

183+
public SSLMode getSSLMode() {
184+
return sslMode;
185+
}
186+
187+
public void setSSLMode(SSLMode sslMode) {
188+
if (sslMode == null) {
189+
throw new IllegalArgumentException("SSL mode cannot be NULL");
190+
}
191+
this.sslMode = sslMode;
192+
}
193+
190194
/**
191195
* @return server id (65535 by default)
192196
* @see #setServerId(long)
@@ -347,6 +351,13 @@ public void setSocketFactory(SocketFactory socketFactory) {
347351
this.socketFactory = socketFactory;
348352
}
349353

354+
/**
355+
* @param sslSocketFactory custom ssl socket factory
356+
*/
357+
public void setSslSocketFactory(SSLSocketFactory sslSocketFactory) {
358+
this.sslSocketFactory = sslSocketFactory;
359+
}
360+
350361
/**
351362
* @param threadFactory custom thread factory. If not provided, threads will be created using simple "new Thread()".
352363
*/
@@ -367,7 +378,7 @@ public void connect() throws IOException {
367378
try {
368379
establishConnection();
369380
GreetingPacket greetingPacket = receiveGreeting();
370-
authenticate(greetingPacket.getScramble(), greetingPacket.getServerCollation());
381+
authenticate(greetingPacket);
371382
connectionId = greetingPacket.getThreadId();
372383
if (binlogFilename == null && gtidSet == null) {
373384
autoPosition();
@@ -474,10 +485,29 @@ private void ensureEventDataDeserializer(EventType eventType,
474485
}
475486
}
476487

477-
private void authenticate(String salt, int collation) throws IOException {
478-
AuthenticateCommand authenticateCommand = new AuthenticateCommand(schema, username, password, salt);
488+
private void authenticate(GreetingPacket greetingPacket) throws IOException {
489+
int collation = greetingPacket.getServerCollation();
490+
int packetNumber = 1;
491+
if (sslMode != SSLMode.DISABLED) {
492+
boolean serverSupportsSSL = (greetingPacket.getServerCapabilities() & ClientCapabilities.SSL) != 0;
493+
if (!serverSupportsSSL && (sslMode == SSLMode.REQUIRED || sslMode == SSLMode.VERIFY_CA ||
494+
sslMode == SSLMode.VERIFY_IDENTITY)) {
495+
throw new IOException("MySQL server does not support SSL");
496+
}
497+
if (serverSupportsSSL) {
498+
SSLRequestCommand sslRequestCommand = new SSLRequestCommand();
499+
sslRequestCommand.setCollation(collation);
500+
channel.write(sslRequestCommand, packetNumber++);
501+
SSLSocketFactory sslSocketFactory = this.sslSocketFactory != null ? this.sslSocketFactory :
502+
DEFAULT_SSL_SOCKET_FACTORY;
503+
channel.upgradeToSSL(sslSocketFactory,
504+
sslMode == SSLMode.VERIFY_IDENTITY ? new TLSHostnameVerifier() : null);
505+
}
506+
}
507+
AuthenticateCommand authenticateCommand = new AuthenticateCommand(schema, username, password,
508+
greetingPacket.getScramble());
479509
authenticateCommand.setCollation(collation);
480-
channel.write(authenticateCommand);
510+
channel.write(authenticateCommand, packetNumber);
481511
byte[] authenticationResult = channel.read();
482512
if (authenticationResult[0] == ErrorPacket.HEADER) {
483513
ErrorPacket errorPacket = new ErrorPacket(authenticationResult, 1);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright 2016 Stanley Shyiko
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.github.shyiko.mysql.binlog.network;
17+
18+
import javax.net.ssl.SSLContext;
19+
import javax.net.ssl.SSLSocket;
20+
import java.io.IOException;
21+
import java.net.Socket;
22+
import java.net.SocketException;
23+
import java.security.GeneralSecurityException;
24+
25+
/**
26+
* @author <a href="mailto:stanley.shyiko@gmail.com">Stanley Shyiko</a>
27+
*/
28+
public class DefaultSSLSocketFactory implements SSLSocketFactory {
29+
30+
private final String protocol;
31+
32+
public DefaultSSLSocketFactory() {
33+
this("TLSv1");
34+
}
35+
36+
/**
37+
* @param protocol TLSv1, TLSv1.1 or TLSv1.2 (the last two require JDK 7+)
38+
*/
39+
public DefaultSSLSocketFactory(String protocol) {
40+
this.protocol = protocol;
41+
}
42+
43+
@Override
44+
public SSLSocket createSocket(Socket socket) throws SocketException {
45+
SSLContext sc;
46+
try {
47+
sc = SSLContext.getInstance(this.protocol);
48+
initSSLContext(sc);
49+
} catch (GeneralSecurityException e) {
50+
throw new SocketException(e.getMessage());
51+
}
52+
try {
53+
return (SSLSocket) sc.getSocketFactory()
54+
.createSocket(socket, socket.getInetAddress().getHostName(), socket.getPort(), true);
55+
} catch (IOException e) {
56+
throw new SocketException(e.getMessage());
57+
}
58+
}
59+
60+
protected void initSSLContext(SSLContext sc) throws GeneralSecurityException {
61+
sc.init(null, null, null);
62+
}
63+
64+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright 2016 Stanley Shyiko
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.github.shyiko.mysql.binlog.network;
17+
18+
import com.github.shyiko.mysql.binlog.io.BufferedSocketInputStream;
19+
20+
import java.io.IOException;
21+
import java.io.InputStream;
22+
import java.net.Socket;
23+
import java.net.SocketException;
24+
25+
/**
26+
* @author <a href="mailto:stanley.shyiko@gmail.com">Stanley Shyiko</a>
27+
*/
28+
public class DefaultSocketFactory implements SocketFactory {
29+
30+
@Override
31+
public Socket createSocket() throws SocketException {
32+
return new Socket() {
33+
34+
private InputStream inputStream;
35+
36+
@Override
37+
public synchronized InputStream getInputStream() throws IOException {
38+
return inputStream != null ? inputStream :
39+
(inputStream = new BufferedSocketInputStream(super.getInputStream()));
40+
}
41+
};
42+
}
43+
44+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright 2016 Stanley Shyiko
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.github.shyiko.mysql.binlog.network;
17+
18+
import javax.net.ssl.SSLException;
19+
20+
/**
21+
* @author <a href="mailto:stanley.shyiko@gmail.com">Stanley Shyiko</a>
22+
*/
23+
public class IdentityVerificationException extends SSLException {
24+
25+
public IdentityVerificationException(String message) {
26+
super(message);
27+
}
28+
29+
}

0 commit comments

Comments
 (0)