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 16612f5d..4f371b0c 100644 --- a/src/main/java/com/github/shyiko/mysql/binlog/BinaryLogClient.java +++ b/src/main/java/com/github/shyiko/mysql/binlog/BinaryLogClient.java @@ -46,6 +46,7 @@ import com.github.shyiko.mysql.binlog.network.protocol.PacketChannel; import com.github.shyiko.mysql.binlog.network.protocol.ResultSetRowPacket; import com.github.shyiko.mysql.binlog.network.protocol.command.AuthenticateCommand; +import com.github.shyiko.mysql.binlog.network.protocol.command.AuthenticateNativePasswordCommand; import com.github.shyiko.mysql.binlog.network.protocol.command.Command; import com.github.shyiko.mysql.binlog.network.protocol.command.DumpBinaryLogCommand; import com.github.shyiko.mysql.binlog.network.protocol.command.DumpBinaryLogGtidCommand; @@ -722,8 +723,39 @@ private void authenticate(GreetingPacket greetingPacket) throws IOException { ErrorPacket errorPacket = new ErrorPacket(bytes); throw new AuthenticationException(errorPacket.getErrorMessage(), errorPacket.getErrorCode(), errorPacket.getSqlState()); + } else if (authenticationResult[0] == (byte) 0xFE) { + switchAuthentication(authenticationResult); + } else { + throw new AuthenticationException("Unexpected authentication result (" + authenticationResult[0] + ")"); + } + } + } + + private void switchAuthentication(byte[] authenticationResult) throws IOException { + /* + Azure-MySQL likes to tell us to switch authentication methods, even though + we haven't advertised that we support any. It uses this for some-odd + reason to send the real password scramble. + */ + ByteArrayInputStream buffer = new ByteArrayInputStream(authenticationResult); + buffer.read(1); + + String authName = buffer.readZeroTerminatedString(); + if ("mysql_native_password".equals(authName)) { + String scramble = buffer.readZeroTerminatedString(); + + Command switchCommand = new AuthenticateNativePasswordCommand(scramble, password); + channel.writeBuffered(switchCommand, 3); + byte[] authResult = channel.read(); + + if (authResult[0] != (byte) 0x00) { + byte[] bytes = Arrays.copyOfRange(authResult, 1, authResult.length); + ErrorPacket errorPacket = new ErrorPacket(bytes); + throw new AuthenticationException(errorPacket.getErrorMessage(), errorPacket.getErrorCode(), + errorPacket.getSqlState()); } - throw new AuthenticationException("Unexpected authentication result (" + authenticationResult[0] + ")"); + } else { + throw new AuthenticationException("Unsupported authentication type: " + authName); } } 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 0ada6542..198616e9 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 @@ -71,6 +71,21 @@ public void write(Command command, int packetNumber) throws IOException { outputStream.flush(); } + /* + Azure's MySQL has bizarre network properties that force us to write an + auth-response challenge in one shot, lest their hair catch on fire and + forcibly disconnect us. + */ + public void writeBuffered(Command command, int packetNumber) throws IOException { + byte[] body = command.toByteArray(); + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + buffer.writeInteger(body.length, 3); // packet length + buffer.writeInteger(packetNumber, 1); + buffer.write(body, 0, body.length); + buffer.flush(); + socket.getOutputStream().write(buffer.toByteArray()); + } + public void write(Command command) throws IOException { write(command, 0); } diff --git a/src/main/java/com/github/shyiko/mysql/binlog/network/protocol/command/AuthenticateCommand.java b/src/main/java/com/github/shyiko/mysql/binlog/network/protocol/command/AuthenticateCommand.java index aba039f3..a045fe24 100644 --- a/src/main/java/com/github/shyiko/mysql/binlog/network/protocol/command/AuthenticateCommand.java +++ b/src/main/java/com/github/shyiko/mysql/binlog/network/protocol/command/AuthenticateCommand.java @@ -79,7 +79,7 @@ public byte[] toByteArray() throws IOException { /** * see mysql/sql/password.c scramble(...) */ - private static byte[] passwordCompatibleWithMySQL411(String password, String salt) { + public static byte[] passwordCompatibleWithMySQL411(String password, String salt) { MessageDigest sha; try { sha = MessageDigest.getInstance("SHA-1"); diff --git a/src/main/java/com/github/shyiko/mysql/binlog/network/protocol/command/AuthenticateNativePasswordCommand.java b/src/main/java/com/github/shyiko/mysql/binlog/network/protocol/command/AuthenticateNativePasswordCommand.java new file mode 100644 index 00000000..f98eced0 --- /dev/null +++ b/src/main/java/com/github/shyiko/mysql/binlog/network/protocol/command/AuthenticateNativePasswordCommand.java @@ -0,0 +1,34 @@ +/* + * Copyright 2013 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 java.io.IOException; + +/** + * @author Ben Osheroff + */ +public class AuthenticateNativePasswordCommand implements Command { + private final String scramble, password; + + public AuthenticateNativePasswordCommand(String scramble, String password) { + this.scramble = scramble; + this.password = password; + } + @Override + public byte[] toByteArray() throws IOException { + return AuthenticateCommand.passwordCompatibleWithMySQL411(password, scramble); + } +}