CryptDB代码分析2-handler与executor

之前已经介绍了SQL语句经过mysql-proxy的lua脚本与C++库交互的过程。在CryptDB的处理中,总体分为两个阶段:rewrite与next。本文介绍在rewrite和next这两个阶段中比较重要的两个类层次:handler以及executor。

SQL改写方式与query恢复介绍

首先考虑如何对SQL语句进行加密。CryptDB不直接处理字符串,而是借用了MySQL5.5版本的parser先对原始SQL语句进行解析,解析完以后获得一个LEX类型结构,是一个MySQL定义的类。 在加密阶段,对LEX内部的各个成员进行加密,并获得一个加密的LEX结构。 在恢复阶段,则需要将这个加密的LEX结构恢复成字符串类型,从而获得加密的SQL语句。

举例来说,对于一个SQL语句SELECT id from student, id 需要被加密。而这个语句解析成LEX 结构以后,id是在item_list成员里面,具体如下:


lex->select_lex.item_list

对这个item_list遍历,可以得到每个被选择的列对应的结构,是一个Item_field类型。其内部就包含了filed的名字,也就是id。加密过程,就是把这个内部的成员修改成自己想要的加密列的名字。完成LEX结构的加密以后,需要从LEX结构恢复成string类型的SQL语句,相关代码位于parser/stringify.hh。比如针对上面的SELECT语句,已经得到了加密以后的LEX结构,要重新得到字符串类型的SQL语句,可以通过如下的代码进行处理:

//代码来源 parser/stringfy.hh
//参数lex是加密以后的lex
static inline std::ostream&
operator<<(std::ostream &out, LEX &lex){
    String s;
    switch (lex.sql_command) {
    //对于select语句,直接调用一次函数就可以恢复SQL语句
    case SQLCOM_SELECT:
        //string类型的结果保存在ostream类型的变量out里面
        out << lex.unit;
        break;
    }
    ....
}

//该函数完成lex.unit到字符串的转化。
static inline std::ostream&
operator<<(std::ostream &out, SELECT_LEX_UNIT &select_lex_unit){
    String s;
    select_lex_unit.print(&s, QT_ORDINARY);
    return out << s;
}

所以, 加密过程其实就是SQL语句的解析,以及解析以后的类的成员的修改。要理解完整的SQL解析,加密,以及LEX结构恢复成字符串的代码流程,就需要了解MySQL的parser的内部类的含义,这些在后续的文章中会逐步介绍。

SQL解析与分类处理

在mysqlproxy/ConnectWrapper.cc的rewrite函数中完成了基本的SQL加密操作。其内部执行SQL加密的入口是:Rewriter::rewrite函数。该函数的作用是获得一个QueryRewrite类,这个类包含了改写以后的SQL语句,以及数据解密所需要的元信息。其部分代码如下:

QueryRewrite
Rewriter::rewrite(const std::string &q, const SchemaInfo &schema,
                  const std::string &default_db, const ProxyState &ps) {
     //辅助信息
    Analysis analysis(default_db, schema, ps.getMasterKey(),
                      ps.defaultSecurityRating());
    //SQL解析,加密,获得executor类型,在next阶段使用
    AbstractQueryExecutor *const executor =
        Rewriter::dispatchOnLex(analysis, q);
    if (!executor) {
        return QueryRewrite(true, analysis.rmeta, analysis.kill_zone,
                            new NoOpExecutor());
    }
    //QueryRewrite类包含所有必要信息
    return QueryRewrite(true, analysis.rmeta, analysis.kill_zone, executor);
}

首先是调用Rewriter::dispatchOnLex函数, 获得executor, 然后返回QueryRewrite类。其中executor中就保存了加密以后的SQL语句。在dispatchOnLex函数中,首先调用MySQL的parer对原始的SQL语句进行解析,获得LEX结构, 然后根据lex.sql_command把SQL语句分为三类:noRewrite,ddl以及dml。不同类型的sql语句分配不同的handler类型进行处理,并返回executor类型,具体如下:

  • 对于noRewrite类型, 直接返回SimpleExecutor类型,其作用是直接返回明文的SQL语句,不进行任何的SQL改写操作。
  • 对于DML以及DDL,则分别由不同的handler类型对SQL语句进行处理,并且返回对应的executor结构。

