View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *     http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.shardingsphere.data.pipeline.mysql.ingest.client.netty;
19  
20  import io.netty.channel.ChannelHandlerContext;
21  import io.netty.channel.ChannelInboundHandlerAdapter;
22  import io.netty.util.concurrent.Promise;
23  import lombok.RequiredArgsConstructor;
24  import lombok.SneakyThrows;
25  import org.apache.shardingsphere.data.pipeline.core.exception.PipelineInternalException;
26  import org.apache.shardingsphere.data.pipeline.mysql.ingest.client.PasswordEncryption;
27  import org.apache.shardingsphere.data.pipeline.mysql.ingest.client.ServerInfo;
28  import org.apache.shardingsphere.data.pipeline.mysql.ingest.client.ServerVersion;
29  import org.apache.shardingsphere.db.protocol.mysql.constant.MySQLAuthenticationMethod;
30  import org.apache.shardingsphere.db.protocol.mysql.constant.MySQLAuthenticationPlugin;
31  import org.apache.shardingsphere.db.protocol.mysql.constant.MySQLCapabilityFlag;
32  import org.apache.shardingsphere.db.protocol.mysql.packet.generic.MySQLErrPacket;
33  import org.apache.shardingsphere.db.protocol.mysql.packet.generic.MySQLOKPacket;
34  import org.apache.shardingsphere.db.protocol.mysql.packet.handshake.MySQLAuthMoreDataPacket;
35  import org.apache.shardingsphere.db.protocol.mysql.packet.handshake.MySQLAuthSwitchRequestPacket;
36  import org.apache.shardingsphere.db.protocol.mysql.packet.handshake.MySQLAuthSwitchResponsePacket;
37  import org.apache.shardingsphere.db.protocol.mysql.packet.handshake.MySQLHandshakePacket;
38  import org.apache.shardingsphere.db.protocol.mysql.packet.handshake.MySQLHandshakeResponse41Packet;
39  
40  import java.security.NoSuchAlgorithmException;
41  
42  /**
43   * MySQL negotiate handler.
44   */
45  @RequiredArgsConstructor
46  public final class MySQLNegotiateHandler extends ChannelInboundHandlerAdapter {
47      
48      private static final int MAX_PACKET_SIZE = 1 << 24;
49      
50      private static final int CHARACTER_SET = 33;
51      
52      private static final int REQUEST_PUBLIC_KEY = 2;
53      
54      private static final int PERFORM_FULL_AUTHENTICATION = 4;
55      
56      private final String username;
57      
58      private final String password;
59      
60      private final Promise<Object> authResultCallback;
61      
62      private ServerInfo serverInfo;
63      
64      private byte[] seed;
65      
66      private boolean publicKeyRequested;
67      
68      @SneakyThrows(NoSuchAlgorithmException.class)
69      @Override
70      public void channelRead(final ChannelHandlerContext ctx, final Object msg) {
71          if (msg instanceof MySQLHandshakePacket) {
72              MySQLHandshakePacket handshake = (MySQLHandshakePacket) msg;
73              MySQLHandshakeResponse41Packet handshakeResponsePacket = new MySQLHandshakeResponse41Packet(MAX_PACKET_SIZE, CHARACTER_SET, username);
74              handshakeResponsePacket.setAuthResponse(generateAuthResponse(handshake.getAuthPluginData().getAuthenticationPluginData()));
75              handshakeResponsePacket.setCapabilityFlags(generateClientCapability());
76              handshakeResponsePacket.setAuthPluginName(MySQLAuthenticationMethod.NATIVE);
77              ctx.channel().writeAndFlush(handshakeResponsePacket);
78              serverInfo = new ServerInfo(new ServerVersion(handshake.getServerVersion()));
79              return;
80          }
81          if (msg instanceof MySQLAuthSwitchRequestPacket) {
82              MySQLAuthSwitchRequestPacket authSwitchRequest = (MySQLAuthSwitchRequestPacket) msg;
83              ctx.channel().writeAndFlush(new MySQLAuthSwitchResponsePacket(getAuthPluginResponse(authSwitchRequest)));
84              seed = authSwitchRequest.getAuthPluginData().getAuthenticationPluginData();
85              return;
86          }
87          if (msg instanceof MySQLAuthMoreDataPacket) {
88              MySQLAuthMoreDataPacket authMoreData = (MySQLAuthMoreDataPacket) msg;
89              handleCachingSha2Auth(ctx, authMoreData);
90              return;
91          }
92          if (msg instanceof MySQLOKPacket) {
93              ctx.channel().pipeline().remove(this);
94              authResultCallback.setSuccess(serverInfo);
95              return;
96          }
97          MySQLErrPacket error = (MySQLErrPacket) msg;
98          ctx.channel().close();
99          throw new PipelineInternalException(error.getErrorMessage());
100     }
101     
102     private byte[] getAuthPluginResponse(final MySQLAuthSwitchRequestPacket authSwitchRequest) throws NoSuchAlgorithmException {
103         // TODO not support sha256_password now
104         switch (MySQLAuthenticationPlugin.getPluginByName(authSwitchRequest.getAuthPluginName())) {
105             case NATIVE:
106                 return PasswordEncryption.encryptWithMySQL41(password.getBytes(), authSwitchRequest.getAuthPluginData().getAuthenticationPluginData());
107             case CACHING_SHA2:
108                 return PasswordEncryption.encryptWithSha2(password.getBytes(), authSwitchRequest.getAuthPluginData().getAuthenticationPluginData());
109             default:
110                 return password.getBytes();
111         }
112     }
113     
114     private void handleCachingSha2Auth(final ChannelHandlerContext ctx, final MySQLAuthMoreDataPacket authMoreData) {
115         if (publicKeyRequested) {
116             ctx.channel().writeAndFlush(new MySQLAuthSwitchResponsePacket(
117                     PasswordEncryption.encryptWithRSAPublicKey(password, seed,
118                             serverInfo.getServerVersion().greaterThanOrEqualTo(8, 0, 5) ? "RSA/ECB/OAEPWithSHA-1AndMGF1Padding" : "RSA/ECB/PKCS1Padding",
119                             new String(authMoreData.getPluginData()))));
120         } else {
121             if (PERFORM_FULL_AUTHENTICATION == authMoreData.getPluginData()[0]) {
122                 publicKeyRequested = true;
123                 ctx.channel().writeAndFlush(new MySQLAuthSwitchResponsePacket(new byte[]{REQUEST_PUBLIC_KEY}));
124             }
125         }
126     }
127     
128     private int generateClientCapability() {
129         return MySQLCapabilityFlag.calculateCapabilityFlags(MySQLCapabilityFlag.CLIENT_LONG_PASSWORD, MySQLCapabilityFlag.CLIENT_LONG_FLAG,
130                 MySQLCapabilityFlag.CLIENT_PROTOCOL_41, MySQLCapabilityFlag.CLIENT_INTERACTIVE, MySQLCapabilityFlag.CLIENT_TRANSACTIONS,
131                 MySQLCapabilityFlag.CLIENT_SECURE_CONNECTION, MySQLCapabilityFlag.CLIENT_MULTI_STATEMENTS, MySQLCapabilityFlag.CLIENT_PLUGIN_AUTH);
132     }
133     
134     @SneakyThrows(NoSuchAlgorithmException.class)
135     private byte[] generateAuthResponse(final byte[] authPluginData) {
136         return null == password || password.isEmpty() ? new byte[0] : PasswordEncryption.encryptWithMySQL41(password.getBytes(), authPluginData);
137     }
138 }