Skip to content

Mysql protocol

Sayapin Alexander edited this page Mar 26, 2016 · 34 revisions

Целью данного упражнения является разработка модуля поддержки протокола Mysql в академических целях. Необходимо разобраться с Netty, реализовав подмножество протокола Mysql достаточное для подключения сторонними клиентами и выполнения простых комманд.

Исходные данные

Замечания по разработке

  • Создаём очень ограниченное подмножество протокола Mysql
  • Не заморачиваемся с SSL
  • Обработка запросов передаётся дальше пользовательскому обработчику
  • Не делаем поддержку сжатия
  • Не делаем поддержку транзакций
  • Не делаем поддержку статуса сервера
  • Используем протокол версии 4.1
  • Не реализуем настраиваемость протокола (например, возврат COM_EOF вместо COM_OK)
  • Не реализуем prepared statements
  • Не реализуем "честную" аутентификацию

Пара слов о протоколе

Протокол mysql представляет собой бинарный клиент-серверный протокол. В качестве модели представления байт используется little endian (то есть, байты передаются перевёрнутыми). Протокол сессионный, то есть перед работой необходимо пройти аутентификацию, и это является фактом начала сессии. В конце работы необходимо отправить пакет COM_QUIT для завершения сеанса. Протокол синхронный, то есть после запроса необходимо дождаться ответа от сервера.

В работе протокола выделяют 2 фазы:

Пакет состоит из 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 - тело пакета

Цель №1: Создать простейший сервер на Netty

Для начала необходимо реализовать простой сервер, который просто принимает подключения и выводит информацию о подключении в лог. Просто возьмём простейший сервер из документации 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

Цель №2: Создать сервер, который возвращает greeting записанный с реального сервера

Для начала рассмотрим описание протокола 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"));
    }
}

Цель №3: Создать сервер, который позволяет проходить handshake

Цель №4: Создать сервер, который принимает комманды COM_QUERY

Цель №5: Создать сервер, который обрабатывает заранее заданную команду и возвращает результат.

Цель №6: Попробовать зайти на сервер с использованием mysql-cli

Цель №7: Попробовать интеграцию с php.

Цель №8: Создать простой клиент, который подключается к серверу Mysql.

Цель №9: Создать простой клиент, который проходит авторизацию в mysql.

Цель №10: Создать простой клиент, который отправляет запрос COM_QUERY и интерпретирует результаты.

Цель №11: Воспользоваться библиотекой jSQLParser для реализации простого SQL-сервера.

Clone this wiki locally