其简化的代码如下:

AbstractQueryExecutor *
Rewriter::dispatchOnLex(Analysis &a, const std::string &query) {
    //使用MySQL的parser进行SQL解析,获得LEX类型
    LEX *const lex = query_parse(query);
    //三种不同的处理
    if(noRewrite(*lex)){
        return new SimpleExecutor();
    }else if(dml_dispatcher->canDo(lex)){
        const SQLHandler &handler = dml_dispatcher->dispatch(lex);
        AbstractQueryExecutor executor = handler.transformLex(lex);
        return executor;
    }else if(ddl_dispatcher->canDo(lex)){
        const SQLHandler &handler = ddl_dispatcher->dispatch(lex);
        AbstractQueryExecutor executor = handler.transformLex(lex);
        return executor;
    }
}

可以看到,这里的加密处理涉及到dispatcher,handler,以及execotor。Dispatcher根据lex的结构,为SQL语句分配handler,不同类型的SQL语句有不同的handler,用来处理sql语句的加密。加密以后的结果则放置在executor中。dispatcher分配handler是基于map的查找来做的,其map初始化的代码如下:

SQLDispatcher *buildDMLDispatcher(){
    DMLHandler *h;
    SQLDispatcher *const dispatcher = new SQLDispatcher();
    h = new InsertHandler();
    dispatcher->addHandler(SQLCOM_INSERT, h);
    h = new InsertHandler();
    dispatcher->addHandler(SQLCOM_REPLACE, h);
    h = new UpdateHandler;
    dispatcher->addHandler(SQLCOM_UPDATE, h);
    h = new DeleteHandler;
    dispatcher->addHandler(SQLCOM_DELETE, h);
    h = new MultiDeleteHandler;
    dispatcher->addHandler(SQLCOM_DELETE_MULTI, h);
    h = new SelectHandler;
    dispatcher->addHandler(SQLCOM_SELECT, h);
    h = new SetHandler;
    dispatcher->addHandler(SQLCOM_SET_OPTION, h);
    h = new ShowTablesHandlers;
    dispatcher->addHandler(SQLCOM_SHOW_TABLES, h);
    h = new ShowCreateTableHandler;
    dispatcher->addHandler(SQLCOM_SHOW_CREATE,h);
    return dispatcher;
}

SQLDispatcher *buildDDLDispatcher(){
    DDLHandler *h;
    SQLDispatcher *dispatcher = new SQLDispatcher();
    h = new CreateTableHandler();
    dispatcher->addHandler(SQLCOM_CREATE_TABLE, h);
    h = new AlterTableHandler();
    dispatcher->addHandler(SQLCOM_ALTER_TABLE, h);
    h = new DropTableHandler();
    dispatcher->addHandler(SQLCOM_DROP_TABLE, h);
    h = new CreateDBHandler();
    dispatcher->addHandler(SQLCOM_CREATE_DB, h);
    h = new ChangeDBHandler();
    dispatcher->addHandler(SQLCOM_CHANGE_DB, h);
    h = new DropDBHandler();
    dispatcher->addHandler(SQLCOM_DROP_DB, h);
    h = new LockTablesHandler();
    dispatcher->addHandler(SQLCOM_LOCK_TABLES, h);
    h = new CreateIndexHandler();
    dispatcher->addHandler(SQLCOM_CREATE_INDEX, h);
    return dispatcher;
}
//初始化两个全局的dispatcher,内部通过map来保存handler结构
const std::unique_ptr Rewriter::dml_dispatcher =
    std::unique_ptr(buildDMLDispatcher());
