-module(my_packet). -author('Manuel Rubio '). -author('Max Lapshin '). -export([encode/1, decode/1, decode_auth/1]). -include("myproto.hrl"). encode(#response{ status=?STATUS_EOF, id=Id, warnings=Warnings, status_flags=StatusFlags }) when Warnings == 0 andalso StatusFlags == 0 -> <<1:24/little, Id:8, ?STATUS_EOF:8>>; encode(#response{ status=?STATUS_EOF, id=Id, warnings=Warnings, status_flags=StatusFlags }) -> <<5:24/little, Id:8, ?STATUS_EOF:8, Warnings:16/little, StatusFlags:16/little>>; encode(#response{ status=?STATUS_ERR, id=Id, error_code=Error, error_info = Code, info = Info }) when Code =/= <<"">> -> Length = byte_size(Info) + 9, <>; encode(#response{ status=?STATUS_ERR, id=Id, error_code=Error, info = Info }) -> Length = byte_size(Info) + 3, <>; encode(#response{ status=?STATUS_OK, id=Id, info={Cols} }) -> %% columns {IdEof, ColsBin} = encode_column(Cols, Id), %% eof ColsEof = encode(#response{ status=?STATUS_EOF, id=IdEof, status_flags=?SERVER_STATUS_AUTOCOMMIT }), <>; encode(#response{ status=?STATUS_OK, id=Id, info={Cols, Rows} }) -> %% Column account ColLen = length(Cols), Head = <<1:24/little, Id:8, ColLen:8>>, %% columns {IdEof, ColsBin} = encode_column(Cols, Id+1), %% eof ColsEof = encode(#response{ status=?STATUS_EOF, id=IdEof, status_flags=?SERVER_STATUS_AUTOCOMMIT }), %% rows {IdEnd, RowsPack} = encode_rows(Rows, Cols, IdEof+1), %% eof RowsEof = encode(#response{ status=?STATUS_EOF, id=IdEnd, status_flags=?SERVER_STATUS_AUTOCOMMIT }), <>; % response for prepare statement encode(#response{status = ?STATUS_OK, id = Id, info = {prepare_statement_response, StatementId, WarningCount, Cols, Params}}) -> NumColumns = length(Cols), NumParams = length(Params), FirstPacketLength = 12, FirstPacket = <>, %% params {IdEof, ParamsPack, ParamsEof} = case NumParams of V2 when V2 > 0 -> {Id2, Bin2} = encode_column(Params, Id+1), %% eof Eof2 = encode(#response{status = ?STATUS_EOF, id = Id2, status_flags = ?SERVER_STATUS_AUTOCOMMIT}), {Id2, Bin2, Eof2}; _ -> {Id, <<>>, <<>>} end, %% Column account {_IdEnd, ColsBin, ColsEof} = case NumColumns of V1 when V1 > 0 -> %% columns {Id1, Bin1} = encode_column(Cols, IdEof+1), %% eof Eof1 = encode(#response{status = ?STATUS_EOF, id = Id1, status_flags = ?SERVER_STATUS_AUTOCOMMIT}), {Id1, Bin1, Eof1}; _ -> {IdEof, <<>>, <<>>} end, <>; encode(#response{status = ?STATUS_OK, id = Id, info = Info, affected_rows = AffectedRows, last_insert_id = LastInsertId, status_flags = StatusFlags, warnings = Warnings}) -> BinAffectedRows = my_datatypes:number_to_var_integer(AffectedRows), BinLastInsertId = my_datatypes:number_to_var_integer(LastInsertId), Length = byte_size(BinAffectedRows) + byte_size(BinLastInsertId) + byte_size(Info) + 5, <>; encode(#response{status = ?STATUS_HELLO, id = Id, info = Hash}) -> 20 == size(Hash) orelse error({invalid_hash_size, size(Hash), need, 20}), ServerSign = case application:get_env(myproto, server_sign) of {ok, SS} when is_binary(SS) -> SS; {ok, SS} when is_list(SS) -> list_to_binary(SS); undefined -> ?SERVER_SIGN end, Caps = ?CLIENT_PLUGIN_AUTH bor %% PLAIN AUTH ?CLIENT_PROTOCOL_41 bor %% PROTOCOL 4.1 ?CLIENT_SECURE_CONNECTION bor %% for mysql_native_password ?CLIENT_TRANSACTIONS bor ?CLIENT_CONNECT_WITH_DB bor 0, <> = <>, <> = Hash, LenAuth = 21, StatusFlags = ?SERVER_STATUS_AUTOCOMMIT bor 0, Charset = ?UTF8_GENERAL_CI, Info = <>, Length = byte_size(Info) + 1, <>. encode_column(Cols, Id) when is_list(Cols) -> lists:foldl(fun(Col, {NewId, Data}) -> BinCol = encode_column(Col, NewId), {NewId+1, <>} end, {Id, <<"">>}, Cols); encode_column(#column{schema = Schema, table = Table, name = Name, charset = Charset, length = L, type = Type, flags = Flags, decimals = Decimals, org_name = ON}, Id) when is_binary(Schema), is_binary(Table), is_binary(Name), is_integer(Charset), is_integer(Type), is_integer(Flags), is_integer(Decimals) -> SchemaLen = my_datatypes:number_to_var_integer(byte_size(Schema)), TableLen = my_datatypes:number_to_var_integer(byte_size(Table)), NameLen = my_datatypes:number_to_var_integer(byte_size(Name)), {OrgNameLen, OrgName} = case ON of undefined -> {NameLen, bin_to_upper(Name)}; ON -> {my_datatypes:number_to_var_integer(byte_size(ON)), ON} end, Length = case {Type, L} of _ when is_integer(L) -> L; {?TYPE_DATETIME, undefined} -> 16#13; {?TYPE_LONGLONG, undefined} -> 16#15; {_, undefined} -> 0 end, Payload = <<3:8, "def", SchemaLen/binary, Schema/binary, TableLen/binary, Table/binary, % table TableLen/binary, Table/binary, % org_table NameLen/binary, Name/binary, % name OrgNameLen/binary, OrgName/binary, % org_name 16#0c:8, Charset:16/little, Length:32/little, Type:8, Flags:16/little, Decimals:8/little, 0:16/little>>, PayloadLen = byte_size(Payload), <>. encode_rows(Rows, Cols, Id) -> lists:foldl(fun(Values, {NewId, Data}) -> F = fun({#column{type = Type, name = Name}, Cell}, Binary) -> Cell1 = case Cell of undefined -> undefined; true -> <<"1">>; false -> <<"0">>; _ when (Type == ?TYPE_TINY orelse Type == ?TYPE_SHORT orelse Type == ?TYPE_LONG orelse Type == ?TYPE_LONGLONG orelse Type == ?TYPE_INT24 orelse Type == ?TYPE_YEAR) andalso is_integer(Cell) -> integer_to_binary(Cell); _ when is_binary(Cell) -> Cell; _ -> error({cannot_encode,Name,Type,Cell}) end, CellEnc = case Cell of undefined -> ?DATA_NULL; _ -> my_datatypes:binary_to_varchar(Cell1) end, <> end, Payload = lists:foldl(F, <<>>, lists:zip(Cols, Values)), Length = byte_size(Payload), {NewId + 1, <>} end, {Id, <<>>}, Rows). -spec decode_auth(binary()) -> {ok, user(), binary()} | {more, binary()}. decode_auth(<>) -> {ok, decode_auth0(Bin), Rest}; decode_auth(<>) -> {more, size(Bin) - Length}; decode_auth(<>) -> {more, 4 - size(Bin)}. -spec decode_auth0(Auth::binary()) -> request(). decode_auth0(<>) -> Caps = unpack_caps(CapsFlag), {User, Info1} = unpack_zero(Info0), {Password,Info2} = case proplists:get_value(auth_lenenc_client_data,Caps) of true -> my_datatypes:read_lenenc_string(Info1); _ -> case proplists:get_value(secure_connection, Caps) of true -> <> = Info1, {Pass, R}; false -> unpack_zero(Info1) end end, HasPluginAuth = proplists:get_value(plugin_auth, Caps), {DB, Info3} = case proplists:get_value(connect_with_db, Caps) of % For some strange reasons mysql 5.0.6 violates protocol and doesn't % send db name % http://dev.mysql.com/doc/internals/en/connection-phase-packets.html % #packet-Protocol::HandshakeResponse41 % so we write here a dirty hack for pymysql true when HasPluginAuth == undefined andalso size(Info2) > 0 -> unpack_zero(Info2); _ -> {undefined, Info2} end, {Plugin, _Info4} = case proplists:get_value(plugin_auth, Caps) of true -> unpack_zero(Info3); _ -> {undefined, Info3} end, UserData = #user{ name = User, password = Password, plugin = Plugin, capabilities = Caps, database = DB, charset = Charset }, #request{command=?COM_AUTH, info=UserData}. -spec unpack_zero(String::binary()) -> {B1::binary(), B2::binary()}. unpack_zero(String) -> [B1, B2] = binary:split(String, <<0>>), {B1, B2}. -spec unpack_caps(Flag::integer()) -> [atom()]. unpack_caps(Flag) -> Caps = [ {?CLIENT_LONG_PASSWORD,long_password}, {?CLIENT_FOUND_ROWS,found_rows}, {?CLIENT_LONG_FLAG, long_flag}, {?CLIENT_CONNECT_WITH_DB, connect_with_db}, {?CLIENT_NO_SCHEMA, no_schema}, {?CLIENT_COMPRESS, compress}, {?CLIENT_ODBC, odbc}, {?CLIENT_LOCAL_FILES, local_files}, {?CLIENT_IGNORE_SPACE, ignore_space}, {?CLIENT_PROTOCOL_41, protocol_41}, {?CLIENT_INTERACTIVE, interactive}, {?CLIENT_SSL, ssl}, {?CLIENT_IGNORE_SIGPIPE, ignore_sigpipe}, {?CLIENT_TRANSACTIONS, transactions}, {?CLIENT_SECURE_CONNECTION, secure_connection}, {?CLIENT_MULTI_STATEMENTS, multi_statements}, {?CLIENT_MULTI_RESULTS, multi_results}, {?CLIENT_PS_MULTI_RESULTS, ps_multi_results}, {?CLIENT_PLUGIN_AUTH, plugin_auth}, {?CLIENT_CONNECT_ATTRS, connect_attrs}, {?CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA, auth_lenenc_client_data} ], lists:flatmap(fun({I,Tag}) -> case Flag band I of 0 -> []; _ -> [Tag] end end, Caps). -spec decode(binary()) -> {ok, response(), binary()} | {more, binary()}. decode(<>) -> {ok, decode0(Length, Id, Bin), Rest}; decode(<>) -> {more, Length - size(Bin)}; decode(<>) -> {more, 4 - size(Bin)}. -spec decode0(Length::integer(), Id::binary(), Bin::binary()) -> request(). decode0(_, Id, <>) -> #request{command = ?COM_FIELD_LIST, id = Id, info = my_datatypes:string_nul_to_binary(Info)}; decode0(16#ffffff, Id, <>) -> #request{command = Command, id = Id, info = Info, continue = true}; decode0(_, Id, <>) -> #request{command = Command, id = Id, info = Info, continue = false}. -spec bin_to_upper(Lower::binary()) -> Upper::binary(). bin_to_upper(Lower) when is_binary(Lower) -> << <<( if X >= $a andalso X =< $z -> X - 32; true -> X end )/integer>> || <> <= Lower >>.