diff --git a/plugins/emqttd_plugin_mysql/.placehodler b/plugins/emqttd_plugin_mysql/.placehodler new file mode 100644 index 000000000..e69de29bb diff --git a/plugins/emqttd_plugin_mysql/Makefile b/plugins/emqttd_plugin_mysql/Makefile new file mode 100755 index 000000000..086a1fa40 --- /dev/null +++ b/plugins/emqttd_plugin_mysql/Makefile @@ -0,0 +1,32 @@ +REBAR?=./rebar + + +all: build + + +clean: + $(REBAR) clean + rm -rf logs + rm -rf .eunit + rm -f test/*.beam + + +distclean: clean + git clean -fxd + +build: depends + $(REBAR) compile + + +eunit: + $(REBAR) eunit skip_deps=true + + +check: build eunit + + +%.beam: %.erl + erlc -o test/ $< + + +.PHONY: all clean distclean depends build eunit check diff --git a/plugins/emqttd_plugin_mysql/README.md b/plugins/emqttd_plugin_mysql/README.md new file mode 100644 index 000000000..481a8f3e7 --- /dev/null +++ b/plugins/emqttd_plugin_mysql/README.md @@ -0,0 +1,49 @@ +## Overview + +Authentication with user table of MySQL database. + +## etc/plugin.config + +```erlang +[ + {emysql, [ + {pool, 4}, + {host, "localhost"}, + {port, 3306}, + {username, ""}, + {password, ""}, + {database, "mqtt"}, + {encoding, utf8} + ]}, + {emqttd_auth_mysql, [ + {user_table, mqtt_users}, + %% plain password only + {password_hash, plain}, + {field_mapper, [ + {username, username}, + {password, password} + ]} + ]} +]. +``` + +## Users Table(Demo) + +Notice: This is a demo table. You could authenticate with any user tables. + +``` +CREATE TABLE `mqtt_users` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `username` varchar(60) DEFAULT NULL, + `password` varchar(60) DEFAULT NULL, + `salt` varchar(20) DEFAULT NULL, + `created` datetime DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `mqtt_users_username` (`username`) +) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; +``` + +## Load Plugin + +Merge the'etc/plugin.config' to emqttd/etc/plugins.config, and the plugin will be loaded by the broker. + diff --git a/plugins/emqttd_plugin_mysql/c_src/base64.c b/plugins/emqttd_plugin_mysql/c_src/base64.c new file mode 100755 index 000000000..457c1a138 --- /dev/null +++ b/plugins/emqttd_plugin_mysql/c_src/base64.c @@ -0,0 +1,151 @@ +/* + * Copyright (c) 1995, 1996, 1997 Kungliga Tekniska Hgskolan + * (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. All advertising materials mentioning features or use of this software + * must display the following acknowledgement: + * This product includes software developed by the Kungliga Tekniska + * Hgskolan and its contributors. + * + * 4. 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. + */ + +#ifdef HAVE_CONFIG_H +#include +/*RCSID("$Id: base64.c,v 1.1 2005/02/11 07:34:35 jpm Exp jpm $");*/ +#endif +#include +#include +#include "base64.h" + +static char base64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +static int pos(char c) +{ + char *p; + for(p = base64; *p; p++) + if(*p == c) + return p - base64; + return -1; +} + +int base64_encode(const void *data, int size, char **str) +{ + char *s, *p; + int i; + int c; + unsigned char *q; + + p = s = (char*)malloc(size*4/3+4); + if (p == NULL) + return -1; + q = (unsigned char*)data; + i=0; + for(i = 0; i < size;){ + c=q[i++]; + c*=256; + if(i < size) + c+=q[i]; + i++; + c*=256; + if(i < size) + c+=q[i]; + i++; + p[0]=base64[(c&0x00fc0000) >> 18]; + p[1]=base64[(c&0x0003f000) >> 12]; + p[2]=base64[(c&0x00000fc0) >> 6]; + p[3]=base64[(c&0x0000003f) >> 0]; + if(i > size) + p[3]='='; + if(i > size+1) + p[2]='='; + p+=4; + } + *p=0; + *str = s; + return strlen(s); +} + +int base64_decode(const char *str, void *data) +{ + const char *p; + unsigned char *q; + int c; + int x; + int done = 0; + q=(unsigned char*)data; + for(p=str; *p && !done; p+=4){ + x = pos(p[0]); + if(x >= 0) + c = x; + else{ + done = 3; + break; + } + c*=64; + + x = pos(p[1]); + if(x >= 0) + c += x; + else + return -1; + c*=64; + + if(p[2] == '=') + done++; + else{ + x = pos(p[2]); + if(x >= 0) + c += x; + else + return -1; + } + c*=64; + + if(p[3] == '=') + done++; + else{ + if(done) + return -1; + x = pos(p[3]); + if(x >= 0) + c += x; + else + return -1; + } + if(done < 3) + *q++=(c&0x00ff0000)>>16; + + if(done < 2) + *q++=(c&0x0000ff00)>>8; + if(done < 1) + *q++=(c&0x000000ff)>>0; + } + return q - (unsigned char*)data; +} diff --git a/plugins/emqttd_plugin_mysql/c_src/base64.h b/plugins/emqttd_plugin_mysql/c_src/base64.h new file mode 100755 index 000000000..380a31d49 --- /dev/null +++ b/plugins/emqttd_plugin_mysql/c_src/base64.h @@ -0,0 +1,47 @@ +/* + * Copyright (c) 1995, 1996, 1997 Kungliga Tekniska Hgskolan + * (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. All advertising materials mentioning features or use of this software + * must display the following acknowledgement: + * This product includes software developed by the Kungliga Tekniska + * Hgskolan and its contributors. + * + * 4. 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. + */ + +/* $Id: base64.h,v 1.1 2005/02/11 07:34:35 jpm Exp jpm $ */ + +#ifndef _BASE64_H_ +#define _BASE64_H_ + +int base64_encode(const void *data, int size, char **str); +int base64_decode(const char *str, void *data); + +#endif diff --git a/plugins/emqttd_plugin_mysql/c_src/emqttd_plugin_mysql_app.c b/plugins/emqttd_plugin_mysql/c_src/emqttd_plugin_mysql_app.c new file mode 100755 index 000000000..374de99ac --- /dev/null +++ b/plugins/emqttd_plugin_mysql/c_src/emqttd_plugin_mysql_app.c @@ -0,0 +1,60 @@ +// This file is part of Jiffy released under the MIT license. +// See the LICENSE file for more information. + +#include "emqttd_plugin_mysql_app.h" + +static int +load(ErlNifEnv* env, void** priv, ERL_NIF_TERM info) +{ + emqttd_plugin_mysql_app_st* st = enif_alloc(sizeof(emqttd_plugin_mysql_app_st)); + if(st == NULL) { + return 1; + } + + st->atom_ok = make_atom(env, "ok"); + st->atom_error = make_atom(env, "error"); + st->atom_null = make_atom(env, "null"); + st->atom_true = make_atom(env, "true"); + st->atom_false = make_atom(env, "false"); + st->atom_bignum = make_atom(env, "bignum"); + st->atom_bignum_e = make_atom(env, "bignum_e"); + st->atom_bigdbl = make_atom(env, "bigdbl"); + st->atom_partial = make_atom(env, "partial"); + st->atom_uescape = make_atom(env, "uescape"); + st->atom_pretty = make_atom(env, "pretty"); + st->atom_force_utf8 = make_atom(env, "force_utf8"); + + // Markers used in encoding + st->ref_object = make_atom(env, "$object_ref$"); + st->ref_array = make_atom(env, "$array_ref$"); + + *priv = (void*) st; + + return 0; +} + +static int +reload(ErlNifEnv* env, void** priv, ERL_NIF_TERM info) +{ + return 0; +} + +static int +upgrade(ErlNifEnv* env, void** priv, void** old_priv, ERL_NIF_TERM info) +{ + return load(env, priv, info); +} + +static void +unload(ErlNifEnv* env, void* priv) +{ + enif_free(priv); + return; +} + +static ErlNifFunc funcs[] = +{ + {"nif_pbkdf2_check", 2, pbkdf2_check} +}; + +ERL_NIF_INIT(emqttd_plugin_mysql_app, funcs, &load, &reload, &upgrade, &unload); diff --git a/plugins/emqttd_plugin_mysql/c_src/emqttd_plugin_mysql_app.h b/plugins/emqttd_plugin_mysql/c_src/emqttd_plugin_mysql_app.h new file mode 100755 index 000000000..e77ae3b29 --- /dev/null +++ b/plugins/emqttd_plugin_mysql/c_src/emqttd_plugin_mysql_app.h @@ -0,0 +1,44 @@ +// This file is part of Jiffy released under the MIT license. +// See the LICENSE file for more information. + +#ifndef EMQTTD_PLUGIN_MYSQL_APP_H +#define EMQTTD_PLUGIN_MYSQL_APP_H + +#include "erl_nif.h" + +typedef struct { + ERL_NIF_TERM atom_ok; + ERL_NIF_TERM atom_error; + ERL_NIF_TERM atom_null; + ERL_NIF_TERM atom_true; + ERL_NIF_TERM atom_false; + ERL_NIF_TERM atom_bignum; + ERL_NIF_TERM atom_bignum_e; + ERL_NIF_TERM atom_bigdbl; + ERL_NIF_TERM atom_partial; + ERL_NIF_TERM atom_uescape; + ERL_NIF_TERM atom_pretty; + ERL_NIF_TERM atom_force_utf8; + + ERL_NIF_TERM ref_object; + ERL_NIF_TERM ref_array; +} emqttd_plugin_mysql_app_st; + +ERL_NIF_TERM make_atom(ErlNifEnv* env, const char* name); +ERL_NIF_TERM make_ok(emqttd_plugin_mysql_app_st* st, ErlNifEnv* env, ERL_NIF_TERM data); +ERL_NIF_TERM make_error(emqttd_plugin_mysql_app_st* st, ErlNifEnv* env, const char* error); + +ERL_NIF_TERM pbkdf2_check(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]); + +int int_from_hex(const unsigned char* p); +int int_to_hex(int val, char* p); +int utf8_len(int c); +int utf8_esc_len(int c); +int utf8_validate(unsigned char* data, size_t size); +int utf8_to_unicode(unsigned char* buf, size_t size); +int unicode_to_utf8(int c, unsigned char* buf); +int unicode_from_pair(int hi, int lo); +int unicode_uescape(int c, char* buf); +int double_to_shortest(char *buf, size_t size, size_t* len, double val); + +#endif // Included EMQTTD_PLUGIN_MYSQL_APP_H diff --git a/plugins/emqttd_plugin_mysql/c_src/pbkdf2_check.c b/plugins/emqttd_plugin_mysql/c_src/pbkdf2_check.c new file mode 100644 index 000000000..0e40b933b --- /dev/null +++ b/plugins/emqttd_plugin_mysql/c_src/pbkdf2_check.c @@ -0,0 +1,278 @@ +/* + * Copyright (c) 2013 Jan-Piet Mens + * 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 mosquitto 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. + */ + +#include +#include +#include +#include +#include +#include "base64.h" +#include "erl_nif.h" +#include "emqttd_plugin_mysql_app.h" + +#define KEY_LENGTH 24 +#define SEPARATOR "$" +#define SEPARATOR1 "_" +#define TRUE (1) +#define FALSE (0) + +/* + * Split PBKDF2$... string into their components. The caller must free() + * the strings. + */ + +static int detoken(char *pbkstr, char **sha, int *iter, char **salt, char **key) +{ + char *p, *s, *save; + int rc = 1; + + save = s = strdup(pbkstr); + + if ((p = strsep(&s, SEPARATOR1)) == NULL) + goto out; + if (strcmp(p, "pbkdf2") != 0) + goto out; + + if ((p = strsep(&s, SEPARATOR)) == NULL) + goto out; + *sha = strdup(p); + + if ((p = strsep(&s, SEPARATOR)) == NULL) + goto out; + *iter = atoi(p); + + if ((p = strsep(&s, SEPARATOR)) == NULL) + goto out; + *salt = strdup(p); + + if ((p = strsep(&s, SEPARATOR)) == NULL) + goto out; + *key = strdup(p); + + rc = 0; + +out: + free(save); + return rc; +} + + ERL_NIF_TERM +pbkdf2_check(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) +{ + ERL_NIF_TERM ret; + ErlNifBinary binps, binhash; + emqttd_plugin_mysql_app_st* st = enif_alloc(sizeof(emqttd_plugin_mysql_app_st)); + if(st == NULL) { + return make_atom(env, "alloc_error"); + } + + st->atom_ok = make_atom(env, "ok"); + st->atom_error = make_atom(env, "error"); + st->atom_null = make_atom(env, "null"); + st->atom_true = make_atom(env, "true"); + st->atom_false = make_atom(env, "false"); + st->atom_bignum = make_atom(env, "bignum"); + st->atom_bignum_e = make_atom(env, "bignum_e"); + st->atom_bigdbl = make_atom(env, "bigdbl"); + st->atom_partial = make_atom(env, "partial"); + st->atom_uescape = make_atom(env, "uescape"); + st->atom_pretty = make_atom(env, "pretty"); + st->atom_force_utf8 = make_atom(env, "force_utf8"); + + // Markers used in encoding + st->ref_object = make_atom(env, "$object_ref$"); + st->ref_array = make_atom(env, "$array_ref$"); + + if(argc != 2) { + return make_error(st, env, "Bad args"); + } else if(!enif_inspect_binary(env, argv[0], &binps)|!enif_inspect_binary(env, argv[1], &binhash)) { + return make_error(st, env, "Bad args password or username inspect error"); + } + + char* password = (char*)binps.data; + char* hash = (char*)binhash.data; + static char *sha, *salt, *h_pw; + int iterations, saltlen, blen; + char *b64, *keybuf; + unsigned char *out; + int match = FALSE; + const EVP_MD *evpmd; + int keylen, rc; + + if (detoken(hash, &sha, &iterations, &salt, &h_pw) != 0) + return match; + + /* Determine key length by decoding base64 */ + if ((keybuf = malloc(strlen(h_pw) + 1)) == NULL) { + return make_error(st, env, "internal_error: Out Of memory"); + } + keylen = base64_decode(h_pw, keybuf); + if (keylen < 1) { + free(keybuf); + return make_atom(env, "false"); + } + free(keybuf); + + if ((out = malloc(keylen)) == NULL) { + return make_error(st, env, "Cannot allocate out; out of memory\n"); + } + +#ifdef PWDEBUG + fprintf(stderr, "sha =[%s]\n", sha); + fprintf(stderr, "iterations =%d\n", iterations); + fprintf(stderr, "salt =[%s]\n", salt); + fprintf(stderr, "h_pw =[%s]\n", h_pw); + fprintf(stderr, "kenlen =[%d]\n", keylen); +#endif + + saltlen = strlen((char *)salt); + + evpmd = EVP_sha256(); + if (strcmp(sha, "sha1") == 0) { + evpmd = EVP_sha1(); + } else if (strcmp(sha, "sha512") == 0) { + evpmd = EVP_sha512(); + } + + rc = PKCS5_PBKDF2_HMAC(password, strlen(password), + (unsigned char *)salt, saltlen, + iterations, + evpmd, keylen, out); + if (rc != 1) { + goto out; + } + + blen = base64_encode(out, keylen, &b64); + if (blen > 0) { + int i, diff = 0, hlen = strlen(h_pw); +#ifdef PWDEBUG + fprintf(stderr, "HMAC b64 =[%s]\n", b64); +#endif + + /* "manual" strcmp() to ensure constant time */ + for (i = 0; (i < blen) && (i < hlen); i++) { + diff |= h_pw[i] ^ b64[i]; + } + + match = diff == 0; + if (hlen != blen) + match = 0; + + free(b64); + } + +out: + free(sha); + free(salt); + free(h_pw); + free(out); + + if(match == 0){ + ret = make_atom(env, "false"); + }else{ + ret = make_atom(env, "true"); + } + return ret; +} + +int pbkdf2_check_native(char *password, char *hash) +{ + static char *sha, *salt, *h_pw; + int iterations, saltlen, blen; + char *b64; + unsigned char key[128]; + int match = FALSE; + const EVP_MD *evpmd; + + if (detoken(hash, &sha, &iterations, &salt, &h_pw) != 0) + return match; + +#ifdef PWDEBUG + fprintf(stderr, "sha =[%s]\n", sha); + fprintf(stderr, "iterations =%d\n", iterations); + fprintf(stderr, "salt =[%s]\n", salt); + fprintf(stderr, "h_pw =[%s]\n", h_pw); +#endif + + saltlen = strlen((char *)salt); + + evpmd = EVP_sha256(); + if (strcmp(sha, "sha1") == 0) { + evpmd = EVP_sha1(); + } else if (strcmp(sha, "sha512") == 0) { + evpmd = EVP_sha512(); + } + + PKCS5_PBKDF2_HMAC(password, strlen(password), + (unsigned char *)salt, saltlen, + iterations, + evpmd, KEY_LENGTH, key); + + blen = base64_encode(key, KEY_LENGTH, &b64); + if (blen > 0) { + int i, diff = 0, hlen = strlen(h_pw); +#ifdef PWDEBUG + fprintf(stderr, "HMAC b64 =[%s]\n", b64); +#endif + + /* "manual" strcmp() to ensure constant time */ + for (i = 0; (i < blen) && (i < hlen); i++) { + diff |= h_pw[i] ^ b64[i]; + } + + match = diff == 0; + if (hlen != blen) + match = 0; + + free(b64); + } + + free(sha); + free(salt); + free(h_pw); + + return match; +} +int main() +{ + // char password[] = "hello"; + // char PB1[] = "PBKDF2$sha256$10000$eytf9sEo8EprP9P3$2eO6tROHiqI3bm+gg+vpmWooWMpz1zji"; + char password[] = "supersecret"; + //char PB1[] = "PBKDF2$sha256$10000$YEbSTt8FaMRDq/ib$Kt97+sMCYg00mqMOBAYinqZlnxX8HqHk"; + char PB1[] = "pbkdf2_sha256$10000$YEbSTt8FaMRDq/ib$Kt97+sMCYg00mqMOBAYinqZlnxX8HqHk"; + // char PB1[] = "PBKDF2$sha1$10000$XWfyPLeC9gsD6SbI$HOnjU4Ux7RpeBHdqYxpIGH1R5qCCtNA1"; + // char PB1[] = "PBKDF2$sha512$10000$v/aaCgBZ+VZN5L8n$BpgjSTyb4weVxr9cA2mvQ+jaCyaAPeYe"; + int match; + + printf("Checking password [%s] for %s\n", password, PB1); + + match = pbkdf2_check_native(password, PB1); + printf("match == %d\n", match); + return match; +} diff --git a/plugins/emqttd_plugin_mysql/c_src/util.c b/plugins/emqttd_plugin_mysql/c_src/util.c new file mode 100755 index 000000000..8c0291b22 --- /dev/null +++ b/plugins/emqttd_plugin_mysql/c_src/util.c @@ -0,0 +1,26 @@ +// This file is part of Jiffy released under the MIT license. +// See the LICENSE file for more information. + +#include "emqttd_plugin_mysql_app.h" + +ERL_NIF_TERM +make_atom(ErlNifEnv* env, const char* name) +{ + ERL_NIF_TERM ret; + if(enif_make_existing_atom(env, name, &ret, ERL_NIF_LATIN1)) { + return ret; + } + return enif_make_atom(env, name); +} + +ERL_NIF_TERM +make_ok(emqttd_plugin_mysql_app_st* st, ErlNifEnv* env, ERL_NIF_TERM value) +{ + return enif_make_tuple2(env, st->atom_ok, value); +} + +ERL_NIF_TERM +make_error(emqttd_plugin_mysql_app_st* st, ErlNifEnv* env, const char* error) +{ + return enif_make_tuple2(env, st->atom_error, make_atom(env, error)); +} diff --git a/plugins/emqttd_plugin_mysql/etc/plugin.config b/plugins/emqttd_plugin_mysql/etc/plugin.config new file mode 100644 index 000000000..4b5044bde --- /dev/null +++ b/plugins/emqttd_plugin_mysql/etc/plugin.config @@ -0,0 +1,23 @@ +[ + {emysql, [ + {pool, 4}, + {host, "localhost"}, + {port, 3306}, + {username, "root"}, + {password, "root"}, + {database, "emqtt"}, + {encoding, utf8} + ]}, + {emqttd_plugin_mysql, [ + {users_table, auth_user}, + {acls_table, auth_acl}, + {field_mapper, [ + {username, username}, + {password, password, pbkdf2}, + {user_super, is_super_user}, + {acl_username, username}, + {acl_rw, rw}, + {acl_topic, topic} + ]} + ]} +]. diff --git a/plugins/emqttd_plugin_mysql/etc/plugins.config b/plugins/emqttd_plugin_mysql/etc/plugins.config new file mode 100644 index 000000000..7b132dd3f --- /dev/null +++ b/plugins/emqttd_plugin_mysql/etc/plugins.config @@ -0,0 +1,23 @@ +[ + {emysql, [ + {pool, 4}, + {host, "59.188.253.198"}, + {port, 3306}, + {username, "root"}, + {password, "lhroot."}, + {database, "musicfield"}, + {encoding, utf8} + ]}, + {emqttd_plugin_mysql, [ + {users_table, auth_user}, + {acls_table, auth_acl}, + {field_mapper, [ + {username, username}, + {password, password, pbkdf2}, + {user_super, is_super_user}, + {acl_username, username}, + {acl_rw, rw}, + {acl_topic, topic} + ]} + ]} +]. diff --git a/plugins/emqttd_plugin_mysql/include/emqttd.hrl b/plugins/emqttd_plugin_mysql/include/emqttd.hrl new file mode 100644 index 000000000..9c2ab934a --- /dev/null +++ b/plugins/emqttd_plugin_mysql/include/emqttd.hrl @@ -0,0 +1,112 @@ +%%------------------------------------------------------------------------------ +%% Copyright (c) 2012-2015, Feng Lee +%% +%% Permission is hereby granted, free of charge, to any person obtaining a copy +%% of this software and associated documentation files (the "Software"), to deal +%% in the Software without restriction, including without limitation the rights +%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +%% copies of the Software, and to permit persons to whom the Software is +%% furnished to do so, subject to the following conditions: +%% +%% The above copyright notice and this permission notice shall be included in all +%% copies or substantial portions of the Software. +%% +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +%% SOFTWARE. +%%------------------------------------------------------------------------------ +%%% @doc +%%% MQTT Broker Header. +%%% +%%% @end +%%%----------------------------------------------------------------------------- + +%%------------------------------------------------------------------------------ +%% Banner +%%------------------------------------------------------------------------------ +-define(COPYRIGHT, "Copyright (C) 2012-2015, Feng Lee "). + +-define(LICENSE_MESSAGE, "Licensed under MIT"). + +-define(PROTOCOL_VERSION, "MQTT/3.1.1"). + +-define(ERTS_MINIMUM, "6.0"). + +%%------------------------------------------------------------------------------ +%% PubSub +%%------------------------------------------------------------------------------ +-type pubsub() :: publish | subscribe. + +%%------------------------------------------------------------------------------ +%% MQTT Topic +%%------------------------------------------------------------------------------ +-record(mqtt_topic, { + topic :: binary(), + node :: node() +}). + +-type mqtt_topic() :: #mqtt_topic{}. + +%%------------------------------------------------------------------------------ +%% MQTT Subscriber +%%------------------------------------------------------------------------------ +-record(mqtt_subscriber, { + topic :: binary(), + qos = 0 :: 0 | 1 | 2, + pid :: pid() +}). + +-type mqtt_subscriber() :: #mqtt_subscriber{}. + +%%------------------------------------------------------------------------------ +%% P2P Queue Subscriber +%%------------------------------------------------------------------------------ +-record(mqtt_queue, { + name :: binary(), + subpid :: pid(), + qos = 0 :: 0 | 1 | 2 +}). + +-type mqtt_queue() :: #mqtt_queue{}. + +%%------------------------------------------------------------------------------ +%% MQTT Client +%%------------------------------------------------------------------------------ +-record(mqtt_client, { + clientid :: binary(), + username :: binary() | undefined, + ipaddr :: inet:ip_address() +}). + +-type mqtt_client() :: #mqtt_client{}. + +%%------------------------------------------------------------------------------ +%% MQTT Session +%%------------------------------------------------------------------------------ +-record(mqtt_session, { + clientid, + session_pid, + subscriptions = [], + awaiting_ack, + awaiting_rel +}). + +-type mqtt_session() :: #mqtt_session{}. + +%%------------------------------------------------------------------------------ +%% MQTT Plugin +%%------------------------------------------------------------------------------ +-record(mqtt_plugin, { + name, + version, + attrs, + description +}). + +-type mqtt_plugin() :: #mqtt_plugin{}. + + diff --git a/plugins/emqttd_plugin_mysql/priv/.placeholder b/plugins/emqttd_plugin_mysql/priv/.placeholder new file mode 100644 index 000000000..e69de29bb diff --git a/plugins/emqttd_plugin_mysql/rebar b/plugins/emqttd_plugin_mysql/rebar new file mode 100755 index 000000000..36ef01107 Binary files /dev/null and b/plugins/emqttd_plugin_mysql/rebar differ diff --git a/plugins/emqttd_plugin_mysql/rebar.config b/plugins/emqttd_plugin_mysql/rebar.config new file mode 100755 index 000000000..df10d9593 --- /dev/null +++ b/plugins/emqttd_plugin_mysql/rebar.config @@ -0,0 +1,32 @@ +{port_specs, [ + {"priv/emqttd_plugin_mysql_app.so", [ + "c_src/*.c" + ]} +]}. + +{port_env, [ + {".*", "CXXFLAGS", "$CXXFLAGS -g -Wall -Werror -O3"}, + + {"(linux|solaris|freebsd|netbsd|openbsd|dragonfly|darwin)", + "LDFLAGS", "$LDFLAGS -lstdc++ -lcrypto"}, + + %% OS X Leopard flags for 64-bit + {"darwin9.*-64$", "CXXFLAGS", "-m64"}, + {"darwin9.*-64$", "LDFLAGS", "-arch x86_64"}, + + %% OS X Snow Leopard flags for 32-bit + {"darwin10.*-32$", "CXXFLAGS", "-m32"}, + {"darwin10.*-32$", "LDFLAGS", "-arch i386"}, + + %% This will merge into basho/rebar/rebar.config eventually + {"win32", "CFLAGS", "/Wall /DWIN32 /D_WINDOWS /D_WIN32 /DWINDOWS"}, + {"win32", "CXXFLAGS", "-g -Wall -O3"} +]}. + + +{eunit_opts, [ + verbose, + {report, { + eunit_surefire, [{dir,"."}] + }} +]}. diff --git a/plugins/emqttd_plugin_mysql/src/emqttd_acl_mysql.erl b/plugins/emqttd_plugin_mysql/src/emqttd_acl_mysql.erl new file mode 100644 index 000000000..a2c995367 --- /dev/null +++ b/plugins/emqttd_plugin_mysql/src/emqttd_acl_mysql.erl @@ -0,0 +1,70 @@ +%%%----------------------------------------------------------------------------- +%%% @Copyright (C) 2012-2015, Feng Lee +%%% +%%% Permission is hereby granted, free of charge, to any person obtaining a copy +%%% of this software and associated documentation files (the "Software"), to deal +%%% in the Software without restriction, including without limitation the rights +%%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +%%% copies of the Software, and to permit persons to whom the Software is +%%% furnished to do so, subject to the following conditions: +%%% +%%% The above copyright notice and this permission notice shall be included in all +%%% copies or substantial portions of the Software. +%%% +%%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +%%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +%%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +%%% SOFTWARE. +%%%----------------------------------------------------------------------------- +%%% @doc +%%% emqttd demo acl module. +%%% +%%% @end +%%%----------------------------------------------------------------------------- +-module(emqttd_acl_mysql). + +-include("emqttd.hrl"). + +-behaviour(emqttd_acl_mod). + +%% ACL callbacks +-export([init/1, check_acl/2, reload_acl/1, description/0]). +-record(state, {user_table, acl_table, acl_username_field, acl_topic_field, acl_rw_field, user_name_field, user_super_field}). + +init(Opts) -> + Mapper = proplists:get_value(field_mapper, Opts), + State = + #state{ + user_table = proplists:get_value(users_table, Opts, auth_user), + user_super_field = proplists:get_value(is_super, Mapper, is_superuser), + user_name_field = proplists:get_value(username, Mapper, username), + acl_table = proplists:get_value(acls_table, Opts, auth_acl), + acl_username_field = proplists:get_value(acl_username, Mapper, username), + acl_rw_field = proplists:get_value(acl_rw, Mapper, rw), + acl_topic_field = proplists:get_value(acl_topic, Mapper, topic) + }, + {ok, State}. + +check_acl({#mqtt_client{username = Username}, PubSub, Topic}, #state{user_table = UserTab, acl_table = AclTab, user_name_field = UsernameField, user_super_field = SuperField, acl_topic_field = TopicField, acl_username_field = AclUserField, acl_rw_field = AclRwField}) -> + Flag = case PubSub of publish -> 2; subscribe -> 1; pubsub -> 2 end, + Where = {'and', {'>=', AclRwField, Flag}, {TopicField, Topic}}, + Where1 = {'or', {AclUserField, Username}, {AclUserField, "*"}}, + Where2 = {'and', Where, Where1}, + case emysql:select(UserTab, {'and', {UsernameField, Username}, {SuperField, 1}}) of + {ok, []} -> + case emysql:select(UserTab, {UsernameField, Username}) of + {ok, []} -> ignore; + {ok, _} -> case emysql:select(AclTab, Where2) of + {ok, []} -> deny; + {ok, _Record} -> allow + end + end; + {ok, _} -> allow + end. + +reload_acl(_State) -> ok. + +description() -> "ACL Module by Mysql". diff --git a/plugins/emqttd_plugin_mysql/src/emqttd_auth_mysql.erl b/plugins/emqttd_plugin_mysql/src/emqttd_auth_mysql.erl new file mode 100644 index 000000000..a913dd587 --- /dev/null +++ b/plugins/emqttd_plugin_mysql/src/emqttd_auth_mysql.erl @@ -0,0 +1,110 @@ +%%%----------------------------------------------------------------------------- +%%% Copyright (c) 2012-2015 eMQTT.IO, All Rights Reserved. +%%% +%%% Permission is hereby granted, free of charge, to any person obtaining a copy +%%% of this software and associated documentation files (the "Software"), to deal +%%% in the Software without restriction, including without limitation the rights +%%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +%%% copies of the Software, and to permit persons to whom the Software is +%%% furnished to do so, subject to the following conditions: +%%% +%%% The above copyright notice and this permission notice shall be included in all +%%% copies or substantial portions of the Software. +%%% +%%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +%%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +%%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +%%% SOFTWARE. +%%%----------------------------------------------------------------------------- +%%% @doc +%%% emqttd authentication by mysql 'user' table. +%%% +%%% @end +%%%----------------------------------------------------------------------------- +-module(emqttd_auth_mysql). + +-author("Feng Lee "). + +-include("emqttd.hrl"). + +-behaviour(emqttd_auth_mod). + +-export([init/1, check/3, description/0]). + +-define(NOT_LOADED, not_loaded(?LINE)). + +-record(state, {user_table, name_field, pass_field, pass_hash}). + +init(Opts) -> + Mapper = proplists:get_value(field_mapper, Opts), + {ok, #state{user_table = proplists:get_value(user_table, Opts, auth_user), + name_field = proplists:get_value(username, Mapper), + pass_field = proplists:get_value(password, Mapper), + pass_hash = proplists:get_value(Opts, password_hash)}}. + +check(#mqtt_client{username = undefined}, _Password, _State) -> + {error, "Username undefined"}; +check(_Client, undefined, _State) -> + {error, "Password undefined"}; +check(#mqtt_client{username = Username}, Password, + #state{user_table = UserTab, pass_hash = Type, + name_field = NameField, pass_field = PassField}) -> + Where = {'and', {NameField, Username}, {PassField, hash(Type, Password)}}, + if Type =:= pbkdf2 -> + case emysql:select(UserTab, [PassField], {NameField, Username}) of + {ok, []} -> {error, "User not exist"}; + {ok, Records} -> + if length(Records) =:= 1 -> + case pbkdf2_check(Password, lists:nth(Records, 1)) of + true -> + {ok, []}; + false -> + {error, "UserName or Password is invalid"}; + ErrorInfo -> + {error, ErrorInfo} + end; + true -> + {error, "UserName is ambiguous"} + end + end; + true -> + case emysql:select(UserTab, Where) of + {ok, []} -> {error, "Username or Password "}; + {ok, _Record} -> ok + end + end. + +description() -> "Authentication by MySQL". + +hash(plain, Password) -> + Password; + +hash(md5, Password) -> + hexstring(crypto:hash(md5, Password)); + +hash(sha, Password) -> + hexstring(crypto:hash(sha, Password)). + +hexstring(<>) -> + lists:flatten(io_lib:format("~32.16.0b", [X])); + +hexstring(<>) -> + lists:flatten(io_lib:format("~40.16.0b", [X])). + +not_loaded(Line) -> + erlang:nif_error({not_loaded, [{module, ?MODULE}, {line, Line}]}). + +pbkdf2_check(Password, Pbkstr) -> + case nif_pbkdf2_check(Password, Pbkstr) of + {error, _} = Error -> + throw(Error); + IOData -> + IOData + end. + +nif_pbkdf2_check(Password, Pbkstr) -> + ?NOT_LOADED. + diff --git a/plugins/emqttd_plugin_mysql/src/emqttd_plugin_mysql.app.src b/plugins/emqttd_plugin_mysql/src/emqttd_plugin_mysql.app.src new file mode 100644 index 000000000..389bd33cb --- /dev/null +++ b/plugins/emqttd_plugin_mysql/src/emqttd_plugin_mysql.app.src @@ -0,0 +1,12 @@ +{application, emqttd_plugin_mysql, + [ + {description, "emqttd MySQL Authentication Plugin"}, + {vsn, "1.0"}, + {registered, []}, + {applications, [ + kernel, + stdlib + ]}, + {mod, {emqttd_plugin_mysql_app, []}}, + {env, []} + ]}. diff --git a/plugins/emqttd_plugin_mysql/src/emqttd_plugin_mysql_app.erl b/plugins/emqttd_plugin_mysql/src/emqttd_plugin_mysql_app.erl new file mode 100644 index 000000000..712e75ea6 --- /dev/null +++ b/plugins/emqttd_plugin_mysql/src/emqttd_plugin_mysql_app.erl @@ -0,0 +1,80 @@ +%%%----------------------------------------------------------------------------- +%%% Copyright (c) 2012-2015 eMQTT.IO, All Rights Reserved. +%%% +%%% Permission is hereby granted, free of charge, to any person obtaining a copy +%%% of this software and associated documentation files (the "Software"), to deal +%%% in the Software without restriction, including without limitation the rights +%%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +%%% copies of the Software, and to permit persons to whom the Software is +%%% furnished to do so, subject to the following conditions: +%%% +%%% The above copyright notice and this permission notice shall be included in all +%%% copies or substantial portions of the Software. +%%% +%%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +%%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +%%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +%%% SOFTWARE. +%%%----------------------------------------------------------------------------- +%%% @doc +%%% emqttd mysql authentication app. +%%% +%%% @end +%%%----------------------------------------------------------------------------- +-module(emqttd_plugin_mysql_app). +-on_load(init/0). +-behaviour(application). +%% Application callbacks +-export([start/2, prep_stop/1, stop/1, nif_pbkdf2_check/2]). + +-behaviour(supervisor). +%% Supervisor callbacks +-export([init/1]). +-define(NOT_LOADED, not_loaded(?LINE)). + + +%%%============================================================================= +%%% Application callbacks +%%%============================================================================= + +start(_StartType, _StartArgs) -> + Env = application:get_all_env(), + emqttd_access_control:register_mod(auth, emqttd_auth_mysql, Env), + emqttd_access_control:register_mod(acl, emqttd_acl_mysql, Env), + crypto:start(), + supervisor:start_link({local, ?MODULE}, ?MODULE, []). + +prep_stop(State) -> + emqttd_access_control:unregister_mod(auth, emqttd_auth_mysql), State, + emqttd_access_control:unregister_mod(acl, emqttd_acl_mysql), State, + crypto:stop(). + +stop(_State) -> + ok. + +init() -> + PrivDir = case code:priv_dir(?MODULE) of + {error, _} -> + EbinDir = filename:dirname(code:which(?MODULE)), + AppPath = filename:dirname(EbinDir), + filename:join(AppPath, "priv"); + Path -> + Path + end, + erlang:load_nif(filename:join(PrivDir, "emqttd_plugin_mysql_app"), 0). + +%%%============================================================================= +%%% Supervisor callbacks(Dummy) +%%%============================================================================= + +init([]) -> + {ok, {{one_for_one, 5, 10}, []}}. + +not_loaded(Line) -> + erlang:nif_error({not_loaded, [{module, ?MODULE}, {line, Line}]}). + +nif_pbkdf2_check(Password, Hash) -> + ?NOT_LOADED. diff --git a/plugins/emysql/src/emysql.erl b/plugins/emysql/src/emysql.erl index bc7cd352f..f6a620cfd 100644 --- a/plugins/emysql/src/emysql.erl +++ b/plugins/emysql/src/emysql.erl @@ -384,9 +384,15 @@ encode_where({like, Field, Value}) -> encode_where({'<', Field, Value}) -> atom_to_list(Field) ++ " < " ++ encode(Value); +encode_where({'<=', Field, Value}) -> + atom_to_list(Field) ++ " <= " ++ encode(Value); + encode_where({'>', Field, Value}) -> atom_to_list(Field) ++ " > " ++ encode(Value); +encode_where({'>=', Field, Value}) -> + atom_to_list(Field) ++ " >= " ++ encode(Value); + encode_where({'in', Field, Values}) -> InStr = string:join([encode(Value) || Value <- Values], ","), atom_to_list(Field) ++ " in (" ++ InStr ++ ")";