const std::unique_ptr Rewriter::ddl_dispatcher =
    std::unique_ptr(buildDDLDispatcher());
    

在实际的代码中,上面的handler处理过程里还包含通过异常处理来调整洋葱层次的代码,这里暂时不做介绍。下面主要关注hander类以及executor类的组织方式以及特点。

三类handler

我们已经知道了通过不同的handler类,可以处理SQL语句,完成加密的操作。这里,首先给出Handler类的层次结构。

CryptDB代码分析2-handler与executor_第1张图片
image

此外,对于ALTER TABLE语句,还有额外的subhandler:

image

AlterDispatcher *buildAlterSubDispatcher() {
    AlterDispatcher *dispatcher = new AlterDispatcher();
    AlterSubHandler *h;

    h = new AddColumnSubHandler();
    dispatcher->addHandler(ALTER_ADD_COLUMN, h);

    h = new DropColumnSubHandler();
    dispatcher->addHandler(ALTER_DROP_COLUMN, h);

    h = new ChangeColumnSubHandler();
    dispatcher->addHandler(ALTER_CHANGE_COLUMN, h);

    h = new ForeignKeySubHandler();
    dispatcher->addHandler(ALTER_FOREIGN_KEY, h);

    h = new AddIndexSubHandler();
    dispatcher->addHandler(ALTER_ADD_INDEX, h);

    h = new DropIndexSubHandler();
    dispatcher->addHandler(ALTER_DROP_INDEX, h);

    h = new DisableOrEnableKeys();
    dispatcher->addHandler(ALTER_KEYS_ONOFF, h);

    return dispatcher;
}

可以看到,不同类型的语句,有不同的handler来处理。这是因为,不同语句的处理流程是不一样的,我们分别来看如下三种handler的特点。

dml handler:

dml系列的handler类定义了如下的函数,其中gather和rewrite函数用于加密处理:


class DMLHandler : public SQLHandler {
public:
    virtual AbstractQueryExecutor *
        transformLex(Analysis &a, LEX *lex) const;
private:
    virtual void gather(Analysis &a, LEX *lex) const = 0;
    virtual AbstractQueryExecutor * rewrite(Analysis &a, LEX *lex) const = 0;
protected:
    DMLHandler() {;}
    virtual ~DMLHandler() {;}
};

每个dml系列的handler都实现了自己的gather和rewrite函数,在gather阶段,对于每个要改写的单元,保存一个rewrite plain,里面记录了这个基本单元可以用什么方式来进行加密。然后在rewrite阶段,使用这些rewrite plain,对解析以后的Lex结构中的基本单元做加密,从而完成加密功能。

ddl handler:

ddl 系列的handler类定义了如下的函数,用于加密处理。


virtual AbstractQueryExecutor *
        rewriteAndUpdate(LEX *lex) const = 0;
                

ddl系列类型的典型处理流程包含了两部分,一是对SQL语句进行加密,二是以delta类来基于数据库的变化,这个delta在next阶段会写入到本地的embedded数据库中。例如CREATE TABLE语句,delta需要记录添加的表的名字, 表有多少列,每列分别采用什么样的加密算法。这些功能全都实现在rewriteAndUpdate函数中了。对于DML来说,由于不会对表结构产生影响,就不需要delta做记录。

Alter table handler

对于ALTER TABLE语句,由于其可能包含多个不同的子命令,所以创建了很多的不同的subhandler来进行处理,其调用subhandler的相关代码如下。


virtual AbstractQueryExecutor *
        rewriteAndUpdate(Analysis &a, LEX *lex, const Preamble &pre) const {
        //获取多个subhandler
        const std::vector &handlers =
            sub_dispatcher->dispatch(lex);
        assert(handlers.size() > 0);
    // 使用多个subhandler对LEX进行加密处理
        LEX *new_lex ;
        for (auto it : handlers) {
            new_lex = it->transformLex(a, new_lex);
        }
}

