p1_mysql-1.0.12/0000755000232200023220000000000013605316475013720 5ustar debalancedebalancep1_mysql-1.0.12/README.md0000644000232200023220000000132713605316475015202 0ustar debalancedebalance# p1_mysql [![Build Status](https://travis-ci.org/processone/p1_mysql.svg?branch=master)](https://travis-ci.org/processone/p1_mysql) [![Coverage Status](https://coveralls.io/repos/processone/p1_mysql/badge.svg?branch=master&service=github)](https://coveralls.io/github/processone/p1_mysql?branch=master) [![Hex version](https://img.shields.io/hexpm/v/p1_mysql.svg "Hex version")](https://hex.pm/packages/p1_mysql) p1_mysql is a pure Erlang MySQL driver. ## Building MySQL driver can be build as follow: make It is a rebar-compatible OTP application. Alternatively, you can build it with rebar: rebar compile ## Development ### Test #### Unit test You can run eunit test with the command: $ rebar eunit p1_mysql-1.0.12/COPYING0000644000232200023220000000630013605316475014752 0ustar debalancedebalance/* * Copyright (c) 2001-2003 Kungliga Tekniska Högskolan * (Royal Institute of Technology, Stockholm, Sweden). * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * 3. Neither the name of the Institute nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE INSTITUTE AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE INSTITUTE OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. */ Copyright (c) 2004, Sektionen för IT och media, Stockholms universitet All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the author nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. p1_mysql-1.0.12/rebar.config.script0000644000232200023220000000765713605316475017524 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : rebar.config.script %%% Author : Mickael Remond %%% Purpose : Rebar build script. Compliant with rebar and rebar3. %%% Created : 24 Nov 2015 by Mickael Remond %%% %%% Copyright (C) 2002-2019 ProcessOne, SARL. All Rights Reserved. %%% %%% Licensed under the Apache License, Version 2.0 (the "License"); %%% you may not use this file except in compliance with the License. %%% You may obtain a copy of the License at %%% %%% http://www.apache.org/licenses/LICENSE-2.0 %%% %%% Unless required by applicable law or agreed to in writing, software %%% distributed under the License is distributed on an "AS IS" BASIS, %%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %%% See the License for the specific language governing permissions and %%% limitations under the License. %%% %%%---------------------------------------------------------------------- SysVersion = lists:map(fun erlang:list_to_integer/1, string:tokens(erlang:system_info(version), ".")), IsRebar3 = case application:get_key(rebar, vsn) of {ok, VSN} -> [VSN1 | _] = string:tokens(VSN, "-"), [Maj|_] = string:tokens(VSN1, "."), (list_to_integer(Maj) >= 3); undefined -> lists:keymember(mix, 1, application:loaded_applications()) end, JobId = case os:getenv("TRAVIS_JOB_ID") of false -> ""; V -> V end, ModCfg0 = fun(F, Cfg, [Key|Tail], Op, Default) -> {OldVal,PartCfg} = case lists:keytake(Key, 1, Cfg) of {value, {_, V1}, V2} -> {V1, V2}; false -> {if Tail == [] -> Default; true -> [] end, Cfg} end, case Tail of [] -> [{Key, Op(OldVal)} | PartCfg]; _ -> [{Key, F(F, OldVal, Tail, Op, Default)} | PartCfg] end end, ModCfg = fun(Cfg, Keys, Op, Default) -> ModCfg0(ModCfg0, Cfg, Keys, Op, Default) end, ModCfgS = fun(Cfg, Keys, Val) -> ModCfg0(ModCfg0, Cfg, Keys, fun(_V) -> Val end, "") end, FilterConfig = fun(F, Cfg, [{Path, true, ModFun, Default} | Tail]) -> F(F, ModCfg0(ModCfg0, Cfg, Path, ModFun, Default), Tail); (F, Cfg, [_ | Tail]) -> F(F, Cfg, Tail); (F, Cfg, []) -> Cfg end, AppendStr = fun(Append) -> fun("") -> Append; (Val) -> Val ++ " " ++ Append end end, AppendList = fun(Append) -> fun(Val) -> Val ++ Append end end, Rebar3DepsFilter = fun(DepsList) -> lists:map(fun({DepName,_, {git,_, {tag,Version}}}) -> {DepName, Version}; (Dep) -> Dep end, DepsList) end, GlobalDepsFilter = fun(Deps) -> DepNames = lists:map(fun({DepName, _, _}) -> DepName; ({DepName, _}) -> DepName end, Deps), lists:filtermap(fun(Dep) -> case code:lib_dir(Dep) of {error, _} -> {true,"Unable to locate dep '"++atom_to_list(Dep)++"' in system deps."}; _ -> false end end, DepNames) end, Rules = [ {[deps], IsRebar3, Rebar3DepsFilter, []}, {[plugins], IsRebar3, AppendList([rebar3_hex, pc]), []}, {[deps], os:getenv("TRAVIS") == "true", AppendList([{coveralls, ".*", {git, "https://github.com/markusn/coveralls-erl.git", "master"}}]), []}, {[post_hooks], os:getenv("TRAVIS") == "true", AppendList([{eunit, "echo '\n%%! -pa .eunit/ deps/coveralls/ebin\nmain(_)->{ok,F}=file:open(\"erlang.json\",[write]),io:fwrite(F,\"~s\",[coveralls:convert_file(\".eunit/cover.coverdata\", \""++JobId++"\", \"travis-ci\",\"\")]).' > getcover.erl"}, {eunit, "escript ./getcover.erl"}]), []}, {[deps], os:getenv("USE_GLOBAL_DEPS") /= false, GlobalDepsFilter, []} ], Config = FilterConfig(FilterConfig, CONFIG, Rules), %io:format("Rules:~n~p~n~nCONFIG:~n~p~n~nConfig:~n~p~n", [Rules, CONFIG, Config]), Config. %% Local Variables: %% mode: erlang %% End: %% vim: set filetype=erlang tabstop=8: p1_mysql-1.0.12/CHANGELOG.md0000644000232200023220000000207213605316475015532 0ustar debalancedebalance# Version 1.0.12 * Properly handle decoding of number of returned fields when there is more than 128 of them. # Version 1.0.11 * Handle close even in do\_recv, this fixes potential connection being stuck after timeout # Version 1.0.10 * Make socket close always lead to terminating p1\_mysql\_conn # Version 1.0.9 * Add contribution guide * Don't log errors on shutdown # Version 1.0.8 * Add support for mysql8 and cache\_sha2\_password authentication # Version 1.0.7 * Fix connection timeout handling # Version 1.0.6 * Add support for ipv6 connections # Version 1.0.5 * Fix compilation with rebar3 # Version 1.0.4 * Update coverall script # Version 1.0.3 * Make it possible to set connect timeout # Version 1.0.2 * accepting query as iodata() on p1_mysql_conn:squery/4 function (Felipe Ripoll) # Version 1.0.1 * Repository is now called p1_mysql for consistency (Mickaël Rémond) * Initial release on Hex.pm (Mickaël Rémond) * Standard ProcessOne build chain (Mickaël Rémond) * Setup Travis-CI and test coverage, tests still needed (Mickaël Rémond) p1_mysql-1.0.12/Makefile0000644000232200023220000000011313605316475015353 0ustar debalancedebalanceall: src src: rebar compile xref clean: rebar clean .PHONY: clean src p1_mysql-1.0.12/src/0000755000232200023220000000000013605316475014507 5ustar debalancedebalancep1_mysql-1.0.12/src/p1_mysql_auth.erl0000644000232200023220000002462213605316475020007 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% File : p1_mysql_auth.erl %%% Author : Fredrik Thulin %%% Descrip.: MySQL client authentication functions. %%% Created : 4 Aug 2005 by Fredrik Thulin %%% %%% Note : All MySQL code was written by Magnus Ahltorp, originally %%% in the file p1_mysql.erl - I just moved it here. %%% %%% Copyright (c) 2001-2004 Kungliga Tekniska Högskolan %%% See the file COPYING %%% %%%------------------------------------------------------------------- -module(p1_mysql_auth). %%-------------------------------------------------------------------- %% External exports (should only be used by the 'p1_mysql_conn' module) %%-------------------------------------------------------------------- -export([do_auth/9, password_sha2/2]). -include("p1_mysql_consts.hrl"). %%-------------------------------------------------------------------- %% Macros %%-------------------------------------------------------------------- -define(LONG_PASSWORD, 1). -define(FOUND_ROWS, 2). -define(LONG_FLAG, 4). -define(PROTOCOL_41, 512). -define(TRANSACTIONS, 8192). -define(SECURE_CONNECTION, 32768). -define(CONNECT_WITH_DB, 8). -define(MAX_PACKET_SIZE, 1000000). %%==================================================================== %% External functions %%==================================================================== %%-------------------------------------------------------------------- %% Function: do_auth(Type, Sock, RecvPid, SeqNum, User, Password, Salt1, %% Salt2, LogFun) %% Type = string(), authentication method offered by server %% Sock = term(), gen_tcp socket %% RecvPid = pid(), receiver process pid %% SeqNum = integer(), first sequence number we should use %% User = string(), MySQL username %% Password = string(), MySQL password %% Salt = string(), salt from server greeting %% Caps = integer(), server capabilities %% LogFun = undefined | function() of arity 3 %% Descrip.: Perform MySQL authentication. %% Returns : result of p1_mysql_conn:do_recv/3 %%-------------------------------------------------------------------- do_auth("old_auth", Sock, RecvPid, SeqNum, User, Password, Salt, _Caps, LogFun) -> Auth = password_old(Password, Salt), Packet2 = make_auth(User, Auth), do_send(Sock, Packet2, SeqNum, LogFun), p1_mysql_conn:do_recv(LogFun, RecvPid, SeqNum); do_auth("mysql_native_password", Sock, RecvPid, SeqNum, User, Password, Salt, Caps, LogFun) when Caps band ?CLIENT_PLUGIN_AUTH == 0 -> Auth = password_new(Password, Salt), Packet2 = make_new_auth(User, Auth, none, ""), do_send(Sock, Packet2, SeqNum, LogFun), case p1_mysql_conn:do_recv(LogFun, RecvPid, SeqNum) of {ok, Packet3, SeqNum2} -> case Packet3 of <<254:8>> -> AuthOld = password_old(Password, string:substr(Salt, 1, 8)), do_send(Sock, <>, SeqNum2 + 1, LogFun), p1_mysql_conn:do_recv(LogFun, RecvPid, SeqNum2 + 1); _ -> {ok, Packet3, SeqNum2} end; {error, Reason} -> {error, Reason} end; do_auth(Type, Sock, RecvPid, SeqNum, User, Password, Salt, Caps, LogFun) when Caps band ?CLIENT_PLUGIN_AUTH /= 0 andalso (Type == "mysql_native_password" orelse Type == "caching_sha2_password") -> Auth = case Type of "mysql_native_password" -> password_new(Password, Salt); _ -> password_sha2(Password, Salt) end, Packet2 = make_new_auth(User, Auth, none, Type), do_send(Sock, Packet2, SeqNum, LogFun), case p1_mysql_conn:do_recv(LogFun, RecvPid, SeqNum) of {ok, Packet3, SeqNum2} -> case Packet3 of <<254:8, Rest/binary>> -> {TypeNew, SaltNew} = p1_mysql:asciz_binary(Rest, []), Len = size(SaltNew) - 1, <> = SaltNew, p1_mysql:log(LogFun, debug, "p1_mysql_auth: do_auth: " "Protocol change ~p ~p", [TypeNew, SaltNew2]), do_auth_switch(TypeNew, Sock, RecvPid, SeqNum2 + 1, Password, SaltNew2, LogFun); <<1:8, 4:8>> -> do_publickey_auth(Sock, RecvPid, SeqNum2 + 1, Password, Salt, LogFun); <<1:8, 3:8>> -> p1_mysql_conn:do_recv(LogFun, RecvPid, SeqNum2); _ -> {ok, Packet3, SeqNum2} end; {error, Reason} -> {error, Reason} end; do_auth(Type, _Sock, _RecvPid, _SeqNum, _User, _Password, _Salt, _Caps, LogFun) -> p1_mysql:log(LogFun, error, "p1_mysql_auth: do_auth: " "Unknown authentication method ~s", [Type]), Err = lists:flatten(io_lib:format("p1_mysql_auth: Unknown " "authentication method ~s", [Type])), {error, Err}. do_auth_switch("mysql_native_password", Sock, RecvPid, SeqNum, Password, Salt, LogFun) -> do_send(Sock, password_new(Password, Salt), SeqNum, LogFun), p1_mysql_conn:do_recv(LogFun, RecvPid, SeqNum); do_auth_switch("caching_sha2_password", Sock, RecvPid, SeqNum, Password, Salt, LogFun) -> do_send(Sock, password_sha2(Password, Salt), SeqNum, LogFun), p1_mysql_conn:do_recv(LogFun, RecvPid, SeqNum); do_auth_switch(Type, _Sock, _RecvPid, _SeqNum, _Password, _Salt, LogFun) -> p1_mysql:log(LogFun, error, "p1_mysql_auth: do_auth_switch: " "Unknown authentication method ~s", [Type]), Err = lists:flatten(io_lib:format("p1_mysql_auth: Server request switch to unknown " "authentication method ~s", [Type])), {error, Err}. do_publickey_auth(Sock, RecvPid, SeqNum, Password, Salt, LogFun) -> do_send(Sock, <<2:8>>, SeqNum, LogFun), case p1_mysql_conn:do_recv(LogFun, RecvPid, SeqNum) of {ok, <<1:8, PublicKey/binary>>, SeqNum2} -> case public_key:pem_decode(PublicKey) of [{'SubjectPublicKeyInfo', _, _} = KeyInfo | _] -> Key = public_key:pem_entry_decode(KeyInfo), PassB = <<(iolist_to_binary(Password))/binary, 0:8>>, PLen = size(PassB), PLenBits = PLen*8, SaltB = repeat_bin(iolist_to_binary(Salt), PLen), <> = PassB, <> = SaltB, Xor = <<(PassN bxor SaltN):PLenBits>>, Encrypted = public_key:encrypt_public(Xor, Key, [{rsa_pad, rsa_pkcs1_oaep_padding}]), do_send(Sock, Encrypted, SeqNum2+1, LogFun), p1_mysql_conn:do_recv(LogFun, RecvPid, SeqNum2+1); _ -> p1_mysql:log(LogFun, error, "p1_mysql_auth: do_publickey_auth: " "Can't decode public key", []), {error, "p1_mysql_auth: do_publickey_auth: Can't decode public key"} end; {ok, PacketUnk, _} -> p1_mysql:log(LogFun, error, "p1_mysql_auth: do_publickey_auth: " "Unknown response to public key request ~p", [PacketUnk]), {error, "p1_mysql_auth: Unknown response to public key request"}; {error, Err} -> p1_mysql:log(LogFun, error, "p1_mysql_auth: do_publickey_auth: " "Error response to public key request ~p", [Err]), {error, Err} end. repeat_bin(Bin, Len) when size(Bin) >= Len -> binary:part(Bin, 0, Len); repeat_bin(Bin, Len) when size(Bin) < Len -> repeat_bin(<>, Len). %%==================================================================== %% Internal functions %%==================================================================== password_old(Password, Salt) -> {P1, P2} = hash(Password), {S1, S2} = hash(Salt), Seed1 = P1 bxor S1, Seed2 = P2 bxor S2, List = rnd(9, Seed1, Seed2), {L, [Extra]} = lists:split(8, List), list_to_binary(lists:map(fun (E) -> E bxor (Extra - 64) end, L)). %% part of do_old_auth/4, which is part of mysql_init/4 make_auth(User, Password) -> Caps = ?LONG_PASSWORD bor ?LONG_FLAG bor ?TRANSACTIONS bor ?FOUND_ROWS, Maxsize = 0, UserB = list_to_binary(User), PasswordB = Password, <>. %% part of do_new_auth/4, which is part of mysql_init/4 make_new_auth(User, Password, Database, AuthMethod) -> {DBCaps, DatabaseB} = case Database of none -> {0, <<>>}; _ -> {?CLIENT_CONNECT_WITH_DB, <<(list_to_binary(Database))/binary, 0>>} end, {AuthCaps, AuthB} = case AuthMethod of "" -> {0, <<>>}; _ -> {?CLIENT_PLUGIN_AUTH, <<(list_to_binary(AuthMethod))/binary, 0>>} end, Caps = ?CLIENT_LONG_PASSWORD bor ?CLIENT_LONG_FLAG bor ?CLIENT_TRANSACTIONS bor ?CLIENT_PROTOCOL_41 bor ?CLIENT_FOUND_ROWS bor ?CLIENT_RESERVED2 bor DBCaps bor AuthCaps, Maxsize = ?MAX_PACKET_SIZE, UserB = list_to_binary(User), PasswordL = size(Password), <>. hash(S) -> hash(S, 1345345333, 305419889, 7). hash([C | S], N1, N2, Add) -> N1_1 = N1 bxor (((N1 band 63) + Add) * C + N1 * 256), N2_1 = N2 + ((N2 * 256) bxor N1_1), Add_1 = Add + C, hash(S, N1_1, N2_1, Add_1); hash([], N1, N2, _Add) -> Mask = (1 bsl 31) - 1, {N1 band Mask , N2 band Mask}. rnd(N, Seed1, Seed2) -> Mod = (1 bsl 30) - 1, rnd(N, [], Seed1 rem Mod, Seed2 rem Mod). rnd(0, List, _, _) -> lists:reverse(List); rnd(N, List, Seed1, Seed2) -> Mod = (1 bsl 30) - 1, NSeed1 = (Seed1 * 3 + Seed2) rem Mod, NSeed2 = (NSeed1 + Seed2 + 33) rem Mod, Float = (float(NSeed1) / float(Mod))*31, Val = trunc(Float)+64, rnd(N - 1, [Val | List], NSeed1, NSeed2). dualmap(_F, [], []) -> []; dualmap(F, [E1 | R1], [E2 | R2]) -> [F(E1, E2) | dualmap(F, R1, R2)]. bxor_binary(B1, B2) -> list_to_binary(dualmap(fun (E1, E2) -> E1 bxor E2 end, binary_to_list(B1), binary_to_list(B2))). password_new(Password, Salt) -> Stage1 = crypto:hash(sha, Password), Stage2 = crypto:hash(sha, Stage1), Res = crypto:hash_final( crypto:hash_update( crypto:hash_update(crypto:hash_init(sha), Salt), Stage2) ), bxor_binary(Res, Stage1). password_sha2(Password, Salt) -> PasswordB = <<(iolist_to_binary(Password))/binary>>, Stage1 = crypto:hash(sha256, PasswordB), Stage2 = crypto:hash(sha256, Stage1), Stage3 = crypto:hash(sha256, <>), <> = Stage1, <> = Stage3, <<(Stage1N bxor Stage3N):256>>. do_send(Sock, Packet, Num, LogFun) -> p1_mysql:log(LogFun, debug, "p1_mysql_auth send packet ~p: ~p", [Num, Packet]), Data = <<(size(Packet)):24/little, Num:8, Packet/binary>>, gen_tcp:send(Sock, Data). p1_mysql-1.0.12/src/p1_mysql_consts.hrl0000644000232200023220000000216213605316475020355 0ustar debalancedebalance-define(CLIENT_LONG_PASSWORD, 1). -define(CLIENT_FOUND_ROWS, 2). -define(CLIENT_LONG_FLAG, 4). -define(CLIENT_CONNECT_WITH_DB, 8). -define(CLIENT_NO_SCHEMA, 16). -define(CLIENT_COMPRESS, 32). -define(CLIENT_ODBC, 64). -define(CLIENT_LOCAL_FILES, 128). -define(CLIENT_IGNORE_SPACE, 256). -define(CLIENT_PROTOCOL_41, 512). -define(CLIENT_INTERACTIVE, 1024). -define(CLIENT_SSL, 2048). -define(CLIENT_IGNORE_SIGPIPE, 4096). -define(CLIENT_TRANSACTIONS, 8192). -define(CLIENT_RESERVED, 16384). -define(CLIENT_RESERVED2, 32768). % Old flag for 41 secure connection -define(CLIENT_MULTI_STATEMENTS, (1 bsl 16)). -define(CLIENT_MULTI_RESULTS, (1 bsl 17)). -define(CLIENT_PS_MULTI_RESULTS, (1 bsl 18)). -define(CLIENT_PLUGIN_AUTH, (1 bsl 19)). -define(CLIENT_CONNECT_ATTRS, (1 bsl 20)). -define(CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA, (1 bsl 21)). -define(CLIENT_CAN_HANDLE_EXPIRED_PASSWORDS, (1 bsl 22)). -define(CLIENT_SESSION_TRACK, (1 bsl 23)). -define(CLIENT_DEPRECATE_EOF, (1 bsl 24)). -define(CLIENT_SSL_VERIFY_SERVER_CERT, (1 bsl 30)). -define(CLIENT_OPTIONAL_RESULTSET_METADATA, (1 bsl 25)). -define(CLIENT_REMEMBER_OPTIONS, (1 bsl 31)). p1_mysql-1.0.12/src/p1_mysql.app.src0000644000232200023220000000265013605316475017547 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : p1_mysql.app.src %%% Author : Evgeniy Khramtsov %%% Purpose : Application package description %%% Created : 4 Apr 2013 by Evgeniy Khramtsov %%% %%% %%% Copyright (C) 2002-2019 ProcessOne, SARL. All Rights Reserved. %%% %%% Licensed under the Apache License, Version 2.0 (the "License"); %%% you may not use this file except in compliance with the License. %%% You may obtain a copy of the License at %%% %%% http://www.apache.org/licenses/LICENSE-2.0 %%% %%% Unless required by applicable law or agreed to in writing, software %%% distributed under the License is distributed on an "AS IS" BASIS, %%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %%% See the License for the specific language governing permissions and %%% limitations under the License. %%% %%%---------------------------------------------------------------------- {application, p1_mysql, [{description, "Pure Erlang MySQL driver"}, {vsn, "1.0.12"}, {modules, []}, {registered, []}, {applications, [kernel, stdlib]}, {mod, {p1_mysql_app,[]}}, %% hex.pm packaging: {licenses, ["BSD-3 Clause", "Apache 2.0"]}, {links, [{"Github", "https://github.com/processone/p1_mysql"}]}]}. %% Local Variables: %% mode: erlang %% End: %% vim: set filetype=erlang tabstop=8: p1_mysql-1.0.12/src/p1_mysql_conn.erl0000644000232200023220000007255113605316475020007 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% File : p1_mysql_conn.erl %%% Author : Fredrik Thulin %%% Descrip.: MySQL connection handler, handles de-framing of messages %%% received by the MySQL receiver process. %%% Created : 5 Aug 2005 by Fredrik Thulin %%% Modified: 11 Jan 2006 by Mickael Remond %%% %%% Note : All MySQL code was written by Magnus Ahltorp, originally %%% in the file p1_mysql.erl - I just moved it here. %%% %%% Copyright (c) 2001-2004 Kungliga Tekniska Högskolan %%% See the file COPYING %%% %%% %%% This module handles a single connection to a single MySQL server. %%% You can use it stand-alone, or through the 'p1_mysql' module if you %%% want to have more than one connection to the server, or %%% connections to different servers. %%% %%% To use it stand-alone, set up the connection with %%% %%% {ok, Pid} = p1_mysql_conn:start(Host, Port, User, Password, %%% Database, LogFun) %%% %%% Host = string() %%% Port = integer() %%% User = string() %%% Password = string() %%% Database = string() %%% LogFun = undefined | (gives logging to console) %%% function() of arity 3 (Level, Fmt, Args) %%% %%% Note: In stand-alone mode you have to start Erlang crypto application by %%% yourself with crypto:start() %%% %%% and then make MySQL querys with %%% %%% Result = p1_mysql_conn:fetch(Pid, Query, self()) %%% %%% Result = {data, MySQLRes} | %%% {updated, MySQLRes} | %%% {error, MySQLRes} %%% Where: MySQLRes = #p1_mysql_result %%% %%% Actual data can be extracted from MySQLRes by calling the following API %%% functions: %%% - on data received: %%% FieldInfo = p1_mysql:get_result_field_info(MysqlRes) %%% AllRows = p1_mysql:get_result_rows(MysqlRes) %%% with FieldInfo = list() of {Table, Field, Length, Name} %%% and AllRows = list() of list() representing records %%% - on update: %%% Affected= p1_mysql:get_result_affected_rows(MysqlRes) %%% with Affected = integer() %%% - on error: %%% Reason = p1_mysql:get_result_reason(MysqlRes) %%% with Reason = string() %%%------------------------------------------------------------------- -module(p1_mysql_conn). -define(CONNECT_TIMEOUT, 5000). %%-------------------------------------------------------------------- %% External exports %%-------------------------------------------------------------------- -export([start/6, start/7, start_link/6, start_link/7, fetch/3, fetch/4, squery/4, stop/1 ]). %%-------------------------------------------------------------------- %% External exports (should only be used by the 'p1_mysql_auth' module) %%-------------------------------------------------------------------- -export([do_recv/3 ]). -include("p1_mysql.hrl"). -include("p1_mysql_consts.hrl"). -record(state, { mysql_version, log_fun, recv_pid, socket, data }). -define(SECURE_CONNECTION, 32768). -define(MYSQL_QUERY_OP, 3). -define(DEFAULT_STANDALONE_TIMEOUT, 5000). -define(DEFAULT_RESULT_TYPE, list). -define(MYSQL_4_0, 40). %% Support for MySQL 4.0.x -define(MYSQL_4_1, 41). %% Support for MySQL 4.1.x et 5.0.x %%==================================================================== %% External functions %%==================================================================== %%-------------------------------------------------------------------- %% Function: start(Host, Port, User, Password, Database, LogFun) %% Function: start_link(Host, Port, User, Password, Database, LogFun) %% Host = string() %% Port = integer() %% User = string() %% Password = string() %% Database = string() %% LogFun = undefined | function() of arity 3 %% Descrip.: Starts a p1_mysql_conn process that connects to a MySQL %% server, logs in and chooses a database. %% Returns : {ok, Pid} | {error, Reason} %% Pid = pid() %% Reason = string() %%-------------------------------------------------------------------- start(Host, Port, User, Password, Database, LogFun) -> start(Host, Port, User, Password, Database, ?CONNECT_TIMEOUT, LogFun). start(Host, Port, User, Password, Database, ConnectTimeout, LogFun) when is_list(Host), is_integer(Port), is_list(User), is_list(Password), is_list(Database) -> ConnPid = self(), Pid = spawn(fun () -> init(Host, Port, User, Password, Database, ConnectTimeout, LogFun, ConnPid) end), post_start(Pid, ConnectTimeout, LogFun). start_link(Host, Port, User, Password, Database, LogFun) -> start_link(Host, Port, User, Password, Database, ?CONNECT_TIMEOUT, LogFun). start_link(Host, Port, User, Password, Database, ConnectTimeout, LogFun) when is_list(Host), is_integer(Port), is_list(User), is_list(Password), is_list(Database) -> ConnPid = self(), Pid = spawn_link(fun () -> init(Host, Port, User, Password, Database, ConnectTimeout, LogFun, ConnPid) end), post_start(Pid, ConnectTimeout, LogFun). %% part of start/6 or start_link/6: post_start(Pid, ConnectTimeout, _LogFun) -> %%Timeout = get_option(timeout, Options, ?DEFAULT_STANDALONE_TIMEOUT), %%TODO find a way to get configured Options here Timeout = if is_integer(ConnectTimeout) -> ConnectTimeout; true -> ?DEFAULT_STANDALONE_TIMEOUT end, receive {p1_mysql_conn, Pid, ok} -> {ok, Pid}; {p1_mysql_conn, Pid, {error, Reason}} -> p1_mysql:log(_LogFun, error, "p1_mysql_conn: post_start error ~p~n", [Reason]), stop(Pid), {error, Reason} % Unknown -> % p1_mysql:log(_LogFun, error, "p1_mysql_conn: Received unknown signal, exiting"), % p1_mysql:log(_LogFun, debug, "p1_mysql_conn: Unknown signal : ~p", [Unknown]), % {error, "unknown signal received"} after Timeout -> p1_mysql:log(_LogFun, error, "p1_mysql_conn: post_start timeout~n", []), stop(Pid), timer:sleep(100), catch exit(Pid, kill), {error, "timed out"} end. %%-------------------------------------------------------------------- %% Function: fetch(Pid, Query, From) %% fetch(Pid, Query, From, Timeout) %% Pid = pid(), p1_mysql_conn to send fetch-request to %% Query = string(), MySQL query in verbatim %% From = pid() or term(), use a From of self() when %% using this module for a single connection, %% or pass the gen_server:call/3 From argument if %% using a gen_server to do the querys (e.g. the %% mysql_dispatcher) %% Timeout = integer() | infinity, gen_server timeout value %% Descrip.: Send a query and wait for the result if running stand- %% alone (From = self()), but don't block the caller if we %% are not running stand-alone (From = gen_server From). %% Returns : ok | (non-stand-alone mode) %% {data, #p1_mysql_result} | (stand-alone mode) %% {updated, #p1_mysql_result} | (stand-alone mode) %% {error, #p1_mysql_result} (stand-alone mode) %% FieldInfo = term() %% Rows = list() of [string()] %% Reason = term() %%-------------------------------------------------------------------- fetch(Pid, Query, From) -> squery(Pid, Query, From, []). fetch(Pid, Query, From, Timeout) -> squery(Pid, Query, From, [{timeout, Timeout}]). squery(Pid, Query, From, Options) when is_pid(Pid), (is_list(Query) or is_binary(Query)) -> Self = self(), Timeout = get_option(timeout, Options, ?DEFAULT_STANDALONE_TIMEOUT), TRef = erlang:start_timer(Timeout, self(), timeout), Pid ! {fetch, TRef, Query, From, Options}, case From of Self -> %% We are not using a mysql_dispatcher, await the response wait_fetch_result(TRef, Pid); _ -> %% From is gen_server From, Pid will do gen_server:reply() %% when it has an answer ok end. wait_fetch_result(TRef, Pid) -> receive {fetch_result, TRef, Pid, Result} -> case erlang:cancel_timer(TRef) of false -> receive {timeout, TRef, _} -> ok after 0 -> ok end; _ -> ok end, Result; {fetch_result, _BadRef, Pid, _Result} -> wait_fetch_result(TRef, Pid); {timeout, TRef, _Info} -> stop(Pid), {error, #p1_mysql_result{error="query timed out"}} end. stop(Pid) -> Pid ! close. %%-------------------------------------------------------------------- %% Function: do_recv(LogFun, RecvPid, SeqNum) %% LogFun = undefined | function() with arity 3 %% RecvPid = pid(), p1_mysql_recv process %% SeqNum = undefined | integer() %% Descrip.: Wait for a frame decoded and sent to us by RecvPid. %% Either wait for a specific frame if SeqNum is an integer, %% or just any frame if SeqNum is undefined. %% Returns : {ok, Packet, Num} | %% {error, Reason} %% Reason = term() %% %% Note : Only to be used externally by the 'p1_mysql_auth' module. %%-------------------------------------------------------------------- do_recv(LogFun, RecvPid, SeqNum) when is_function(LogFun); LogFun == undefined, SeqNum == undefined -> receive {p1_mysql_recv, RecvPid, data, Packet, Num} -> %%p1_mysql:log(LogFun, debug, "p1_mysql_conn: recv packet ~p: %%~p", [Num, Packet]), {ok, Packet, Num}; {p1_mysql_recv, RecvPid, closed, _E} -> p1_mysql:log(LogFun, error, "p1_mysql_conn: p1_mysql_recv:" " socket was closed ~p~n", [{RecvPid, _E}]), {error, "p1_mysql_recv: socket was closed"}; close -> p1_mysql:log(LogFun, error, "p1_mysql_conn: p1_mysql_recv:" " received close~n", []), {error, "p1_mysql_recv: socket was closed"} end; do_recv(LogFun, RecvPid, SeqNum) when is_function(LogFun); LogFun == undefined, is_integer(SeqNum) -> ResponseNum = SeqNum + 1, receive {p1_mysql_recv, RecvPid, data, Packet, ResponseNum} -> %%p1_mysql:log(LogFun, debug, "p1_mysql_conn: recv packet ~p: %%~p", [ResponseNum, Packet]), {ok, Packet, ResponseNum}; {p1_mysql_recv, RecvPid, closed, _E} -> p1_mysql:log(LogFun, error, "p1_mysql_conn: p1_mysql_recv:" " socket was closed 2 ~p~n", [{RecvPid, _E}]), {error, "p1_mysql_recv: socket was closed"}; close -> p1_mysql:log(LogFun, error, "p1_mysql_conn: p1_mysql_recv:" " received close~n", []), {error, "p1_mysql_recv: socket was closed"} end. %%==================================================================== %% Internal functions %%==================================================================== %%-------------------------------------------------------------------- %% Function: init(Host, Port, User, Password, Database, LogFun, %% Parent) %% Host = string() %% Port = integer() %% User = string() %% Password = string() %% Database = string() %% LogFun = undefined | function() of arity 3 %% Parent = pid() of process starting this p1_mysql_conn %% Descrip.: Connect to a MySQL server, log in and chooses a database. %% Report result of this to Parent, and then enter loop() if %% we were successfull. %% Returns : void() | does not return %%-------------------------------------------------------------------- init(Host, Port, User, Password, Database, ConnectTimeout, LogFun, Parent) -> case p1_mysql_recv:start_link(Host, Port, ConnectTimeout, LogFun, self()) of {ok, RecvPid, Sock} -> case mysql_init(Sock, RecvPid, User, Password, LogFun) of {ok, Version} -> case do_query(Sock, RecvPid, LogFun, "use " ++ Database, Version, [{result_type, binary}]) of {error, MySQLRes} -> p1_mysql:log(LogFun, error, "p1_mysql_conn: Failed changing" " to database ~p : ~p", [Database, p1_mysql:get_result_reason(MySQLRes)]), gen_tcp:close(Sock), Parent ! {p1_mysql_conn, self(), {error, failed_changing_database}}; %% ResultType: data | updated {_ResultType, _MySQLRes} -> Parent ! {p1_mysql_conn, self(), ok}, State = #state{mysql_version=Version, recv_pid = RecvPid, socket = Sock, log_fun = LogFun, data = <<>> }, loop(State) end; {error, _Reason} -> Parent ! {p1_mysql_conn, self(), {error, login_failed}} end; E -> p1_mysql:log(LogFun, error, "p1_mysql_conn: " "Failed connecting to ~p:~p : ~p", [Host, Port, E]), Parent ! {p1_mysql_conn, self(), {error, connect_failed}} end. %%-------------------------------------------------------------------- %% Function: loop(State) %% State = state record() %% Descrip.: Wait for signals asking us to perform a MySQL query, or %% signals that the socket was closed. %% Returns : error | does not return %%-------------------------------------------------------------------- loop(State) -> RecvPid = State#state.recv_pid, receive {fetch, Ref, Query, GenSrvFrom, Options} -> %% GenSrvFrom is either a gen_server:call/3 From term(), %% or a pid if no gen_server was used to make the query Res = do_query(State, Query, Options), case is_pid(GenSrvFrom) of true -> %% The query was not sent using gen_server mechanisms GenSrvFrom ! {fetch_result, Ref, self(), Res}; false -> %% the timer is canceled in wait_fetch_result/2, but we wait on that funtion only if the query %% was not sent using the mysql gen_server. So we at least should try to cancel the timer here %% (no warranty, the gen_server can still receive timeout messages) erlang:cancel_timer(Ref), gen_server:reply(GenSrvFrom, Res) end, case Res of {error, #p1_mysql_result{error="p1_mysql_recv: socket was closed"}} -> p1_mysql:log(State#state.log_fun, error, "p1_mysql_conn: " "Connection closed, exiting.", []), close_connection(State); _ -> loop(State) end; {p1_mysql_recv, RecvPid, data, Packet, Num} -> p1_mysql:log(State#state.log_fun, error, "p1_mysql_conn: " "Received MySQL data when not expecting any " "(num ~p) - ignoring it", [Num]), p1_mysql:log(State#state.log_fun, error, "p1_mysql_conn: " "Unexpected MySQL data (num ~p) :~n~p", [Num, Packet]), loop(State); {p1_mysql_recv, RecvPid, closed, _Reason} -> p1_mysql:log(State#state.log_fun, error, "p1_mysql_conn: " "Connection closed, exiting.", []), close_connection(State); close -> p1_mysql:log(State#state.log_fun, info, "p1_mysql_conn: " "Received close signal, exiting.", []), close_connection(State); Unknown -> p1_mysql:log(State#state.log_fun, error, "p1_mysql_conn: " "Received unknown signal, exiting : ~p", [Unknown]), close_connection(State), error end. %%-------------------------------------------------------------------- %% Function: mysql_init(Sock, RecvPid, User, Password, LogFun) %% Sock = term(), gen_tcp socket %% RecvPid = pid(), p1_mysql_recv process %% User = string() %% Password = string() %% LogFun = undefined | function() with arity 3 %% Descrip.: Try to authenticate on our new socket. %% Returns : ok | {error, Reason} %% Reason = string() %%-------------------------------------------------------------------- mysql_init(Sock, RecvPid, User, Password, LogFun) -> case do_recv(LogFun, RecvPid, undefined) of {ok, Packet, InitSeqNum} -> {Version, Salt, Caps, AuthPlug} = greeting(Packet, LogFun), AuthRes = p1_mysql_auth:do_auth(AuthPlug, Sock, RecvPid, InitSeqNum + 1, User, Password, Salt, Caps, LogFun), case AuthRes of {ok, <<0:8, _Rest/binary>>, _RecvNum} -> {ok,Version}; {ok, <<255:8, Code:16/little, Message/binary>>, _RecvNum} -> p1_mysql:log(LogFun, error, "p1_mysql_conn: " "init error ~p: ~p~n", [Code, binary_to_list(Message)]), {error, binary_to_list(Message)}; {ok, RecvPacket, _RecvNum} -> p1_mysql:log(LogFun, error, "p1_mysql_conn: " "init unknown error ~p~n", [binary_to_list(RecvPacket)]), {error, binary_to_list(RecvPacket)}; {error, Reason} -> p1_mysql:log(LogFun, error, "p1_mysql_conn: " "init failed receiving data : ~p~n", [Reason]), {error, Reason} end; {error, Reason} -> {error, Reason} end. %% part of mysql_init/4 greeting(Packet, LogFun) -> <> = Packet, case Protocol of 9 -> {ServerStatusStr, Rest2} = asciz(Rest), <<_TreadID:32/little, Rest3/binary>> = Rest2, {Salt, _} = asciz(Rest3), p1_mysql:log(LogFun, debug, "p1_mysql_conn: greeting version ~p (protocol ~p) " "salt ~p", [ServerStatusStr, Protocol, Salt]), {?MYSQL_4_0, Salt, 0, "old_pass"}; 10 -> {ServerStatusStr, Rest2} = asciz(Rest), <<_TreadID:32/little, Salt1:8/binary, _:8, Caps1:16/little, CharSet:8, _StatusFlags:16/little, Caps2:16/little, AuthPlugLen:8, _:10/binary, Rest3/binary>> = Rest2, Caps = (Caps2 bsl 16) bor Caps1, {Salt2, AuthPlug} = case {Caps band ?CLIENT_PLUGIN_AUTH, AuthPlugLen} of {0, 0} -> Len = max(13, AuthPlugLen - 8) - 1, <> = Rest3, {S, "mysql_native_password"}; {?CLIENT_PLUGIN_AUTH, _} -> Len = max(13, AuthPlugLen - 8) - 1, <> = Rest3, {AuthPlugName, _} = asciz(Rest4), {S, AuthPlugName} end, Salt = binary_to_list(<>), p1_mysql:log(LogFun, debug, "p1_mysql_conn: greeting version ~p (protocol ~p) " "salt ~p caps ~p serverchar ~p auth_plug: ~p", [ServerStatusStr, Protocol, Salt, Caps, CharSet, AuthPlug]), {?MYSQL_4_1, Salt, Caps, AuthPlug} end. %% part of greeting/2 asciz(Data) when is_binary(Data) -> p1_mysql:asciz_binary(Data, []); asciz(Data) when is_list(Data) -> {String, [0 | Rest]} = lists:splitwith(fun (C) -> C /= 0 end, Data), {String, Rest}. %%-------------------------------------------------------------------- %% Function: get_query_response(LogFun, RecvPid) %% LogFun = undefined | function() with arity 3 %% RecvPid = pid(), p1_mysql_recv process %% Version = integer(), Representing MySQL version used %% Descrip.: Wait for frames until we have a complete query response. %% Returns : {data, #p1_mysql_result} %% {updated, #p1_mysql_result} %% {error, #p1_mysql_result} %% FieldInfo = list() of term() %% Rows = list() of [string()] %% AffectedRows = int() %% Reason = term() %%-------------------------------------------------------------------- get_query_response(LogFun, RecvPid, Version, Options) -> case do_recv(LogFun, RecvPid, undefined) of {ok, <>, _} -> case Fieldcount of 0 -> %% No Tabular data AffectedRows = case Rest of <<16#fc, Value:16/little, _/binary>> -> Value; <<16#fd, Value:24/little, _/binary>> -> Value; <<16#fe, Value:64/little, _/binary>> -> Value; <> -> Value end, {updated, #p1_mysql_result{affectedrows=AffectedRows}}; 255 -> <<_Code:16/little, Message/binary>> = Rest, {error, #p1_mysql_result{error=binary_to_list(Message)}}; _ -> %% Tabular data received ResultType = get_option(result_type, Options, ?DEFAULT_RESULT_TYPE), case get_fields(LogFun, RecvPid, [], Version, ResultType) of {ok, Fields} -> case get_rows(Fieldcount, LogFun, RecvPid, ResultType, []) of {ok, Rows} -> {data, #p1_mysql_result{fieldinfo=Fields, rows=Rows}}; {error, Reason} -> {error, #p1_mysql_result{error=Reason}} end; {error, Reason} -> {error, #p1_mysql_result{error=Reason}} end end; {error, Reason} -> {error, #p1_mysql_result{error=Reason}} end. %%-------------------------------------------------------------------- %% Function: get_fields(LogFun, RecvPid, [], Version) %% LogFun = undefined | function() with arity 3 %% RecvPid = pid(), p1_mysql_recv process %% Version = integer(), Representing MySQL version used %% Descrip.: Received and decode field information. %% Returns : {ok, FieldInfo} | %% {error, Reason} %% FieldInfo = list() of term() %% Reason = term() %%-------------------------------------------------------------------- %% Support for MySQL 4.0.x: get_fields(LogFun, RecvPid, Res, ?MYSQL_4_0, ResultType) -> case do_recv(LogFun, RecvPid, undefined) of {ok, Packet, _Num} -> case Packet of <<254:8>> -> {ok, lists:reverse(Res)}; <<254:8, Rest/binary>> when size(Rest) < 8 -> {ok, lists:reverse(Res)}; _ -> {Table, Rest} = get_with_length(Packet), {Field, Rest2} = get_with_length(Rest), {LengthB, Rest3} = get_with_length(Rest2), LengthL = size(LengthB) * 8, <> = LengthB, {Type, Rest4} = get_with_length(Rest3), {_Flags, _Rest5} = get_with_length(Rest4), if ResultType == list -> This = {binary_to_list(Table), binary_to_list(Field), Length, %% TODO: Check on MySQL 4.0 if types are specified %% using the same 4.1 formalism and could %% be expanded to atoms: binary_to_list(Type)}; ResultType == binary -> This = {Table, Field, Length, Type} end, get_fields(LogFun, RecvPid, [This | Res], ?MYSQL_4_0, ResultType) end; {error, Reason} -> {error, Reason} end; %% Support for MySQL 4.1.x and 5.x: get_fields(LogFun, RecvPid, Res, ?MYSQL_4_1, ResultType) -> case do_recv(LogFun, RecvPid, undefined) of {ok, Packet, _Num} -> case Packet of <<254:8>> -> {ok, lists:reverse(Res)}; <<254:8, Rest/binary>> when size(Rest) < 8 -> {ok, lists:reverse(Res)}; _ -> {_Catalog, Rest} = get_with_length(Packet), {_Database, Rest2} = get_with_length(Rest), {Table, Rest3} = get_with_length(Rest2), %% OrgTable is the real table name if Table is an alias {_OrgTable, Rest4} = get_with_length(Rest3), {Field, Rest5} = get_with_length(Rest4), %% OrgField is the real field name if Field is an alias {_OrgField, Rest6} = get_with_length(Rest5), <<_Metadata:8/little, _Charset:16/little, Length:32/little, Type:8/little, _Flags:16/little, _Decimals:8/little, _Rest7/binary>> = Rest6, if ResultType == list -> This = {binary_to_list(Table), binary_to_list(Field), Length, get_field_datatype(Type)}; ResultType == binary -> This = {Table, Field, Length, get_field_datatype(Type)} end, get_fields(LogFun, RecvPid, [This | Res], ?MYSQL_4_1, ResultType) end; {error, Reason} -> {error, Reason} end. %%-------------------------------------------------------------------- %% Function: get_rows(N, LogFun, RecvPid, []) %% N = integer(), number of rows to get %% LogFun = undefined | function() with arity 3 %% RecvPid = pid(), p1_mysql_recv process %% Descrip.: Receive and decode a number of rows. %% Returns : {ok, Rows} | %% {error, Reason} %% Rows = list() of [string()] %%-------------------------------------------------------------------- get_rows(N, LogFun, RecvPid, ResultType, Res) -> case do_recv(LogFun, RecvPid, undefined) of {ok, Packet, _Num} -> case Packet of <<254:8, Rest/binary>> when size(Rest) < 8 -> {ok, lists:reverse(Res)}; _ -> {ok, This} = get_row(N, Packet, ResultType, []), get_rows(N, LogFun, RecvPid, ResultType, [This | Res]) end; {error, Reason} -> {error, Reason} end. %% part of get_rows/4 get_row(0, _Data, _ResultType, Res) -> {ok, lists:reverse(Res)}; get_row(N, Data, ResultType, Res) -> {Col, Rest} = get_with_length(Data), This = case Col of null -> null; _ -> if ResultType == list -> binary_to_list(Col); ResultType == binary -> Col end end, get_row(N - 1, Rest, ResultType, [This | Res]). get_with_length(<<251:8, Rest/binary>>) -> {null, Rest}; get_with_length(<<252:8, Length:16/little, Rest/binary>>) -> split_binary(Rest, Length); get_with_length(<<253:8, Length:24/little, Rest/binary>>) -> split_binary(Rest, Length); get_with_length(<<254:8, Length:64/little, Rest/binary>>) -> split_binary(Rest, Length); get_with_length(<>) when Length < 251 -> split_binary(Rest, Length). close_connection(State) -> Result = gen_tcp:close(State#state.socket), p1_mysql:log(State#state.log_fun, normal, "Closing connection ~p: ~p~n", [State#state.socket, Result]), Result. %%-------------------------------------------------------------------- %% Function: do_query(State, Query) %% do_query(Sock, RecvPid, LogFun, Query) %% Sock = term(), gen_tcp socket %% RecvPid = pid(), p1_mysql_recv process %% LogFun = undefined | function() with arity 3 %% Query = string() %% Descrip.: Send a MySQL query and block awaiting it's response. %% Returns : result of get_query_response/2 | {error, Reason} %%-------------------------------------------------------------------- do_query(State, Query, Options) when is_record(State, state) -> do_query(State#state.socket, State#state.recv_pid, State#state.log_fun, Query, State#state.mysql_version, Options ). do_query(Sock, RecvPid, LogFun, Query, Version, Options) when is_pid(RecvPid), (is_list(Query) or is_binary(Query)) -> Packet = list_to_binary([?MYSQL_QUERY_OP, Query]), case do_send(Sock, Packet, 0, LogFun) of ok -> get_query_response(LogFun, RecvPid, Version, Options); {error, Reason} -> Msg = io_lib:format("Failed sending data on socket : ~p", [Reason]), {error, Msg} end. %%-------------------------------------------------------------------- %% Function: do_send(Sock, Packet, SeqNum, LogFun) %% Sock = term(), gen_tcp socket %% Packet = binary() %% SeqNum = integer(), packet sequence number %% LogFun = undefined | function() with arity 3 %% Descrip.: Send a packet to the MySQL server. %% Returns : result of gen_tcp:send/2 %%-------------------------------------------------------------------- do_send(Sock, Packet, SeqNum, _LogFun) when is_binary(Packet), is_integer(SeqNum) -> Data = <<(size(Packet)):24/little, SeqNum:8, Packet/binary>>, %%p1_mysql:log(LogFun, debug, "p1_mysql_conn: send packet ~p: ~p", %%[SeqNum, Data]), gen_tcp:send(Sock, Data). %%-------------------------------------------------------------------- %% Function: get_field_datatype(DataType) %% DataType = integer(), MySQL datatype %% Descrip.: Return MySQL field datatype as description string %% Returns : String, MySQL datatype %%-------------------------------------------------------------------- get_field_datatype(0) -> 'DECIMAL'; get_field_datatype(1) -> 'TINY'; get_field_datatype(2) -> 'SHORT'; get_field_datatype(3) -> 'LONG'; get_field_datatype(4) -> 'FLOAT'; get_field_datatype(5) -> 'DOUBLE'; get_field_datatype(6) -> 'NULL'; get_field_datatype(7) -> 'TIMESTAMP'; get_field_datatype(8) -> 'LONGLONG'; get_field_datatype(9) -> 'INT24'; get_field_datatype(10) -> 'DATE'; get_field_datatype(11) -> 'TIME'; get_field_datatype(12) -> 'DATETIME'; get_field_datatype(13) -> 'YEAR'; get_field_datatype(14) -> 'NEWDATE'; get_field_datatype(16) -> 'BIT'; get_field_datatype(246) -> 'DECIMAL'; get_field_datatype(247) -> 'ENUM'; get_field_datatype(248) -> 'SET'; get_field_datatype(249) -> 'TINYBLOB'; get_field_datatype(250) -> 'MEDIUM_BLOG'; get_field_datatype(251) -> 'LONG_BLOG'; get_field_datatype(252) -> 'BLOB'; get_field_datatype(253) -> 'VAR_STRING'; get_field_datatype(254) -> 'STRING'; get_field_datatype(255) -> 'GEOMETRY'. %%-------------------------------------------------------------------- %% Function: get_option(Key1, Options, Default) -> Value1 %% Options = [Option] %% Option = {Key2, Value2} %% Key1 = Key2 = atom() %% Value1 = Value2 = Default = term() %% Descrip.: Return the option associated with Key passed to squery/4 %%-------------------------------------------------------------------- get_option(Key, Options, Default) -> case lists:keysearch(Key, 1, Options) of {value, {_, Value}} -> Value; false -> Default end. p1_mysql-1.0.12/src/p1_mysql_sup.erl0000644000232200023220000000361013605316475017647 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : p1_mysql_sup.erl %%% Author : Evgeniy Khramtsov %%% Purpose : MySQL erlang driver supervisor %%% Created : 15 May 2013 by Evgeniy Khramtsov %%% %%% %%% p1_mysql, Copyright (C) 2002-2019 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA %%% 02111-1307 USA %%% %%%---------------------------------------------------------------------- -module(p1_mysql_sup). -behaviour(supervisor). %% API -export([start_link/0]). %% Supervisor callbacks -export([init/1]). -define(SERVER, ?MODULE). %%%=================================================================== %%% API functions %%%=================================================================== start_link() -> supervisor:start_link({local, ?SERVER}, ?MODULE, []). %%%=================================================================== %%% Supervisor callbacks %%%=================================================================== init([]) -> {ok, {{one_for_one, 10, 1}, []}}. %%%=================================================================== %%% Internal functions %%%=================================================================== p1_mysql-1.0.12/src/p1_mysql_recv.erl0000644000232200023220000001763013605316475020006 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% File : p1_mysql_recv.erl %%% Author : Fredrik Thulin %%% Descrip.: Handles data being received on a MySQL socket. Decodes %%% per-row framing and sends each row to parent. %%% %%% Created : 4 Aug 2005 by Fredrik Thulin %%% %%% Note : All MySQL code was written by Magnus Ahltorp, originally %%% in the file p1_mysql.erl - I just moved it here. %%% %%% Copyright (c) 2001-2004 Kungliga Tekniska Högskolan %%% See the file COPYING %%% %%% Signals this receiver process can send to it's parent %%% (the parent is a p1_mysql_conn connection handler) : %%% %%% {p1_mysql_recv, self(), data, Packet, Num} %%% {p1_mysql_recv, self(), closed, {error, Reason}} %%% {p1_mysql_recv, self(), closed, normal} %%% %%% Internally (from inside init/4 to start_link/4) the %%% following signals may be sent to the parent process : %%% %%% {p1_mysql_recv, self(), init, {ok, Sock}} %%% {p1_mysql_recv, self(), init, {error, E}} %%% %%%------------------------------------------------------------------- -module(p1_mysql_recv). %%-------------------------------------------------------------------- %% External exports (should only be used by the 'p1_mysql_conn' module) %%-------------------------------------------------------------------- -export([start_link/5 ]). -include_lib("kernel/include/inet.hrl"). -record(state, { socket, parent, log_fun, data }). -define(SECURE_CONNECTION, 32768). -define(DNS_LOOKUP_TIMEOUT, 5000). %%==================================================================== %% External functions %%==================================================================== %%-------------------------------------------------------------------- %% Function: start_link(Host, Port, LogFun, Parent) %% Host = string() %% Port = integer() %% LogFun = undefined | function() of arity 3 %% Parent = pid(), process that should get received frames %% Descrip.: Start a process that connects to Host:Port and waits for %% data. When it has received a MySQL frame, it sends it to %% Parent and waits for the next frame. %% Returns : {ok, RecvPid, Socket} | %% {error, Reason} %% RecvPid = pid(), receiver process pid %% Socket = term(), gen_tcp socket %% Reason = atom() | string() %%-------------------------------------------------------------------- start_link(Host, Port, ConnectTimeout, LogFun, Parent) when is_list(Host), is_integer(Port) -> RecvPid = spawn_link(fun () -> init(Host, Port, LogFun, Parent) end), %% wait for the socket from the spawned pid receive {p1_mysql_recv, RecvPid, init, {error, E}} -> {error, E}; {p1_mysql_recv, RecvPid, init, {ok, Socket}} -> {ok, RecvPid, Socket} after ConnectTimeout -> catch exit(RecvPid, kill), {error, "timeout"} end. %%==================================================================== %% Internal functions %%==================================================================== %%-------------------------------------------------------------------- %% Function: init((Host, Port, LogFun, Parent) %% Host = string() %% Port = integer() %% LogFun = undefined | function() of arity 3 %% Parent = pid(), process that should get received frames %% Descrip.: Connect to Host:Port and then enter receive-loop. %% Returns : error | never returns %%-------------------------------------------------------------------- init(Host, Port, LogFun, Parent) -> case connect(Host, Port) of {ok, Sock} -> Parent ! {p1_mysql_recv, self(), init, {ok, Sock}}, State = #state{socket = Sock, parent = Parent, log_fun = LogFun, data = <<>> }, loop(State); {error, E} -> Reason = format_inet_error(E), p1_mysql:log(LogFun, error, "p1_mysql_recv: Failed connecting to ~s:~p: ~s", [Host, Port, Reason]), Msg = lists:flatten(io_lib:format("connect failed: ~s", [Reason])), Parent ! {p1_mysql_recv, self(), init, {error, Msg}} end. %%-------------------------------------------------------------------- %% Function: loop(State) %% State = state record() %% Descrip.: The main loop. Wait for data from our TCP socket and act %% on received data or signals that our socket was closed. %% Returns : error | never returns %%-------------------------------------------------------------------- loop(State) -> Sock = State#state.socket, receive {tcp, Sock, InData} -> NewData = list_to_binary([State#state.data, InData]), %% send data to parent if we have enough data Rest = sendpacket(State#state.parent, NewData), loop(State#state{data = Rest}); {tcp_error, Sock, Reason} -> p1_mysql:log(State#state.log_fun, error, "p1_mysql_recv: " "Socket ~p closed : ~p", [Sock, Reason]), State#state.parent ! {p1_mysql_recv, self(), closed, {error, Reason}}, error; {tcp_closed, Sock} -> p1_mysql:log(State#state.log_fun, debug, "p1_mysql_recv: " "Socket ~p closed", [Sock]), State#state.parent ! {p1_mysql_recv, self(), closed, normal}, error end. %%-------------------------------------------------------------------- %% Function: sendpacket(Parent, Data) %% Parent = pid() %% Data = binary() %% Descrip.: Check if we have received one or more complete frames by %% now, and if so - send them to Parent. %% Returns : Rest = binary() %%-------------------------------------------------------------------- %% send data to parent if we have enough data sendpacket(Parent, Data) -> case Data of <> -> if Length =< size(D) -> {Packet, Rest} = split_binary(D, Length), Parent ! {p1_mysql_recv, self(), data, Packet, Num}, sendpacket(Parent, Rest); true -> Data end; _ -> Data end. %%-------------------------------------------------------------------- %% Connecting stuff %%-------------------------------------------------------------------- connect(Host, Port) -> case lookup(Host) of {ok, AddrsFamilies} -> do_connect(AddrsFamilies, Port, {error, nxdomain}); {error, _} = Err -> Err end. do_connect([{IP, Family}|AddrsFamilies], Port, _Err) -> case gen_tcp:connect(IP, Port, [binary, {packet, 0}, Family]) of {ok, Sock} -> {ok, Sock}; {error, _} = Err -> do_connect(AddrsFamilies, Port, Err) end; do_connect([], _Port, Err) -> Err. lookup(Host) -> case inet:parse_address(Host) of {ok, IP} -> {ok, [{IP, get_addr_type(IP)}]}; {error, _} -> do_lookup([{Host, Family} || Family <- [inet6, inet]], [], {error, nxdomain}) end. do_lookup([{Host, Family}|HostFamilies], AddrFamilies, Err) -> case inet:gethostbyname(Host, Family, ?DNS_LOOKUP_TIMEOUT) of {ok, HostEntry} -> Addrs = host_entry_to_addrs(HostEntry), AddrFamilies1 = [{Addr, Family} || Addr <- Addrs], do_lookup(HostFamilies, AddrFamilies ++ AddrFamilies1, Err); {error, _} = Err1 -> do_lookup(HostFamilies, AddrFamilies, Err1) end; do_lookup([], [], Err) -> Err; do_lookup([], AddrFamilies, _Err) -> {ok, AddrFamilies}. host_entry_to_addrs(#hostent{h_addr_list = AddrList}) -> lists:filter( fun(Addr) -> try get_addr_type(Addr) of _ -> true catch _:badarg -> false end end, AddrList). get_addr_type({_, _, _, _}) -> inet; get_addr_type({_, _, _, _, _, _, _, _}) -> inet6; get_addr_type(_) -> erlang:error(badarg). format_inet_error(closed) -> "connection closed"; format_inet_error(timeout) -> format_inet_error(etimedout); format_inet_error(Reason) -> case inet:format_error(Reason) of "unknown POSIX error" -> atom_to_list(Reason); Txt -> Txt end. p1_mysql-1.0.12/src/p1_mysql.erl0000644000232200023220000006704513605316475016774 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% File : p1_mysql.erl %%% Author : Magnus Ahltorp %%% Descrip.: MySQL client. %%% %%% Created : 4 Aug 2005 by Magnus Ahltorp %%% %%% Copyright (c) 2001-2004 Kungliga Tekniska Högskolan %%% See the file COPYING %%% %%% Usage: %%% %%% %%% Call one of the start-functions before any call to fetch/2 %%% %%% start_link(Id, Host, User, Password, Database) %%% start_link(Id, Host, Port, User, Password, Database) %%% start_link(Id, Host, User, Password, Database, LogFun) %%% start_link(Id, Host, Port, User, Password, Database, LogFun) %%% %%% Id is a connection group identifier. If you want to have more %%% than one connection to a server (or a set of MySQL replicas), %%% add more with %%% %%% connect(Id, Host, Port, User, Password, Database, Reconnect) %%% %%% use 'undefined' as Port to get default MySQL port number (3306). %%% MySQL querys will be sent in a per-Id round-robin fashion. %%% Set Reconnect to 'true' if you want the dispatcher to try and %%% open a new connection, should this one die. %%% %%% When you have a p1_mysql_dispatcher running, this is how you make a %%% query : %%% %%% fetch(Id, "select * from hello") -> Result %%% Result = {data, MySQLRes} | {updated, MySQLRes} | %%% {error, MySQLRes} %%% %%% Actual data can be extracted from MySQLRes by calling the following API %%% functions: %%% - on data received: %%% FieldInfo = p1_mysql:get_result_field_info(MysqlRes) %%% AllRows = p1_mysql:get_result_rows(MysqlRes) %%% with FieldInfo = list() of {Table, Field, Length, Name} %%% and AllRows = list() of list() representing records %%% - on update: %%% Affected = p1_mysql:get_result_affected_rows(MysqlRes) %%% with Affected = integer() %%% - on error: %%% Reason = p1_mysql:get_result_reason(MysqlRes) %%% with Reason = string() %%% %%% If you just want a single MySQL connection, or want to manage your %%% connections yourself, you can use the p1_mysql_conn module as a %%% stand-alone single MySQL connection. See the comment at the top of %%% p1_mysql_conn.erl. %%% %%%------------------------------------------------------------------- -module(p1_mysql). -behaviour(gen_server). %%-------------------------------------------------------------------- %% External exports %%-------------------------------------------------------------------- -export([start_link/5, start_link/6, start_link/7, fetch/2, fetch/3, get_result_field_info/1, get_result_rows/1, get_result_affected_rows/1, get_result_reason/1, quote/1, asciz_binary/2, connect/7, stop/0, gc_each/1 ]). %%-------------------------------------------------------------------- %% Internal exports - just for p1_mysql_* modules %%-------------------------------------------------------------------- -export([log/3, log/4 ]). %%-------------------------------------------------------------------- %% Internal exports - gen_server callbacks %%-------------------------------------------------------------------- -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3 ]). %%-------------------------------------------------------------------- %% Records %%-------------------------------------------------------------------- -include("p1_mysql.hrl"). -record(state, { conn_list, %% list() of p1_mysql_connection record() log_fun, %% undefined | function for logging, gc_tref %% undefined | timer:TRef }). -record(p1_mysql_connection, { id, %% term(), user of 'p1_mysql' modules id of this socket group conn_pid, %% pid(), p1_mysql_conn process reconnect, %% true | false, should p1_mysql_dispatcher try to reconnect if this connection dies? host, %% undefined | string() port, %% undefined | integer() user, %% undefined | string() password, %% undefined | string() database %% undefined | string() }). %%-------------------------------------------------------------------- %% Macros %%-------------------------------------------------------------------- -define(SERVER, p1_mysql_dispatcher). -define(CONNECT_TIMEOUT, 5000). -define(LOCAL_FILES, 128). -define(PORT, 3306). %%==================================================================== %% External functions %%==================================================================== %%-------------------------------------------------------------------- %% Function: start_link(Id, Host, User, Password, Database) %% start_link(Id, Host, Port, User, Password, Database) %% start_link(Id, Host, User, Password, Database, LogFun) %% start_link(Id, Host, Port, User, Password, Database, %% LogFun) %% Id = term(), first connection-group Id %% Host = string() %% Port = integer() %% User = string() %% Password = string() %% Database = string() %% LogFun = undefined | function() of arity 3 %% Descrip.: Starts the MySQL client gen_server process. %% Returns : {ok, Pid} | ignore | {error, Error} %%-------------------------------------------------------------------- start_link(Id, Host, User, Password, Database) when is_list(Host), is_list(User), is_list(Password), is_list(Database) -> start_link(Id, Host, ?PORT, User, Password, Database, undefined). start_link(Id, Host, Port, User, Password, Database) when is_list(Host), is_integer(Port), is_list(User), is_list(Password), is_list(Database) -> start_link(Id, Host, Port, User, Password, Database, undefined); start_link(Id, Host, User, Password, Database, LogFun) when is_list(Host), is_list(User), is_list(Password), is_list(Database) -> start_link(Id, Host, ?PORT, User, Password, Database, LogFun). start_link(Id, Host, Port, User, Password, Database, LogFun) when is_list(Host), is_integer(Port), is_list(User), is_list(Password), is_list(Database) -> crypto:start(), gen_server:start_link({local, ?SERVER}, ?MODULE, [Id, Host, Port, User, Password, Database, LogFun], []). stop() -> gen_server:call(?SERVER, stop). gc_each(Millisec) -> gen_server:call(?SERVER, {gc_each, Millisec}). %%-------------------------------------------------------------------- %% Function: fetch(Id, Query) %% fetch(Id, Query, Timeout) %% Id = term(), connection-group Id %% Query = string(), MySQL query in verbatim %% Timeout = integer() | infinity, gen_server timeout value %% Descrip.: Send a query and wait for the result. %% Returns : {data, MySQLRes} | %% {updated, MySQLRes} | %% {error, MySQLRes} %% MySQLRes = term() %%-------------------------------------------------------------------- fetch(Id, Query) when is_list(Query) -> gen_server:call(?SERVER, {fetch, Id, Query}). fetch(Id, Query, Timeout) when is_list(Query) -> gen_server:call(?SERVER, {fetch, Id, Query}, Timeout). %%-------------------------------------------------------------------- %% Function: get_result_field_info(MySQLRes) %% MySQLRes = term(), result of fetch function on "data" %% Descrip.: Extract the FieldInfo from MySQL Result on data received %% Returns : FieldInfo %% FieldInfo = list() of {Table, Field, Length, Name} %%-------------------------------------------------------------------- get_result_field_info(#p1_mysql_result{fieldinfo = FieldInfo}) -> FieldInfo. %%-------------------------------------------------------------------- %% Function: get_result_rows(MySQLRes) %% MySQLRes = term(), result of fetch function on "data" %% Descrip.: Extract the Rows from MySQL Result on data received %% Returns : Rows %% Rows = list() of list() representing records %%-------------------------------------------------------------------- get_result_rows(#p1_mysql_result{rows=AllRows}) -> AllRows. %%-------------------------------------------------------------------- %% Function: get_result_affected_rows(MySQLRes) %% MySQLRes = term(), result of fetch function on "updated" %% Descrip.: Extract the Rows from MySQL Result on update %% Returns : AffectedRows %% AffectedRows = integer() %%-------------------------------------------------------------------- get_result_affected_rows(#p1_mysql_result{affectedrows=AffectedRows}) -> AffectedRows. %%-------------------------------------------------------------------- %% Function: get_result_reason(MySQLRes) %% MySQLRes = term(), result of fetch function on "error" %% Descrip.: Extract the error Reason from MySQL Result on error %% Returns : Reason %% Reason = string() %%-------------------------------------------------------------------- get_result_reason(#p1_mysql_result{error=Reason}) -> Reason. %%-------------------------------------------------------------------- %% Function: quote(String) %% String = string() %% Descrip.: Quote a string so that it can be included safely in a %% MySQL query. %% Returns : Quoted = string() %%-------------------------------------------------------------------- quote(String) when is_list(String) -> [34 | lists:reverse([34 | quote(String, [])])]. %% 34 is $" quote([], Acc) -> Acc; quote([0 | Rest], Acc) -> quote(Rest, [$0, $\\ | Acc]); quote([10 | Rest], Acc) -> quote(Rest, [$n, $\\ | Acc]); quote([13 | Rest], Acc) -> quote(Rest, [$r, $\\ | Acc]); quote([$\\ | Rest], Acc) -> quote(Rest, [$\\ , $\\ | Acc]); quote([39 | Rest], Acc) -> %% 39 is $' quote(Rest, [39, $\\ | Acc]); %% 39 is $' quote([34 | Rest], Acc) -> %% 34 is $" quote(Rest, [34, $\\ | Acc]); %% 34 is $" quote([26 | Rest], Acc) -> quote(Rest, [$Z, $\\ | Acc]); quote([C | Rest], Acc) -> quote(Rest, [C | Acc]). %%-------------------------------------------------------------------- %% Function: asciz_binary(Data, Acc) %% Data = binary() %% Acc = list(), input accumulator %% Descrip.: Find the first zero-byte in Data and add everything %% before it to Acc, as a string. %% Returns : {NewList, Rest} %% NewList = list(), Acc plus what we extracted from Data %% Rest = binary(), whatever was left of Data, not %% including the zero-byte %%-------------------------------------------------------------------- asciz_binary(<<>>, Acc) -> {lists:reverse(Acc), <<>>}; asciz_binary(<<0:8, Rest/binary>>, Acc) -> {lists:reverse(Acc), Rest}; asciz_binary(<>, Acc) -> asciz_binary(Rest, [C | Acc]). %%-------------------------------------------------------------------- %% Function: connect(Id, Host, Port, User, Password, Database, %% Reconnect) %% Id = term(), connection-group Id %% Host = string() %% Port = undefined | integer() %% User = string() %% Password = string() %% Database = string() %% Reconnect = true | false %% Descrip.: Starts a MySQL connection and, if successfull, registers %% it with the p1_mysql_dispatcher. %% Returns : {ok, ConnPid} | {error, Reason} %%-------------------------------------------------------------------- connect(Id, Host, undefined, User, Password, Database, Reconnect) -> connect(Id, Host, ?PORT, User, Password, Database, Reconnect); connect(Id, Host, Port, User, Password, Database, Reconnect) -> {ok, LogFun} = gen_server:call(?SERVER, get_logfun), case p1_mysql_conn:start(Host, Port, User, Password, Database, LogFun) of {ok, ConnPid} -> MysqlConn = case Reconnect of true -> #p1_mysql_connection{id = Id, conn_pid = ConnPid, reconnect = true, host = Host, port = Port, user = User, password = Password, database = Database }; false -> #p1_mysql_connection{id = Id, conn_pid = ConnPid, reconnect = false } end, case gen_server:call(?SERVER, {add_mysql_connection, MysqlConn}) of ok -> {ok, ConnPid}; Res -> Res end; {error, Reason} -> {error, Reason} end. %%-------------------------------------------------------------------- %% Function: log(LogFun, Level, Format) %% log(LogFun, Level, Format, Arguments) %% LogFun = undefined | function() with arity 3 %% Level = debug | normal | error %% Format = string() %% Arguments = list() of term() %% Descrip.: Either call the function LogFun with the Level, Format %% and Arguments as parameters or log it to the console if %% LogFun is undefined. %% Returns : void() %% %% Note : Exported only for use by the p1_mysql_* modules. %% %%-------------------------------------------------------------------- log(LogFun, Level, Format) -> log(LogFun, Level, Format, []). log(LogFun, Level, Format, Arguments) when is_function(LogFun) -> LogFun(Level, Format, Arguments); log(undefined, _Level, Format, Arguments) -> %% default is to log to console io:format(Format, Arguments), io:format("~n", []). %%==================================================================== %% gen_server callbacks %%==================================================================== %%-------------------------------------------------------------------- %% Function: init(Args) -> {ok, State} | %% {ok, State, Timeout} | %% ignore | %% {stop, Reason} %% Args = [Id, Host, Port, User, Password, Database, LogFun] %% Id = term(), connection-group Id %% Host = string() %% Port = integer() %% User = string() %% Password = string() %% Database = string() %% LogFun = undefined | function() with arity 3 %% Descrip.: Initiates the gen_server (MySQL dispatcher). %%-------------------------------------------------------------------- init([Id, Host, Port, User, Password, Database, LogFun]) -> case p1_mysql_conn:start(Host, Port, User, Password, Database, LogFun) of {ok, ConnPid} -> MysqlConn = #p1_mysql_connection{id = Id, conn_pid = ConnPid, reconnect = true, host = Host, port = Port, user = User, password = Password, database = Database }, case add_mysql_conn(MysqlConn, []) of {ok, ConnList} -> {ok, #state{log_fun = LogFun, conn_list = ConnList, gc_tref = undefined }}; error -> Msg = "p1_mysql: Failed adding first MySQL connection handler to my list, exiting", log(LogFun, error, Msg), {error, Msg} end; {error, Reason} -> log(LogFun, error, "p1_mysql: Failed starting first MySQL connection handler, exiting"), {stop, {error, Reason}} end. %%-------------------------------------------------------------------- %% Function: handle_call(Msg, From, State) %% Descrip.: Handling call messages. %% Returns : {reply, Reply, State} | %% {reply, Reply, State, Timeout} | %% {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, Reply, State} | (terminate/2 is called) %% {stop, Reason, State} (terminate/2 is called) %%-------------------------------------------------------------------- %%-------------------------------------------------------------------- %% Function: handle_call({fetch, Id, Query}, From, State) %% Id = term(), connection-group id %% Query = string(), MySQL query %% Descrip.: Make a MySQL query. Use the first connection matching Id %% in our connection-list. Don't block the p1_mysql_dispatcher %% by returning {noreply, ...} here and let the p1_mysql_conn %% do gen_server:reply(...) when it has an answer. %% Returns : {noreply, NewState} | %% {reply, {error, Reason}, State} %% NewState = state record() %% Reason = atom() | string() %%-------------------------------------------------------------------- handle_call({fetch, Id, Query}, From, State) -> log(State#state.log_fun, debug, "p1_mysql: fetch ~p (id ~p)", [Query, Id]), case get_next_mysql_connection_for_id(Id, State#state.conn_list) of {ok, MysqlConn, RestOfConnList} when is_record(MysqlConn, p1_mysql_connection) -> p1_mysql_conn:fetch(MysqlConn#p1_mysql_connection.conn_pid, Query, From), %% move this mysql socket to the back of the list NewConnList = RestOfConnList ++ [MysqlConn], %% The ConnPid process does a gen_server:reply() when it has an answer {noreply, State#state{conn_list = NewConnList}}; nomatch -> %% we have no active connection matching Id {reply, {error, no_connection}, State} end; %%-------------------------------------------------------------------- %% Function: handle_call({add_mysql_connection, Conn}, From, State) %% Conn = p1_mysql_connection record() %% Descrip.: Add Conn to our list of connections. %% Returns : {reply, Reply, NewState} %% Reply = ok | {error, Reason} %% NewState = state record() %% Reason = string() %%-------------------------------------------------------------------- handle_call({add_mysql_connection, Conn}, _From, State) when is_record(Conn, p1_mysql_connection) -> case add_mysql_conn(Conn, State#state.conn_list) of {ok, NewConnList} -> {Id, ConnPid} = {Conn#p1_mysql_connection.id, Conn#p1_mysql_connection.conn_pid}, log(State#state.log_fun, normal, "p1_mysql: Added connection with id '~p' (pid ~p) to my list", [Id, ConnPid]), {reply, ok, State#state{conn_list = NewConnList}}; error -> {reply, {error, "failed adding MySQL connection to my list"}, State} end; %%-------------------------------------------------------------------- %% Function: handle_call(get_logfun, From, State) %% Descrip.: Fetch our logfun. %% Returns : {reply, {ok, LogFun}, State} %% LogFun = undefined | function() with arity 3 %%-------------------------------------------------------------------- handle_call(get_logfun, _From, State) -> {reply, {ok, State#state.log_fun}, State}; handle_call(stop, _From, State) -> {stop, normal, State}; handle_call({gc_each, Millisec}, _From, State) -> case State#state.gc_tref of undefined -> ok; TRef -> timer:cancel(TRef) end, case timer:send_interval(Millisec, gc) of {ok, NewTRef} -> {reply, ok, State#state{gc_tref = NewTRef}}; {error, Reason} -> {reply, {error, Reason}, State} end; handle_call(Unknown, _From, State) -> log(State#state.log_fun, error, "p1_mysql: Received unknown gen_server call : ~p", [Unknown]), {reply, {error, "unknown gen_server call in p1_mysql client"}, State}. %%-------------------------------------------------------------------- %% Function: handle_cast(Msg, State) %% Descrip.: Handling cast messages %% Returns : {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} (terminate/2 is called) %%-------------------------------------------------------------------- handle_cast(Unknown, State) -> log(State#state.log_fun, error, "p1_mysql: Received unknown gen_server cast : ~p", [Unknown]), {noreply, State}. %%-------------------------------------------------------------------- %% Function: handle_info(Msg, State) %% Descrip.: Handling all non call/cast messages %% Returns : {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} (terminate/2 is called) %%-------------------------------------------------------------------- %%-------------------------------------------------------------------- %% Function: handle_info({'DOWN', ...}, State) %% Descrip.: Handle a message that one of our monitored processes %% (p1_mysql_conn processes in our connection list) has exited. %% Remove the entry from our list. %% Returns : {noreply, NewState} | %% {stop, normal, State} %% NewState = state record() %% %% Note : For now, we stop if our connection list becomes empty. %% We should try to reconnect for a while first, to not %% eventually stop the whole OTP application if the MySQL- %% server is shut down and the p1_mysql_dispatcher was super- %% vised by an OTP supervisor. %%-------------------------------------------------------------------- handle_info({'DOWN', _MonitorRef, process, Pid, Info}, State) -> LogFun = State#state.log_fun, case remove_mysql_connection_using_pid(Pid, State#state.conn_list, []) of {ok, Conn, NewConnList} -> LogLevel = case Info of normal -> normal; _ -> error end, log(LogFun, LogLevel, "p1_mysql: MySQL connection pid ~p exited : ~p", [Pid, Info]), log(LogFun, normal, "p1_mysql: Removed MySQL connection with pid ~p from list", [Pid]), case Conn#p1_mysql_connection.reconnect of true -> start_reconnect(Conn, LogFun); false -> ok end, {noreply, State#state{conn_list = NewConnList}}; nomatch -> log(LogFun, error, "p1_mysql: Received 'DOWN' signal from pid ~p not in my list", [Pid]), {noreply, State} end; handle_info(gc, #state{conn_list = Connections} = State) -> [erlang:garbage_collect(C#p1_mysql_connection.conn_pid) || C <- Connections], erlang:garbage_collect(self()), {noreply, State}; handle_info(Info, State) -> log(State#state.log_fun, error, "p1_mysql: Received unknown signal : ~p", [Info]), {noreply, State}. %%-------------------------------------------------------------------- %% Function: terminate(Reason, State) %% Descrip.: Shutdown the server %% Returns : Reason %%-------------------------------------------------------------------- terminate(Reason, State) -> LogFun = State#state.log_fun, LogLevel = case Reason of normal -> debug; _ -> error end, log(LogFun, LogLevel, "p1_mysql: Terminating with reason : ~p", [Reason]), lists:foreach(fun(MysqlConn) -> MysqlConn#p1_mysql_connection.conn_pid ! close end, State#state.conn_list), Reason. %%-------------------------------------------------------------------- %% Function: code_change(_OldVsn, State, _Extra) %% Descrip.: Convert process state when code is changed %% Returns : {ok, State} %%-------------------------------------------------------------------- code_change(_OldVsn, State, _Extra) -> {ok, State}. %%==================================================================== %% Internal functions %%==================================================================== %%-------------------------------------------------------------------- %% Function: add_mysql_conn(Conn, ConnList) %% Conn = p1_mysql_connection record() %% ConnList = list() of p1_mysql_connection record() %% Descrip.: Set up process monitoring of the p1_mysql_conn process and %% then add it (first) to ConnList. %% Returns : NewConnList = list() of p1_mysql_connection record() %%-------------------------------------------------------------------- add_mysql_conn(Conn, ConnList) when is_record(Conn, p1_mysql_connection), is_list(ConnList) -> erlang:monitor(process, Conn#p1_mysql_connection.conn_pid), {ok, [Conn | ConnList]}. %%-------------------------------------------------------------------- %% Function: remove_mysql_connection_using_pid(Pid, ConnList) %% Pid = pid() %% ConnList = list() of p1_mysql_connection record() %% Descrip.: Removes the first p1_mysql_connection in ConnList that has %% a pid matching Pid. %% Returns : {ok, Conn, NewConnList} | nomatch %% Conn = p1_mysql_connection record() %% NewConnList = list() of p1_mysql_connection record() %%-------------------------------------------------------------------- remove_mysql_connection_using_pid(Pid, [#p1_mysql_connection{conn_pid = Pid} = H | T], Res) -> {ok, H, lists:reverse(Res) ++ T}; remove_mysql_connection_using_pid(Pid, [H | T], Res) when is_record(H, p1_mysql_connection) -> remove_mysql_connection_using_pid(Pid, T, [H | Res]); remove_mysql_connection_using_pid(_Pid, [], _Res) -> nomatch. %%-------------------------------------------------------------------- %% Function: get_next_mysql_connection_for_id(Id, ConnList) %% Id = term(), connection-group id %% ConnList = list() of p1_mysql_connection record() %% Descrip.: Find the first p1_mysql_connection in ConnList that has an %% id matching Id. %% Returns : {ok, Conn, NewConnList} | nomatch %% Conn = p1_mysql_connection record() %% NewConnList = list() of p1_mysql_connection record(), same %% as ConnList but without Conn %%-------------------------------------------------------------------- get_next_mysql_connection_for_id(Id, ConnList) -> get_next_mysql_connection_for_id(Id, ConnList, []). get_next_mysql_connection_for_id(Id, [#p1_mysql_connection{id = Id} = H | T], Res) -> {ok, H, lists:reverse(Res) ++ T}; get_next_mysql_connection_for_id(Id, [H | T], Res) when is_record(H, p1_mysql_connection) -> get_next_mysql_connection_for_id(Id, T, [H | Res]); get_next_mysql_connection_for_id(_Id, [], _Res) -> nomatch. %%-------------------------------------------------------------------- %% Function: start_reconnect(Conn, LogFun) %% Conn = p1_mysql_connection record() %% LogFun = undefined | function() with arity 3 %% Descrip.: Spawns a process that will try to re-establish a new %% connection instead of the one in Conn which has just %% died. %% Returns : ok %%-------------------------------------------------------------------- start_reconnect(Conn, LogFun) when is_record(Conn, p1_mysql_connection) -> Pid = spawn(fun () -> reconnect_loop(Conn#p1_mysql_connection{conn_pid = undefined}, LogFun, 0) end), {Id, Host, Port} = {Conn#p1_mysql_connection.id, Conn#p1_mysql_connection.host, Conn#p1_mysql_connection.port}, log(LogFun, debug, "p1_mysql: Started pid ~p to try and reconnect to ~p:~s:~p (replacing " "connection with pid ~p)", [Pid, Id, Host, Port, Conn#p1_mysql_connection.conn_pid]), ok. %%-------------------------------------------------------------------- %% Function: reconnect_loop(Conn, LogFun, 0) %% Conn = p1_mysql_connection record() %% LogFun = undefined | function() with arity 3 %% Descrip.: Loop indefinately until we are able to reconnect to the %% server specified in the now dead connection Conn. %% Returns : ok %%-------------------------------------------------------------------- reconnect_loop(Conn, LogFun, N) when is_record(Conn, p1_mysql_connection) -> {Id, Host, Port} = {Conn#p1_mysql_connection.id, Conn#p1_mysql_connection.host, Conn#p1_mysql_connection.port}, case connect(Id, Host, Port, Conn#p1_mysql_connection.user, Conn#p1_mysql_connection.password, Conn#p1_mysql_connection.database, Conn#p1_mysql_connection.reconnect) of {ok, ConnPid} -> log(LogFun, debug, "p1_mysql_reconnect: Managed to reconnect to ~p:~s:~p (connection pid ~p)", [Id, Host, Port, ConnPid]), ok; {error, Reason} -> %% log every once in a while NewN = case N of 10 -> log(LogFun, debug, "p1_mysql_reconnect: Still unable to connect to ~p:~s:~p (~p)", [Id, Host, Port, Reason]), 0; _ -> N + 1 end, %% sleep between every unsuccessfull attempt timer:sleep(20 * 1000), reconnect_loop(Conn, LogFun, NewN) end. p1_mysql-1.0.12/src/p1_mysql_app.erl0000644000232200023220000000322513605316475017622 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : p1_mysql_app.erl %%% Author : Evgeniy Khramtsov %%% Purpose : MySQL erlang driver application %%% Created : 15 May 2013 by Evgeniy Khramtsov %%% %%% %%% p1_mysql, Copyright (C) 2002-2019 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA %%% 02111-1307 USA %%% %%%---------------------------------------------------------------------- -module(p1_mysql_app). -behaviour(application). %% Application callbacks -export([start/2, stop/1]). %%%=================================================================== %%% Application callbacks %%%=================================================================== start(_StartType, _StartArgs) -> p1_mysql_sup:start_link(). stop(_State) -> ok. %%%=================================================================== %%% Internal functions %%%=================================================================== p1_mysql-1.0.12/rebar.config0000644000232200023220000000256413605316475016211 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : rebar.config %%% Author : Mickael Remond %%% Purpose : Rebar build script. Compliant with rebar and rebar3. %%% Created : 15 Dec 2015 by Mickael Remond %%% %%% Copyright (C) 2002-2019 ProcessOne, SARL. All Rights Reserved. %%% %%% Licensed under the Apache License, Version 2.0 (the "License"); %%% you may not use this file except in compliance with the License. %%% You may obtain a copy of the License at %%% %%% http://www.apache.org/licenses/LICENSE-2.0 %%% %%% Unless required by applicable law or agreed to in writing, software %%% distributed under the License is distributed on an "AS IS" BASIS, %%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %%% See the License for the specific language governing permissions and %%% limitations under the License. %%% %%%---------------------------------------------------------------------- {erl_opts, [debug_info, {src_dirs, ["src"]}, {i, "include"}]}. {cover_enabled, true}. {cover_export_enabled, true}. {xref_checks, [undefined_function_calls, undefined_functions, deprecated_function_calls, deprecated_functions]}. {profiles, [{test, [{erl_opts, [{src_dirs, ["src", "test"]}]}]}]}. %% Local Variables: %% mode: erlang %% End: %% vim: set filetype=erlang tabstop=8: p1_mysql-1.0.12/CODE_OF_CONDUCT.md0000644000232200023220000000643313605316475016525 0ustar debalancedebalance# Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at contact@process-one.net. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq p1_mysql-1.0.12/include/0000755000232200023220000000000013605316475015343 5ustar debalancedebalancep1_mysql-1.0.12/include/p1_mysql.hrl0000644000232200023220000000015413605316475017617 0ustar debalancedebalance%% MySQL result record: -record(p1_mysql_result, {fieldinfo=[], rows=[], affectedrows=0, error=""}). p1_mysql-1.0.12/CONTRIBUTING.md0000644000232200023220000001361113605316475016153 0ustar debalancedebalance# Contributing We'd love for you to contribute to our source code and to make our project even better than it is today! Here are the guidelines we'd like you to follow: * [Code of Conduct](#coc) * [Questions and Problems](#question) * [Issues and Bugs](#issue) * [Feature Requests](#feature) * [Issue Submission Guidelines](#submit) * [Pull Request Submission Guidelines](#submit-pr) * [Signing the CLA](#cla) ## Code of Conduct Help us keep our community open-minded and inclusive. Please read and follow our [Code of Conduct][coc]. ## Questions, Bugs, Features ### Got a Question or Problem? Do not open issues for general support questions as we want to keep GitHub issues for bug reports and feature requests. You've got much better chances of getting your question answered on dedicated support platforms, the best being [Stack Overflow][stackoverflow]. Stack Overflow is a much better place to ask questions since: - there are thousands of people willing to help on Stack Overflow - questions and answers stay available for public viewing so your question / answer might help someone else - Stack Overflow's voting system assures that the best answers are prominently visible. To save your and our time, we will systematically close all issues that are requests for general support and redirect people to the section you are reading right now. ### Found an Issue or Bug? If you find a bug in the source code, you can help us by submitting an issue to our [GitHub Repository][github]. Even better, you can submit a Pull Request with a fix. ### Missing a Feature? You can request a new feature by submitting an issue to our [GitHub Repository][github-issues]. If you would like to implement a new feature then consider what kind of change it is: * **Major Changes** that you wish to contribute to the project should be discussed first in an [GitHub issue][github-issues] that clearly outlines the changes and benefits of the feature. * **Small Changes** can directly be crafted and submitted to the [GitHub Repository][github] as a Pull Request. See the section about [Pull Request Submission Guidelines](#submit-pr). ## Issue Submission Guidelines Before you submit your issue search the archive, maybe your question was already answered. If your issue appears to be a bug, and hasn't been reported, open a new issue. Help us to maximize the effort we can spend fixing issues and adding new features, by not reporting duplicate issues. The "[new issue][github-new-issue]" form contains a number of prompts that you should fill out to make it easier to understand and categorize the issue. ## Pull Request Submission Guidelines By submitting a pull request for a code or doc contribution, you need to have the right to grant your contribution's copyright license to ProcessOne. Please check [ProcessOne CLA][cla] for details. Before you submit your pull request consider the following guidelines: * Search [GitHub][github-pr] for an open or closed Pull Request that relates to your submission. You don't want to duplicate effort. * Make your changes in a new git branch: ```shell git checkout -b my-fix-branch master ``` * Test your changes and, if relevant, expand the automated test suite. * Create your patch commit, including appropriate test cases. * If the changes affect public APIs, change or add relevant documentation. * Commit your changes using a descriptive commit message. ```shell git commit -a ``` Note: the optional commit `-a` command line option will automatically "add" and "rm" edited files. * Push your branch to GitHub: ```shell git push origin my-fix-branch ``` * In GitHub, send a pull request to `master` branch. This will trigger the continuous integration and run the test. We will also notify you if you have not yet signed the [contribution agreement][cla]. * If you find that the continunous integration has failed, look into the logs to find out if your changes caused test failures, the commit message was malformed etc. If you find that the tests failed or times out for unrelated reasons, you can ping a team member so that the build can be restarted. * If we suggest changes, then: * Make the required updates. * Test your changes and test cases. * Commit your changes to your branch (e.g. `my-fix-branch`). * Push the changes to your GitHub repository (this will update your Pull Request). You can also amend the initial commits and force push them to the branch. ```shell git rebase master -i git push origin my-fix-branch -f ``` This is generally easier to follow, but separate commits are useful if the Pull Request contains iterations that might be interesting to see side-by-side. That's it! Thank you for your contribution! ## Signing the Contributor License Agreement (CLA) Upon submitting a Pull Request, we will ask you to sign our CLA if you haven't done so before. It's a quick process, we promise, and you will be able to do it all online You can read [ProcessOne Contribution License Agreement][cla] in PDF. This is part of the legal framework of the open-source ecosystem that adds some red tape, but protects both the contributor and the company / foundation behind the project. It also gives us the option to relicense the code with a more permissive license in the future. [coc]: https://github.com/processone/p1_mysql/blob/master/CODE_OF_CONDUCT.md [stackoverflow]: https://stackoverflow.com/ [github]: https://github.com/processone/p1_mysql [github-issues]: https://github.com/processone/p1_mysql/issues [github-new-issue]: https://github.com/processone/p1_mysql/issues/new [github-pr]: https://github.com/processone/p1_mysql/pulls [cla]: https://www.process-one.net/resources/ejabberd-cla.pdf [license]: https://github.com/processone/p1_mysql/blob/master/COPYING