mirror of
https://github.com/fhmq/hmq.git
synced 2026-04-26 11:38:33 +00:00
'acl'
This commit is contained in:
@@ -54,6 +54,8 @@ $ go run main.go
|
||||
|
||||
* TLS/SSL Support
|
||||
|
||||
* Flexible ACL
|
||||
|
||||
### QUEUE SUBSCRIBE
|
||||
~~~
|
||||
| Prefix | Examples |
|
||||
|
||||
29
broker/auth.go
Normal file
29
broker/auth.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package broker
|
||||
|
||||
import (
|
||||
"hmq/lib/acl"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
PUB = 1
|
||||
SUB = 2
|
||||
)
|
||||
|
||||
func (c *client) CheckTopicAuth(typ int, topic string) bool {
|
||||
if !c.broker.config.Acl {
|
||||
return true
|
||||
}
|
||||
if strings.HasPrefix(topic, "$queue/") {
|
||||
topic = string([]byte(topic)[7:])
|
||||
if topic == "" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
ip := c.info.remoteIP
|
||||
username := string(c.info.username)
|
||||
clientid := string(c.info.clientID)
|
||||
aclInfo := c.broker.AclConfig
|
||||
return acl.CheckTopicAuth(aclInfo, typ, ip, username, clientid, topic)
|
||||
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package broker
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"hmq/lib/acl"
|
||||
"hmq/lib/message"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -18,6 +19,7 @@ type Broker struct {
|
||||
cid uint64
|
||||
config *Config
|
||||
tlsConfig *tls.Config
|
||||
AclConfig *acl.ACLConfig
|
||||
clients cMap
|
||||
routes cMap
|
||||
remotes cMap
|
||||
@@ -45,10 +47,22 @@ func NewBroker(config *Config) *Broker {
|
||||
}
|
||||
b.tlsConfig = tlsconfig
|
||||
}
|
||||
if b.config.Acl {
|
||||
aclconfig, err := acl.AclConfigLoad(b.config.AclConf)
|
||||
if err != nil {
|
||||
log.Error("Load acl conf error: ", err)
|
||||
return nil
|
||||
}
|
||||
b.AclConfig = aclconfig
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *Broker) Start() {
|
||||
if b == nil {
|
||||
log.Error("broker is null")
|
||||
return
|
||||
}
|
||||
if b.config.Port != "" {
|
||||
go b.StartListening(CLIENT)
|
||||
}
|
||||
|
||||
@@ -146,11 +146,16 @@ func (c *client) ProcessPublish(buf []byte) {
|
||||
c.Close()
|
||||
return
|
||||
}
|
||||
topic := msg.Topic()
|
||||
|
||||
if c.typ != CLIENT || !c.CheckTopicAuth(PUB, string(topic)) {
|
||||
return
|
||||
}
|
||||
c.ProcessPublishMessage(buf, msg)
|
||||
|
||||
if msg.Retain() {
|
||||
if b := c.broker; b != nil {
|
||||
err := b.rl.Insert(msg.Topic(), buf)
|
||||
err := b.rl.Insert(topic, buf)
|
||||
if err != nil {
|
||||
log.Error("Insert Retain Message error: ", err)
|
||||
}
|
||||
@@ -246,12 +251,13 @@ func (c *client) ProcessSubscribe(buf []byte) {
|
||||
for i, t := range topics {
|
||||
topic := string(t)
|
||||
//check topic auth for client
|
||||
// if !c.CheckTopicAuth(topic, SUB) {
|
||||
// log.Error("CheckSubAuth failed")
|
||||
// retcodes = append(retcodes, message.QosFailure)
|
||||
// continue
|
||||
// }
|
||||
|
||||
if c.typ == CLIENT {
|
||||
if !c.CheckTopicAuth(SUB, topic) {
|
||||
log.Error("CheckSubAuth failed")
|
||||
retcodes = append(retcodes, message.QosFailure)
|
||||
continue
|
||||
}
|
||||
}
|
||||
if _, exist := c.subs[topic]; !exist {
|
||||
queue := false
|
||||
if strings.HasPrefix(topic, "$queue/") {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package broker
|
||||
|
||||
import "sync"
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
type RetainList struct {
|
||||
sync.RWMutex
|
||||
|
||||
12
conf/acl.conf
Normal file
12
conf/acl.conf
Normal file
@@ -0,0 +1,12 @@
|
||||
## pub 1 , sub 2, pubsub 3
|
||||
## %c is clientid , %s is username
|
||||
##auth type value pub/sub topic
|
||||
allow ip 127.0.0.1 2 $SYS/#
|
||||
allow clientid 0001 3 #
|
||||
deny username admin 3 #
|
||||
allow username joy 3 /test,hello/world
|
||||
allow clientid * 1 toCloud/%c
|
||||
allow username * 1 toCloud/%u
|
||||
allow clientid * 2 toDevice/%c
|
||||
allow username * 2 toDevice/%u
|
||||
deny clientid * 3 #
|
||||
114
lib/acl/acl.go
Normal file
114
lib/acl/acl.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package acl
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
PUB = 1
|
||||
SUB = 2
|
||||
PUBSUB = 3
|
||||
CLIENTID = "clientid"
|
||||
USERNAME = "username"
|
||||
IP = "ip"
|
||||
ALLOW = "allow"
|
||||
DENY = "deny"
|
||||
)
|
||||
|
||||
type AuthInfo struct {
|
||||
Auth string
|
||||
Typ string
|
||||
Val string
|
||||
PubSub int
|
||||
Topics []string
|
||||
}
|
||||
|
||||
type ACLConfig struct {
|
||||
File string
|
||||
Info []*AuthInfo
|
||||
}
|
||||
|
||||
func AclConfigLoad(file string) (*ACLConfig, error) {
|
||||
if file == "" {
|
||||
file = "./conf/acl.conf"
|
||||
}
|
||||
aclconifg := &ACLConfig{
|
||||
File: file,
|
||||
Info: make([]*AuthInfo, 0, 4),
|
||||
}
|
||||
err := aclconifg.Prase()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return aclconifg, err
|
||||
}
|
||||
|
||||
func (c *ACLConfig) Prase() error {
|
||||
f, err := os.Open(c.File)
|
||||
defer f.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
buf := bufio.NewReader(f)
|
||||
var parseErr error
|
||||
for {
|
||||
line, err := buf.ReadString('\n')
|
||||
line = strings.TrimSpace(line)
|
||||
if isCommentOut(line) {
|
||||
continue
|
||||
}
|
||||
if line == "" {
|
||||
return parseErr
|
||||
}
|
||||
// fmt.Println(line)
|
||||
tmpArr := strings.Fields(line)
|
||||
if len(tmpArr) != 5 {
|
||||
parseErr = errors.New("\"" + line + "\" format is error")
|
||||
break
|
||||
}
|
||||
if tmpArr[0] != ALLOW && tmpArr[0] != DENY {
|
||||
parseErr = errors.New("\"" + line + "\" format is error")
|
||||
break
|
||||
}
|
||||
if tmpArr[1] != CLIENTID && tmpArr[1] != USERNAME && tmpArr[1] != IP {
|
||||
parseErr = errors.New("\"" + line + "\" format is error")
|
||||
break
|
||||
}
|
||||
var pubsub int
|
||||
pubsub, err = strconv.Atoi(tmpArr[3])
|
||||
if err != nil {
|
||||
parseErr = errors.New("\"" + line + "\" format is error")
|
||||
break
|
||||
}
|
||||
topicStr := strings.Replace(tmpArr[4], " ", "", -1)
|
||||
topicStr = strings.Replace(topicStr, "\n", "", -1)
|
||||
topics := strings.Split(topicStr, ",")
|
||||
tmpAuth := &AuthInfo{
|
||||
Auth: tmpArr[0],
|
||||
Typ: tmpArr[1],
|
||||
Val: tmpArr[2],
|
||||
Topics: topics,
|
||||
PubSub: pubsub,
|
||||
}
|
||||
c.Info = append(c.Info, tmpAuth)
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
parseErr = err
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
return parseErr
|
||||
}
|
||||
func isCommentOut(line string) bool {
|
||||
if strings.HasPrefix(line, "#") || strings.HasPrefix(line, ";") || strings.HasPrefix(line, "//") || strings.HasPrefix(line, "*") {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
148
lib/acl/aclcheck.go
Normal file
148
lib/acl/aclcheck.go
Normal file
@@ -0,0 +1,148 @@
|
||||
package acl
|
||||
|
||||
import "strings"
|
||||
|
||||
func CheckTopicAuth(ACLInfo *ACLConfig, typ int, ip, username, clientid, topic string) bool {
|
||||
for _, info := range ACLInfo.Info {
|
||||
ctyp := info.Typ
|
||||
switch ctyp {
|
||||
case CLIENTID:
|
||||
if match, auth := info.checkWithClientID(typ, clientid, topic); match {
|
||||
return auth
|
||||
}
|
||||
case USERNAME:
|
||||
if match, auth := info.checkWithUsername(typ, username, topic); match {
|
||||
return auth
|
||||
}
|
||||
case IP:
|
||||
if match, auth := info.checkWithIP(typ, ip, topic); match {
|
||||
return auth
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (a *AuthInfo) checkWithClientID(typ int, clientid, topic string) (bool, bool) {
|
||||
auth := false
|
||||
match := false
|
||||
if a.Val == "*" || a.Val == clientid {
|
||||
for _, tp := range a.Topics {
|
||||
des := strings.Replace(tp, "%c", clientid, -1)
|
||||
if typ == PUB {
|
||||
if pubTopicMatch(topic, des) {
|
||||
match = true
|
||||
auth = a.checkAuth(PUB)
|
||||
}
|
||||
} else if typ == SUB {
|
||||
if subTopicMatch(topic, des) {
|
||||
match = true
|
||||
auth = a.checkAuth(SUB)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return match, auth
|
||||
}
|
||||
|
||||
func (a *AuthInfo) checkWithUsername(typ int, username, topic string) (bool, bool) {
|
||||
auth := false
|
||||
match := false
|
||||
if a.Val == "*" || a.Val == username {
|
||||
for _, tp := range a.Topics {
|
||||
des := strings.Replace(tp, "%u", username, -1)
|
||||
if typ == PUB {
|
||||
if pubTopicMatch(topic, des) {
|
||||
match = true
|
||||
auth = a.checkAuth(PUB)
|
||||
}
|
||||
} else if typ == SUB {
|
||||
if subTopicMatch(topic, des) {
|
||||
match = true
|
||||
auth = a.checkAuth(SUB)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return match, auth
|
||||
}
|
||||
|
||||
func (a *AuthInfo) checkWithIP(typ int, ip, topic string) (bool, bool) {
|
||||
auth := false
|
||||
match := false
|
||||
if a.Val == "*" || a.Val == ip {
|
||||
for _, tp := range a.Topics {
|
||||
des := tp
|
||||
if typ == PUB {
|
||||
if pubTopicMatch(topic, des) {
|
||||
auth = a.checkAuth(PUB)
|
||||
match = true
|
||||
}
|
||||
} else if typ == SUB {
|
||||
if subTopicMatch(topic, des) {
|
||||
auth = a.checkAuth(SUB)
|
||||
match = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return match, auth
|
||||
}
|
||||
|
||||
func (a *AuthInfo) checkAuth(typ int) bool {
|
||||
auth := false
|
||||
if typ == PUB {
|
||||
if a.Auth == ALLOW && (a.PubSub == PUB || a.PubSub == PUBSUB) {
|
||||
auth = true
|
||||
} else if a.Auth == DENY && a.PubSub == SUB {
|
||||
auth = true
|
||||
}
|
||||
} else if typ == SUB {
|
||||
if a.Auth == ALLOW && (a.PubSub == SUB || a.PubSub == PUBSUB) {
|
||||
auth = true
|
||||
} else if a.Auth == DENY && a.PubSub == PUB {
|
||||
auth = true
|
||||
}
|
||||
}
|
||||
return auth
|
||||
}
|
||||
|
||||
func pubTopicMatch(pub, des string) bool {
|
||||
dest, _ := SubscribeTopicSpilt(des)
|
||||
topic, _ := PublishTopicSpilt(pub)
|
||||
for i, t := range dest {
|
||||
if i > len(topic)-1 {
|
||||
return false
|
||||
}
|
||||
if t == "#" {
|
||||
return true
|
||||
}
|
||||
if t == "+" || t == topic[i] {
|
||||
continue
|
||||
}
|
||||
if t != topic[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func subTopicMatch(pub, des string) bool {
|
||||
dest, _ := SubscribeTopicSpilt(des)
|
||||
topic, _ := SubscribeTopicSpilt(pub)
|
||||
for i, t := range dest {
|
||||
if i > len(topic)-1 {
|
||||
return false
|
||||
}
|
||||
if t == "#" {
|
||||
return true
|
||||
}
|
||||
if t == "+" || "+" == topic[i] || t == topic[i] {
|
||||
continue
|
||||
}
|
||||
if t != topic[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
52
lib/acl/spilt.go
Normal file
52
lib/acl/spilt.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package acl
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func SubscribeTopicSpilt(topic string) ([]string, error) {
|
||||
subject := []byte(topic)
|
||||
if bytes.IndexByte(subject, '#') != -1 {
|
||||
if bytes.IndexByte(subject, '#') != len(subject)-1 {
|
||||
return nil, errors.New("Topic format error with index of #")
|
||||
}
|
||||
}
|
||||
re := strings.Split(topic, "/")
|
||||
for i, v := range re {
|
||||
if i != 0 && i != (len(re)-1) {
|
||||
if v == "" {
|
||||
return nil, errors.New("Topic format error with index of //")
|
||||
}
|
||||
if strings.Contains(v, "+") && v != "+" {
|
||||
return nil, errors.New("Topic format error with index of +")
|
||||
}
|
||||
} else {
|
||||
if v == "" {
|
||||
re[i] = "/"
|
||||
}
|
||||
}
|
||||
}
|
||||
return re, nil
|
||||
|
||||
}
|
||||
|
||||
func PublishTopicSpilt(topic string) ([]string, error) {
|
||||
subject := []byte(topic)
|
||||
if bytes.IndexByte(subject, '#') != -1 || bytes.IndexByte(subject, '+') != -1 {
|
||||
return nil, errors.New("Publish Topic format error with + and #")
|
||||
}
|
||||
re := strings.Split(topic, "/")
|
||||
for i, v := range re {
|
||||
if v == "" {
|
||||
if i != 0 && i != (len(re)-1) {
|
||||
return nil, errors.New("Topic format error with index of //")
|
||||
} else {
|
||||
re[i] = "/"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return re, nil
|
||||
}
|
||||
Reference in New Issue
Block a user