可以看到,对于一个ALTER TABLE语句,存在多个subhandler的情况。这个信息保存在解析出来LEX结构的alter_info成员里面,通过位操作的方式,获得多个subhandler,分别进行加密处理。 部分代码如下:

//flags的可能值如下
lex->alter_info.flags;

#define ALTER_ADD_COLUMN        (1L << 0)
#define ALTER_DROP_COLUMN       (1L << 1)
#define ALTER_CHANGE_COLUMN     (1L << 2)
#define ALTER_ADD_INDEX         (1L << 3)
#define ALTER_DROP_INDEX        (1L << 4)
#define ALTER_RENAME            (1L << 5)
#define ALTER_ORDER             (1L << 6)
#define ALTER_OPTIONS           (1L << 7)
#define ALTER_CHANGE_COLUMN_DEFAULT (1L << 8)
#define ALTER_KEYS_ONOFF        (1L << 9)
#define ALTER_CONVERT           (1L << 10)
#define ALTER_RECREATE          (1L << 11)
#define ALTER_ADD_PARTITION     (1L << 12)
#define ALTER_DROP_PARTITION    (1L << 13)
#define ALTER_COALESCE_PARTITION (1L << 14)
#define ALTER_REORGANIZE_PARTITION (1L << 15)
#define ALTER_PARTITION          (1L << 16)
#define ALTER_ADMIN_PARTITION    (1L << 17)
#define ALTER_TABLE_REORG        (1L << 18)
#define ALTER_REBUILD_PARTITION  (1L << 19)
#define ALTER_ALL_PARTITION      (1L << 20)
#define ALTER_REMOVE_PARTITIONING (1L << 21)
#define ALTER_FOREIGN_KEY        (1L << 22)
#define ALTER_TRUNCATE_PARTITION (1L << 23)

从handler到executor

executor 类型包含了很多的功能,其类型层次结构如下图所示。

CryptDB代码分析2-handler与executor_第2张图片
image

其中主要的函数是:nextImpl。该函数一般基于boost 的coroutine机制来实现,将一个完整的功能分成几个部分。该函数在mysqlproxy/ConnectWrapper.cc 中的next函数里被调用。举例来说,对于SELECT语句,在mysqlproxy/ConnectWrapper.cc的rewrite阶段,得到的是DMLQueryExecutor。在mysqlproxy/ConnectWrapper.cc的next函数中,调用这个DMLQueryExecutor的nextImpl函数。第一次进入该函数的时候,返回加密以后的SQL。这个SQL传递给lua脚本,然后转发给MySQL处理并且获得加密以后的结果。在第二次进入nextImpl函数的时候,会对返回的加密结果进行解密,并将解密的结果返回给lua脚本,转发给客户端。这样,executor的函数执行,就可以和“CryptDB代码分析1-lua与加密库”中的介绍联系起来了。

一个非常简单的例子

介绍完了原理,现在给出一个非常简单的SQL语句加密的例子,将之前介绍的内容串联起来。我们考虑SHOW TABLES这个命令,该命令会获取当前db总的表名,由于表名是加密过的,所以mysql-proxy还是对表名进行解密才可以将结果返回给客户端。

首先,客户端发送SHOW TABLES命令的时候,被mysql-proxy接收到,并且调用mysqlproxy/ConnectWrapper.cc中的rewrite函数,其内部进入Rewriter::dispatchOnLex函数,首先调用parser进行解析,获得了LEX类型。然后进行判断。

if (noRewrite(*lex)) {


} else if (dml_dispatcher->canDo(lex)) {
     const SQLHandler &handler = dml_dispatcher->dispatch(lex);
     AbstractQueryExecutor * executor = handler.transformLex(a, lex);
} else if (ddl_dispatcher->canDo(lex)) {

}

由于是dml语句,所以进入了第二个分支,得到了一个DMLHandler,并且调用transformLex函数。在transformlext函数内部,包含了gather和rewrite两个函数,首先获得rewrite plain,然后根据rewrite plain多lex的内部成员进行改写,最后返回的executor,这里的executor类型是ShowTableExecutor。

