-
Notifications
You must be signed in to change notification settings - Fork 56
Mysql protocol
Целью данного упражнения является разработка модуля поддержки протокола Mysql в академических целях. Необходимо разобраться с Netty, реализовав подмножество протокола Mysql достаточное для подключения сторонними клиентами и выполнения простых комманд.
- Netty 4.0 http://netty.io/wiki/user-guide-for-4.x.html
- Java 1.8
- Описание протокола Mysql https://dev.mysql.com/doc/internals/en/client-server-protocol.html
- Создаём очень ограниченное подмножество протокола Mysql
- Не заморачиваемся с SSL
- Обработка запросов передаётся дальше пользовательскому обработчику
- Не делаем поддержку сжатия
- Не делаем поддержку транзакций
- Не делаем поддержку статуса сервера
- Используем протокол версии 4.1
- Не реализуем настраиваемость протокола (например, возврат COM_EOF вместо COM_OK)
- Не реализуем prepared statements
- Не реализуем "честную" аутентификацию
Протокол mysql представляет собой бинарный клиент-серверный протокол. В качестве модели представления байт используется little endian (то есть, байты передаются перевёрнутыми). Протокол сессионный, то есть перед работой необходимо пройти аутентификацию, и это является фактом начала сессии. В конце работы необходимо отправить пакет COM_QUIT для завершения сеанса. Протокол синхронный, то есть после запроса необходимо дождаться ответа от сервера.
В работе протокола выделяют 2 фазы:
-
Connection phase - фаза устновки соединения и начала сессии (в простейшемслучае состоит из 3-х шагов: initial handshake, auth request, auth response)
-
Command phase - ффаза непосредственного выполнения команд.
-
Типы данных: https://dev.mysql.com/doc/internals/en/basic-types.html
-
Целочисленные типы: https://dev.mysql.com/doc/internals/en/integer.html
-
Типы данных для результатов: https://dev.mysql.com/doc/internals/en/binary-protocol-value.html
-
Список пакетов: https://dev.mysql.com/doc/internals/en/text-protocol.html
-
Connection phase: https://dev.mysql.com/doc/internals/en/connection-phase.html
- Простая аутентификация: https://dev.mysql.com/doc/internals/en/plain-handshake.html
- Greetings от сервера: https://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::Handshake
- Запрос на аутентификацию (на самом деле Handshake response) https://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::HandshakeResponse
- Результат аутентификации: https://dev.mysql.com/doc/internals/en/successful-authentication.html
- Флаги свойств: https://dev.mysql.com/doc/internals/en/capability-flags.html
- Кодировки: https://dev.mysql.com/doc/internals/en/character-set.html#packet-Protocol::CharacterSet
- Флаги статуса: https://dev.mysql.com/doc/internals/en/status-flags.html#packet-Protocol::StatusFlags
-
Выполнени запроса: https://dev.mysql.com/doc/internals/en/com-query.html
Пакет состоит из 2-х частей ( https://dev.mysql.com/doc/internals/en/mysql-packet.html ):
- Заголовка
- Тела пакета
Заголовок состоит из 2-х полей:
- Длины тела пакета (3 байта)
- Номер пакета в группе (1 байт)
пример пакета COM_QUIT.
01 00 00 00 01
-
010000- длина тела пакета (3 байта) -
00- номер пакета -
01- тело пакета
Для начала необходимо реализовать простой сервер, который просто принимает подключения и выводит информацию о подключении в лог. Просто возьмём простейший сервер из документации Netty ( http://netty.io/wiki/user-guide-for-4.x.html ).
Основной класс. App.java
public class App {
public static final Logger logger = LoggerFactory.getLogger(App.class);
public static int port = 1234;
public static void main(String[] args) {
logger.info("Application start");
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
logger.info("Client connected from:{}", ch.remoteAddress()); // Вывод информации о клиента
ByteBuf greetingBuffer = ch.alloc().buffer();
greetingBuffer.writeBytes("Greeting".getBytes()); // Выводим Greeting при подключении клиента
ch.writeAndFlush(greetingBuffer);
ch.pipeline().addLast(new DiscardServerHandler());
}
})
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true);
ChannelFuture f = b.bind(port).sync();
f.channel().closeFuture().sync();
} catch (InterruptedException e) {
logger.error("Error on create bootstrap", e);
} finally {
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
logger.info("Application shutted down");
}
}Код DiscardServerHandler.
public class DiscardServerHandler extends ChannelInboundHandlerAdapter {
public static final Logger logger = LoggerFactory.getLogger(DiscardServerHandler.class);
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
logger.debug("channel read");
ByteBuf byteBuf = (ByteBuf)msg;
if (logger.isTraceEnabled()) {
logger.trace("\n{}", ByteBufUtil.prettyHexDump(byteBuf));
}
ByteBuf buffer = ctx.alloc().buffer();
buffer.writeBytes("Hello\n".getBytes());
ctx.writeAndFlush(buffer);
((ByteBuf) msg).release();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
logger.debug("Exception", cause);
cause.printStackTrace();
ctx.close();
}
}Подключимся с помощью telnet и отправим пару строк.
$ telnet 127.0.0.1 1234
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
Greetings adsadad
Hello
Hello
Hello
Hello
Hello
Hello
Connection closed by foreign host.
Логи приложения.
2016-03-24 19:54:38 nioEventLoopGroup-3-2 INFO com.lrn.nettymysqlprotocol.App Client connected from:/127.0.0.1:53562
2016-03-24 19:54:41 nioEventLoopGroup-3-2 DEBUG com.lrn.nettymysqlprotocol.DiscardServerHandler channel read
2016-03-24 19:54:41 nioEventLoopGroup-3-2 TRACE com.lrn.nettymysqlprotocol.DiscardServerHandler
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 73 61 64 73 61 64 61 64 0d 0a |sadsadad.. |
+--------+-------------------------------------------------+----------------+
2016-03-24 19:54:42 nioEventLoopGroup-3-2 DEBUG com.lrn.nettymysqlprotocol.DiscardServerHandler channel read
2016-03-24 19:54:42 nioEventLoopGroup-3-2 TRACE com.lrn.nettymysqlprotocol.DiscardServerHandler
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 0d 0a |.. |
+--------+-------------------------------------------------+----------------+
2016-03-24 19:54:43 nioEventLoopGroup-3-2 DEBUG com.lrn.nettymysqlprotocol.DiscardServerHandler channel read
2016-03-24 19:54:43 nioEventLoopGroup-3-2 TRACE com.lrn.nettymysqlprotocol.DiscardServerHandler
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 0d 0a |.. |
+--------+-------------------------------------------------+----------------+
2016-03-24 19:54:43 nioEventLoopGroup-3-2 DEBUG com.lrn.nettymysqlprotocol.DiscardServerHandler channel read
2016-03-24 19:54:43 nioEventLoopGroup-3-2 TRACE com.lrn.nettymysqlprotocol.DiscardServerHandler
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 0d 0a |.. |
+--------+-------------------------------------------------+----------------+
2016-03-24 19:54:43 nioEventLoopGroup-3-2 DEBUG com.lrn.nettymysqlprotocol.DiscardServerHandler channel read
2016-03-24 19:54:43 nioEventLoopGroup-3-2 TRACE com.lrn.nettymysqlprotocol.DiscardServerHandler
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 0d 0a |.. |
+--------+-------------------------------------------------+----------------+
2016-03-24 19:54:43 nioEventLoopGroup-3-2 DEBUG com.lrn.nettymysqlprotocol.DiscardServerHandler channel read
2016-03-24 19:54:43 nioEventLoopGroup-3-2 TRACE com.lrn.nettymysqlprotocol.DiscardServerHandler
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 0d 0a |.. |
+--------+-------------------------------------------------+----------------+
Пример сервера в ветке pres_0_14_0
Для начала рассмотрим описание протокола MySQL из документации. https://dev.mysql.com/doc/internals/en/connection-phase.html
Когда клиент подключается к серверу, сервер отправляет initial handshake (или greetings) пакет. В ответ клиент отправляет запрос логина, а сервер возвращает результат аутентификации пользователя.
На текущем этапе детали протокола Mysql не интересуют. Наша задача записать initial handshake с реального сервера и отправить его клиенту в ответ на подключение. Воспользовавшись wireshark был записан обмен пакетами между сервером и клиентом. (src/main/resources/mysqlproto.pcap).
Initial handshake от сервера
0000 5b 00 00 00 0a 35 2e 36 2e 32 38 2d 30 75 62 75 [....5.6.28-0ubu
0010 6e 74 75 30 2e 31 35 2e 30 34 2e 31 00 0b 00 00 ntu0.15.04.1....
0020 00 65 27 31 5e 68 6e 71 6b 00 ff f7 08 02 00 7f .e'1^hnqk.......
0030 80 15 00 00 00 00 00 00 00 00 00 00 48 63 3c 40 ............Hc<@
0040 6a 78 3d 63 5d 29 51 3e 00 6d 79 73 71 6c 5f 6e jx=c])Q>.mysql_n
0050 61 74 69 76 65 5f 70 61 73 73 77 6f 72 64 00 ative_password.
Клиент отправляет пакет с запросом аутентификации
0000 ae 00 00 01 05 a6 7f 00 00 00 00 01 21 00 00 00 ............!...
0010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0020 00 00 00 00 72 6f 6f 74 00 00 6d 79 73 71 6c 5f ....root..mysql_
0030 6e 61 74 69 76 65 5f 70 61 73 73 77 6f 72 64 00 native_password.
0040 71 03 5f 6f 73 10 64 65 62 69 61 6e 2d 6c 69 6e q._os.debian-lin
0050 75 78 2d 67 6e 75 0c 5f 63 6c 69 65 6e 74 5f 6e ux-gnu._client_n
0060 61 6d 65 08 6c 69 62 6d 79 73 71 6c 04 5f 70 69 ame.libmysql._pi
0070 64 05 32 37 38 31 38 0f 5f 63 6c 69 65 6e 74 5f d.27818._client_
0080 76 65 72 73 69 6f 6e 06 35 2e 36 2e 32 38 09 5f version.5.6.28._
0090 70 6c 61 74 66 6f 72 6d 06 78 38 36 5f 36 34 0c platform.x86_64.
00a0 70 72 6f 67 72 61 6d 5f 6e 61 6d 65 05 6d 79 73 program_name.mys
00b0 71 6c ql
Сервер проводит аутентификацию и возвращает успешный ответ.
0000 00 00 03 04 00 06 00 00 00 00 00 00 00 00 08 00 ................
0010 45 08 00 3f 44 40 40 00 40 06 f8 6e 7f 00 00 01 E..?D@@[email protected]....
0020 7f 00 00 01 0c ea ee 0d 70 ce 57 ae 11 66 c0 0a ........p.W..f..
0030 80 18 01 5e fe 33 00 00 01 01 08 0a 02 ac 13 81 ...^.3..........
0040 02 ac 13 81 07 00 00 02 00 00 00 02 00 00 00 ...............
Если мы просто попытаемся подключиться к серверу, например, клиентом mysql, то получим ошибку.
$ mysql -h 127.0.0.1 --port 1234
ERROR 2013 (HY000): Lost connection to MySQL server at 'reading initial communication packet', system error: 2
Таким образом, наша задача вернуть клиенту заранее записанную последовательность initial handshake.
5b0000000a352e362e32382d307562756e7475302e31352e30342e31000b0000006527315e686e716b00fff70802007f80150000000000000000000048633c406a783d635d29513e006d7973716c5f6e61746976655f70617373776f726400
Для начала реализуем простой класс преобразования hex-строки в массив байт.
public class HexUtils {
public static byte[] hexToByte(String hex) throws Exception {
if (hex == null || "".equals(hex)) {
return new byte[]{};
} else {
checkHexString(hex);
byte[] result = new byte[hex.length() / 2];
short tmp;
int j=0;
for (int i=0;i<hex.length();i+=2) {
tmp = (short) (charToShort(hex.charAt(i)) << 4);
tmp |= charToShort(hex.charAt(i + 1));
result[j] = (byte) tmp;
j++;
}
return result;
}
}
public static final short charToShort(char c) {
if (c >= 0x30 && c <= 0x39) {
return (short) (c - 0x30);
} else {
if (c >= 'a' && c<='f') {
return (short) (c - 'a'+10);
} else {
if (c>='A' && c<='F') {
return (short) (c - 'A'+10);
} else {
throw new RuntimeException("Conversion exception");
}
}
}
}
public static void checkHexString(String hex) throws Exception {
if (hex != null) {
if ( (hex.length() % 2) != 0 ) {
throw new Exception("Invalid length");
}
if (!"".equals(hex) && !hex.matches("^[0-9a-fA-F]+$")) {
throw new Exception("Invalid string");
}
}
}
}Тест для этого класса.
import com.lrn.nettymysqlprotocol.HexUtils;
import org.junit.Test;
import static org.junit.Assert.*;
public class HexUtilsTest {
@Test
public void testValid() throws Exception {
HexUtils.checkHexString(null);
HexUtils.checkHexString("");
HexUtils.checkHexString("0123456789AbCdEf");
}
@Test(expected = Exception.class)
public void testInvalidLength() throws Exception {
HexUtils.checkHexString("012");
}
@Test(expected = Exception.class)
public void testInvalidStringSymbols() throws Exception {
HexUtils.checkHexString(" FakeString");
}
@Test
public void testCharToShort() {
assertEquals(0, HexUtils.charToShort('0'));
assertEquals(1, HexUtils.charToShort('1'));
assertEquals(2, HexUtils.charToShort('2'));
assertEquals(3, HexUtils.charToShort('3'));
assertEquals(4, HexUtils.charToShort('4'));
assertEquals(5, HexUtils.charToShort('5'));
assertEquals(6, HexUtils.charToShort('6'));
assertEquals(7, HexUtils.charToShort('7'));
assertEquals(8, HexUtils.charToShort('8'));
assertEquals(9, HexUtils.charToShort('9'));
assertEquals(10, HexUtils.charToShort('a'));
assertEquals(10, HexUtils.charToShort('A'));
assertEquals(11, HexUtils.charToShort('b'));
assertEquals(11, HexUtils.charToShort('B'));
assertEquals(12, HexUtils.charToShort('c'));
assertEquals(12, HexUtils.charToShort('C'));
assertEquals(13, HexUtils.charToShort('d'));
assertEquals(13, HexUtils.charToShort('D'));
assertEquals(14, HexUtils.charToShort('e'));
assertEquals(14, HexUtils.charToShort('E'));
assertEquals(15, HexUtils.charToShort('f'));
assertEquals(15, HexUtils.charToShort('F'));
}
@Test(expected = RuntimeException.class)
public void testInvalidCharInCharToShort() {
HexUtils.charToShort('_');
}
@Test
public void testValidConversion() throws Exception {
assertArrayEquals(new byte[]{0x01}, HexUtils.hexToByte("01"));
assertArrayEquals(new byte[]{0x23, 0x45}, HexUtils.hexToByte("2345"));
assertArrayEquals(new byte[]{0x23, 0x45, (byte)0xab,(byte)0xcd,(byte)0xef}, HexUtils.hexToByte("2345aBcDeF"));
assertArrayEquals(new byte[]{0x23, 0x45, (byte)0xab,(byte)0xcd,(byte)0xef}, HexUtils.hexToByte("2345AbCdEf"));
}
}