class ShowTablesHandlers : public DMLHandler {
    virtual void gather(Analysis &a, LEX *const lex) const
    {
    }

    virtual AbstractQueryExecutor *rewrite(Analysis &a, LEX *lex) const
    {
        return new ShowTablesExecutor();
    }
};

这样,rewrite阶段完成。在ShowTableExecutor中保存了加密以后的SQL语句。需要主要的是,由于show tables语句比较简单,没有加密,gather阶段没有获取任何的rewrite plain,rewrite阶段自然也就没有多lex的内部成员进行修改,所以加密以后的SQL语句依然是SHOW TABLES

然后到了next阶段,需要执行ShowTableExecutor中的nextImpl函数了,其实现如下所示:

ShowTablesExecutor::
nextImpl(const ResType &res, const NextParams &nparams)
{
    reenter(this->corot) {
        //返回加密以后的SQL语句,在这里是 SHOW TABLES
        yield return CR_QUERY_AGAIN(nparams.original_query);
        yield {
            const std::shared_ptr &schema =
                nparams.ps.getSchemaInfo();
            const DatabaseMeta *const dm =
                schema->getChild(IdentityMetaKey(nparams.default_db));
            TEST_ErrPkt(dm, "failed to find the database '"
                            + nparams.default_db + "'");
            std::vector > new_rows;
            for (const auto &it : res.rows) {
                assert(1 == it.size());
                for (const auto &table : dm->getChildren()) {
                    assert(table.second);
                    if (table.second->getAnonTableName()
                        == ItemToString(*it.front())) {

                        const IdentityMetaKey &plain_table_name
                            = dm->getKey(*table.second.get());
                        new_rows.push_back(std::vector
                            {make_item_string(plain_table_name.getValue())});
                    }
                }
            }
            //返回加密以后的表名
            return CR_RESULTS(ResType(res, new_rows));
        }
    }
}

第一次调用的时候,返回了加密以后的命令,进入之前介绍的mysqlproxy/ConnectWrapper.cc中的next函数中的QUERY_COME_AGAIN分支,这个SQL语句传递给lua脚本,并转发给MySQL执行,然后返回加密的结果给mysql-proxy。之后会再次调用next函数,进入到ShowTablesExecutor::nextImpl函数,执行第二个return,返回解密以后的表名,并且进入RESULTS分支,这次就可以将解密以后的表名字返回给客户端,这样该SQL语句的执行就结束了。关于带rewrite plain的操作过程,涉及到更多复杂的处理,将在后续文章中给出。

总结

之前已经介绍过,在CryptDB的模型下,一个SQL语句通过mysqlproxy的lua脚本进行处理,其主要的处理函数是rewrite和next。rewrite被调用一次,用于SQL语句的加密,next则会被多次调用,和mysql-proxy交互,来完成数据解密等功能。本文首先介绍了rewrite阶段。 该阶段通过SQL解析获得LEX结构进行加密,然后将LEX结构恢复成字符串。由于不同类型的SQL有不同的加密方法,需要使用dispatcher类为其分配不同的handler。rewrite阶段结束以后,得到了executor类,其中就包含了加密以后的SQL以及其他相关的信息。executor类型的nextImpl函数在next阶段被调用,分阶段完成返回加密的SQL,数据解密等功能。

相关文献

正在开发新功能的CryptDB分支: https://github.com/yiwenshao/Practical-Cryptdb
https://yiwenshao.github.io/2018/02/26/CryptDB代码分析1-lua与加密库/

原始链接:yiwenshao.github.io/2018/03/06/CryptDB代码分析2-handler与executor/

文章作者:Yiwen Shao

许可协议: Attribution-NonCommercial 4.0

转载请保留以上信息, 谢谢!

你可能感兴趣的:(CryptDB代码分析2-handler与executor)