4
0
mirror of https://github.com/cwinfo/matterbridge.git synced 2025-06-26 20:09:24 +00:00

Compare commits

...

47 Commits

Author SHA1 Message Date
Wim
c2c135bca2 Release v0.6.0-beta1 2016-08-20 18:09:00 +02:00
Wim
eb20cb237d Update documentation 2016-08-20 18:08:59 +02:00
Wim
106404d32f Fix info message 2016-08-20 18:08:59 +02:00
Wim
e06efbad9f Remove unused code 2016-08-20 18:08:58 +02:00
Wim
3311c7f923 Refactor handleReceive 2016-08-20 18:08:58 +02:00
Wim
3a6c655dfb Remove redundant function 2016-08-20 18:08:58 +02:00
Wim
e11d786775 Move nickformatting into bridge 2016-08-20 18:08:57 +02:00
Wim
889b6debc4 Add Connect() to Bridger interface 2016-08-20 18:08:57 +02:00
Wim
9cb3413d9c Add Enable per section (protocol) instead of in general section 2016-08-20 18:08:57 +02:00
Wim
131826e1d1 Fix crash on exit 2016-08-19 22:58:42 +02:00
Wim
96e21dd051 Add documentation about breaking API changes for mattermost 3.3.0. Start work on 0.6.0-dev 2016-08-15 21:11:50 +02:00
Wim
32e5f396e7 Make sure login works after logout 2016-08-15 20:27:36 +02:00
Wim
6c6000dbbd Update code to mattermost 3.3.0 API changes 2016-08-15 18:49:17 +02:00
Wim
24defcb970 Sync with mattermost 3.3.0 2016-08-15 18:47:31 +02:00
Wim
a1a11a88b3 Fix nil pointers 2016-08-14 23:04:28 +02:00
Wim
a997ae29ad Add StatusLoop(), keeps connection alive 2016-08-14 22:54:57 +02:00
Wim
ff94796700 Refactor bridge. Allows bridging between every protocol 2016-08-14 22:44:59 +02:00
Wim
1f72ca4c4e Add initial XMPP support 2016-08-14 22:40:26 +02:00
Wim
46faad8b57 Vendor go-xmpp 2016-08-14 22:40:25 +02:00
Wim
30f30364d5 Release v0.5.0 2016-07-27 22:42:59 +02:00
Wim
073d90da88 Fix docker build 2016-07-23 00:03:54 +02:00
Wim
c769e23a9a Fix crash on invalid team 2016-07-22 23:47:25 +02:00
Wim
9db48f4794 Update readme 2016-07-22 23:36:54 +02:00
Wim
911c597377 Sync with mattermost 3.2.0 2016-07-22 23:15:59 +02:00
Wim
28244ffd9a Fix pointer reuse problem 2016-07-22 23:04:08 +02:00
Wim
3e38c7945c Actually add sasl.go 2016-07-22 22:51:11 +02:00
Wim
79ffb76f6e Add (PLAIN) SASL support 2016-07-21 23:47:44 +02:00
Wim
5fe4b749cf Do not check bindaddress when not using the server 2016-07-17 22:15:19 +02:00
Wim
6991d85da9 Add FAQ section 2016-07-15 21:59:14 +02:00
Wim
c1c187a1ab Fix markdown 2016-07-12 22:00:38 +02:00
Wim
055d12e3ef Release v0.5.0-beta1 2016-07-12 21:32:15 +02:00
Wim
b49429d722 Add migration info 2016-07-12 01:23:36 +02:00
Wim
815c7f8d64 Update version 2016-07-12 01:07:37 +02:00
Wim
c879f79456 Update documentation 2016-07-12 01:02:56 +02:00
Wim
3bc25f4707 Update documentation 2016-07-12 00:25:32 +02:00
Wim
300cfe044a Remove token check 2016-07-12 00:23:36 +02:00
Wim
fb586f4a96 Remove Port from IRC config. Specify it with server 2016-07-11 23:30:42 +02:00
Wim
ced371bece Add port to BindAddress 2016-07-11 23:22:56 +02:00
Wim
a87cac1982 Remove multiple Token config. Use same channel setup as from matterbridge-plus 2016-07-11 22:55:58 +02:00
Wim
8fb5c7afa6 Remove UseSlackCircumfix. Use RemoteNickFormat 2016-07-11 21:26:13 +02:00
Wim
aceb830378 Converge with matterbridge-plus 2016-07-11 21:23:33 +02:00
Wim
0f2976c5ce Release v0.4.2 2016-06-23 20:31:12 +02:00
Wim
78b17977c5 Fix mattermost 3.1.0 API change. Closes #24 2016-06-23 20:29:51 +02:00
Wim
6ec77e06ea Sync with mattermost 3.1.0 2016-06-23 20:28:05 +02:00
Wim
e48db67649 Fix sample documentation. Closes #23 2016-06-16 21:01:46 +02:00
Wim
e03f331f55 Add Dockerfile 2016-05-21 16:30:39 +02:00
Wim
ff5aeeb1e1 Add support for ignoring messages from specific users. 2016-05-21 16:03:19 +02:00
68 changed files with 5674 additions and 841 deletions

11
Dockerfile Normal file
View File

@ -0,0 +1,11 @@
FROM alpine:edge
ENTRYPOINT ["/bin/matterbridge"]
COPY . /go/src/github.com/42wim/matterbridge
RUN apk update && apk add go git gcc musl-dev ca-certificates \
&& cd /go/src/github.com/42wim/matterbridge \
&& export GOPATH=/go \
&& go get \
&& go build -o /bin/matterbridge \
&& rm -rf /go \
&& apk del --purge git go gcc musl-dev

118
README.md
View File

@ -1,14 +1,34 @@
# matterbridge
Simple bridge between mattermost and IRC. Uses the in/outgoing webhooks.
Relays public channel messages between mattermost and IRC.
Simple bridge between mattermost and IRC.
Requires mattermost 1.2.0+
* Relays public channel messages between mattermost and IRC.
* Supports multiple mattermost and irc channels.
* Matterbridge -plus also works with private groups on your mattermost.
There is also [matterbridge-plus] (https://github.com/42wim/matterbridge-plus) which uses the mattermost API and needs a dedicated user (bot). But requires no incoming/outgoing webhook setup.
This project has now [matterbridge-plus](https://github.com/42wim/matterbridge-plus/) merged in.
Breaking changes for matterbridge can be found in [migration](https://github.com/42wim/matterbridge/blob/master/migration.md)
Look at [matterbridge.conf.sample] (https://github.com/42wim/matterbridge/blob/master/matterbridge.conf.sample) for an example.
Configuration changes since v0.5.0 can be found in [changelog.md] (https://github.com/42wim/matterbridge/blob/master/changelog.md)
## Requirements:
* [Mattermost] (https://github.com/mattermost/platform/)
### Compatibility
* Matterbridge v0.6.0 works with mattermost 3.3.0 and higher [3.3.0 release](https://github.com/mattermost/platform/releases/tag/v3.3.0)
* Matterbridge v0.5.0 works with mattermost 3.0.0 - 3.2.0 [3.2.0 release](https://github.com/mattermost/platform/releases/tag/v3.2.0)
### Webhooks version
* Configured incoming/outgoing [webhooks](https://www.mattermost.org/webhooks/) on your mattermost instance.
### Plus (API) version
* A dedicated user(bot) on your mattermost instance.
## binaries
Binaries can be found [here] (https://github.com/42wim/matterbridge/releases/tag/v0.4)
Binaries can be found [here] (https://github.com/42wim/matterbridge/releases/)
* For use with mattermost 3.3.0 [v0.6.0-beta1](https://github.com/42wim/matterircd/releases/tag/v0.6.0-beta1)
* For use with mattermost 3.0.0-3.2.0 [v0.5.0](https://github.com/42wim/matterircd/releases/tag/v0.5.0)
## building
Go 1.6+ is required. Make sure you have [Go](https://golang.org/doc/install) properly installed, including setting up your [GOPATH] (https://golang.org/doc/code.html#GOPATH)
@ -31,73 +51,26 @@ matterbridge
3) Now you can run matterbridge.
```
Usage of matterbridge:
-conf="matterbridge.conf": config file
Usage of ./matterbridge:
-conf string
config file (default "matterbridge.conf")
-debug
enable debug
-plus
running using API instead of webhooks (deprecated, set Plus flag in [general] config)
-version
show version
```
Matterbridge will:
* start a webserver listening on the port specified in the configuration.
* connect to specified irc server and channel.
* send messages from mattermost to irc and vice versa, messages in mattermost will appear with irc-nick
## config
### matterbridge
matterbridge looks for matterbridge.conf in current directory. (use -conf to specify another file)
Look at matterbridge.conf.sample for an example
```
[IRC]
server="irc.freenode.net"
port=6667
UseTLS=false
SkipTLSVerify=true
nick="matterbot"
channel="#matterbridge"
UseSlackCircumfix=false
[mattermost]
#url is your incoming webhook url (account settings - integrations - incoming webhooks)
url="http://mattermost.yourdomain.com/hooks/incomingwebhookkey"
#port the bridge webserver will listen on
port=9999
#address the webserver will bind to
BindAddress="0.0.0.0"
showjoinpart=true #show irc users joining and parting
#the token you get from the outgoing webhook in mattermost. If empty no token check will be done.
#if you use multiple IRC channel (see below, this must be empty!)
token=yourtokenfrommattermost
#disable certificate checking (selfsigned certificates)
#SkipTLSVerify=true
#whether to prefix messages from IRC to mattermost with the sender's nick. Useful if username overrides for incoming webhooks isn't enabled on the mattermost server
PrefixMessagesWithNick=false
#how to format the list of IRC nicks when displayed in mattermost. Possible options are "table" and "plain"
NickFormatter=plain
#how many nicks to list per row for formatters that support this
NicksPerRow=4
#Freenode nickserv
NickServNick="nickserv"
#Password for nickserv
NickServPassword="secret"
#multiple channel config
#token you can find in your outgoing webhook
[Token "outgoingwebhooktoken1"]
IRCChannel="#off-topic"
MMChannel="off-topic"
[Token "outgoingwebhooktoken2"]
IRCChannel="#testing"
MMChannel="testing"
[general]
#request your API key on https://github.com/giphy/GiphyAPI. This is a public beta key
GiphyApiKey="dc6zaTOxFJmzC"
```
Look at [matterbridge.conf.sample] (https://github.com/42wim/matterbridge/blob/master/matterbridge.conf.sample) for an example.
### mattermost
You'll have to configure the incoming en outgoing webhooks.
#### webhooks version
You'll have to configure the incoming and outgoing webhooks.
* incoming webhooks
Go to "account settings" - integrations - "incoming webhooks".
@ -108,5 +81,20 @@ This URL should be set in the matterbridge.conf in the [mattermost] section (see
Go to "account settings" - integrations - "outgoing webhooks".
Choose a channel (the same as the one from incoming webhooks) and fill in the address and port of the server matterbridge will run on.
e.g. http://192.168.1.1:9999 (9999 is the port specified in [mattermost] section of matterbridge.conf)
e.g. http://192.168.1.1:9999 (192.168.1.1:9999 is the BindAddress specified in [mattermost] section of matterbridge.conf)
#### plus version
You'll have to create a new dedicated user on your mattermost instance.
Specify the login and password in [mattermost] section of matterbridge.conf
## FAQ
Please look at [matterbridge.conf.sample] (https://github.com/42wim/matterbridge/blob/master/matterbridge.conf.sample) for more information first.
### Mattermost doesn't show the IRC nicks
If you're running the webhooks version, this can be fixed by either:
* enabling "override usernames". See [mattermost documentation](http://docs.mattermost.com/developer/webhooks-incoming.html#enabling-incoming-webhooks)
* setting ```PrefixMessagesWithNick``` to ```true``` in ```mattermost``` section of your matterbridge.conf.
If you're running the plus version you'll need to:
* setting ```PrefixMessagesWithNick``` to ```true``` in ```mattermost``` section of your matterbridge.conf.
Also look at the ```RemoteNickFormat``` setting.

134
bridge/bridge.go Normal file
View File

@ -0,0 +1,134 @@
package bridge
import (
//"fmt"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/irc"
"github.com/42wim/matterbridge/bridge/mattermost"
"github.com/42wim/matterbridge/bridge/xmpp"
log "github.com/Sirupsen/logrus"
"strings"
)
type Bridge struct {
*config.Config
Source string
Bridges []Bridger
Channels []map[string]string
ignoreNicks map[string][]string
}
type Bridger interface {
Send(msg config.Message) error
Name() string
Connect() error
//Command(cmd string) string
}
func NewBridge(cfg *config.Config) error {
c := make(chan config.Message)
b := &Bridge{}
b.Config = cfg
if cfg.IRC.Enable {
b.Bridges = append(b.Bridges, birc.New(cfg, c))
}
if cfg.Mattermost.Enable {
b.Bridges = append(b.Bridges, bmattermost.New(cfg, c))
}
if cfg.Xmpp.Enable {
b.Bridges = append(b.Bridges, bxmpp.New(cfg, c))
}
if len(b.Bridges) < 2 {
log.Fatalf("only %d sections enabled. Need at least 2 sections enabled (eg [IRC] and [mattermost]", len(b.Bridges))
}
for _, br := range b.Bridges {
br.Connect()
}
b.mapChannels()
b.mapIgnores()
b.handleReceive(c)
return nil
}
func (b *Bridge) handleReceive(c chan config.Message) {
for {
select {
case msg := <-c:
for _, br := range b.Bridges {
b.handleMessage(msg, br)
}
}
}
}
func (b *Bridge) mapChannels() error {
for _, val := range b.Config.Channel {
m := make(map[string]string)
m["irc"] = val.IRC
m["mattermost"] = val.Mattermost
m["xmpp"] = val.Xmpp
b.Channels = append(b.Channels, m)
}
return nil
}
func (b *Bridge) mapIgnores() {
m := make(map[string][]string)
m["irc"] = strings.Fields(b.Config.IRC.IgnoreNicks)
m["mattermost"] = strings.Fields(b.Config.Mattermost.IgnoreNicks)
m["xmpp"] = strings.Fields(b.Config.Mattermost.IgnoreNicks)
b.ignoreNicks = m
}
func (b *Bridge) getDestChannel(msg *config.Message, dest string) string {
for _, v := range b.Channels {
if v[msg.Origin] == msg.Channel {
return v[dest]
}
}
return ""
}
func (b *Bridge) handleMessage(msg config.Message, dest Bridger) {
if b.ignoreMessage(&msg) {
return
}
if dest.Name() != msg.Origin {
msg.Channel = b.getDestChannel(&msg, dest.Name())
if msg.Channel == "" {
return
}
b.modifyMessage(&msg, dest.Name())
dest.Send(msg)
}
}
func (b *Bridge) ignoreMessage(msg *config.Message) bool {
// should we discard messages ?
for _, entry := range b.ignoreNicks[msg.Origin] {
if msg.Username == entry {
return true
}
}
return false
}
func setNickFormat(msg *config.Message, format string) {
if format == "" {
msg.Username = msg.Origin + "-" + msg.Username + ": "
return
}
msg.Username = strings.Replace(format, "{NICK}", msg.Username, -1)
msg.Username = strings.Replace(msg.Username, "{BRIDGE}", msg.Origin, -1)
}
func (b *Bridge) modifyMessage(msg *config.Message, dest string) {
switch dest {
case "irc":
setNickFormat(msg, b.Config.IRC.RemoteNickFormat)
case "xmpp":
setNickFormat(msg, b.Config.Xmpp.RemoteNickFormat)
case "mattermost":
setNickFormat(msg, b.Config.Mattermost.RemoteNickFormat)
}
}

84
bridge/config/config.go Normal file
View File

@ -0,0 +1,84 @@
package config
import (
"gopkg.in/gcfg.v1"
"io/ioutil"
"log"
)
type Message struct {
Text string
Channel string
Username string
Origin string
}
type Config struct {
IRC struct {
UseTLS bool
UseSASL bool
SkipTLSVerify bool
Server string
Nick string
Password string
Channel string
NickServNick string
NickServPassword string
RemoteNickFormat string
IgnoreNicks string
Enable bool
}
Mattermost struct {
URL string
ShowJoinPart bool
IconURL string
SkipTLSVerify bool
BindAddress string
Channel string
PrefixMessagesWithNick bool
NicksPerRow int
NickFormatter string
Server string
Team string
Login string
Password string
RemoteNickFormat string
IgnoreNicks string
NoTLS bool
Enable bool
}
Xmpp struct {
Jid string
Password string
Server string
Muc string
Nick string
RemoteNickFormat string
Enable bool
}
Channel map[string]*struct {
IRC string
Mattermost string
Xmpp string
}
General struct {
GiphyAPIKey string
Xmpp bool
Irc bool
Mattermost bool
Plus bool
}
}
func NewConfig(cfgfile string) *Config {
var cfg Config
content, err := ioutil.ReadFile(cfgfile)
if err != nil {
log.Fatal(err)
}
err = gcfg.ReadStringInto(&cfg, string(content))
if err != nil {
log.Fatal("Failed to parse "+cfgfile+":", err)
}
return &cfg
}

59
bridge/irc/helper.go Normal file
View File

@ -0,0 +1,59 @@
package birc
import (
"strings"
)
func tableformatter(nicks []string, nicksPerRow int, continued bool) string {
result := "|IRC users"
if continued {
result = "|(continued)"
}
for i := 0; i < 2; i++ {
for j := 1; j <= nicksPerRow && j <= len(nicks); j++ {
if i == 0 {
result += "|"
} else {
result += ":-|"
}
}
result += "\r\n|"
}
result += nicks[0] + "|"
for i := 1; i < len(nicks); i++ {
if i%nicksPerRow == 0 {
result += "\r\n|" + nicks[i] + "|"
} else {
result += nicks[i] + "|"
}
}
return result
}
func plainformatter(nicks []string, nicksPerRow int) string {
return strings.Join(nicks, ", ") + " currently on IRC"
}
func IsMarkup(message string) bool {
switch message[0] {
case '|':
fallthrough
case '#':
fallthrough
case '_':
fallthrough
case '*':
fallthrough
case '~':
fallthrough
case '-':
fallthrough
case ':':
fallthrough
case '>':
fallthrough
case '=':
return true
}
return false
}

194
bridge/irc/irc.go Normal file
View File

@ -0,0 +1,194 @@
package birc
import (
"crypto/tls"
"github.com/42wim/matterbridge/bridge/config"
log "github.com/Sirupsen/logrus"
ircm "github.com/sorcix/irc"
"github.com/thoj/go-ircevent"
"sort"
"strconv"
"strings"
"time"
)
//type Bridge struct {
type Birc struct {
i *irc.Connection
ircNick string
ircMap map[string]string
names map[string][]string
ircIgnoreNicks []string
*config.Config
Remote chan config.Message
}
type FancyLog struct {
irc *log.Entry
}
var flog FancyLog
func init() {
flog.irc = log.WithFields(log.Fields{"module": "irc"})
}
func New(config *config.Config, c chan config.Message) *Birc {
b := &Birc{}
b.Config = config
b.Remote = c
b.ircNick = b.Config.IRC.Nick
b.ircMap = make(map[string]string)
b.names = make(map[string][]string)
b.ircIgnoreNicks = strings.Fields(b.Config.IRC.IgnoreNicks)
return b
}
func (b *Birc) Command(msg *config.Message) string {
switch msg.Text {
case "!users":
b.i.SendRaw("NAMES " + msg.Channel)
}
return ""
}
func (b *Birc) Connect() error {
flog.irc.Info("Trying IRC connection")
i := irc.IRC(b.Config.IRC.Nick, b.Config.IRC.Nick)
i.UseTLS = b.Config.IRC.UseTLS
i.UseSASL = b.Config.IRC.UseSASL
i.SASLLogin = b.Config.IRC.NickServNick
i.SASLPassword = b.Config.IRC.NickServPassword
i.TLSConfig = &tls.Config{InsecureSkipVerify: b.Config.IRC.SkipTLSVerify}
if b.Config.IRC.Password != "" {
i.Password = b.Config.IRC.Password
}
i.AddCallback(ircm.RPL_WELCOME, b.handleNewConnection)
err := i.Connect(b.Config.IRC.Server)
if err != nil {
return err
}
flog.irc.Info("Connection succeeded")
b.i = i
return nil
}
func (b *Birc) Name() string {
return "irc"
}
func (b *Birc) Send(msg config.Message) error {
if msg.Origin == "irc" {
return nil
}
if strings.HasPrefix(msg.Text, "!") {
b.Command(&msg)
return nil
}
b.i.Privmsg(msg.Channel, msg.Username+msg.Text)
return nil
}
func (b *Birc) endNames(event *irc.Event) {
channel := event.Arguments[1]
sort.Strings(b.names[channel])
maxNamesPerPost := (300 / b.nicksPerRow()) * b.nicksPerRow()
continued := false
for len(b.names[channel]) > maxNamesPerPost {
b.Remote <- config.Message{Username: b.ircNick, Text: b.formatnicks(b.names[channel][0:maxNamesPerPost], continued), Channel: channel, Origin: "irc"}
b.names[channel] = b.names[channel][maxNamesPerPost:]
continued = true
}
b.Remote <- config.Message{Username: b.ircNick, Text: b.formatnicks(b.names[channel], continued), Channel: channel, Origin: "irc"}
b.names[channel] = nil
}
func (b *Birc) handleNewConnection(event *irc.Event) {
flog.irc.Info("Registering callbacks")
i := b.i
b.ircNick = event.Arguments[0]
i.AddCallback("PRIVMSG", b.handlePrivMsg)
i.AddCallback("CTCP_ACTION", b.handlePrivMsg)
i.AddCallback(ircm.RPL_TOPICWHOTIME, b.handleTopicWhoTime)
i.AddCallback(ircm.RPL_ENDOFNAMES, b.endNames)
i.AddCallback(ircm.RPL_NAMREPLY, b.storeNames)
i.AddCallback(ircm.NOTICE, b.handleNotice)
i.AddCallback(ircm.RPL_MYINFO, func(e *irc.Event) { flog.irc.Infof("%s: %s", e.Code, strings.Join(e.Arguments[1:], " ")) })
i.AddCallback("PING", func(e *irc.Event) {
i.SendRaw("PONG :" + e.Message())
flog.irc.Debugf("PING/PONG")
})
if b.Config.Mattermost.ShowJoinPart {
i.AddCallback("JOIN", b.handleJoinPart)
i.AddCallback("PART", b.handleJoinPart)
}
i.AddCallback("*", b.handleOther)
b.setupChannels()
}
func (b *Birc) handleJoinPart(event *irc.Event) {
//b.Send(b.ircNick, b.ircNickFormat(event.Nick)+" "+strings.ToLower(event.Code)+"s "+event.Message(), b.getMMChannel(event.Arguments[0]))
}
func (b *Birc) handleNotice(event *irc.Event) {
if strings.Contains(event.Message(), "This nickname is registered") {
b.i.Privmsg(b.Config.IRC.NickServNick, "IDENTIFY "+b.Config.IRC.NickServPassword)
}
}
func (b *Birc) handleOther(event *irc.Event) {
flog.irc.Debugf("%#v", event)
}
func (b *Birc) handlePrivMsg(event *irc.Event) {
flog.irc.Debugf("handlePrivMsg() %s %s", event.Nick, event.Message())
msg := ""
if event.Code == "CTCP_ACTION" {
msg = event.Nick + " "
}
msg += event.Message()
b.Remote <- config.Message{Username: event.Nick, Text: msg, Channel: event.Arguments[0], Origin: "irc"}
}
func (b *Birc) handleTopicWhoTime(event *irc.Event) {
parts := strings.Split(event.Arguments[2], "!")
t, err := strconv.ParseInt(event.Arguments[3], 10, 64)
if err != nil {
flog.irc.Errorf("Invalid time stamp: %s", event.Arguments[3])
}
user := parts[0]
if len(parts) > 1 {
user += " [" + parts[1] + "]"
}
flog.irc.Infof("%s: Topic set by %s [%s]", event.Code, user, time.Unix(t, 0))
}
func (b *Birc) nicksPerRow() int {
if b.Config.Mattermost.NicksPerRow < 1 {
return 4
}
return b.Config.Mattermost.NicksPerRow
}
func (b *Birc) setupChannels() {
for _, val := range b.Config.Channel {
flog.irc.Infof("Joining %s as %s", val.IRC, b.ircNick)
b.i.Join(val.IRC)
}
}
func (b *Birc) storeNames(event *irc.Event) {
channel := event.Arguments[2]
b.names[channel] = append(
b.names[channel],
strings.Split(strings.TrimSpace(event.Message()), " ")...)
}
func (b *Birc) formatnicks(nicks []string, continued bool) string {
switch b.Config.Mattermost.NickFormatter {
case "table":
return tableformatter(nicks, b.nicksPerRow(), continued)
default:
return plainformatter(nicks, b.nicksPerRow())
}
}

View File

@ -0,0 +1,174 @@
package bmattermost
import (
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/matterclient"
"github.com/42wim/matterbridge/matterhook"
log "github.com/Sirupsen/logrus"
"strings"
)
//type Bridge struct {
type MMhook struct {
mh *matterhook.Client
}
type MMapi struct {
mc *matterclient.MMClient
mmMap map[string]string
mmIgnoreNicks []string
}
type MMMessage struct {
Text string
Channel string
Username string
}
type Bmattermost struct {
MMhook
MMapi
*config.Config
Plus bool
Remote chan config.Message
}
type FancyLog struct {
irc *log.Entry
mm *log.Entry
xmpp *log.Entry
}
var flog FancyLog
const Legacy = "legacy"
func init() {
flog.irc = log.WithFields(log.Fields{"module": "irc"})
flog.mm = log.WithFields(log.Fields{"module": "mattermost"})
flog.xmpp = log.WithFields(log.Fields{"module": "xmpp"})
}
func New(cfg *config.Config, c chan config.Message) *Bmattermost {
b := &Bmattermost{}
b.Config = cfg
b.Remote = c
b.Plus = cfg.General.Plus
b.mmMap = make(map[string]string)
return b
}
func (b *Bmattermost) Command(cmd string) string {
return ""
}
func (b *Bmattermost) Connect() error {
if !b.Plus {
b.mh = matterhook.New(b.Config.Mattermost.URL,
matterhook.Config{InsecureSkipVerify: b.Config.Mattermost.SkipTLSVerify,
BindAddress: b.Config.Mattermost.BindAddress})
} else {
b.mc = matterclient.New(b.Config.Mattermost.Login, b.Config.Mattermost.Password,
b.Config.Mattermost.Team, b.Config.Mattermost.Server)
b.mc.SkipTLSVerify = b.Config.Mattermost.SkipTLSVerify
b.mc.NoTLS = b.Config.Mattermost.NoTLS
flog.mm.Infof("Trying login %s (team: %s) on %s", b.Config.Mattermost.Login, b.Config.Mattermost.Team, b.Config.Mattermost.Server)
err := b.mc.Login()
if err != nil {
return err
}
flog.mm.Info("Login ok")
b.mc.JoinChannel(b.Config.Mattermost.Channel)
for _, val := range b.Config.Channel {
b.mc.JoinChannel(val.Mattermost)
}
go b.mc.WsReceiver()
}
go b.handleMatter()
return nil
}
func (b *Bmattermost) Name() string {
return "mattermost"
}
func (b *Bmattermost) Send(msg config.Message) error {
flog.mm.Infof("mattermost send %#v", msg)
if msg.Origin != "mattermost" {
return b.SendType(msg.Username, msg.Text, msg.Channel, "")
}
return nil
}
func (b *Bmattermost) SendType(nick string, message string, channel string, mtype string) error {
if b.Config.Mattermost.PrefixMessagesWithNick {
/*if IsMarkup(message) {
message = nick + "\n\n" + message
} else {
*/
message = nick + " " + message
//}
}
if !b.Plus {
matterMessage := matterhook.OMessage{IconURL: b.Config.Mattermost.IconURL}
matterMessage.Channel = channel
matterMessage.UserName = nick
matterMessage.Type = mtype
matterMessage.Text = message
err := b.mh.Send(matterMessage)
if err != nil {
flog.mm.Info(err)
return err
}
flog.mm.Debug("->mattermost channel: ", channel, " ", message)
return nil
}
flog.mm.Debug("->mattermost channel plus: ", channel, " ", message)
b.mc.PostMessage(b.mc.GetChannelId(channel, ""), message)
return nil
}
func (b *Bmattermost) handleMatter() {
flog.mm.Infof("Choosing API based Mattermost connection: %t", b.Plus)
mchan := make(chan *MMMessage)
if b.Plus {
go b.handleMatterClient(mchan)
} else {
go b.handleMatterHook(mchan)
}
flog.mm.Info("Start listening for Mattermost messages")
for message := range mchan {
texts := strings.Split(message.Text, "\n")
for _, text := range texts {
flog.mm.Debug("Sending message from " + message.Username + " to " + message.Channel)
b.Remote <- config.Message{Text: text, Username: message.Username, Channel: message.Channel, Origin: "mattermost"}
}
}
}
func (b *Bmattermost) handleMatterClient(mchan chan *MMMessage) {
for message := range b.mc.MessageChan {
// do not post our own messages back to irc
if message.Raw.Event == "posted" && b.mc.User.Username != message.Username {
flog.mm.Debugf("receiving from matterclient %#v", message)
flog.mm.Debugf("receiving from matterclient %#v", message.Raw)
m := &MMMessage{}
m.Username = message.Username
m.Channel = message.Channel
m.Text = message.Text
mchan <- m
}
}
}
func (b *Bmattermost) handleMatterHook(mchan chan *MMMessage) {
for {
message := b.mh.Receive()
flog.mm.Debugf("receiving from matterhook %#v", message)
m := &MMMessage{}
m.Username = message.UserName
m.Text = message.Text
m.Channel = message.ChannelName
mchan <- m
}
}

133
bridge/xmpp/xmpp.go Normal file
View File

@ -0,0 +1,133 @@
package bxmpp
import (
"github.com/42wim/matterbridge/bridge/config"
log "github.com/Sirupsen/logrus"
"github.com/mattn/go-xmpp"
"strings"
"time"
)
type Bxmpp struct {
xc *xmpp.Client
xmppMap map[string]string
*config.Config
Remote chan config.Message
}
type FancyLog struct {
xmpp *log.Entry
}
type Message struct {
Text string
Channel string
Username string
}
var flog FancyLog
func init() {
flog.xmpp = log.WithFields(log.Fields{"module": "xmpp"})
}
func New(config *config.Config, c chan config.Message) *Bxmpp {
b := &Bxmpp{}
b.xmppMap = make(map[string]string)
b.Config = config
b.Remote = c
return b
}
func (b *Bxmpp) Connect() error {
var err error
flog.xmpp.Info("Trying XMPP connection")
b.xc, err = b.createXMPP()
if err != nil {
flog.xmpp.Debugf("%#v", err)
return err
}
flog.xmpp.Info("Connection succeeded")
b.setupChannels()
go b.handleXmpp()
return nil
}
func (b *Bxmpp) Name() string {
return "xmpp"
}
func (b *Bxmpp) Send(msg config.Message) error {
b.xc.Send(xmpp.Chat{Type: "groupchat", Remote: msg.Channel + "@" + b.Xmpp.Muc, Text: msg.Username + msg.Text})
return nil
}
func (b *Bxmpp) createXMPP() (*xmpp.Client, error) {
options := xmpp.Options{
Host: b.Config.Xmpp.Server,
User: b.Config.Xmpp.Jid,
Password: b.Config.Xmpp.Password,
NoTLS: true,
StartTLS: true,
//StartTLS: false,
Debug: true,
Session: true,
Status: "",
StatusMessage: "",
Resource: "",
InsecureAllowUnencryptedAuth: false,
//InsecureAllowUnencryptedAuth: true,
}
var err error
b.xc, err = options.NewClient()
return b.xc, err
}
func (b *Bxmpp) setupChannels() {
for _, val := range b.Config.Channel {
flog.xmpp.Infof("Joining %s as %s", val.Xmpp, b.Xmpp.Nick)
b.xc.JoinMUCNoHistory(val.Xmpp+"@"+b.Xmpp.Muc, b.Xmpp.Nick)
}
}
func (b *Bxmpp) xmppKeepAlive() {
go func() {
ticker := time.NewTicker(90 * time.Second)
for {
select {
case <-ticker.C:
b.xc.Send(xmpp.Chat{})
}
}
}()
}
func (b *Bxmpp) handleXmpp() error {
for {
m, err := b.xc.Recv()
if err != nil {
return err
}
switch v := m.(type) {
case xmpp.Chat:
var channel, nick string
if v.Type == "groupchat" {
s := strings.Split(v.Remote, "@")
if len(s) == 2 {
channel = s[0]
}
s = strings.Split(s[1], "/")
if len(s) == 2 {
nick = s[1]
}
if nick != b.Xmpp.Nick {
flog.xmpp.Infof("sending message to remote %s %s %s", nick, v.Text, channel)
b.Remote <- config.Message{Username: nick, Text: v.Text, Channel: channel, Origin: "xmpp"}
}
}
case xmpp.Presence:
// do nothing
}
}
}

19
changelog.md Normal file
View File

@ -0,0 +1,19 @@
# v0.6.0
## Breaking changes from 0.5 to 0.6
### commandline
* -plus switch deprecated. Use ```Plus=true``` or ```Plus``` in ```[general]``` section
### IRC section
* ```Enabled``` added (default false)
Add ```Enabled=true``` or ```Enabled``` to the ```[IRC]``` section if you want to enable the IRC bridge
### Mattermost section
* ```Enabled``` added (default false)
Add ```Enabled=true``` or ```Enabled``` to the ```[mattermost]``` section if you want to enable the mattermostbridge
### General section
* Use ```Plus=true``` or ```Plus``` in ```[general]``` section to enable the API version of matterbridge
## New features
* Matterbridge now bridges between any specified protocol (not only mattermost anymore)
* XMPP support added. See matterbridge.conf.sample for more information
* RemoteNickFormat {BRIDGE} variable added
You can now add the originating bridge to ```RemoteNickFormat```
eg ```RemoteNickFormat="[{BRIDGE}] <{NICK}> "```

View File

@ -1,37 +1,190 @@
#This is configuration for matterbridge.
###################################################################
#IRC section
###################################################################
[IRC]
server="irc.freenode.net"
port=6667
#Enable enables this bridge
#OPTIONAL (default false)
Enable=true
#irc server to connect to.
#REQUIRED
Server="irc.freenode.net:6667"
#Enable to use TLS connection to your irc server.
#OPTIONAL (default false)
UseTLS=false
#Enable SASL (PLAIN) authentication. (freenode requires this from eg AWS hosts)
#It uses NickServNick and NickServPassword as login and password
#OPTIONAL (default false)
UseSASL=false
#Enable to not verify the certificate on your irc server. i
#e.g. when using selfsigned certificates
#OPTIONAL (default false)
SkipTLSVerify=true
nick="matterbot"
channel="#matterbridge"
UseSlackCircumfix=false
#Your nick on irc.
#REQUIRED
Nick="matterbot"
#If you registered your bot with a service like Nickserv on freenode.
#Also being used when UseSASL=true
#OPTIONAL
NickServNick="nickserv"
NickServPassword="secret"
#RemoteNickFormat defines how Mattermost users appear on irc
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
#OPTIONAL (default NICK:)
RemoteNickFormat="{NICK}: "
#Nicks you want to ignore.
#Messages from those users will not be sent to mattermost.
#OPTIONAL
IgnoreNicks="ircspammer1 ircspammer2"
###################################################################
#XMPP section
###################################################################
[XMPP]
#Enable enables this bridge
#OPTIONAL (default false)
Enable=true
#xmpp server to connect to.
#REQUIRED
Server="jabber.example.com:5222"
#Jid
#REQUIRED
Jid="user@example.com"
#Password
#REQUIRED
Password="yourpass"
#MUC
#REQUIRED
Muc="conference.jabber.example.com"
#Your nick in the rooms
#REQUIRED
Nick="xmppbot"
###################################################################
#mattermost section
###################################################################
[mattermost]
url="http://yourdomain/hooks/yourhookkey"
port=9999
showjoinpart=true
#remove token when using multiple channels!
token=yourtokenfrommattermost
#Enable enables this bridge
#OPTIONAL (default false)
Enable=true
#### Settings for webhook matterbridge.
#### These settings will not be used when using -plus switch which doesn't use
#### webhooks.
#Url is your incoming webhook url as specified in mattermost.
#See account settings - integrations - incoming webhooks on mattermost.
#REQUIRED
URL="https://yourdomain/hooks/yourhookkey"
#Address to listen on for outgoing webhook requests from mattermost.
#See account settings - integrations - outgoing webhooks on mattermost.
#This setting will not be used when using -plus switch which doesn't use
#webhooks
#REQUIRED
BindAddress="0.0.0.0:9999"
#Icon that will be showed in mattermost.
#OPTIONAL
IconURL="http://youricon.png"
#SkipTLSVerify=true
#BindAddress="0.0.0.0"
#### Settings for matterbridge -plus
#### Thse settings will only be used when using the -plus switch.
#The mattermost hostname.
#REQUIRED
Server="yourmattermostserver.domain"
#Your team on mattermost.
#REQUIRED
Team="yourteam"
#login/pass of your bot.
#Use a dedicated user for this and not your own!
#REQUIRED
Login="yourlogin"
Password="yourpass"
#Enable this to make a http connection (instead of https) to your mattermost.
#OPTIONAL (default false)
NoTLS=false
#### Shared settings for matterbridge and -plus
#Enable to not verify the certificate on your mattermost server.
#e.g. when using selfsigned certificates
#OPTIONAL (default false)
SkipTLSVerify=true
#Enable to show IRC joins/parts in mattermost.
#OPTIONAL (default false)
ShowJoinPart=false
#Whether to prefix messages from IRC to mattermost with the sender's nick.
#Useful if username overrides for incoming webhooks isn't enabled on the
#mattermost server. If you set PrefixMessagesWithNick to true, each message
#from IRC to Mattermost will by default be prefixed by "irc-" + nick. You can,
#however, modify how the messages appear, by setting (and modifying) RemoteNickFormat
#OPTIONAL (default false)
PrefixMessagesWithNick=false
#RemoteNickFormat defines how IRC users appear on Mattermost.
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
#OPTIONAL (default irc-NICK)
RemoteNickFormat="irc-{NICK}"
#how to format the list of IRC nicks when displayed in mattermost.
#Possible options are "table" and "plain"
#OPTIONAL (default plain)
NickFormatter=plain
#How many nicks to list per row for formatters that support this.
#OPTIONAL (default 4)
NicksPerRow=4
#NickServNick="nickserv"
#NickServPassword="secret"
[general]
GiphyAPIKey=dc6zaTOxFJmzC
#Nicks you want to ignore. Messages from those users will not be sent to IRC.
#OPTIONAL
IgnoreNicks="mmbot spammer2"
###################################################################
#multiple channel config
#token you can find in your outgoing webhook
[Token "outgoingwebhooktoken1"]
IRCChannel="#off-topic"
MMChannel="off-topic"
###################################################################
#You can specify multiple channels.
#The name is just an identifier for you.
#REQUIRED (at least 1 channel)
[Channel "channel1"]
#Choose the IRC channel to send mattermost messages to.
IRC="#off-topic"
#Choose the mattermost channel to send IRC messages to.
mattermost="off-topic"
#Choose the mattermost channel to send IRC messages to.
xmpp="off-topic"
[Token "outgoingwebhooktoken2"]
IRCChannel="#testing"
MMChannel="testing"
[Channel "testchannel"]
IRC="#testing"
mattermost="testing"
xmpp="testing"
###################################################################
#general
###################################################################
[general]
#request your API key on https://github.com/giphy/GiphyAPI. This is a public beta key.
#OPTIONAL
GiphyApiKey="dc6zaTOxFJmzC"
#Enabling plus means you'll use the API version instead of the webhooks one
Plus=false

View File

@ -2,10 +2,14 @@ package main
import (
"flag"
"github.com/42wim/matterbridge-plus/bridge"
"fmt"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
log "github.com/Sirupsen/logrus"
)
var version = "0.6.0-beta1"
func init() {
log.SetFormatter(&log.TextFormatter{FullTimestamp: true})
}
@ -13,11 +17,25 @@ func init() {
func main() {
flagConfig := flag.String("conf", "matterbridge.conf", "config file")
flagDebug := flag.Bool("debug", false, "enable debug")
flagVersion := flag.Bool("version", false, "show version")
flagPlus := flag.Bool("plus", false, "running using API instead of webhooks (deprecated, set Plus flag in [general] config)")
flag.Parse()
if *flagVersion {
fmt.Println("version:", version)
return
}
flag.Parse()
if *flagDebug {
log.Info("enabling debug")
log.SetLevel(log.DebugLevel)
}
bridge.NewBridge("matterbot", bridge.NewConfig(*flagConfig), "legacy")
select {}
fmt.Println("running version", version)
cfg := config.NewConfig(*flagConfig)
if *flagPlus {
cfg.General.Plus = true
}
err := bridge.NewBridge(cfg)
if err != nil {
log.Debugf("starting bridge failed %#v", err)
}
}

View File

@ -0,0 +1,642 @@
package matterclient
import (
"crypto/tls"
"encoding/json"
"errors"
"net/http"
"net/http/cookiejar"
"net/url"
"strings"
"sync"
"time"
log "github.com/Sirupsen/logrus"
"github.com/gorilla/websocket"
"github.com/jpillora/backoff"
"github.com/mattermost/platform/model"
)
type Credentials struct {
Login string
Team string
Pass string
Server string
NoTLS bool
SkipTLSVerify bool
}
type Message struct {
Raw *model.WebSocketEvent
Post *model.Post
Team string
Channel string
Username string
Text string
}
type Team struct {
Team *model.Team
Id string
Channels *model.ChannelList
MoreChannels *model.ChannelList
Users map[string]*model.User
}
type MMClient struct {
sync.RWMutex
*Credentials
Team *Team
OtherTeams []*Team
Client *model.Client
User *model.User
Users map[string]*model.User
MessageChan chan *Message
log *log.Entry
WsClient *websocket.Conn
WsQuit bool
WsAway bool
WsConnected bool
WsSequence int64
WsPingChan chan *model.WebSocketResponse
}
func New(login, pass, team, server string) *MMClient {
cred := &Credentials{Login: login, Pass: pass, Team: team, Server: server}
mmclient := &MMClient{Credentials: cred, MessageChan: make(chan *Message, 100), Users: make(map[string]*model.User)}
mmclient.log = log.WithFields(log.Fields{"module": "matterclient"})
log.SetFormatter(&log.TextFormatter{FullTimestamp: true})
return mmclient
}
func (m *MMClient) SetLogLevel(level string) {
l, err := log.ParseLevel(level)
if err != nil {
log.SetLevel(log.InfoLevel)
return
}
log.SetLevel(l)
}
func (m *MMClient) Login() error {
m.WsConnected = false
if m.WsQuit {
return nil
}
b := &backoff.Backoff{
Min: time.Second,
Max: 5 * time.Minute,
Jitter: true,
}
uriScheme := "https://"
wsScheme := "wss://"
if m.NoTLS {
uriScheme = "http://"
wsScheme = "ws://"
}
// login to mattermost
m.Client = model.NewClient(uriScheme + m.Credentials.Server)
m.Client.HttpClient.Transport = &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: m.SkipTLSVerify}}
var myinfo *model.Result
var appErr *model.AppError
var logmsg = "trying login"
for {
m.log.Debugf("%s %s %s %s", logmsg, m.Credentials.Team, m.Credentials.Login, m.Credentials.Server)
if strings.Contains(m.Credentials.Pass, model.SESSION_COOKIE_TOKEN) {
m.log.Debugf(logmsg+" with %s", model.SESSION_COOKIE_TOKEN)
token := strings.Split(m.Credentials.Pass, model.SESSION_COOKIE_TOKEN+"=")
if len(token) != 2 {
return errors.New("incorrect MMAUTHTOKEN. valid input is MMAUTHTOKEN=yourtoken")
}
m.Client.HttpClient.Jar = m.createCookieJar(token[1])
m.Client.MockSession(token[1])
myinfo, appErr = m.Client.GetMe("")
if appErr != nil {
return errors.New(appErr.DetailedError)
}
if myinfo.Data.(*model.User) == nil {
m.log.Errorf("LOGIN TOKEN: %s is invalid", m.Credentials.Pass)
return errors.New("invalid " + model.SESSION_COOKIE_TOKEN)
}
} else {
myinfo, appErr = m.Client.Login(m.Credentials.Login, m.Credentials.Pass)
}
if appErr != nil {
d := b.Duration()
m.log.Debug(appErr.DetailedError)
if !strings.Contains(appErr.DetailedError, "connection refused") &&
!strings.Contains(appErr.DetailedError, "invalid character") {
if appErr.Message == "" {
return errors.New(appErr.DetailedError)
}
return errors.New(appErr.Message)
}
m.log.Debugf("LOGIN: %s, reconnecting in %s", appErr, d)
time.Sleep(d)
logmsg = "retrying login"
continue
}
break
}
// reset timer
b.Reset()
err := m.initUser()
if err != nil {
return err
}
if m.Team == nil {
return errors.New("team not found")
}
// set our team id as default route
m.Client.SetTeamId(m.Team.Id)
// setup websocket connection
wsurl := wsScheme + m.Credentials.Server + model.API_URL_SUFFIX + "/users/websocket"
header := http.Header{}
header.Set(model.HEADER_AUTH, "BEARER "+m.Client.AuthToken)
m.log.Debug("WsClient: making connection")
for {
wsDialer := &websocket.Dialer{Proxy: http.ProxyFromEnvironment, TLSClientConfig: &tls.Config{InsecureSkipVerify: m.SkipTLSVerify}}
m.WsClient, _, err = wsDialer.Dial(wsurl, header)
if err != nil {
d := b.Duration()
m.log.Debugf("WSS: %s, reconnecting in %s", err, d)
time.Sleep(d)
continue
}
break
}
b.Reset()
m.WsSequence = 1
m.WsPingChan = make(chan *model.WebSocketResponse)
// only start to parse WS messages when login is completely done
m.WsConnected = true
return nil
}
func (m *MMClient) Logout() error {
m.log.Debugf("logout as %s (team: %s) on %s", m.Credentials.Login, m.Credentials.Team, m.Credentials.Server)
m.WsQuit = true
m.WsClient.Close()
m.WsClient.UnderlyingConn().Close()
m.WsClient = nil
_, err := m.Client.Logout()
if err != nil {
return err
}
return nil
}
func (m *MMClient) WsReceiver() {
for {
var rawMsg json.RawMessage
var err error
if !m.WsConnected {
continue
}
if m.WsQuit {
m.log.Debug("exiting WsReceiver")
return
}
if _, rawMsg, err = m.WsClient.ReadMessage(); err != nil {
m.log.Error("error:", err)
// reconnect
m.Login()
}
var event model.WebSocketEvent
if err := json.Unmarshal(rawMsg, &event); err == nil && event.IsValid() {
m.log.Debugf("WsReceiver: %#v", event)
msg := &Message{Raw: &event, Team: m.Credentials.Team}
m.parseMessage(msg)
m.MessageChan <- msg
continue
}
var response model.WebSocketResponse
if err := json.Unmarshal(rawMsg, &response); err == nil && response.IsValid() {
m.log.Debugf("WsReceiver: %#v", response)
m.parseResponse(response)
continue
}
}
}
func (m *MMClient) parseMessage(rmsg *Message) {
switch rmsg.Raw.Event {
case model.WEBSOCKET_EVENT_POSTED:
m.parseActionPost(rmsg)
/*
case model.ACTION_USER_REMOVED:
m.handleWsActionUserRemoved(&rmsg)
case model.ACTION_USER_ADDED:
m.handleWsActionUserAdded(&rmsg)
*/
}
}
func (m *MMClient) parseResponse(rmsg model.WebSocketResponse) {
if rmsg.Data != nil {
// ping reply
if rmsg.Data["text"].(string) == "pong" {
m.WsPingChan <- &rmsg
}
}
}
func (m *MMClient) parseActionPost(rmsg *Message) {
data := model.PostFromJson(strings.NewReader(rmsg.Raw.Data["post"].(string)))
// we don't have the user, refresh the userlist
if m.GetUser(data.UserId) == nil {
m.UpdateUsers()
}
rmsg.Username = m.GetUser(data.UserId).Username
rmsg.Channel = m.GetChannelName(data.ChannelId)
rmsg.Team = m.GetTeamName(rmsg.Raw.TeamId)
// direct message
if rmsg.Raw.Data["channel_type"] == "D" {
rmsg.Channel = m.GetUser(data.UserId).Username
}
rmsg.Text = data.Message
rmsg.Post = data
return
}
func (m *MMClient) UpdateUsers() error {
mmusers, err := m.Client.GetProfilesForDirectMessageList(m.Team.Id)
if err != nil {
return errors.New(err.DetailedError)
}
m.Lock()
m.Users = mmusers.Data.(map[string]*model.User)
m.Unlock()
return nil
}
func (m *MMClient) UpdateChannels() error {
mmchannels, err := m.Client.GetChannels("")
if err != nil {
return errors.New(err.DetailedError)
}
mmchannels2, err := m.Client.GetMoreChannels("")
if err != nil {
return errors.New(err.DetailedError)
}
m.Lock()
m.Team.Channels = mmchannels.Data.(*model.ChannelList)
m.Team.MoreChannels = mmchannels2.Data.(*model.ChannelList)
m.Unlock()
return nil
}
func (m *MMClient) GetChannelName(channelId string) string {
m.RLock()
defer m.RUnlock()
for _, t := range m.OtherTeams {
for _, channel := range append(t.Channels.Channels, t.MoreChannels.Channels...) {
if channel.Id == channelId {
return channel.Name
}
}
}
return ""
}
func (m *MMClient) GetChannelId(name string, teamId string) string {
m.RLock()
defer m.RUnlock()
if teamId == "" {
teamId = m.Team.Id
}
for _, t := range m.OtherTeams {
if t.Id == teamId {
for _, channel := range append(t.Channels.Channels, t.MoreChannels.Channels...) {
if channel.Name == name {
return channel.Id
}
}
}
}
return ""
}
func (m *MMClient) GetChannelHeader(channelId string) string {
m.RLock()
defer m.RUnlock()
for _, t := range m.OtherTeams {
for _, channel := range append(t.Channels.Channels, t.MoreChannels.Channels...) {
if channel.Id == channelId {
return channel.Header
}
}
}
return ""
}
func (m *MMClient) PostMessage(channelId string, text string) {
post := &model.Post{ChannelId: channelId, Message: text}
m.Client.CreatePost(post)
}
func (m *MMClient) JoinChannel(channelId string) error {
m.RLock()
defer m.RUnlock()
for _, c := range m.Team.Channels.Channels {
if c.Id == channelId {
m.log.Debug("Not joining ", channelId, " already joined.")
return nil
}
}
m.log.Debug("Joining ", channelId)
_, err := m.Client.JoinChannel(channelId)
if err != nil {
return errors.New("failed to join")
}
return nil
}
func (m *MMClient) GetPostsSince(channelId string, time int64) *model.PostList {
res, err := m.Client.GetPostsSince(channelId, time)
if err != nil {
return nil
}
return res.Data.(*model.PostList)
}
func (m *MMClient) SearchPosts(query string) *model.PostList {
res, err := m.Client.SearchPosts(query, false)
if err != nil {
return nil
}
return res.Data.(*model.PostList)
}
func (m *MMClient) GetPosts(channelId string, limit int) *model.PostList {
res, err := m.Client.GetPosts(channelId, 0, limit, "")
if err != nil {
return nil
}
return res.Data.(*model.PostList)
}
func (m *MMClient) GetPublicLink(filename string) string {
res, err := m.Client.GetPublicLink(filename)
if err != nil {
return ""
}
return res.Data.(string)
}
func (m *MMClient) GetPublicLinks(filenames []string) []string {
var output []string
for _, f := range filenames {
res, err := m.Client.GetPublicLink(f)
if err != nil {
continue
}
output = append(output, res.Data.(string))
}
return output
}
func (m *MMClient) UpdateChannelHeader(channelId string, header string) {
data := make(map[string]string)
data["channel_id"] = channelId
data["channel_header"] = header
m.log.Debugf("updating channelheader %#v, %#v", channelId, header)
_, err := m.Client.UpdateChannelHeader(data)
if err != nil {
log.Error(err)
}
}
func (m *MMClient) UpdateLastViewed(channelId string) {
m.log.Debugf("posting lastview %#v", channelId)
_, err := m.Client.UpdateLastViewedAt(channelId)
if err != nil {
m.log.Error(err)
}
}
func (m *MMClient) UsernamesInChannel(channelId string) []string {
ceiRes, err := m.Client.GetChannelExtraInfo(channelId, 5000, "")
if err != nil {
m.log.Errorf("UsernamesInChannel(%s) failed: %s", channelId, err)
return []string{}
}
extra := ceiRes.Data.(*model.ChannelExtra)
result := []string{}
for _, member := range extra.Members {
result = append(result, member.Username)
}
return result
}
func (m *MMClient) createCookieJar(token string) *cookiejar.Jar {
var cookies []*http.Cookie
jar, _ := cookiejar.New(nil)
firstCookie := &http.Cookie{
Name: "MMAUTHTOKEN",
Value: token,
Path: "/",
Domain: m.Credentials.Server,
}
cookies = append(cookies, firstCookie)
cookieURL, _ := url.Parse("https://" + m.Credentials.Server)
jar.SetCookies(cookieURL, cookies)
return jar
}
// SendDirectMessage sends a direct message to specified user
func (m *MMClient) SendDirectMessage(toUserId string, msg string) {
m.log.Debugf("SendDirectMessage to %s, msg %s", toUserId, msg)
// create DM channel (only happens on first message)
_, err := m.Client.CreateDirectChannel(toUserId)
if err != nil {
m.log.Debugf("SendDirectMessage to %#v failed: %s", toUserId, err)
}
channelName := model.GetDMNameFromIds(toUserId, m.User.Id)
// update our channels
mmchannels, _ := m.Client.GetChannels("")
m.Lock()
m.Team.Channels = mmchannels.Data.(*model.ChannelList)
m.Unlock()
// build & send the message
msg = strings.Replace(msg, "\r", "", -1)
post := &model.Post{ChannelId: m.GetChannelId(channelName, ""), Message: msg}
m.Client.CreatePost(post)
}
// GetTeamName returns the name of the specified teamId
func (m *MMClient) GetTeamName(teamId string) string {
m.RLock()
defer m.RUnlock()
for _, t := range m.OtherTeams {
if t.Id == teamId {
return t.Team.Name
}
}
return ""
}
// GetChannels returns all channels we're members off
func (m *MMClient) GetChannels() []*model.Channel {
m.RLock()
defer m.RUnlock()
var channels []*model.Channel
// our primary team channels first
channels = append(channels, m.Team.Channels.Channels...)
for _, t := range m.OtherTeams {
if t.Id != m.Team.Id {
channels = append(channels, t.Channels.Channels...)
}
}
return channels
}
// GetMoreChannels returns existing channels where we're not a member off.
func (m *MMClient) GetMoreChannels() []*model.Channel {
m.RLock()
defer m.RUnlock()
var channels []*model.Channel
for _, t := range m.OtherTeams {
channels = append(channels, t.MoreChannels.Channels...)
}
return channels
}
// GetTeamFromChannel returns teamId belonging to channel (DM channels have no teamId).
func (m *MMClient) GetTeamFromChannel(channelId string) string {
m.RLock()
defer m.RUnlock()
var channels []*model.Channel
for _, t := range m.OtherTeams {
channels = append(channels, t.Channels.Channels...)
for _, c := range channels {
if c.Id == channelId {
return t.Id
}
}
}
return ""
}
func (m *MMClient) GetLastViewedAt(channelId string) int64 {
m.RLock()
defer m.RUnlock()
for _, t := range m.OtherTeams {
if _, ok := t.Channels.Members[channelId]; ok {
return t.Channels.Members[channelId].LastViewedAt
}
}
return 0
}
func (m *MMClient) GetUsers() map[string]*model.User {
users := make(map[string]*model.User)
m.RLock()
defer m.RUnlock()
for k, v := range m.Users {
users[k] = v
}
return users
}
func (m *MMClient) GetUser(userId string) *model.User {
m.RLock()
defer m.RUnlock()
return m.Users[userId]
}
func (m *MMClient) GetStatus(userId string) string {
res, err := m.Client.GetStatuses()
if err != nil {
return ""
}
status := res.Data.(map[string]string)
if status[userId] == model.STATUS_AWAY {
return "away"
}
if status[userId] == model.STATUS_ONLINE {
return "online"
}
return "offline"
}
func (m *MMClient) StatusLoop() {
for {
if m.WsQuit {
return
}
if m.WsConnected {
m.log.Debug("WS PING")
m.sendWSRequest("ping", nil)
select {
case <-m.WsPingChan:
m.log.Debug("WS PONG received")
case <-time.After(time.Second * 5):
m.Logout()
m.WsQuit = false
m.Login()
}
}
time.Sleep(time.Second * 60)
}
}
// initialize user and teams
func (m *MMClient) initUser() error {
m.Lock()
defer m.Unlock()
m.log.Debug("initUser()")
initLoad, err := m.Client.GetInitialLoad()
if err != nil {
return err
}
initData := initLoad.Data.(*model.InitialLoad)
m.User = initData.User
// we only load all team data on initial login.
// all other updates are for channels from our (primary) team only.
m.log.Debug("initUser(): loading all team data")
for _, v := range initData.Teams {
m.Client.SetTeamId(v.Id)
mmusers, _ := m.Client.GetProfiles(v.Id, "")
t := &Team{Team: v, Users: mmusers.Data.(map[string]*model.User), Id: v.Id}
mmchannels, _ := m.Client.GetChannels("")
t.Channels = mmchannels.Data.(*model.ChannelList)
mmchannels, _ = m.Client.GetMoreChannels("")
t.MoreChannels = mmchannels.Data.(*model.ChannelList)
m.OtherTeams = append(m.OtherTeams, t)
if v.Name == m.Credentials.Team {
m.Team = t
m.log.Debugf("initUser(): found our team %s (id: %s)", v.Name, v.Id)
}
// add all users
for k, v := range t.Users {
m.Users[k] = v
}
}
return nil
}
func (m *MMClient) sendWSRequest(action string, data map[string]interface{}) error {
req := &model.WebSocketRequest{}
req.Seq = m.WsSequence
req.Action = action
req.Data = data
m.WsSequence++
m.log.Debugf("sendWsRequest %#v", req)
m.WsClient.WriteJSON(req)
return nil
}

View File

@ -10,8 +10,8 @@ import (
"io"
"io/ioutil"
"log"
"net"
"net/http"
"strconv"
)
// OMessage for mattermost incoming webhook. (send to mattermost)
@ -31,11 +31,11 @@ type IMessage struct {
TeamID string `schema:"team_id"`
TeamDomain string `schema:"team_domain"`
ChannelID string `schema:"channel_id"`
ServiceID string `schema:"service_id"`
ChannelName string `schema:"channel_name"`
Timestamp string `schema:"timestamp"`
UserID string `schema:"user_id"`
UserName string `schema:"user_name"`
PostId string `schema:"post_id"`
Text string `schema:"text"`
TriggerWord string `schema:"trigger_word"`
}
@ -51,7 +51,6 @@ type Client struct {
// Config for client.
type Config struct {
Port int // Port to listen on.
BindAddress string // Address to listen on
Token string // Only allow this token from Mattermost. (Allow everything when empty)
InsecureSkipVerify bool // disable certificate checking
@ -61,15 +60,15 @@ type Config struct {
// New Mattermost client.
func New(url string, config Config) *Client {
c := &Client{Url: url, In: make(chan IMessage), Out: make(chan OMessage), Config: config}
if c.Port == 0 {
c.Port = 9999
}
c.BindAddress += ":"
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: config.InsecureSkipVerify},
}
c.httpclient = &http.Client{Transport: tr}
if !c.DisableServer {
_, _, err := net.SplitHostPort(c.BindAddress)
if err != nil {
log.Fatalf("incorrect bindaddress %s", c.BindAddress)
}
go c.StartServer()
}
return c
@ -79,8 +78,8 @@ func New(url string, config Config) *Client {
func (c *Client) StartServer() {
mux := http.NewServeMux()
mux.Handle("/", c)
log.Printf("Listening on http://%v:%v...\n", c.BindAddress, c.Port)
if err := http.ListenAndServe((c.BindAddress + strconv.Itoa(c.Port)), mux); err != nil {
log.Printf("Listening on http://%v...\n", c.BindAddress)
if err := http.ListenAndServe(c.BindAddress, mux); err != nil {
log.Fatal(err)
}
}

50
migration.md Normal file
View File

@ -0,0 +1,50 @@
# Breaking changes from 0.4 to 0.5 for matterbridge (webhooks version)
## IRC section
### Server
Port removed, added to server
```
server="irc.freenode.net"
port=6667
```
changed to
```
server="irc.freenode.net:6667"
```
### Channel
Removed see Channels section below
### UseSlackCircumfix=true
Removed, can be done by using ```RemoteNickFormat="<{NICK}> "```
## Mattermost section
### BindAddress
Port removed, added to BindAddress
```
BindAddress="0.0.0.0"
port=9999
```
changed to
```
BindAddress="0.0.0.0:9999"
```
### Token
Removed
## Channels section
```
[Token "outgoingwebhooktoken1"]
IRCChannel="#off-topic"
MMChannel="off-topic"
```
changed to
```
[Channel "channelnameofchoice"]
IRC="#off-topic"
Mattermost="off-topic"
```

View File

@ -21,15 +21,17 @@ type MMhook struct {
}
type MMapi struct {
mc *matterclient.MMClient
mmMap map[string]string
mc *matterclient.MMClient
mmMap map[string]string
mmIgnoreNicks []string
}
type MMirc struct {
i *irc.Connection
ircNick string
ircMap map[string]string
names map[string][]string
i *irc.Connection
ircNick string
ircMap map[string]string
names map[string][]string
ircIgnoreNicks []string
}
type MMMessage struct {
@ -53,6 +55,8 @@ type FancyLog struct {
var flog FancyLog
const Legacy = "legacy"
func initFLog() {
flog.irc = log.WithFields(log.Fields{"module": "irc"})
flog.mm = log.WithFields(log.Fields{"module": "mattermost"})
@ -66,7 +70,9 @@ func NewBridge(name string, config *Config, kind string) *Bridge {
b.ircNick = b.Config.IRC.Nick
b.ircMap = make(map[string]string)
b.MMirc.names = make(map[string][]string)
if kind == "legacy" {
b.ircIgnoreNicks = strings.Fields(b.Config.IRC.IgnoreNicks)
b.mmIgnoreNicks = strings.Fields(b.Config.Mattermost.IgnoreNicks)
if kind == Legacy {
if len(b.Config.Token) > 0 {
for _, val := range b.Config.Token {
b.ircMap[val.IRCChannel] = val.MMChannel
@ -87,10 +93,14 @@ func NewBridge(name string, config *Config, kind string) *Bridge {
}
b.mc = matterclient.New(b.Config.Mattermost.Login, b.Config.Mattermost.Password,
b.Config.Mattermost.Team, b.Config.Mattermost.Server)
b.mc.SkipTLSVerify = b.Config.Mattermost.SkipTLSVerify
b.mc.NoTLS = b.Config.Mattermost.NoTLS
flog.mm.Infof("Trying login %s (team: %s) on %s", b.Config.Mattermost.Login, b.Config.Mattermost.Team, b.Config.Mattermost.Server)
err := b.mc.Login()
if err != nil {
flog.mm.Fatal("can not connect", err)
flog.mm.Fatal("Can not connect", err)
}
flog.mm.Info("Login ok")
b.mc.JoinChannel(b.Config.Mattermost.Channel)
if len(b.Config.Channel) > 0 {
for _, val := range b.Config.Channel {
@ -99,7 +109,9 @@ func NewBridge(name string, config *Config, kind string) *Bridge {
}
go b.mc.WsReceiver()
}
flog.irc.Info("Trying IRC connection")
b.i = b.createIRC(name)
flog.irc.Info("Connection succeeded")
go b.handleMatter()
return b
}
@ -111,37 +123,51 @@ func (b *Bridge) createIRC(name string) *irc.Connection {
if b.Config.IRC.Password != "" {
i.Password = b.Config.IRC.Password
}
i.AddCallback("*", b.handleOther)
i.AddCallback(ircm.RPL_WELCOME, b.handleNewConnection)
i.Connect(b.Config.IRC.Server + ":" + strconv.Itoa(b.Config.IRC.Port))
return i
}
func (b *Bridge) handleNewConnection(event *irc.Event) {
flog.irc.Info("Registering callbacks")
i := b.i
b.ircNick = event.Arguments[0]
i.AddCallback("PRIVMSG", b.handlePrivMsg)
i.AddCallback("CTCP_ACTION", b.handlePrivMsg)
i.AddCallback(ircm.RPL_ENDOFNAMES, b.endNames)
i.AddCallback(ircm.RPL_NAMREPLY, b.storeNames)
i.AddCallback(ircm.RPL_TOPICWHOTIME, b.handleTopicWhoTime)
i.AddCallback(ircm.NOTICE, b.handleNotice)
i.AddCallback(ircm.RPL_MYINFO, func(e *irc.Event) { flog.irc.Infof("%s: %s", e.Code, strings.Join(e.Arguments[1:], " ")) })
i.AddCallback("PING", func(e *irc.Event) {
i.SendRaw("PONG :" + e.Message())
flog.irc.Debugf("PING/PONG")
})
if b.Config.Mattermost.ShowJoinPart {
i.AddCallback("JOIN", b.handleJoinPart)
i.AddCallback("PART", b.handleJoinPart)
}
i.AddCallback("*", b.handleOther)
b.setupChannels()
}
func (b *Bridge) setupChannels() {
i := b.i
flog.irc.Info("Joining ", b.Config.IRC.Channel, " as ", b.ircNick)
i.Join(b.Config.IRC.Channel)
if b.kind == "legacy" {
if b.Config.IRC.Channel != "" {
flog.irc.Infof("Joining %s as %s", b.Config.IRC.Channel, b.ircNick)
i.Join(b.Config.IRC.Channel)
}
if b.kind == Legacy {
for _, val := range b.Config.Token {
flog.irc.Info("Joining ", val.IRCChannel, " as ", b.ircNick)
flog.irc.Infof("Joining %s as %s", val.IRCChannel, b.ircNick)
i.Join(val.IRCChannel)
}
} else {
for _, val := range b.Config.Channel {
flog.irc.Info("Joining ", val.IRC, " as ", b.ircNick)
flog.irc.Infof("Joining %s as %s", val.IRC, b.ircNick)
i.Join(val.IRC)
}
}
i.AddCallback("PRIVMSG", b.handlePrivMsg)
i.AddCallback("CTCP_ACTION", b.handlePrivMsg)
if b.Config.Mattermost.ShowJoinPart {
i.AddCallback("JOIN", b.handleJoinPart)
i.AddCallback("PART", b.handleJoinPart)
}
}
func (b *Bridge) handleIrcBotCommand(event *irc.Event) bool {
@ -177,6 +203,9 @@ func (b *Bridge) ircNickFormat(nick string) string {
}
func (b *Bridge) handlePrivMsg(event *irc.Event) {
if b.ignoreMessage(event.Nick, event.Message(), "irc") {
return
}
if b.handleIrcBotCommand(event) {
return
}
@ -238,81 +267,21 @@ func (b *Bridge) endNames(event *irc.Event) {
b.MMirc.names[channel] = nil
}
func (b *Bridge) handleTopicWhoTime(event *irc.Event) bool {
func (b *Bridge) handleTopicWhoTime(event *irc.Event) {
parts := strings.Split(event.Arguments[2], "!")
t_i, err := strconv.ParseInt(event.Arguments[3], 10, 64)
t, err := strconv.ParseInt(event.Arguments[3], 10, 64)
if err != nil {
flog.irc.Errorf("Invalid time stamp: %s", event.Arguments[3])
return false
}
user := parts[0]
if len(parts) > 1 {
user += " [" + parts[1] + "]"
}
flog.irc.Infof("%s: Topic set by %s [%s]", event.Code, user, time.Unix(t_i, 0))
return true
flog.irc.Infof("%s: Topic set by %s [%s]", event.Code, user, time.Unix(t, 0))
}
func (b *Bridge) handleOther(event *irc.Event) {
flog.irc.Debugf("%#v", event)
switch event.Code {
case ircm.RPL_WELCOME:
b.handleNewConnection(event)
case ircm.RPL_ENDOFNAMES:
b.endNames(event)
case ircm.RPL_NAMREPLY:
b.storeNames(event)
case ircm.RPL_ISUPPORT:
fallthrough
case ircm.RPL_LUSEROP:
fallthrough
case ircm.RPL_LUSERUNKNOWN:
fallthrough
case ircm.RPL_LUSERCHANNELS:
fallthrough
case ircm.RPL_MYINFO:
flog.irc.Infof("%s: %s", event.Code, strings.Join(event.Arguments[1:], " "))
case ircm.RPL_YOURHOST:
fallthrough
case ircm.RPL_CREATED:
fallthrough
case ircm.RPL_STATSDLINE:
fallthrough
case ircm.RPL_LUSERCLIENT:
fallthrough
case ircm.RPL_LUSERME:
fallthrough
case ircm.RPL_LOCALUSERS:
fallthrough
case ircm.RPL_GLOBALUSERS:
fallthrough
case ircm.RPL_MOTD:
flog.irc.Infof("%s: %s", event.Code, event.Message())
// flog.irc.Info(event.Message())
case ircm.RPL_TOPIC:
flog.irc.Infof("%s: Topic for %s: %s", event.Code, event.Arguments[1], event.Message())
case ircm.RPL_TOPICWHOTIME:
if !b.handleTopicWhoTime(event) {
break
}
case ircm.MODE:
flog.irc.Infof("%s: %s %s", event.Code, event.Arguments[1], event.Arguments[0])
case ircm.JOIN:
fallthrough
case ircm.PING:
fallthrough
case ircm.PONG:
flog.irc.Infof("%s: %s", event.Code, event.Message())
case ircm.RPL_ENDOFMOTD:
case ircm.RPL_MOTDSTART:
case ircm.ERR_NICKNAMEINUSE:
flog.irc.Warn(event.Message())
case ircm.NOTICE:
b.handleNotice(event)
default:
flog.irc.Infof("UNKNOWN EVENT: %#v", event)
return
}
}
func (b *Bridge) Send(nick string, message string, channel string) error {
@ -327,7 +296,7 @@ func (b *Bridge) SendType(nick string, message string, channel string, mtype str
message = nick + " " + message
}
}
if b.kind == "legacy" {
if b.kind == Legacy {
matterMessage := matterhook.OMessage{IconURL: b.Config.Mattermost.IconURL}
matterMessage.Channel = channel
matterMessage.UserName = nick
@ -371,24 +340,34 @@ func (b *Bridge) handleMatterClient(mchan chan *MMMessage) {
}
func (b *Bridge) handleMatter() {
flog.mm.Infof("Choosing Mattermost connection type %s", b.kind)
mchan := make(chan *MMMessage)
if b.kind == "legacy" {
if b.kind == Legacy {
go b.handleMatterHook(mchan)
} else {
go b.handleMatterClient(mchan)
}
flog.mm.Info("Start listening for Mattermost messages")
for message := range mchan {
var username string
if b.ignoreMessage(message.Username, message.Text, "mattermost") {
continue
}
username = message.Username + ": "
if b.Config.IRC.RemoteNickFormat != "" {
username = strings.Replace(b.Config.IRC.RemoteNickFormat, "{NICK}", message.Username, -1)
} else if b.Config.IRC.UseSlackCircumfix {
username = "<" + message.Username + "> "
}
cmd := strings.Fields(message.Text)[0]
cmds := strings.Fields(message.Text)
// empty message
if len(cmds) == 0 {
continue
}
cmd := cmds[0]
switch cmd {
case "!users":
flog.mm.Info("received !users from ", message.Username)
flog.mm.Info("Received !users from ", message.Username)
b.i.SendRaw("NAMES " + b.getIRCChannel(message.Channel))
continue
case "!gif":
@ -425,7 +404,7 @@ func (b *Bridge) getMMChannel(ircChannel string) string {
}
func (b *Bridge) getIRCChannel(channel string) string {
if b.kind == "legacy" {
if b.kind == Legacy {
ircchannel := b.Config.IRC.Channel
_, ok := b.Config.Token[channel]
if ok {
@ -439,3 +418,17 @@ func (b *Bridge) getIRCChannel(channel string) string {
}
return ircchannel
}
func (b *Bridge) ignoreMessage(nick string, message string, protocol string) bool {
var ignoreNicks = b.mmIgnoreNicks
if protocol == "irc" {
ignoreNicks = b.ircIgnoreNicks
}
// should we discard messages ?
for _, entry := range ignoreNicks {
if nick == entry {
return true
}
}
return false
}

View File

@ -19,6 +19,7 @@ type Config struct {
NickServNick string
NickServPassword string
RemoteNickFormat string
IgnoreNicks string
}
Mattermost struct {
URL string
@ -37,6 +38,8 @@ type Config struct {
Login string
Password string
RemoteNickFormat *string
IgnoreNicks string
NoTLS bool
}
Token map[string]*struct {
IRCChannel string

View File

@ -1,9 +1,12 @@
package matterclient
import (
"crypto/tls"
"errors"
log "github.com/Sirupsen/logrus"
"net/http"
"net/http/cookiejar"
"net/url"
"strings"
"time"
@ -13,11 +16,12 @@ import (
)
type Credentials struct {
Login string
Team string
Pass string
Server string
NoTLS bool
Login string
Team string
Pass string
Server string
NoTLS bool
SkipTLSVerify bool
}
type Message struct {
@ -33,6 +37,8 @@ type MMClient struct {
*Credentials
Client *model.Client
WsClient *websocket.Conn
WsQuit bool
WsAway bool
Channels *model.ChannelList
MoreChannels *model.ChannelList
User *model.User
@ -60,6 +66,9 @@ func (m *MMClient) SetLogLevel(level string) {
}
func (m *MMClient) Login() error {
if m.WsQuit {
return nil
}
b := &backoff.Backoff{
Min: time.Second,
Max: 5 * time.Minute,
@ -73,12 +82,25 @@ func (m *MMClient) Login() error {
}
// login to mattermost
m.Client = model.NewClient(uriScheme + m.Credentials.Server)
m.Client.HttpClient.Transport = &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: m.SkipTLSVerify}}
var myinfo *model.Result
var appErr *model.AppError
var logmsg = "trying login"
for {
m.log.Debugf(logmsg+" %s %s %s", m.Credentials.Team, m.Credentials.Login, m.Credentials.Server)
myinfo, appErr = m.Client.Login(m.Credentials.Login, m.Credentials.Pass)
m.log.Debugf("%s %s %s %s", logmsg, m.Credentials.Team, m.Credentials.Login, m.Credentials.Server)
if strings.Contains(m.Credentials.Pass, model.SESSION_COOKIE_TOKEN) {
m.log.Debugf(logmsg+" with ", model.SESSION_COOKIE_TOKEN)
token := strings.Split(m.Credentials.Pass, model.SESSION_COOKIE_TOKEN+"=")
m.Client.HttpClient.Jar = m.createCookieJar(token[1])
m.Client.MockSession(token[1])
myinfo, appErr = m.Client.GetMe("")
if myinfo.Data.(*model.User) == nil {
m.log.Errorf("LOGIN TOKEN: %s is invalid", m.Credentials.Pass)
return errors.New("invalid " + model.SESSION_COOKIE_TOKEN)
}
} else {
myinfo, appErr = m.Client.Login(m.Credentials.Login, m.Credentials.Pass)
}
if appErr != nil {
d := b.Duration()
m.log.Debug(appErr.DetailedError)
@ -89,7 +111,7 @@ func (m *MMClient) Login() error {
}
return errors.New(appErr.Message)
}
m.log.Debug("LOGIN: %s, reconnecting in %s", appErr, d)
m.log.Debugf("LOGIN: %s, reconnecting in %s", appErr, d)
time.Sleep(d)
logmsg = "retrying login"
continue
@ -98,15 +120,16 @@ func (m *MMClient) Login() error {
}
// reset timer
b.Reset()
m.User = myinfo.Data.(*model.User)
teamdata, _ := m.Client.GetAllTeamListings()
teams := teamdata.Data.(map[string]*model.Team)
for k, v := range teams {
initLoad, _ := m.Client.GetInitialLoad()
initData := initLoad.Data.(*model.InitialLoad)
m.User = initData.User
for _, v := range initData.Teams {
m.log.Debugf("trying %s (id: %s)", v.Name, v.Id)
if v.Name == m.Credentials.Team {
m.Client.SetTeamId(k)
m.Client.SetTeamId(v.Id)
m.Team = v
m.log.Debug("GetallTeamListings: found id ", k)
m.log.Debugf("GetallTeamListings: found id %s for team %s", v.Id, v.Name)
break
}
}
@ -119,13 +142,14 @@ func (m *MMClient) Login() error {
header := http.Header{}
header.Set(model.HEADER_AUTH, "BEARER "+m.Client.AuthToken)
var WsClient *websocket.Conn
m.log.Debug("WsClient: making connection")
var err error
for {
WsClient, _, err = websocket.DefaultDialer.Dial(wsurl, header)
wsDialer := &websocket.Dialer{Proxy: http.ProxyFromEnvironment, TLSClientConfig: &tls.Config{InsecureSkipVerify: m.SkipTLSVerify}}
m.WsClient, _, err = wsDialer.Dial(wsurl, header)
if err != nil {
d := b.Duration()
log.Printf("WSS: %s, reconnecting in %s", err, d)
m.log.Debugf("WSS: %s, reconnecting in %s", err, d)
time.Sleep(d)
continue
}
@ -133,8 +157,6 @@ func (m *MMClient) Login() error {
}
b.Reset()
m.WsClient = WsClient
// populating users
m.UpdateUsers()
@ -147,12 +169,19 @@ func (m *MMClient) Login() error {
func (m *MMClient) WsReceiver() {
var rmsg model.Message
for {
if m.WsQuit {
m.log.Debug("exiting WsReceiver")
return
}
if err := m.WsClient.ReadJSON(&rmsg); err != nil {
log.Println("error:", err)
m.log.Error("error:", err)
// reconnect
m.Login()
}
//log.Printf("WsReceiver: %#v", rmsg)
if rmsg.Action == "ping" {
m.handleWsPing()
continue
}
msg := &Message{Raw: &rmsg, Team: m.Credentials.Team}
m.parseMessage(msg)
m.MessageChan <- msg
@ -160,6 +189,14 @@ func (m *MMClient) WsReceiver() {
}
func (m *MMClient) handleWsPing() {
m.log.Debug("Ws PING")
if !m.WsQuit && !m.WsAway {
m.log.Debug("Ws PONG")
m.WsClient.WriteMessage(websocket.PongMessage, []byte{})
}
}
func (m *MMClient) parseMessage(rmsg *Message) {
switch rmsg.Raw.Action {
case model.ACTION_POSTED:
@ -198,7 +235,7 @@ func (m *MMClient) parseActionPost(rmsg *Message) {
}
func (m *MMClient) UpdateUsers() error {
mmusers, _ := m.Client.GetProfiles(m.Client.GetTeamId(), "")
mmusers, _ := m.Client.GetProfilesForDirectMessageList(m.Team.Id)
m.Users = mmusers.Data.(map[string]*model.User)
return nil
}
@ -294,29 +331,49 @@ func (m *MMClient) GetPosts(channelId string, limit int) *model.PostList {
return res.Data.(*model.PostList)
}
func (m *MMClient) GetPublicLink(filename string) string {
res, err := m.Client.GetPublicLink(filename)
if err != nil {
return ""
}
return res.Data.(string)
}
func (m *MMClient) GetPublicLinks(filenames []string) []string {
var output []string
for _, f := range filenames {
res, err := m.Client.GetPublicLink(f)
if err != nil {
continue
}
output = append(output, res.Data.(string))
}
return output
}
func (m *MMClient) UpdateChannelHeader(channelId string, header string) {
data := make(map[string]string)
data["channel_id"] = channelId
data["channel_header"] = header
log.Printf("updating channelheader %#v, %#v", channelId, header)
m.log.Debugf("updating channelheader %#v, %#v", channelId, header)
_, err := m.Client.UpdateChannelHeader(data)
if err != nil {
log.Print(err)
log.Error(err)
}
}
func (m *MMClient) UpdateLastViewed(channelId string) {
log.Printf("posting lastview %#v", channelId)
m.log.Debugf("posting lastview %#v", channelId)
_, err := m.Client.UpdateLastViewedAt(channelId)
if err != nil {
log.Print(err)
m.log.Error(err)
}
}
func (m *MMClient) UsernamesInChannel(channelName string) []string {
ceiRes, err := m.Client.GetChannelExtraInfo(m.GetChannelId(channelName), 5000, "")
if err != nil {
log.Errorf("UsernamesInChannel(%s) failed: %s", channelName, err)
m.log.Errorf("UsernamesInChannel(%s) failed: %s", channelName, err)
return []string{}
}
extra := ceiRes.Data.(*model.ChannelExtra)
@ -326,3 +383,59 @@ func (m *MMClient) UsernamesInChannel(channelName string) []string {
}
return result
}
func (m *MMClient) createCookieJar(token string) *cookiejar.Jar {
var cookies []*http.Cookie
jar, _ := cookiejar.New(nil)
firstCookie := &http.Cookie{
Name: "MMAUTHTOKEN",
Value: token,
Path: "/",
Domain: m.Credentials.Server,
}
cookies = append(cookies, firstCookie)
cookieURL, _ := url.Parse("https://" + m.Credentials.Server)
jar.SetCookies(cookieURL, cookies)
return jar
}
func (m *MMClient) GetOtherUserDM(channel string) *model.User {
m.UpdateUsers()
var rcvuser *model.User
if strings.Contains(channel, "__") {
rcvusers := strings.Split(channel, "__")
if rcvusers[0] != m.User.Id {
rcvuser = m.Users[rcvusers[0]]
} else {
rcvuser = m.Users[rcvusers[1]]
}
}
return rcvuser
}
func (m *MMClient) SendDirectMessage(toUserId string, msg string) {
m.log.Debugf("SendDirectMessage to %s, msg %s", toUserId, msg)
var channel string
// We don't have a DM with this user yet.
if m.GetChannelId(toUserId+"__"+m.User.Id) == "" && m.GetChannelId(m.User.Id+"__"+toUserId) == "" {
// create DM channel
_, err := m.Client.CreateDirectChannel(toUserId)
if err != nil {
m.log.Debugf("SendDirectMessage to %#v failed: %s", toUserId, err)
}
// update our channels
mmchannels, _ := m.Client.GetChannels("")
m.Channels = mmchannels.Data.(*model.ChannelList)
}
// build the channel name
if toUserId > m.User.Id {
channel = m.User.Id + "__" + toUserId
} else {
channel = toUserId + "__" + m.User.Id
}
// build & send the message
msg = strings.Replace(msg, "\r", "", -1)
post := &model.Post{ChannelId: m.GetChannelId(channel), Message: msg}
m.Client.CreatePost(post)
}

View File

@ -1,202 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
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.

View File

@ -1,154 +0,0 @@
//Package matterhook provides interaction with mattermost incoming/outgoing webhooks
package matterhook
import (
"bytes"
"crypto/tls"
"encoding/json"
"fmt"
"github.com/gorilla/schema"
"io"
"io/ioutil"
"log"
"net/http"
"strconv"
)
// OMessage for mattermost incoming webhook. (send to mattermost)
type OMessage struct {
Channel string `json:"channel,omitempty"`
IconURL string `json:"icon_url,omitempty"`
IconEmoji string `json:"icon_emoji,omitempty"`
UserName string `json:"username,omitempty"`
Text string `json:"text"`
Attachments interface{} `json:"attachments,omitempty"`
Type string `json:"type,omitempty"`
}
// IMessage for mattermost outgoing webhook. (received from mattermost)
type IMessage struct {
Token string `schema:"token"`
TeamID string `schema:"team_id"`
TeamDomain string `schema:"team_domain"`
ChannelID string `schema:"channel_id"`
ServiceID string `schema:"service_id"`
ChannelName string `schema:"channel_name"`
Timestamp string `schema:"timestamp"`
UserID string `schema:"user_id"`
UserName string `schema:"user_name"`
Text string `schema:"text"`
TriggerWord string `schema:"trigger_word"`
}
// Client for Mattermost.
type Client struct {
Url string // URL for incoming webhooks on mattermost.
In chan IMessage
Out chan OMessage
httpclient *http.Client
Config
}
// Config for client.
type Config struct {
Port int // Port to listen on.
BindAddress string // Address to listen on
Token string // Only allow this token from Mattermost. (Allow everything when empty)
InsecureSkipVerify bool // disable certificate checking
DisableServer bool // Do not start server for outgoing webhooks from Mattermost.
}
// New Mattermost client.
func New(url string, config Config) *Client {
c := &Client{Url: url, In: make(chan IMessage), Out: make(chan OMessage), Config: config}
if c.Port == 0 {
c.Port = 9999
}
c.BindAddress += ":"
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: config.InsecureSkipVerify},
}
c.httpclient = &http.Client{Transport: tr}
if !c.DisableServer {
go c.StartServer()
}
return c
}
// StartServer starts a webserver listening for incoming mattermost POSTS.
func (c *Client) StartServer() {
mux := http.NewServeMux()
mux.Handle("/", c)
log.Printf("Listening on http://%v:%v...\n", c.BindAddress, c.Port)
if err := http.ListenAndServe((c.BindAddress + strconv.Itoa(c.Port)), mux); err != nil {
log.Fatal(err)
}
}
// ServeHTTP implementation.
func (c *Client) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
log.Println("invalid " + r.Method + " connection from " + r.RemoteAddr)
http.NotFound(w, r)
return
}
msg := IMessage{}
err := r.ParseForm()
if err != nil {
log.Println(err)
http.NotFound(w, r)
return
}
defer r.Body.Close()
decoder := schema.NewDecoder()
err = decoder.Decode(&msg, r.PostForm)
if err != nil {
log.Println(err)
http.NotFound(w, r)
return
}
if msg.Token == "" {
log.Println("no token from " + r.RemoteAddr)
http.NotFound(w, r)
return
}
if c.Token != "" {
if msg.Token != c.Token {
log.Println("invalid token " + msg.Token + " from " + r.RemoteAddr)
http.NotFound(w, r)
return
}
}
c.In <- msg
}
// Receive returns an incoming message from mattermost outgoing webhooks URL.
func (c *Client) Receive() IMessage {
for {
select {
case msg := <-c.In:
return msg
}
}
}
// Send sends a msg to mattermost incoming webhooks URL.
func (c *Client) Send(msg OMessage) error {
buf, err := json.Marshal(msg)
if err != nil {
return err
}
resp, err := c.httpclient.Post(c.Url, "application/json", bytes.NewReader(buf))
if err != nil {
return err
}
defer resp.Body.Close()
// Read entire body to completion to re-use keep-alive connections.
io.Copy(ioutil.Discard, resp.Body)
if resp.StatusCode != 200 {
return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
return nil
}

View File

@ -0,0 +1,20 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package einterfaces
import "github.com/mattermost/platform/model"
type AccountMigrationInterface interface {
MigrateToLdap(fromAuthService string, forignUserFieldNameToMatch string) *model.AppError
}
var theAccountMigrationInterface AccountMigrationInterface
func RegisterAccountMigrationInterface(newInterface AccountMigrationInterface) {
theAccountMigrationInterface = newInterface
}
func GetAccountMigrationInterface() AccountMigrationInterface {
return theAccountMigrationInterface
}

View File

@ -0,0 +1,32 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package einterfaces
import (
"github.com/mattermost/platform/model"
)
type ClusterInterface interface {
StartInterNodeCommunication()
StopInterNodeCommunication()
GetClusterInfos() []*model.ClusterInfo
RemoveAllSessionsForUserId(userId string)
InvalidateCacheForUser(userId string)
InvalidateCacheForChannel(channelId string)
Publish(event *model.WebSocketEvent)
UpdateStatus(status *model.Status)
GetLogs() ([]string, *model.AppError)
GetClusterId() string
ConfigChanged(previousConfig *model.Config, newConfig *model.Config, sendToOtherServer bool) *model.AppError
}
var theClusterInterface ClusterInterface
func RegisterClusterInterface(newInterface ClusterInterface) {
theClusterInterface = newInterface
}
func GetClusterInterface() ClusterInterface {
return theClusterInterface
}

View File

@ -0,0 +1,22 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package einterfaces
import (
"github.com/mattermost/platform/model"
)
type EmojiInterface interface {
CanUserCreateEmoji(string, []*model.TeamMember) bool
}
var theEmojiInterface EmojiInterface
func RegisterEmojiInterface(newInterface EmojiInterface) {
theEmojiInterface = newInterface
}
func GetEmojiInterface() EmojiInterface {
return theEmojiInterface
}

View File

@ -13,6 +13,10 @@ type LdapInterface interface {
CheckPassword(id string, password string) *model.AppError
SwitchToLdap(userId, ldapId, ldapPassword string) *model.AppError
ValidateFilter(filter string) *model.AppError
Syncronize() *model.AppError
StartLdapSyncJob()
SyncNow()
GetAllLdapUsers() ([]*model.User, *model.AppError)
}
var theLdapInterface LdapInterface

View File

@ -0,0 +1,25 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package einterfaces
import (
"github.com/mattermost/platform/model"
)
type SamlInterface interface {
ConfigureSP() *model.AppError
BuildRequest(relayState string) (*model.SamlAuthRequest, *model.AppError)
DoLogin(encodedXML string, relayState map[string]string) (*model.User, *model.AppError)
GetMetadata() (string, *model.AppError)
}
var theSamlInterface SamlInterface
func RegisterSamlInterface(newInterface SamlInterface) {
theSamlInterface = newInterface
}
func GetSamlInterface() SamlInterface {
return theSamlInterface
}

View File

@ -15,10 +15,12 @@ const (
)
type AccessData struct {
AuthCode string `json:"auth_code"`
ClientId string `json:"client_id"`
UserId string `json:"user_id"`
Token string `json:"token"`
RefreshToken string `json:"refresh_token"`
RedirectUri string `json:"redirect_uri"`
ExpiresAt int64 `json:"expires_at"`
}
type AccessResponse struct {
@ -33,8 +35,12 @@ type AccessResponse struct {
// correctly.
func (ad *AccessData) IsValid() *AppError {
if len(ad.AuthCode) == 0 || len(ad.AuthCode) > 128 {
return NewLocAppError("AccessData.IsValid", "model.access.is_valid.auth_code.app_error", nil, "")
if len(ad.ClientId) == 0 || len(ad.ClientId) > 26 {
return NewLocAppError("AccessData.IsValid", "model.access.is_valid.client_id.app_error", nil, "")
}
if len(ad.UserId) == 0 || len(ad.UserId) > 26 {
return NewLocAppError("AccessData.IsValid", "model.access.is_valid.user_id.app_error", nil, "")
}
if len(ad.Token) != 26 {
@ -52,6 +58,19 @@ func (ad *AccessData) IsValid() *AppError {
return nil
}
func (me *AccessData) IsExpired() bool {
if me.ExpiresAt <= 0 {
return false
}
if GetMillis() > me.ExpiresAt {
return true
}
return false
}
func (ad *AccessData) ToJson() string {
b, err := json.Marshal(ad)
if err != nil {

View File

@ -11,6 +11,7 @@ import (
const (
AUTHCODE_EXPIRE_TIME = 60 * 10 // 10 minutes
AUTHCODE_RESPONSE_TYPE = "code"
DEFAULT_SCOPE = "user"
)
type AuthData struct {
@ -71,6 +72,10 @@ func (ad *AuthData) PreSave() {
if ad.CreateAt == 0 {
ad.CreateAt = GetMillis()
}
if len(ad.Scope) == 0 {
ad.Scope = DEFAULT_SCOPE
}
}
func (ad *AuthData) ToJson() string {

View File

@ -124,9 +124,6 @@ func (o *Channel) ExtraUpdated() {
o.ExtraUpdateAt = GetMillis()
}
func (o *Channel) PreExport() {
}
func GetDMNameFromIds(userId1, userId2 string) string {
if userId1 > userId2 {
return userId2 + "__" + userId1

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,66 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"encoding/json"
"io"
)
type ClusterInfo struct {
Id string `json:"id"`
Version string `json:"version"`
ConfigHash string `json:"config_hash"`
InterNodeUrl string `json:"internode_url"`
Hostname string `json:"hostname"`
LastSuccessfulPing int64 `json:"last_ping"`
IsAlive bool `json:"is_alive"`
}
func (me *ClusterInfo) ToJson() string {
b, err := json.Marshal(me)
if err != nil {
return ""
} else {
return string(b)
}
}
func ClusterInfoFromJson(data io.Reader) *ClusterInfo {
decoder := json.NewDecoder(data)
var me ClusterInfo
err := decoder.Decode(&me)
if err == nil {
return &me
} else {
return nil
}
}
func (me *ClusterInfo) HaveEstablishedInitialContact() bool {
if me.Id != "" {
return true
}
return false
}
func ClusterInfosToJson(objmap []*ClusterInfo) string {
if b, err := json.Marshal(objmap); err != nil {
return ""
} else {
return string(b)
}
}
func ClusterInfosFromJson(data io.Reader) []*ClusterInfo {
decoder := json.NewDecoder(data)
var objmap []*ClusterInfo
if err := decoder.Decode(&objmap); err != nil {
return make([]*ClusterInfo, 0)
} else {
return objmap
}
}

View File

@ -6,11 +6,14 @@ package model
import (
"encoding/json"
"io"
"strings"
)
const (
COMMAND_METHOD_POST = "P"
COMMAND_METHOD_GET = "G"
MIN_TRIGGER_LENGTH = 1
MAX_TRIGGER_LENGTH = 128
)
type Command struct {
@ -99,7 +102,7 @@ func (o *Command) IsValid() *AppError {
return NewLocAppError("Command.IsValid", "model.command.is_valid.team_id.app_error", nil, "")
}
if len(o.Trigger) == 0 || len(o.Trigger) > 128 {
if len(o.Trigger) < MIN_TRIGGER_LENGTH || len(o.Trigger) > MAX_TRIGGER_LENGTH || strings.Index(o.Trigger, "/") == 0 || strings.Contains(o.Trigger, " ") {
return NewLocAppError("Command.IsValid", "model.command.is_valid.trigger.app_error", nil, "")
}

View File

@ -6,6 +6,7 @@ package model
import (
"encoding/json"
"io"
"net/url"
)
const (
@ -19,8 +20,12 @@ const (
DATABASE_DRIVER_MYSQL = "mysql"
DATABASE_DRIVER_POSTGRES = "postgres"
SERVICE_GITLAB = "gitlab"
SERVICE_GOOGLE = "google"
PASSWORD_MAXIMUM_LENGTH = 64
PASSWORD_MINIMUM_LENGTH = 5
SERVICE_GITLAB = "gitlab"
SERVICE_GOOGLE = "google"
SERVICE_OFFICE365 = "office365"
WEBSERVER_MODE_REGULAR = "regular"
WEBSERVER_MODE_GZIP = "gzip"
@ -32,10 +37,21 @@ const (
DIRECT_MESSAGE_ANY = "any"
DIRECT_MESSAGE_TEAM = "team"
PERMISSIONS_ALL = "all"
PERMISSIONS_TEAM_ADMIN = "team_admin"
PERMISSIONS_SYSTEM_ADMIN = "system_admin"
FAKE_SETTING = "********************************"
RESTRICT_EMOJI_CREATION_ALL = "all"
RESTRICT_EMOJI_CREATION_ADMIN = "admin"
RESTRICT_EMOJI_CREATION_SYSTEM_ADMIN = "system_admin"
SITENAME_MAX_LENGTH = 30
)
type ServiceSettings struct {
SiteURL *string
ListenAddress string
MaximumLoginAttempts int
SegmentDeveloperKey string
@ -60,6 +76,14 @@ type ServiceSettings struct {
WebsocketSecurePort *int
WebsocketPort *int
WebserverMode *string
EnableCustomEmoji *bool
RestrictCustomEmojiCreation *string
}
type ClusterSettings struct {
Enable *bool
InterNodeListenAddress *string
InterNodeUrls []string
}
type SSOSettings struct {
@ -83,15 +107,25 @@ type SqlSettings struct {
}
type LogSettings struct {
EnableConsole bool
ConsoleLevel string
EnableFile bool
FileLevel string
FileFormat string
FileLocation string
EnableConsole bool
ConsoleLevel string
EnableFile bool
FileLevel string
FileFormat string
FileLocation string
EnableWebhookDebugging bool
}
type PasswordSettings struct {
MinimumLength *int
Lowercase *bool
Number *bool
Uppercase *bool
Symbol *bool
}
type FileSettings struct {
MaxFileSize *int64
DriverName string
Directory string
EnablePublicLink bool
@ -121,6 +155,7 @@ type EmailSettings struct {
RequireEmailVerification bool
FeedbackName string
FeedbackEmail string
FeedbackOrganization *string
SMTPUsername string
SMTPPassword string
SMTPServer string
@ -156,16 +191,21 @@ type SupportSettings struct {
}
type TeamSettings struct {
SiteName string
MaxUsersPerTeam int
EnableTeamCreation bool
EnableUserCreation bool
EnableOpenServer *bool
RestrictCreationToDomains string
RestrictTeamNames *bool
EnableCustomBrand *bool
CustomBrandText *string
RestrictDirectMessage *string
SiteName string
MaxUsersPerTeam int
EnableTeamCreation bool
EnableUserCreation bool
EnableOpenServer *bool
RestrictCreationToDomains string
RestrictTeamNames *bool
EnableCustomBrand *bool
CustomBrandText *string
CustomDescriptionText *string
RestrictDirectMessage *string
RestrictTeamInvite *string
RestrictPublicChannelManagement *string
RestrictPrivateChannelManagement *string
UserStatusAwayTimeout *int64
}
type LdapSettings struct {
@ -189,9 +229,13 @@ type LdapSettings struct {
NicknameAttribute *string
IdAttribute *string
// Syncronization
SyncIntervalMinutes *int
// Advanced
SkipCertificateVerification *bool
QueryTimeout *int
MaxPageSize *int
// Customization
LoginFieldName *string
@ -203,20 +247,63 @@ type ComplianceSettings struct {
EnableDaily *bool
}
type LocalizationSettings struct {
DefaultServerLocale *string
DefaultClientLocale *string
AvailableLocales *string
}
type SamlSettings struct {
// Basic
Enable *bool
Verify *bool
Encrypt *bool
IdpUrl *string
IdpDescriptorUrl *string
AssertionConsumerServiceURL *string
IdpCertificateFile *string
PublicCertificateFile *string
PrivateKeyFile *string
// User Mapping
FirstNameAttribute *string
LastNameAttribute *string
EmailAttribute *string
UsernameAttribute *string
NicknameAttribute *string
LocaleAttribute *string
LoginButtonText *string
}
type NativeAppSettings struct {
AppDownloadLink *string
AndroidAppDownloadLink *string
IosAppDownloadLink *string
}
type Config struct {
ServiceSettings ServiceSettings
TeamSettings TeamSettings
SqlSettings SqlSettings
LogSettings LogSettings
FileSettings FileSettings
EmailSettings EmailSettings
RateLimitSettings RateLimitSettings
PrivacySettings PrivacySettings
SupportSettings SupportSettings
GitLabSettings SSOSettings
GoogleSettings SSOSettings
LdapSettings LdapSettings
ComplianceSettings ComplianceSettings
ServiceSettings ServiceSettings
TeamSettings TeamSettings
SqlSettings SqlSettings
LogSettings LogSettings
PasswordSettings PasswordSettings
FileSettings FileSettings
EmailSettings EmailSettings
RateLimitSettings RateLimitSettings
PrivacySettings PrivacySettings
SupportSettings SupportSettings
GitLabSettings SSOSettings
GoogleSettings SSOSettings
Office365Settings SSOSettings
LdapSettings LdapSettings
ComplianceSettings ComplianceSettings
LocalizationSettings LocalizationSettings
SamlSettings SamlSettings
NativeAppSettings NativeAppSettings
ClusterSettings ClusterSettings
}
func (o *Config) ToJson() string {
@ -234,6 +321,8 @@ func (o *Config) GetSSOService(service string) *SSOSettings {
return &o.GitLabSettings
case SERVICE_GOOGLE:
return &o.GoogleSettings
case SERVICE_OFFICE365:
return &o.Office365Settings
}
return nil
@ -256,6 +345,11 @@ func (o *Config) SetDefaults() {
o.SqlSettings.AtRestEncryptKey = NewRandomString(32)
}
if o.FileSettings.MaxFileSize == nil {
o.FileSettings.MaxFileSize = new(int64)
*o.FileSettings.MaxFileSize = 52428800 // 50 MB
}
if len(o.FileSettings.PublicLinkSalt) == 0 {
o.FileSettings.PublicLinkSalt = NewRandomString(32)
}
@ -278,6 +372,11 @@ func (o *Config) SetDefaults() {
o.EmailSettings.PasswordResetSalt = NewRandomString(32)
}
if o.ServiceSettings.SiteURL == nil {
o.ServiceSettings.SiteURL = new(string)
*o.ServiceSettings.SiteURL = ""
}
if o.ServiceSettings.EnableDeveloper == nil {
o.ServiceSettings.EnableDeveloper = new(bool)
*o.ServiceSettings.EnableDeveloper = false
@ -298,6 +397,31 @@ func (o *Config) SetDefaults() {
*o.ServiceSettings.EnableMultifactorAuthentication = false
}
if o.PasswordSettings.MinimumLength == nil {
o.PasswordSettings.MinimumLength = new(int)
*o.PasswordSettings.MinimumLength = PASSWORD_MINIMUM_LENGTH
}
if o.PasswordSettings.Lowercase == nil {
o.PasswordSettings.Lowercase = new(bool)
*o.PasswordSettings.Lowercase = false
}
if o.PasswordSettings.Number == nil {
o.PasswordSettings.Number = new(bool)
*o.PasswordSettings.Number = false
}
if o.PasswordSettings.Uppercase == nil {
o.PasswordSettings.Uppercase = new(bool)
*o.PasswordSettings.Uppercase = false
}
if o.PasswordSettings.Symbol == nil {
o.PasswordSettings.Symbol = new(bool)
*o.PasswordSettings.Symbol = false
}
if o.TeamSettings.RestrictTeamNames == nil {
o.TeamSettings.RestrictTeamNames = new(bool)
*o.TeamSettings.RestrictTeamNames = true
@ -313,6 +437,11 @@ func (o *Config) SetDefaults() {
*o.TeamSettings.CustomBrandText = ""
}
if o.TeamSettings.CustomDescriptionText == nil {
o.TeamSettings.CustomDescriptionText = new(string)
*o.TeamSettings.CustomDescriptionText = ""
}
if o.TeamSettings.EnableOpenServer == nil {
o.TeamSettings.EnableOpenServer = new(bool)
*o.TeamSettings.EnableOpenServer = false
@ -323,6 +452,26 @@ func (o *Config) SetDefaults() {
*o.TeamSettings.RestrictDirectMessage = DIRECT_MESSAGE_ANY
}
if o.TeamSettings.RestrictTeamInvite == nil {
o.TeamSettings.RestrictTeamInvite = new(string)
*o.TeamSettings.RestrictTeamInvite = PERMISSIONS_ALL
}
if o.TeamSettings.RestrictPublicChannelManagement == nil {
o.TeamSettings.RestrictPublicChannelManagement = new(string)
*o.TeamSettings.RestrictPublicChannelManagement = PERMISSIONS_ALL
}
if o.TeamSettings.RestrictPrivateChannelManagement == nil {
o.TeamSettings.RestrictPrivateChannelManagement = new(string)
*o.TeamSettings.RestrictPrivateChannelManagement = PERMISSIONS_ALL
}
if o.TeamSettings.UserStatusAwayTimeout == nil {
o.TeamSettings.UserStatusAwayTimeout = new(int64)
*o.TeamSettings.UserStatusAwayTimeout = 300
}
if o.EmailSettings.EnableSignInWithEmail == nil {
o.EmailSettings.EnableSignInWithEmail = new(bool)
@ -353,13 +502,18 @@ func (o *Config) SetDefaults() {
*o.EmailSettings.PushNotificationContents = GENERIC_NOTIFICATION
}
if o.EmailSettings.FeedbackOrganization == nil {
o.EmailSettings.FeedbackOrganization = new(string)
*o.EmailSettings.FeedbackOrganization = ""
}
if !IsSafeLink(o.SupportSettings.TermsOfServiceLink) {
o.SupportSettings.TermsOfServiceLink = nil
}
if o.SupportSettings.TermsOfServiceLink == nil {
o.SupportSettings.TermsOfServiceLink = new(string)
*o.SupportSettings.TermsOfServiceLink = "/static/help/terms.html"
*o.SupportSettings.TermsOfServiceLink = "https://about.mattermost.com/default-terms/"
}
if !IsSafeLink(o.SupportSettings.PrivacyPolicyLink) {
@ -368,7 +522,7 @@ func (o *Config) SetDefaults() {
if o.SupportSettings.PrivacyPolicyLink == nil {
o.SupportSettings.PrivacyPolicyLink = new(string)
*o.SupportSettings.PrivacyPolicyLink = "/static/help/privacy.html"
*o.SupportSettings.PrivacyPolicyLink = ""
}
if !IsSafeLink(o.SupportSettings.AboutLink) {
@ -377,7 +531,7 @@ func (o *Config) SetDefaults() {
if o.SupportSettings.AboutLink == nil {
o.SupportSettings.AboutLink = new(string)
*o.SupportSettings.AboutLink = "/static/help/about.html"
*o.SupportSettings.AboutLink = ""
}
if !IsSafeLink(o.SupportSettings.HelpLink) {
@ -386,7 +540,7 @@ func (o *Config) SetDefaults() {
if o.SupportSettings.HelpLink == nil {
o.SupportSettings.HelpLink = new(string)
*o.SupportSettings.HelpLink = "/static/help/help.html"
*o.SupportSettings.HelpLink = ""
}
if !IsSafeLink(o.SupportSettings.ReportAProblemLink) {
@ -395,7 +549,7 @@ func (o *Config) SetDefaults() {
if o.SupportSettings.ReportAProblemLink == nil {
o.SupportSettings.ReportAProblemLink = new(string)
*o.SupportSettings.ReportAProblemLink = "/static/help/report_problem.html"
*o.SupportSettings.ReportAProblemLink = ""
}
if o.SupportSettings.SupportEmail == nil {
@ -403,24 +557,94 @@ func (o *Config) SetDefaults() {
*o.SupportSettings.SupportEmail = "feedback@mattermost.com"
}
if o.LdapSettings.Enable == nil {
o.LdapSettings.Enable = new(bool)
*o.LdapSettings.Enable = false
}
if o.LdapSettings.LdapServer == nil {
o.LdapSettings.LdapServer = new(string)
*o.LdapSettings.LdapServer = ""
}
if o.LdapSettings.LdapPort == nil {
o.LdapSettings.LdapPort = new(int)
*o.LdapSettings.LdapPort = 389
}
if o.LdapSettings.ConnectionSecurity == nil {
o.LdapSettings.ConnectionSecurity = new(string)
*o.LdapSettings.ConnectionSecurity = ""
}
if o.LdapSettings.BaseDN == nil {
o.LdapSettings.BaseDN = new(string)
*o.LdapSettings.BaseDN = ""
}
if o.LdapSettings.BindUsername == nil {
o.LdapSettings.BindUsername = new(string)
*o.LdapSettings.BindUsername = ""
}
if o.LdapSettings.BindPassword == nil {
o.LdapSettings.BindPassword = new(string)
*o.LdapSettings.BindPassword = ""
}
if o.LdapSettings.UserFilter == nil {
o.LdapSettings.UserFilter = new(string)
*o.LdapSettings.UserFilter = ""
}
if o.LdapSettings.FirstNameAttribute == nil {
o.LdapSettings.FirstNameAttribute = new(string)
*o.LdapSettings.FirstNameAttribute = ""
}
if o.LdapSettings.LastNameAttribute == nil {
o.LdapSettings.LastNameAttribute = new(string)
*o.LdapSettings.LastNameAttribute = ""
}
if o.LdapSettings.EmailAttribute == nil {
o.LdapSettings.EmailAttribute = new(string)
*o.LdapSettings.EmailAttribute = ""
}
if o.LdapSettings.UsernameAttribute == nil {
o.LdapSettings.UsernameAttribute = new(string)
*o.LdapSettings.UsernameAttribute = ""
}
if o.LdapSettings.NicknameAttribute == nil {
o.LdapSettings.NicknameAttribute = new(string)
*o.LdapSettings.NicknameAttribute = ""
}
if o.LdapSettings.IdAttribute == nil {
o.LdapSettings.IdAttribute = new(string)
*o.LdapSettings.IdAttribute = ""
}
if o.LdapSettings.SyncIntervalMinutes == nil {
o.LdapSettings.SyncIntervalMinutes = new(int)
*o.LdapSettings.SyncIntervalMinutes = 60
}
if o.LdapSettings.SkipCertificateVerification == nil {
o.LdapSettings.SkipCertificateVerification = new(bool)
*o.LdapSettings.SkipCertificateVerification = false
}
if o.LdapSettings.QueryTimeout == nil {
o.LdapSettings.QueryTimeout = new(int)
*o.LdapSettings.QueryTimeout = 60
}
if o.LdapSettings.Enable == nil {
o.LdapSettings.Enable = new(bool)
*o.LdapSettings.Enable = false
}
if o.LdapSettings.UserFilter == nil {
o.LdapSettings.UserFilter = new(string)
*o.LdapSettings.UserFilter = ""
if o.LdapSettings.MaxPageSize == nil {
o.LdapSettings.MaxPageSize = new(int)
*o.LdapSettings.MaxPageSize = 0
}
if o.LdapSettings.LoginFieldName == nil {
@ -475,7 +699,33 @@ func (o *Config) SetDefaults() {
if o.ServiceSettings.WebserverMode == nil {
o.ServiceSettings.WebserverMode = new(string)
*o.ServiceSettings.WebserverMode = "regular"
*o.ServiceSettings.WebserverMode = "gzip"
} else if *o.ServiceSettings.WebserverMode == "regular" {
*o.ServiceSettings.WebserverMode = "gzip"
}
if o.ServiceSettings.EnableCustomEmoji == nil {
o.ServiceSettings.EnableCustomEmoji = new(bool)
*o.ServiceSettings.EnableCustomEmoji = true
}
if o.ServiceSettings.RestrictCustomEmojiCreation == nil {
o.ServiceSettings.RestrictCustomEmojiCreation = new(string)
*o.ServiceSettings.RestrictCustomEmojiCreation = RESTRICT_EMOJI_CREATION_ALL
}
if o.ClusterSettings.InterNodeListenAddress == nil {
o.ClusterSettings.InterNodeListenAddress = new(string)
*o.ClusterSettings.InterNodeListenAddress = ":8075"
}
if o.ClusterSettings.Enable == nil {
o.ClusterSettings.Enable = new(bool)
*o.ClusterSettings.Enable = false
}
if o.ClusterSettings.InterNodeUrls == nil {
o.ClusterSettings.InterNodeUrls = []string{}
}
if o.ComplianceSettings.Enable == nil {
@ -493,19 +743,114 @@ func (o *Config) SetDefaults() {
*o.ComplianceSettings.EnableDaily = false
}
if o.LdapSettings.ConnectionSecurity == nil {
o.LdapSettings.ConnectionSecurity = new(string)
*o.LdapSettings.ConnectionSecurity = ""
if o.LocalizationSettings.DefaultServerLocale == nil {
o.LocalizationSettings.DefaultServerLocale = new(string)
*o.LocalizationSettings.DefaultServerLocale = DEFAULT_LOCALE
}
if o.LdapSettings.SkipCertificateVerification == nil {
o.LdapSettings.SkipCertificateVerification = new(bool)
*o.LdapSettings.SkipCertificateVerification = false
if o.LocalizationSettings.DefaultClientLocale == nil {
o.LocalizationSettings.DefaultClientLocale = new(string)
*o.LocalizationSettings.DefaultClientLocale = DEFAULT_LOCALE
}
if o.LdapSettings.NicknameAttribute == nil {
o.LdapSettings.NicknameAttribute = new(string)
*o.LdapSettings.NicknameAttribute = ""
if o.LocalizationSettings.AvailableLocales == nil {
o.LocalizationSettings.AvailableLocales = new(string)
*o.LocalizationSettings.AvailableLocales = ""
}
if o.SamlSettings.Enable == nil {
o.SamlSettings.Enable = new(bool)
*o.SamlSettings.Enable = false
}
if o.SamlSettings.Verify == nil {
o.SamlSettings.Verify = new(bool)
*o.SamlSettings.Verify = false
}
if o.SamlSettings.Encrypt == nil {
o.SamlSettings.Encrypt = new(bool)
*o.SamlSettings.Encrypt = false
}
if o.SamlSettings.IdpUrl == nil {
o.SamlSettings.IdpUrl = new(string)
*o.SamlSettings.IdpUrl = ""
}
if o.SamlSettings.IdpDescriptorUrl == nil {
o.SamlSettings.IdpDescriptorUrl = new(string)
*o.SamlSettings.IdpDescriptorUrl = ""
}
if o.SamlSettings.IdpCertificateFile == nil {
o.SamlSettings.IdpCertificateFile = new(string)
*o.SamlSettings.IdpCertificateFile = ""
}
if o.SamlSettings.PublicCertificateFile == nil {
o.SamlSettings.PublicCertificateFile = new(string)
*o.SamlSettings.PublicCertificateFile = ""
}
if o.SamlSettings.PrivateKeyFile == nil {
o.SamlSettings.PrivateKeyFile = new(string)
*o.SamlSettings.PrivateKeyFile = ""
}
if o.SamlSettings.AssertionConsumerServiceURL == nil {
o.SamlSettings.AssertionConsumerServiceURL = new(string)
*o.SamlSettings.AssertionConsumerServiceURL = ""
}
if o.SamlSettings.LoginButtonText == nil || *o.SamlSettings.LoginButtonText == "" {
o.SamlSettings.LoginButtonText = new(string)
*o.SamlSettings.LoginButtonText = USER_AUTH_SERVICE_SAML_TEXT
}
if o.SamlSettings.FirstNameAttribute == nil {
o.SamlSettings.FirstNameAttribute = new(string)
*o.SamlSettings.FirstNameAttribute = ""
}
if o.SamlSettings.LastNameAttribute == nil {
o.SamlSettings.LastNameAttribute = new(string)
*o.SamlSettings.LastNameAttribute = ""
}
if o.SamlSettings.EmailAttribute == nil {
o.SamlSettings.EmailAttribute = new(string)
*o.SamlSettings.EmailAttribute = ""
}
if o.SamlSettings.UsernameAttribute == nil {
o.SamlSettings.UsernameAttribute = new(string)
*o.SamlSettings.UsernameAttribute = ""
}
if o.SamlSettings.NicknameAttribute == nil {
o.SamlSettings.NicknameAttribute = new(string)
*o.SamlSettings.NicknameAttribute = ""
}
if o.SamlSettings.LocaleAttribute == nil {
o.SamlSettings.LocaleAttribute = new(string)
*o.SamlSettings.LocaleAttribute = ""
}
if o.NativeAppSettings.AppDownloadLink == nil {
o.NativeAppSettings.AppDownloadLink = new(string)
*o.NativeAppSettings.AppDownloadLink = "https://about.mattermost.com/downloads/"
}
if o.NativeAppSettings.AndroidAppDownloadLink == nil {
o.NativeAppSettings.AndroidAppDownloadLink = new(string)
*o.NativeAppSettings.AndroidAppDownloadLink = "https://about.mattermost.com/mattermost-android-app/"
}
if o.NativeAppSettings.IosAppDownloadLink == nil {
o.NativeAppSettings.IosAppDownloadLink = new(string)
*o.NativeAppSettings.IosAppDownloadLink = "https://about.mattermost.com/mattermost-ios-app/"
}
}
@ -515,6 +860,12 @@ func (o *Config) IsValid() *AppError {
return NewLocAppError("Config.IsValid", "model.config.is_valid.login_attempts.app_error", nil, "")
}
if len(*o.ServiceSettings.SiteURL) != 0 {
if _, err := url.ParseRequestURI(*o.ServiceSettings.SiteURL); err != nil {
return NewLocAppError("Config.IsValid", "model.config.is_valid.site_url.app_error", nil, "")
}
}
if len(o.ServiceSettings.ListenAddress) == 0 {
return NewLocAppError("Config.IsValid", "model.config.is_valid.listen_address.app_error", nil, "")
}
@ -547,6 +898,10 @@ func (o *Config) IsValid() *AppError {
return NewLocAppError("Config.IsValid", "model.config.is_valid.sql_max_conn.app_error", nil, "")
}
if *o.FileSettings.MaxFileSize <= 0 {
return NewLocAppError("Config.IsValid", "model.config.is_valid.max_file_size.app_error", nil, "")
}
if !(o.FileSettings.DriverName == IMAGE_DRIVER_LOCAL || o.FileSettings.DriverName == IMAGE_DRIVER_S3) {
return NewLocAppError("Config.IsValid", "model.config.is_valid.file_driver.app_error", nil, "")
}
@ -603,6 +958,102 @@ func (o *Config) IsValid() *AppError {
return NewLocAppError("Config.IsValid", "model.config.is_valid.ldap_security.app_error", nil, "")
}
if *o.LdapSettings.SyncIntervalMinutes <= 0 {
return NewLocAppError("Config.IsValid", "model.config.is_valid.ldap_sync_interval.app_error", nil, "")
}
if *o.LdapSettings.MaxPageSize < 0 {
return NewLocAppError("Config.IsValid", "model.config.is_valid.ldap_max_page_size.app_error", nil, "")
}
if *o.LdapSettings.Enable {
if *o.LdapSettings.LdapServer == "" {
return NewLocAppError("Config.IsValid", "model.config.is_valid.ldap_server", nil, "")
}
if *o.LdapSettings.BaseDN == "" {
return NewLocAppError("Config.IsValid", "model.config.is_valid.ldap_basedn", nil, "")
}
if *o.LdapSettings.FirstNameAttribute == "" {
return NewLocAppError("Config.IsValid", "model.config.is_valid.ldap_firstname", nil, "")
}
if *o.LdapSettings.LastNameAttribute == "" {
return NewLocAppError("Config.IsValid", "model.config.is_valid.ldap_lastname", nil, "")
}
if *o.LdapSettings.EmailAttribute == "" {
return NewLocAppError("Config.IsValid", "model.config.is_valid.ldap_email", nil, "")
}
if *o.LdapSettings.UsernameAttribute == "" {
return NewLocAppError("Config.IsValid", "model.config.is_valid.ldap_username", nil, "")
}
if *o.LdapSettings.IdAttribute == "" {
return NewLocAppError("Config.IsValid", "model.config.is_valid.ldap_id", nil, "")
}
}
if *o.SamlSettings.Enable {
if len(*o.SamlSettings.IdpUrl) == 0 || !IsValidHttpUrl(*o.SamlSettings.IdpUrl) {
return NewLocAppError("Config.IsValid", "model.config.is_valid.saml_idp_url.app_error", nil, "")
}
if len(*o.SamlSettings.IdpDescriptorUrl) == 0 || !IsValidHttpUrl(*o.SamlSettings.IdpDescriptorUrl) {
return NewLocAppError("Config.IsValid", "model.config.is_valid.saml_idp_descriptor_url.app_error", nil, "")
}
if len(*o.SamlSettings.IdpCertificateFile) == 0 {
return NewLocAppError("Config.IsValid", "model.config.is_valid.saml_idp_cert.app_error", nil, "")
}
if len(*o.SamlSettings.EmailAttribute) == 0 {
return NewLocAppError("Config.IsValid", "model.config.is_valid.saml_email_attribute.app_error", nil, "")
}
if len(*o.SamlSettings.UsernameAttribute) == 0 {
return NewLocAppError("Config.IsValid", "model.config.is_valid.saml_username_attribute.app_error", nil, "")
}
if len(*o.SamlSettings.FirstNameAttribute) == 0 {
return NewLocAppError("Config.IsValid", "model.config.is_valid.saml_first_name_attribute.app_error", nil, "")
}
if len(*o.SamlSettings.LastNameAttribute) == 0 {
return NewLocAppError("Config.IsValid", "model.config.is_valid.saml_last_name_attribute.app_error", nil, "")
}
if *o.SamlSettings.Verify {
if len(*o.SamlSettings.AssertionConsumerServiceURL) == 0 || !IsValidHttpUrl(*o.SamlSettings.AssertionConsumerServiceURL) {
return NewLocAppError("Config.IsValid", "model.config.is_valid.saml_assertion_consumer_service_url.app_error", nil, "")
}
}
if *o.SamlSettings.Encrypt {
if len(*o.SamlSettings.PrivateKeyFile) == 0 {
return NewLocAppError("Config.IsValid", "model.config.is_valid.saml_private_key.app_error", nil, "")
}
if len(*o.SamlSettings.PublicCertificateFile) == 0 {
return NewLocAppError("Config.IsValid", "model.config.is_valid.saml_public_cert.app_error", nil, "")
}
}
if len(*o.SamlSettings.EmailAttribute) == 0 {
return NewLocAppError("Config.IsValid", "model.config.is_valid.saml_email_attribute.app_error", nil, "")
}
}
if *o.PasswordSettings.MinimumLength < PASSWORD_MINIMUM_LENGTH || *o.PasswordSettings.MinimumLength > PASSWORD_MAXIMUM_LENGTH {
return NewLocAppError("Config.IsValid", "model.config.is_valid.password_length.app_error", map[string]interface{}{"MinLength": PASSWORD_MINIMUM_LENGTH, "MaxLength": PASSWORD_MAXIMUM_LENGTH}, "")
}
if len(o.TeamSettings.SiteName) > SITENAME_MAX_LENGTH {
return NewLocAppError("Config.IsValid", "model.config.is_valid.sitename_length.app_error", map[string]interface{}{"MaxLength": SITENAME_MAX_LENGTH}, "")
}
return nil
}
@ -615,7 +1066,7 @@ func (o *Config) GetSanitizeOptions() map[string]bool {
}
func (o *Config) Sanitize() {
if &o.LdapSettings != nil && len(*o.LdapSettings.BindPassword) > 0 {
if o.LdapSettings.BindPassword != nil && len(*o.LdapSettings.BindPassword) > 0 {
*o.LdapSettings.BindPassword = FAKE_SETTING
}

95
vendor/github.com/mattermost/platform/model/emoji.go generated vendored Normal file
View File

@ -0,0 +1,95 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"encoding/json"
"io"
)
type Emoji struct {
Id string `json:"id"`
CreateAt int64 `json:"create_at"`
UpdateAt int64 `json:"update_at"`
DeleteAt int64 `json:"delete_at"`
CreatorId string `json:"creator_id"`
Name string `json:"name"`
}
func (emoji *Emoji) IsValid() *AppError {
if len(emoji.Id) != 26 {
return NewLocAppError("Emoji.IsValid", "model.emoji.id.app_error", nil, "")
}
if emoji.CreateAt == 0 {
return NewLocAppError("Emoji.IsValid", "model.emoji.create_at.app_error", nil, "id="+emoji.Id)
}
if emoji.UpdateAt == 0 {
return NewLocAppError("Emoji.IsValid", "model.emoji.update_at.app_error", nil, "id="+emoji.Id)
}
if len(emoji.CreatorId) != 26 {
return NewLocAppError("Emoji.IsValid", "model.emoji.user_id.app_error", nil, "")
}
if len(emoji.Name) == 0 || len(emoji.Name) > 64 {
return NewLocAppError("Emoji.IsValid", "model.emoji.name.app_error", nil, "")
}
return nil
}
func (emoji *Emoji) PreSave() {
if emoji.Id == "" {
emoji.Id = NewId()
}
emoji.CreateAt = GetMillis()
emoji.UpdateAt = emoji.CreateAt
}
func (emoji *Emoji) PreUpdate() {
emoji.UpdateAt = GetMillis()
}
func (emoji *Emoji) ToJson() string {
b, err := json.Marshal(emoji)
if err != nil {
return ""
} else {
return string(b)
}
}
func EmojiFromJson(data io.Reader) *Emoji {
decoder := json.NewDecoder(data)
var emoji Emoji
err := decoder.Decode(&emoji)
if err == nil {
return &emoji
} else {
return nil
}
}
func EmojiListToJson(emojiList []*Emoji) string {
b, err := json.Marshal(emojiList)
if err != nil {
return ""
} else {
return string(b)
}
}
func EmojiListFromJson(data io.Reader) []*Emoji {
decoder := json.NewDecoder(data)
var emojiList []*Emoji
err := decoder.Decode(&emojiList)
if err == nil {
return emojiList
} else {
return nil
}
}

View File

@ -8,10 +8,6 @@ import (
"io"
)
const (
MAX_FILE_SIZE = 50000000 // 50 MB
)
var (
IMAGE_EXTENSIONS = [5]string{".jpg", ".jpeg", ".gif", ".bmp", ".png"}
IMAGE_MIME_TYPES = map[string]string{".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".bmp": "image/bmp", ".png": "image/png", ".tiff": "image/tiff"}

View File

@ -47,7 +47,8 @@ func userFromGitLabUser(glu *GitLabUser) *model.User {
}
strings.TrimSpace(user.Email)
user.Email = glu.Email
*user.AuthData = strconv.FormatInt(glu.Id, 10)
userId := strconv.FormatInt(glu.Id, 10)
user.AuthData = &userId
user.AuthService = model.USER_AUTH_SERVICE_GITLAB
return user

View File

@ -4,13 +4,15 @@
package model
import (
"bytes"
"encoding/json"
"io"
"regexp"
"strings"
)
const (
DEFAULT_WEBHOOK_USERNAME = "webhook"
DEFAULT_WEBHOOK_ICON = "/static/images/webhook_icon.jpg"
)
type IncomingWebhook struct {
@ -125,13 +127,136 @@ func (o *IncomingWebhook) PreUpdate() {
o.UpdateAt = GetMillis()
}
func IncomingWebhookRequestFromJson(data io.Reader) *IncomingWebhookRequest {
decoder := json.NewDecoder(data)
// escapeControlCharsFromPayload escapes control chars (\n, \t) from a byte slice.
// Context:
// JSON strings are not supposed to contain control characters such as \n, \t,
// ... but some incoming webhooks might still send invalid JSON and we want to
// try to handle that. An example invalid JSON string from an incoming webhook
// might look like this (strings for both "text" and "fallback" attributes are
// invalid JSON strings because they contain unescaped newlines and tabs):
// `{
// "text": "this is a test
// that contains a newline and tabs",
// "attachments": [
// {
// "fallback": "Required plain-text summary of the attachment
// that contains a newline and tabs",
// "color": "#36a64f",
// ...
// "text": "Optional text that appears within the attachment
// that contains a newline and tabs",
// ...
// "thumb_url": "http://example.com/path/to/thumb.png"
// }
// ]
// }`
// This function will search for `"key": "value"` pairs, and escape \n, \t
// from the value.
func escapeControlCharsFromPayload(by []byte) []byte {
// we'll search for `"text": "..."` or `"fallback": "..."`, ...
keys := "text|fallback|pretext|author_name|title|value"
// the regexp reads like this:
// (?s): this flag let . match \n (default is false)
// "(keys)": we search for the keys defined above
// \s*:\s*: followed by 0..n spaces/tabs, a colon then 0..n spaces/tabs
// ": a double-quote
// (\\"|[^"])*: any number of times the `\"` string or any char but a double-quote
// ": a double-quote
r := `(?s)"(` + keys + `)"\s*:\s*"(\\"|[^"])*"`
re := regexp.MustCompile(r)
// the function that will escape \n and \t on the regexp matches
repl := func(b []byte) []byte {
if bytes.Contains(b, []byte("\n")) {
b = bytes.Replace(b, []byte("\n"), []byte("\\n"), -1)
}
if bytes.Contains(b, []byte("\t")) {
b = bytes.Replace(b, []byte("\t"), []byte("\\t"), -1)
}
return b
}
return re.ReplaceAllFunc(by, repl)
}
func decodeIncomingWebhookRequest(by []byte) (*IncomingWebhookRequest, error) {
decoder := json.NewDecoder(bytes.NewReader(by))
var o IncomingWebhookRequest
err := decoder.Decode(&o)
if err == nil {
return &o
return &o, nil
} else {
return nil
return nil, err
}
}
// To mention @channel via a webhook in Slack, the message should contain
// <!channel>, as explained at the bottom of this article:
// https://get.slack.help/hc/en-us/articles/202009646-Making-announcements
func expandAnnouncement(text string) string {
c1 := "<!channel>"
c2 := "@channel"
if strings.Contains(text, c1) {
return strings.Replace(text, c1, c2, -1)
}
return text
}
// Expand announcements in incoming webhooks from Slack. Those announcements
// can be found in the text attribute, or in the pretext, text, title and value
// attributes of the attachment structure. The Slack attachment structure is
// documented here: https://api.slack.com/docs/attachments
func expandAnnouncements(i *IncomingWebhookRequest) {
i.Text = expandAnnouncement(i.Text)
if i.Attachments != nil {
attachments := i.Attachments.([]interface{})
for _, attachment := range attachments {
a := attachment.(map[string]interface{})
if a["pretext"] != nil {
a["pretext"] = expandAnnouncement(a["pretext"].(string))
}
if a["text"] != nil {
a["text"] = expandAnnouncement(a["text"].(string))
}
if a["title"] != nil {
a["title"] = expandAnnouncement(a["title"].(string))
}
if a["fields"] != nil {
fields := a["fields"].([]interface{})
for _, field := range fields {
f := field.(map[string]interface{})
if f["value"] != nil {
f["value"] = expandAnnouncement(f["value"].(string))
}
}
}
}
}
}
func IncomingWebhookRequestFromJson(data io.Reader) *IncomingWebhookRequest {
buf := new(bytes.Buffer)
buf.ReadFrom(data)
by := buf.Bytes()
// Try to decode the JSON data. Only if it fails, try to escape control
// characters from the strings contained in the JSON data.
o, err := decodeIncomingWebhookRequest(by)
if err != nil {
o, err = decodeIncomingWebhookRequest(escapeControlCharsFromPayload(by))
if err != nil {
return nil
}
}
expandAnnouncements(o)
return o
}

100
vendor/github.com/mattermost/platform/model/job.go generated vendored Normal file
View File

@ -0,0 +1,100 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"fmt"
"time"
)
type TaskFunc func()
type ScheduledTask struct {
Name string `json:"name"`
Interval time.Duration `json:"interval"`
Recurring bool `json:"recurring"`
function TaskFunc `json:",omitempty"`
timer *time.Timer `json:",omitempty"`
}
var tasks = make(map[string]*ScheduledTask)
func addTask(task *ScheduledTask) {
tasks[task.Name] = task
}
func removeTaskByName(name string) {
delete(tasks, name)
}
func GetTaskByName(name string) *ScheduledTask {
if task, ok := tasks[name]; ok {
return task
}
return nil
}
func GetAllTasks() *map[string]*ScheduledTask {
return &tasks
}
func CreateTask(name string, function TaskFunc, timeToExecution time.Duration) *ScheduledTask {
task := &ScheduledTask{
Name: name,
Interval: timeToExecution,
Recurring: false,
function: function,
}
taskRunner := func() {
go task.function()
removeTaskByName(task.Name)
}
task.timer = time.AfterFunc(timeToExecution, taskRunner)
addTask(task)
return task
}
func CreateRecurringTask(name string, function TaskFunc, interval time.Duration) *ScheduledTask {
task := &ScheduledTask{
Name: name,
Interval: interval,
Recurring: true,
function: function,
}
taskRecurer := func() {
go task.function()
task.timer.Reset(task.Interval)
}
task.timer = time.AfterFunc(interval, taskRecurer)
addTask(task)
return task
}
func (task *ScheduledTask) Cancel() {
task.timer.Stop()
removeTaskByName(task.Name)
}
// Executes the task immediatly. A recurring task will be run regularally after interval.
func (task *ScheduledTask) Execute() {
task.function()
task.timer.Reset(task.Interval)
}
func (task *ScheduledTask) String() string {
return fmt.Sprintf(
"%s\nInterval: %s\nRecurring: %t\n",
task.Name,
task.Interval.String(),
task.Recurring,
)
}

View File

@ -5,4 +5,5 @@ package model
const (
USER_AUTH_SERVICE_LDAP = "ldap"
LDAP_SYNC_TASK_NAME = "LDAP Syncronization"
)

View File

@ -32,14 +32,18 @@ type Customer struct {
}
type Features struct {
Users *int `json:"users"`
LDAP *bool `json:"ldap"`
MFA *bool `json:"mfa"`
GoogleSSO *bool `json:"google_sso"`
Compliance *bool `json:"compliance"`
CustomBrand *bool `json:"custom_brand"`
MHPNS *bool `json:"mhpns"`
FutureFeatures *bool `json:"future_features"`
Users *int `json:"users"`
LDAP *bool `json:"ldap"`
MFA *bool `json:"mfa"`
GoogleOAuth *bool `json:"google_oauth"`
Office365OAuth *bool `json:"office365_oauth"`
Compliance *bool `json:"compliance"`
Cluster *bool `json:"cluster"`
CustomBrand *bool `json:"custom_brand"`
MHPNS *bool `json:"mhpns"`
SAML *bool `json:"saml"`
PasswordRequirements *bool `json:"password_requirements"`
FutureFeatures *bool `json:"future_features"`
}
func (f *Features) SetDefaults() {
@ -63,9 +67,14 @@ func (f *Features) SetDefaults() {
*f.MFA = *f.FutureFeatures
}
if f.GoogleSSO == nil {
f.GoogleSSO = new(bool)
*f.GoogleSSO = *f.FutureFeatures
if f.GoogleOAuth == nil {
f.GoogleOAuth = new(bool)
*f.GoogleOAuth = *f.FutureFeatures
}
if f.Office365OAuth == nil {
f.Office365OAuth = new(bool)
*f.Office365OAuth = *f.FutureFeatures
}
if f.Compliance == nil {
@ -73,6 +82,11 @@ func (f *Features) SetDefaults() {
*f.Compliance = *f.FutureFeatures
}
if f.Cluster == nil {
f.Cluster = new(bool)
*f.Cluster = *f.FutureFeatures
}
if f.CustomBrand == nil {
f.CustomBrand = new(bool)
*f.CustomBrand = *f.FutureFeatures
@ -82,6 +96,16 @@ func (f *Features) SetDefaults() {
f.MHPNS = new(bool)
*f.MHPNS = *f.FutureFeatures
}
if f.SAML == nil {
f.SAML = new(bool)
*f.SAML = *f.FutureFeatures
}
if f.PasswordRequirements == nil {
f.PasswordRequirements = new(bool)
*f.PasswordRequirements = *f.FutureFeatures
}
}
func (l *License) IsExpired() bool {

View File

@ -1,60 +0,0 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"encoding/json"
"io"
)
const (
ACTION_TYPING = "typing"
ACTION_POSTED = "posted"
ACTION_POST_EDITED = "post_edited"
ACTION_POST_DELETED = "post_deleted"
ACTION_CHANNEL_DELETED = "channel_deleted"
ACTION_CHANNEL_VIEWED = "channel_viewed"
ACTION_DIRECT_ADDED = "direct_added"
ACTION_NEW_USER = "new_user"
ACTION_USER_ADDED = "user_added"
ACTION_USER_REMOVED = "user_removed"
ACTION_PREFERENCE_CHANGED = "preference_changed"
ACTION_EPHEMERAL_MESSAGE = "ephemeral_message"
)
type Message struct {
TeamId string `json:"team_id"`
ChannelId string `json:"channel_id"`
UserId string `json:"user_id"`
Action string `json:"action"`
Props map[string]string `json:"props"`
}
func (m *Message) Add(key string, value string) {
m.Props[key] = value
}
func NewMessage(teamId string, channelId string, userId string, action string) *Message {
return &Message{TeamId: teamId, ChannelId: channelId, UserId: userId, Action: action, Props: make(map[string]string)}
}
func (o *Message) ToJson() string {
b, err := json.Marshal(o)
if err != nil {
return ""
} else {
return string(b)
}
}
func MessageFromJson(data io.Reader) *Message {
decoder := json.NewDecoder(data)
var o Message
err := decoder.Decode(&o)
if err == nil {
return &o
} else {
return nil
}
}

View File

@ -25,8 +25,10 @@ type OAuthApp struct {
ClientSecret string `json:"client_secret"`
Name string `json:"name"`
Description string `json:"description"`
IconURL string `json:"icon_url"`
CallbackUrls StringArray `json:"callback_urls"`
Homepage string `json:"homepage"`
IsTrusted bool `json:"is_trusted"`
}
// IsValid validates the app and returns an error if it isn't configured
@ -61,7 +63,13 @@ func (a *OAuthApp) IsValid() *AppError {
return NewLocAppError("OAuthApp.IsValid", "model.oauth.is_valid.callback.app_error", nil, "app_id="+a.Id)
}
if len(a.Homepage) == 0 || len(a.Homepage) > 256 {
for _, callback := range a.CallbackUrls {
if !IsValidHttpUrl(callback) {
return NewLocAppError("OAuthApp.IsValid", "model.oauth.is_valid.callback.app_error", nil, "")
}
}
if len(a.Homepage) == 0 || len(a.Homepage) > 256 || !IsValidHttpUrl(a.Homepage) {
return NewLocAppError("OAuthApp.IsValid", "model.oauth.is_valid.homepage.app_error", nil, "app_id="+a.Id)
}
@ -69,6 +77,12 @@ func (a *OAuthApp) IsValid() *AppError {
return NewLocAppError("OAuthApp.IsValid", "model.oauth.is_valid.description.app_error", nil, "app_id="+a.Id)
}
if len(a.IconURL) > 0 {
if len(a.IconURL) > 512 || !IsValidHttpUrl(a.IconURL) {
return NewLocAppError("OAuthApp.IsValid", "model.oauth.is_valid.icon_url.app_error", nil, "app_id="+a.Id)
}
}
return nil
}
@ -85,10 +99,6 @@ func (a *OAuthApp) PreSave() {
a.CreateAt = GetMillis()
a.UpdateAt = a.CreateAt
if len(a.ClientSecret) > 0 {
a.ClientSecret = HashPassword(a.ClientSecret)
}
}
// PreUpdate should be run before updating the app in the db.
@ -157,3 +167,23 @@ func OAuthAppMapFromJson(data io.Reader) map[string]*OAuthApp {
return nil
}
}
func OAuthAppListToJson(l []*OAuthApp) string {
b, err := json.Marshal(l)
if err != nil {
return ""
} else {
return string(b)
}
}
func OAuthAppListFromJson(data io.Reader) []*OAuthApp {
decoder := json.NewDecoder(data)
var o []*OAuthApp
err := decoder.Decode(&o)
if err == nil {
return o
} else {
return nil
}
}

View File

@ -7,6 +7,9 @@ import (
"encoding/json"
"fmt"
"io"
"net/url"
"strconv"
"strings"
)
type OutgoingWebhook struct {
@ -19,9 +22,51 @@ type OutgoingWebhook struct {
ChannelId string `json:"channel_id"`
TeamId string `json:"team_id"`
TriggerWords StringArray `json:"trigger_words"`
TriggerWhen int `json:"trigger_when"`
CallbackURLs StringArray `json:"callback_urls"`
DisplayName string `json:"display_name"`
Description string `json:"description"`
ContentType string `json:"content_type"`
}
type OutgoingWebhookPayload struct {
Token string `json:"token"`
TeamId string `json:"team_id"`
TeamDomain string `json:"team_domain"`
ChannelId string `json:"channel_id"`
ChannelName string `json:"channel_name"`
Timestamp int64 `json:"timestamp"`
UserId string `json:"user_id"`
UserName string `json:"user_name"`
PostId string `json:"post_id"`
Text string `json:"text"`
TriggerWord string `json:"trigger_word"`
}
func (o *OutgoingWebhookPayload) ToJSON() string {
b, err := json.Marshal(o)
if err != nil {
return ""
} else {
return string(b)
}
}
func (o *OutgoingWebhookPayload) ToFormValues() string {
v := url.Values{}
v.Set("token", o.Token)
v.Set("team_id", o.TeamId)
v.Set("team_domain", o.TeamDomain)
v.Set("channel_id", o.ChannelId)
v.Set("channel_name", o.ChannelName)
v.Set("timestamp", strconv.FormatInt(o.Timestamp/1000, 10))
v.Set("user_id", o.UserId)
v.Set("user_name", o.UserName)
v.Set("post_id", o.PostId)
v.Set("text", o.Text)
v.Set("trigger_word", o.TriggerWord)
return v.Encode()
}
func (o *OutgoingWebhook) ToJson() string {
@ -124,6 +169,14 @@ func (o *OutgoingWebhook) IsValid() *AppError {
return NewLocAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.description.app_error", nil, "")
}
if len(o.ContentType) > 128 {
return NewLocAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.content_type.app_error", nil, "")
}
if o.TriggerWhen > 1 {
return NewLocAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.content_type.app_error", nil, "")
}
return nil
}
@ -157,3 +210,17 @@ func (o *OutgoingWebhook) HasTriggerWord(word string) bool {
return false
}
func (o *OutgoingWebhook) TriggerWordStartsWith(word string) bool {
if len(o.TriggerWords) == 0 || len(word) == 0 {
return false
}
for _, trigger := range o.TriggerWords {
if strings.HasPrefix(word, trigger) {
return true
}
}
return false
}

View File

@ -162,9 +162,6 @@ func (o *Post) AddProp(key string, value interface{}) {
o.Props[key] = value
}
func (o *Post) PreExport() {
}
func (o *Post) IsSystemMessage() bool {
return len(o.Type) >= len(POST_SYSTEM_MESSAGE_PREFIX) && o.Type[:len(POST_SYSTEM_MESSAGE_PREFIX)] == POST_SYSTEM_MESSAGE_PREFIX
}

View File

@ -6,6 +6,8 @@ package model
import (
"encoding/json"
"io"
"regexp"
"strings"
"unicode/utf8"
)
@ -13,6 +15,16 @@ const (
PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW = "direct_channel_show"
PREFERENCE_CATEGORY_TUTORIAL_STEPS = "tutorial_step"
PREFERENCE_CATEGORY_ADVANCED_SETTINGS = "advanced_settings"
PREFERENCE_CATEGORY_FLAGGED_POST = "flagged_post"
PREFERENCE_CATEGORY_DISPLAY_SETTINGS = "display_settings"
PREFERENCE_NAME_COLLAPSE_SETTING = "collapse_previews"
PREFERENCE_CATEGORY_THEME = "theme"
// the name for theme props is the team id
PREFERENCE_CATEGORY_AUTHORIZED_OAUTH_APP = "oauth_app"
// the name for oauth_app is the client_id and value is the current scope
PREFERENCE_CATEGORY_LAST = "last"
PREFERENCE_NAME_LAST_CHANNEL = "channel"
@ -54,13 +66,48 @@ func (o *Preference) IsValid() *AppError {
return NewLocAppError("Preference.IsValid", "model.preference.is_valid.category.app_error", nil, "category="+o.Category)
}
if len(o.Name) == 0 || len(o.Name) > 32 {
if len(o.Name) > 32 {
return NewLocAppError("Preference.IsValid", "model.preference.is_valid.name.app_error", nil, "name="+o.Name)
}
if utf8.RuneCountInString(o.Value) > 128 {
if utf8.RuneCountInString(o.Value) > 2000 {
return NewLocAppError("Preference.IsValid", "model.preference.is_valid.value.app_error", nil, "value="+o.Value)
}
if o.Category == PREFERENCE_CATEGORY_THEME {
var unused map[string]string
if err := json.NewDecoder(strings.NewReader(o.Value)).Decode(&unused); err != nil {
return NewLocAppError("Preference.IsValid", "model.preference.is_valid.theme.app_error", nil, "value="+o.Value)
}
}
return nil
}
func (o *Preference) PreUpdate() {
if o.Category == PREFERENCE_CATEGORY_THEME {
// decode the value of theme (a map of strings to string) and eliminate any invalid values
var props map[string]string
if err := json.NewDecoder(strings.NewReader(o.Value)).Decode(&props); err != nil {
// just continue, the invalid preference value should get caught by IsValid before saving
return
}
colorPattern := regexp.MustCompile(`^#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?$`)
// blank out any invalid theme values
for name, value := range props {
if name == "image" || name == "type" || name == "codeTheme" {
continue
}
if !colorPattern.MatchString(value) {
props[name] = "#ffffff"
}
}
if b, err := json.Marshal(props); err == nil {
o.Value = string(b)
}
}
}

18
vendor/github.com/mattermost/platform/model/saml.go generated vendored Normal file
View File

@ -0,0 +1,18 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
const (
USER_AUTH_SERVICE_SAML = "saml"
USER_AUTH_SERVICE_SAML_TEXT = "With SAML"
SAML_IDP_CERTIFICATE = 1
SAML_PRIVATE_KEY = 2
SAML_PUBLIC_CERT = 3
)
type SamlAuthRequest struct {
Base64AuthRequest string
URL string
RelayState string
}

View File

@ -4,9 +4,13 @@
package model
import (
"regexp"
"strings"
)
var searchTermPuncStart = regexp.MustCompile(`^[^\pL\d\s#"]+`)
var searchTermPuncEnd = regexp.MustCompile(`[^\pL\d\s*"]+$`)
type SearchParams struct {
Terms string
IsHashtag bool
@ -91,8 +95,8 @@ func parseSearchFlags(input []string) ([]string, [][2]string) {
if !isFlag {
// trim off surrounding punctuation (note that we leave trailing asterisks to allow wildcards)
word = puncStart.ReplaceAllString(word, "")
word = puncEndWildcard.ReplaceAllString(word, "")
word = searchTermPuncStart.ReplaceAllString(word, "")
word = searchTermPuncEnd.ReplaceAllString(word, "")
// and remove extra pound #s
word = hashtagStart.ReplaceAllString(word, "#")

View File

@ -83,7 +83,11 @@ func (me *Session) IsExpired() bool {
}
func (me *Session) SetExpireInDays(days int) {
me.ExpiresAt = GetMillis() + (1000 * 60 * 60 * 24 * int64(days))
if me.CreateAt == 0 {
me.ExpiresAt = GetMillis() + (1000 * 60 * 60 * 24 * int64(days))
} else {
me.ExpiresAt = me.CreateAt + (1000 * 60 * 60 * 24 * int64(days))
}
}
func (me *Session) AddProp(key string, value string) {

42
vendor/github.com/mattermost/platform/model/status.go generated vendored Normal file
View File

@ -0,0 +1,42 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"encoding/json"
"io"
)
const (
STATUS_OFFLINE = "offline"
STATUS_AWAY = "away"
STATUS_ONLINE = "online"
STATUS_CACHE_SIZE = 10000
)
type Status struct {
UserId string `json:"user_id"`
Status string `json:"status"`
LastActivityAt int64 `json:"last_activity_at"`
}
func (o *Status) ToJson() string {
b, err := json.Marshal(o)
if err != nil {
return ""
} else {
return string(b)
}
}
func StatusFromJson(data io.Reader) *Status {
decoder := json.NewDecoder(data)
var o Status
err := decoder.Decode(&o)
if err == nil {
return &o
} else {
return nil
}
}

View File

@ -224,9 +224,6 @@ func CleanTeamName(s string) string {
return s
}
func (o *Team) PreExport() {
}
func (o *Team) Sanitize() {
o.Email = ""
o.AllowedDomains = ""

View File

@ -14,9 +14,10 @@ const (
)
type TeamMember struct {
TeamId string `json:"team_id"`
UserId string `json:"user_id"`
Roles string `json:"roles"`
TeamId string `json:"team_id"`
UserId string `json:"user_id"`
Roles string `json:"roles"`
DeleteAt int64 `json:"delete_at"`
}
func (o *TeamMember) ToJson() string {

View File

@ -16,18 +16,12 @@ import (
const (
ROLE_SYSTEM_ADMIN = "system_admin"
USER_AWAY_TIMEOUT = 5 * 60 * 1000 // 5 minutes
USER_OFFLINE_TIMEOUT = 1 * 60 * 1000 // 1 minute
USER_OFFLINE = "offline"
USER_AWAY = "away"
USER_ONLINE = "online"
USER_NOTIFY_ALL = "all"
USER_NOTIFY_MENTION = "mention"
USER_NOTIFY_NONE = "none"
DEFAULT_LOCALE = "en"
USER_AUTH_SERVICE_EMAIL = "email"
USER_AUTH_SERVICE_USERNAME = "username"
MIN_PASSWORD_LENGTH = 5
)
type User struct {
@ -45,12 +39,9 @@ type User struct {
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Roles string `json:"roles"`
LastActivityAt int64 `json:"last_activity_at,omitempty"`
LastPingAt int64 `json:"last_ping_at,omitempty"`
AllowMarketing bool `json:"allow_marketing,omitempty"`
Props StringMap `json:"props,omitempty"`
NotifyProps StringMap `json:"notify_props,omitempty"`
ThemeProps StringMap `json:"theme_props,omitempty"`
LastPasswordUpdate int64 `json:"last_password_update,omitempty"`
LastPictureUpdate int64 `json:"last_picture_update,omitempty"`
FailedAttempts int `json:"failed_attempts,omitempty"`
@ -95,10 +86,6 @@ func (u *User) IsValid() *AppError {
return NewLocAppError("User.IsValid", "model.user.is_valid.last_name.app_error", nil, "user_id="+u.Id)
}
if len(u.Password) > 128 {
return NewLocAppError("User.IsValid", "model.user.is_valid.pwd.app_error", nil, "user_id="+u.Id)
}
if u.AuthData != nil && len(*u.AuthData) > 128 {
return NewLocAppError("User.IsValid", "model.user.is_valid.auth_data.app_error", nil, "user_id="+u.Id)
}
@ -111,10 +98,6 @@ func (u *User) IsValid() *AppError {
return NewLocAppError("User.IsValid", "model.user.is_valid.auth_data_pwd.app_error", nil, "user_id="+u.Id)
}
if len(u.ThemeProps) > 2000 {
return NewLocAppError("User.IsValid", "model.user.is_valid.theme.app_error", nil, "user_id="+u.Id)
}
return nil
}
@ -136,7 +119,6 @@ func (u *User) PreSave() {
u.Username = strings.ToLower(u.Username)
u.Email = strings.ToLower(u.Email)
u.Locale = strings.ToLower(u.Locale)
u.CreateAt = GetMillis()
u.UpdateAt = u.CreateAt
@ -166,7 +148,6 @@ func (u *User) PreSave() {
func (u *User) PreUpdate() {
u.Username = strings.ToLower(u.Username)
u.Email = strings.ToLower(u.Email)
u.Locale = strings.ToLower(u.Locale)
u.UpdateAt = GetMillis()
if u.AuthData != nil && *u.AuthData == "" {
@ -191,10 +172,10 @@ func (u *User) PreUpdate() {
func (u *User) SetDefaultNotifications() {
u.NotifyProps = make(map[string]string)
u.NotifyProps["email"] = "true"
u.NotifyProps["push"] = USER_NOTIFY_MENTION
u.NotifyProps["desktop"] = USER_NOTIFY_ALL
u.NotifyProps["desktop_sound"] = "true"
u.NotifyProps["mention_keys"] = u.Username + ",@" + u.Username
u.NotifyProps["all"] = "true"
u.NotifyProps["channel"] = "true"
if u.FirstName == "" {
@ -230,16 +211,8 @@ func (u *User) ToJson() string {
}
// Generate a valid strong etag so the browser can cache the results
func (u *User) Etag() string {
return Etag(u.Id, u.UpdateAt)
}
func (u *User) IsOffline() bool {
return (GetMillis()-u.LastPingAt) > USER_OFFLINE_TIMEOUT && (GetMillis()-u.LastActivityAt) > USER_OFFLINE_TIMEOUT
}
func (u *User) IsAway() bool {
return (GetMillis() - u.LastActivityAt) > USER_AWAY_TIMEOUT
func (u *User) Etag(showFullName, showEmail bool) string {
return Etag(u.Id, u.UpdateAt, showFullName, showEmail)
}
// Remove any private data from the user object
@ -270,11 +243,9 @@ func (u *User) ClearNonProfileFields() {
u.MfaActive = false
u.MfaSecret = ""
u.EmailVerified = false
u.LastPingAt = 0
u.AllowMarketing = false
u.Props = StringMap{}
u.NotifyProps = StringMap{}
u.ThemeProps = StringMap{}
u.LastPasswordUpdate = 0
u.LastPictureUpdate = 0
u.FailedAttempts = 0
@ -349,13 +320,13 @@ func isValidRole(role string) bool {
return false
}
// Make sure you acually want to use this function. In context.go there are functions to check permssions
// Make sure you acually want to use this function. In context.go there are functions to check permissions
// This function should not be used to check permissions.
func (u *User) IsInRole(inRole string) bool {
return IsInRole(u.Roles, inRole)
}
// Make sure you acually want to use this function. In context.go there are functions to check permssions
// Make sure you acually want to use this function. In context.go there are functions to check permissions
// This function should not be used to check permissions.
func IsInRole(userRoles string, inRole string) bool {
roles := strings.Split(userRoles, " ")
@ -384,17 +355,6 @@ func (u *User) IsLDAPUser() bool {
return false
}
func (u *User) PreExport() {
u.Password = ""
u.AuthData = new(string)
*u.AuthData = ""
u.LastActivityAt = 0
u.LastPingAt = 0
u.LastPasswordUpdate = 0
u.LastPictureUpdate = 0
u.FailedAttempts = 0
}
// UserFromJson will decode the input and return a User
func UserFromJson(data io.Reader) *User {
decoder := json.NewDecoder(data)
@ -453,6 +413,7 @@ var validUsernameChars = regexp.MustCompile(`^[a-z0-9\.\-_]+$`)
var restrictedUsernames = []string{
"all",
"channel",
"matterbot",
}
func IsValidUsername(s string) bool {

View File

@ -20,6 +20,13 @@ import (
"github.com/pborman/uuid"
)
const (
LOWERCASE_LETTERS = "abcdefghijklmnopqrstuvwxyz"
UPPERCASE_LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
NUMBERS = "0123456789"
SYMBOLS = " !\"\\#$%&'()*+,-./:;<=>?@[]^_`|~"
)
type StringInterface map[string]interface{}
type StringMap map[string]string
type StringArray []string
@ -27,12 +34,12 @@ type EncryptStringMap map[string]string
type AppError struct {
Id string `json:"id"`
Message string `json:"message"` // Message to be display to the end user without debugging information
DetailedError string `json:"detailed_error"` // Internal error string to help the developer
RequestId string `json:"request_id"` // The RequestId that's also set in the header
StatusCode int `json:"status_code"` // The http status code
Where string `json:"-"` // The function where it happened in the form of Struct.Func
IsOAuth bool `json:"is_oauth"` // Whether the error is OAuth specific
Message string `json:"message"` // Message to be display to the end user without debugging information
DetailedError string `json:"detailed_error"` // Internal error string to help the developer
RequestId string `json:"request_id,omitempty"` // The RequestId that's also set in the header
StatusCode int `json:"status_code,omitempty"` // The http status code
Where string `json:"-"` // The function where it happened in the form of Struct.Func
IsOAuth bool `json:"is_oauth,omitempty"` // Whether the error is OAuth specific
params map[string]interface{} `json:"-"`
}
@ -315,10 +322,9 @@ func Etag(parts ...interface{}) string {
}
var validHashtag = regexp.MustCompile(`^(#[A-Za-zäöüÄÖÜß]+[A-Za-z0-9äöüÄÖÜß_\-]*[A-Za-z0-9äöüÄÖÜß])$`)
var puncStart = regexp.MustCompile(`^[.,()&$!\?\[\]{}':;\\<>\-+=%^*|]+`)
var puncStart = regexp.MustCompile(`^[^\pL\d\s#]+`)
var hashtagStart = regexp.MustCompile(`^#{2,}`)
var puncEnd = regexp.MustCompile(`[.,()&$#!\?\[\]{}':;\\<>\-+=%^*|]+$`)
var puncEndWildcard = regexp.MustCompile(`[.,()&$#!\?\[\]{}':;\\<>\-+=%^|]+$`)
var puncEnd = regexp.MustCompile(`[^\pL\d\s]+$`)
func ParseHashtags(text string) (string, string) {
words := strings.Fields(text)

View File

@ -13,6 +13,9 @@ import (
// It should be maitained in chronological order with most current
// release at the front of the list.
var versions = []string{
"3.3.0",
"3.2.0",
"3.1.0",
"3.0.0",
"2.2.0",
"2.1.0",
@ -33,6 +36,7 @@ var CurrentVersion string = versions[0]
var BuildNumber string
var BuildDate string
var BuildHash string
var BuildHashEnterprise string
var BuildEnterpriseReady string
var versionsWithoutHotFixes []string

View File

@ -0,0 +1,109 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"encoding/json"
"github.com/gorilla/websocket"
"net/http"
)
type WebSocketClient struct {
Url string // The location of the server like "ws://localhost:8065"
ApiUrl string // The api location of the server like "ws://localhost:8065/api/v3"
Conn *websocket.Conn // The WebSocket connection
AuthToken string // The token used to open the WebSocket
Sequence int64 // The ever-incrementing sequence attached to each WebSocket action
EventChannel chan *WebSocketEvent
ResponseChannel chan *WebSocketResponse
}
// NewWebSocketClient constructs a new WebSocket client with convienence
// methods for talking to the server.
func NewWebSocketClient(url, authToken string) (*WebSocketClient, *AppError) {
header := http.Header{}
header.Set(HEADER_AUTH, "BEARER "+authToken)
conn, _, err := websocket.DefaultDialer.Dial(url+API_URL_SUFFIX+"/users/websocket", header)
if err != nil {
return nil, NewLocAppError("NewWebSocketClient", "model.websocket_client.connect_fail.app_error", nil, err.Error())
}
return &WebSocketClient{
url,
url + API_URL_SUFFIX,
conn,
authToken,
1,
make(chan *WebSocketEvent, 100),
make(chan *WebSocketResponse, 100),
}, nil
}
func (wsc *WebSocketClient) Connect() *AppError {
header := http.Header{}
header.Set(HEADER_AUTH, "BEARER "+wsc.AuthToken)
var err error
wsc.Conn, _, err = websocket.DefaultDialer.Dial(wsc.ApiUrl+"/users/websocket", header)
if err != nil {
return NewLocAppError("NewWebSocketClient", "model.websocket_client.connect_fail.app_error", nil, err.Error())
}
return nil
}
func (wsc *WebSocketClient) Close() {
wsc.Conn.Close()
}
func (wsc *WebSocketClient) Listen() {
go func() {
for {
var rawMsg json.RawMessage
var err error
if _, rawMsg, err = wsc.Conn.ReadMessage(); err != nil {
return
}
var event WebSocketEvent
if err := json.Unmarshal(rawMsg, &event); err == nil && event.IsValid() {
wsc.EventChannel <- &event
continue
}
var response WebSocketResponse
if err := json.Unmarshal(rawMsg, &response); err == nil && response.IsValid() {
wsc.ResponseChannel <- &response
continue
}
}
}()
}
func (wsc *WebSocketClient) SendMessage(action string, data map[string]interface{}) {
req := &WebSocketRequest{}
req.Seq = wsc.Sequence
req.Action = action
req.Data = data
wsc.Sequence++
wsc.Conn.WriteJSON(req)
}
// UserTyping will push a user_typing event out to all connected users
// who are in the specified channel
func (wsc *WebSocketClient) UserTyping(channelId, parentId string) {
data := map[string]interface{}{
"channel_id": channelId,
"parent_id": parentId,
}
wsc.SendMessage("user_typing", data)
}
// GetStatuses will return a map of string statuses using user id as the key
func (wsc *WebSocketClient) GetStatuses() {
wsc.SendMessage("get_statuses", nil)
}

View File

@ -0,0 +1,114 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"encoding/json"
"io"
)
const (
WEBSOCKET_EVENT_TYPING = "typing"
WEBSOCKET_EVENT_POSTED = "posted"
WEBSOCKET_EVENT_POST_EDITED = "post_edited"
WEBSOCKET_EVENT_POST_DELETED = "post_deleted"
WEBSOCKET_EVENT_CHANNEL_DELETED = "channel_deleted"
WEBSOCKET_EVENT_CHANNEL_VIEWED = "channel_viewed"
WEBSOCKET_EVENT_DIRECT_ADDED = "direct_added"
WEBSOCKET_EVENT_NEW_USER = "new_user"
WEBSOCKET_EVENT_LEAVE_TEAM = "leave_team"
WEBSOCKET_EVENT_USER_ADDED = "user_added"
WEBSOCKET_EVENT_USER_REMOVED = "user_removed"
WEBSOCKET_EVENT_PREFERENCE_CHANGED = "preference_changed"
WEBSOCKET_EVENT_EPHEMERAL_MESSAGE = "ephemeral_message"
WEBSOCKET_EVENT_STATUS_CHANGE = "status_change"
)
type WebSocketMessage interface {
ToJson() string
IsValid() bool
}
type WebSocketEvent struct {
TeamId string `json:"team_id"`
ChannelId string `json:"channel_id"`
UserId string `json:"user_id"`
Event string `json:"event"`
Data map[string]interface{} `json:"data"`
}
func (m *WebSocketEvent) Add(key string, value interface{}) {
m.Data[key] = value
}
func NewWebSocketEvent(teamId string, channelId string, userId string, event string) *WebSocketEvent {
return &WebSocketEvent{TeamId: teamId, ChannelId: channelId, UserId: userId, Event: event, Data: make(map[string]interface{})}
}
func (o *WebSocketEvent) IsValid() bool {
return o.Event != ""
}
func (o *WebSocketEvent) ToJson() string {
b, err := json.Marshal(o)
if err != nil {
return ""
} else {
return string(b)
}
}
func WebSocketEventFromJson(data io.Reader) *WebSocketEvent {
decoder := json.NewDecoder(data)
var o WebSocketEvent
err := decoder.Decode(&o)
if err == nil {
return &o
} else {
return nil
}
}
type WebSocketResponse struct {
Status string `json:"status"`
SeqReply int64 `json:"seq_reply,omitempty"`
Data map[string]interface{} `json:"data,omitempty"`
Error *AppError `json:"error,omitempty"`
}
func (m *WebSocketResponse) Add(key string, value interface{}) {
m.Data[key] = value
}
func NewWebSocketResponse(status string, seqReply int64, data map[string]interface{}) *WebSocketResponse {
return &WebSocketResponse{Status: status, SeqReply: seqReply, Data: data}
}
func NewWebSocketError(seqReply int64, err *AppError) *WebSocketResponse {
return &WebSocketResponse{Status: STATUS_FAIL, SeqReply: seqReply, Error: err}
}
func (o *WebSocketResponse) IsValid() bool {
return o.Status != ""
}
func (o *WebSocketResponse) ToJson() string {
b, err := json.Marshal(o)
if err != nil {
return ""
} else {
return string(b)
}
}
func WebSocketResponseFromJson(data io.Reader) *WebSocketResponse {
decoder := json.NewDecoder(data)
var o WebSocketResponse
err := decoder.Decode(&o)
if err == nil {
return &o
} else {
return nil
}
}

View File

@ -0,0 +1,43 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"encoding/json"
"io"
goi18n "github.com/nicksnyder/go-i18n/i18n"
)
type WebSocketRequest struct {
// Client-provided fields
Seq int64 `json:"seq"`
Action string `json:"action"`
Data map[string]interface{} `json:"data"`
// Server-provided fields
Session Session `json:"-"`
T goi18n.TranslateFunc `json:"-"`
Locale string `json:"-"`
}
func (o *WebSocketRequest) ToJson() string {
b, err := json.Marshal(o)
if err != nil {
return ""
} else {
return string(b)
}
}
func WebSocketRequestFromJson(data io.Reader) *WebSocketRequest {
decoder := json.NewDecoder(data)
var o WebSocketRequest
err := decoder.Decode(&o)
if err == nil {
return &o
} else {
return nil
}
}

27
vendor/github.com/mattn/go-xmpp/LICENSE generated vendored Normal file
View File

@ -0,0 +1,27 @@
Copyright (c) 2009 The Go Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* 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.
* Neither the name of Google Inc. 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.

881
vendor/github.com/mattn/go-xmpp/xmpp.go generated vendored Normal file
View File

@ -0,0 +1,881 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// TODO(rsc):
// More precise error handling.
// Presence functionality.
// TODO(mattn):
// Add proxy authentication.
// Package xmpp implements a simple Google Talk client
// using the XMPP protocol described in RFC 3920 and RFC 3921.
package xmpp
import (
"bufio"
"bytes"
"crypto/md5"
"crypto/rand"
"crypto/tls"
"encoding/base64"
"encoding/binary"
"encoding/xml"
"errors"
"fmt"
"io"
"math/big"
"net"
"net/http"
"net/url"
"os"
"strings"
"time"
)
const (
nsStream = "http://etherx.jabber.org/streams"
nsTLS = "urn:ietf:params:xml:ns:xmpp-tls"
nsSASL = "urn:ietf:params:xml:ns:xmpp-sasl"
nsBind = "urn:ietf:params:xml:ns:xmpp-bind"
nsClient = "jabber:client"
nsSession = "urn:ietf:params:xml:ns:xmpp-session"
)
// Default TLS configuration options
var DefaultConfig tls.Config
// Cookie is a unique XMPP session identifier
type Cookie uint64
func getCookie() Cookie {
var buf [8]byte
if _, err := rand.Reader.Read(buf[:]); err != nil {
panic("Failed to read random bytes: " + err.Error())
}
return Cookie(binary.LittleEndian.Uint64(buf[:]))
}
// Client holds XMPP connection opitons
type Client struct {
conn net.Conn // connection to server
jid string // Jabber ID for our connection
domain string
p *xml.Decoder
}
func (c *Client) JID() string {
return c.jid
}
func connect(host, user, passwd string) (net.Conn, error) {
addr := host
if strings.TrimSpace(host) == "" {
a := strings.SplitN(user, "@", 2)
if len(a) == 2 {
addr = a[1]
}
}
a := strings.SplitN(host, ":", 2)
if len(a) == 1 {
addr += ":5222"
}
proxy := os.Getenv("HTTP_PROXY")
if proxy == "" {
proxy = os.Getenv("http_proxy")
}
if proxy != "" {
url, err := url.Parse(proxy)
if err == nil {
addr = url.Host
}
}
c, err := net.Dial("tcp", addr)
if err != nil {
return nil, err
}
if proxy != "" {
fmt.Fprintf(c, "CONNECT %s HTTP/1.1\r\n", host)
fmt.Fprintf(c, "Host: %s\r\n", host)
fmt.Fprintf(c, "\r\n")
br := bufio.NewReader(c)
req, _ := http.NewRequest("CONNECT", host, nil)
resp, err := http.ReadResponse(br, req)
if err != nil {
return nil, err
}
if resp.StatusCode != 200 {
f := strings.SplitN(resp.Status, " ", 2)
return nil, errors.New(f[1])
}
}
return c, nil
}
// Options are used to specify additional options for new clients, such as a Resource.
type Options struct {
// Host specifies what host to connect to, as either "hostname" or "hostname:port"
// If host is not specified, the DNS SRV should be used to find the host from the domainpart of the JID.
// Default the port to 5222.
Host string
// User specifies what user to authenticate to the remote server.
User string
// Password supplies the password to use for authentication with the remote server.
Password string
// Resource specifies an XMPP client resource, like "bot", instead of accepting one
// from the server. Use "" to let the server generate one for your client.
Resource string
// OAuthScope provides go-xmpp the required scope for OAuth2 authentication.
OAuthScope string
// OAuthToken provides go-xmpp with the required OAuth2 token used to authenticate
OAuthToken string
// OAuthXmlNs provides go-xmpp with the required namespaced used for OAuth2 authentication. This is
// provided to the server as the xmlns:auth attribute of the OAuth2 authentication request.
OAuthXmlNs string
// TLS Config
TLSConfig *tls.Config
// InsecureAllowUnencryptedAuth permits authentication over a TCP connection that has not been promoted to
// TLS by STARTTLS; this could leak authentication information over the network, or permit man in the middle
// attacks.
InsecureAllowUnencryptedAuth bool
// NoTLS directs go-xmpp to not use TLS initially to contact the server; instead, a plain old unencrypted
// TCP connection should be used. (Can be combined with StartTLS to support STARTTLS-based servers.)
NoTLS bool
// StartTLS directs go-xmpp to STARTTLS if the server supports it; go-xmpp will automatically STARTTLS
// if the server requires it regardless of this option.
StartTLS bool
// Debug output
Debug bool
// Use server sessions
Session bool
// Presence Status
Status string
// Status message
StatusMessage string
}
// NewClient establishes a new Client connection based on a set of Options.
func (o Options) NewClient() (*Client, error) {
host := o.Host
c, err := connect(host, o.User, o.Password)
if err != nil {
return nil, err
}
if strings.LastIndex(o.Host, ":") > 0 {
host = host[:strings.LastIndex(o.Host, ":")]
}
client := new(Client)
if o.NoTLS {
client.conn = c
} else {
var tlsconn *tls.Conn
if o.TLSConfig != nil {
tlsconn = tls.Client(c, o.TLSConfig)
} else {
DefaultConfig.ServerName = host
tlsconn = tls.Client(c, &DefaultConfig)
}
if err = tlsconn.Handshake(); err != nil {
return nil, err
}
insecureSkipVerify := DefaultConfig.InsecureSkipVerify
if o.TLSConfig != nil {
insecureSkipVerify = o.TLSConfig.InsecureSkipVerify
}
if !insecureSkipVerify {
if err = tlsconn.VerifyHostname(host); err != nil {
return nil, err
}
}
client.conn = tlsconn
}
if err := client.init(&o); err != nil {
client.Close()
return nil, err
}
return client, nil
}
// NewClient creates a new connection to a host given as "hostname" or "hostname:port".
// If host is not specified, the DNS SRV should be used to find the host from the domainpart of the JID.
// Default the port to 5222.
func NewClient(host, user, passwd string, debug bool) (*Client, error) {
opts := Options{
Host: host,
User: user,
Password: passwd,
Debug: debug,
Session: false,
}
return opts.NewClient()
}
// NewClientNoTLS creates a new client without TLS
func NewClientNoTLS(host, user, passwd string, debug bool) (*Client, error) {
opts := Options{
Host: host,
User: user,
Password: passwd,
NoTLS: true,
Debug: debug,
Session: false,
}
return opts.NewClient()
}
// Close closes the XMPP connection
func (c *Client) Close() error {
if c.conn != (*tls.Conn)(nil) {
return c.conn.Close()
}
return nil
}
func saslDigestResponse(username, realm, passwd, nonce, cnonceStr, authenticate, digestURI, nonceCountStr string) string {
h := func(text string) []byte {
h := md5.New()
h.Write([]byte(text))
return h.Sum(nil)
}
hex := func(bytes []byte) string {
return fmt.Sprintf("%x", bytes)
}
kd := func(secret, data string) []byte {
return h(secret + ":" + data)
}
a1 := string(h(username+":"+realm+":"+passwd)) + ":" + nonce + ":" + cnonceStr
a2 := authenticate + ":" + digestURI
response := hex(kd(hex(h(a1)), nonce+":"+nonceCountStr+":"+cnonceStr+":auth:"+hex(h(a2))))
return response
}
func cnonce() string {
randSize := big.NewInt(0)
randSize.Lsh(big.NewInt(1), 64)
cn, err := rand.Int(rand.Reader, randSize)
if err != nil {
return ""
}
return fmt.Sprintf("%016x", cn)
}
func (c *Client) init(o *Options) error {
var domain string
var user string
a := strings.SplitN(o.User, "@", 2)
if len(o.User) > 0 {
if len(a) != 2 {
return errors.New("xmpp: invalid username (want user@domain): " + o.User)
}
user = a[0]
domain = a[1]
} // Otherwise, we'll be attempting ANONYMOUS
// Declare intent to be a jabber client and gather stream features.
f, err := c.startStream(o, domain)
if err != nil {
return err
}
// If the server requires we STARTTLS, attempt to do so.
if f, err = c.startTLSIfRequired(f, o, domain); err != nil {
return err
}
if o.User == "" && o.Password == "" {
foundAnonymous := false
for _, m := range f.Mechanisms.Mechanism {
if m == "ANONYMOUS" {
fmt.Fprintf(c.conn, "<auth xmlns='%s' mechanism='ANONYMOUS' />\n", nsSASL)
foundAnonymous = true
break
}
}
if !foundAnonymous {
return fmt.Errorf("ANONYMOUS authentication is not an option and username and password were not specified")
}
} else {
// Even digest forms of authentication are unsafe if we do not know that the host
// we are talking to is the actual server, and not a man in the middle playing
// proxy.
if !c.IsEncrypted() && !o.InsecureAllowUnencryptedAuth {
return errors.New("refusing to authenticate over unencrypted TCP connection")
}
mechanism := ""
for _, m := range f.Mechanisms.Mechanism {
if m == "X-OAUTH2" && o.OAuthToken != "" && o.OAuthScope != "" {
mechanism = m
// Oauth authentication: send base64-encoded \x00 user \x00 token.
raw := "\x00" + user + "\x00" + o.OAuthToken
enc := make([]byte, base64.StdEncoding.EncodedLen(len(raw)))
base64.StdEncoding.Encode(enc, []byte(raw))
fmt.Fprintf(c.conn, "<auth xmlns='%s' mechanism='X-OAUTH2' auth:service='oauth2' "+
"xmlns:auth='%s'>%s</auth>\n", nsSASL, o.OAuthXmlNs, enc)
break
}
if m == "PLAIN" {
mechanism = m
// Plain authentication: send base64-encoded \x00 user \x00 password.
raw := "\x00" + user + "\x00" + o.Password
enc := make([]byte, base64.StdEncoding.EncodedLen(len(raw)))
base64.StdEncoding.Encode(enc, []byte(raw))
fmt.Fprintf(c.conn, "<auth xmlns='%s' mechanism='PLAIN'>%s</auth>\n", nsSASL, enc)
break
}
if m == "DIGEST-MD5" {
mechanism = m
// Digest-MD5 authentication
fmt.Fprintf(c.conn, "<auth xmlns='%s' mechanism='DIGEST-MD5'/>\n", nsSASL)
var ch saslChallenge
if err = c.p.DecodeElement(&ch, nil); err != nil {
return errors.New("unmarshal <challenge>: " + err.Error())
}
b, err := base64.StdEncoding.DecodeString(string(ch))
if err != nil {
return err
}
tokens := map[string]string{}
for _, token := range strings.Split(string(b), ",") {
kv := strings.SplitN(strings.TrimSpace(token), "=", 2)
if len(kv) == 2 {
if kv[1][0] == '"' && kv[1][len(kv[1])-1] == '"' {
kv[1] = kv[1][1 : len(kv[1])-1]
}
tokens[kv[0]] = kv[1]
}
}
realm, _ := tokens["realm"]
nonce, _ := tokens["nonce"]
qop, _ := tokens["qop"]
charset, _ := tokens["charset"]
cnonceStr := cnonce()
digestURI := "xmpp/" + domain
nonceCount := fmt.Sprintf("%08x", 1)
digest := saslDigestResponse(user, realm, o.Password, nonce, cnonceStr, "AUTHENTICATE", digestURI, nonceCount)
message := "username=\"" + user + "\", realm=\"" + realm + "\", nonce=\"" + nonce + "\", cnonce=\"" + cnonceStr +
"\", nc=" + nonceCount + ", qop=" + qop + ", digest-uri=\"" + digestURI + "\", response=" + digest + ", charset=" + charset
fmt.Fprintf(c.conn, "<response xmlns='%s'>%s</response>\n", nsSASL, base64.StdEncoding.EncodeToString([]byte(message)))
var rspauth saslRspAuth
if err = c.p.DecodeElement(&rspauth, nil); err != nil {
return errors.New("unmarshal <challenge>: " + err.Error())
}
b, err = base64.StdEncoding.DecodeString(string(rspauth))
if err != nil {
return err
}
fmt.Fprintf(c.conn, "<response xmlns='%s'/>\n", nsSASL)
break
}
}
if mechanism == "" {
return fmt.Errorf("PLAIN authentication is not an option: %v", f.Mechanisms.Mechanism)
}
}
// Next message should be either success or failure.
name, val, err := next(c.p)
if err != nil {
return err
}
switch v := val.(type) {
case *saslSuccess:
case *saslFailure:
// v.Any is type of sub-element in failure,
// which gives a description of what failed.
return errors.New("auth failure: " + v.Any.Local)
default:
return errors.New("expected <success> or <failure>, got <" + name.Local + "> in " + name.Space)
}
// Now that we're authenticated, we're supposed to start the stream over again.
// Declare intent to be a jabber client.
if f, err = c.startStream(o, domain); err != nil {
return err
}
// Generate a unique cookie
cookie := getCookie()
// Send IQ message asking to bind to the local user name.
if o.Resource == "" {
fmt.Fprintf(c.conn, "<iq type='set' id='%x'><bind xmlns='%s'></bind></iq>\n", cookie, nsBind)
} else {
fmt.Fprintf(c.conn, "<iq type='set' id='%x'><bind xmlns='%s'><resource>%s</resource></bind></iq>\n", cookie, nsBind, o.Resource)
}
var iq clientIQ
if err = c.p.DecodeElement(&iq, nil); err != nil {
return errors.New("unmarshal <iq>: " + err.Error())
}
if &iq.Bind == nil {
return errors.New("<iq> result missing <bind>")
}
c.jid = iq.Bind.Jid // our local id
c.domain = domain
if o.Session {
//if server support session, open it
fmt.Fprintf(c.conn, "<iq to='%s' type='set' id='%x'><session xmlns='%s'/></iq>", xmlEscape(domain), cookie, nsSession)
}
// We're connected and can now receive and send messages.
fmt.Fprintf(c.conn, "<presence xml:lang='en'><show>%s</show><status>%s</status></presence>", o.Status, o.StatusMessage)
return nil
}
// startTlsIfRequired examines the server's stream features and, if STARTTLS is required or supported, performs the TLS handshake.
// f will be updated if the handshake completes, as the new stream's features are typically different from the original.
func (c *Client) startTLSIfRequired(f *streamFeatures, o *Options, domain string) (*streamFeatures, error) {
// whether we start tls is a matter of opinion: the server's and the user's.
switch {
case f.StartTLS == nil:
// the server does not support STARTTLS
return f, nil
case f.StartTLS.Required != nil:
// the server requires STARTTLS.
case !o.StartTLS:
// the user wants STARTTLS and the server supports it.
}
var err error
fmt.Fprintf(c.conn, "<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>\n")
var k tlsProceed
if err = c.p.DecodeElement(&k, nil); err != nil {
return f, errors.New("unmarshal <proceed>: " + err.Error())
}
tc := o.TLSConfig
if tc == nil {
tc = new(tls.Config)
*tc = DefaultConfig
//TODO(scott): we should consider using the server's address or reverse lookup
tc.ServerName = domain
}
t := tls.Client(c.conn, tc)
if err = t.Handshake(); err != nil {
return f, errors.New("starttls handshake: " + err.Error())
}
c.conn = t
// restart our declaration of XMPP stream intentions.
tf, err := c.startStream(o, domain)
if err != nil {
return f, err
}
return tf, nil
}
// startStream will start a new XML decoder for the connection, signal the start of a stream to the server and verify that the server has
// also started the stream; if o.Debug is true, startStream will tee decoded XML data to stderr. The features advertised by the server
// will be returned.
func (c *Client) startStream(o *Options, domain string) (*streamFeatures, error) {
if o.Debug {
c.p = xml.NewDecoder(tee{c.conn, os.Stderr})
} else {
c.p = xml.NewDecoder(c.conn)
}
_, err := fmt.Fprintf(c.conn, "<?xml version='1.0'?>\n"+
"<stream:stream to='%s' xmlns='%s'\n"+
" xmlns:stream='%s' version='1.0'>\n",
xmlEscape(domain), nsClient, nsStream)
if err != nil {
return nil, err
}
// We expect the server to start a <stream>.
se, err := nextStart(c.p)
if err != nil {
return nil, err
}
if se.Name.Space != nsStream || se.Name.Local != "stream" {
return nil, fmt.Errorf("expected <stream> but got <%v> in %v", se.Name.Local, se.Name.Space)
}
// Now we're in the stream and can use Unmarshal.
// Next message should be <features> to tell us authentication options.
// See section 4.6 in RFC 3920.
f := new(streamFeatures)
if err = c.p.DecodeElement(f, nil); err != nil {
return f, errors.New("unmarshal <features>: " + err.Error())
}
return f, nil
}
// IsEncrypted will return true if the client is connected using a TLS transport, either because it used.
// TLS to connect from the outset, or because it successfully used STARTTLS to promote a TCP connection to TLS.
func (c *Client) IsEncrypted() bool {
_, ok := c.conn.(*tls.Conn)
return ok
}
// Chat is an incoming or outgoing XMPP chat message.
type Chat struct {
Remote string
Type string
Text string
Roster Roster
Other []string
Stamp time.Time
}
type Roster []Contact
type Contact struct {
Remote string
Name string
Group []string
}
// Presence is an XMPP presence notification.
type Presence struct {
From string
To string
Type string
Show string
Status string
}
type IQ struct {
ID string
From string
To string
Type string
Query []byte
}
// Recv waits to receive the next XMPP stanza.
// Return type is either a presence notification or a chat message.
func (c *Client) Recv() (stanza interface{}, err error) {
for {
_, val, err := next(c.p)
if err != nil {
return Chat{}, err
}
switch v := val.(type) {
case *clientMessage:
stamp, _ := time.Parse(
"2006-01-02T15:04:05Z",
v.Delay.Stamp,
)
chat := Chat{
Remote: v.From,
Type: v.Type,
Text: v.Body,
Other: v.Other,
Stamp: stamp,
}
return chat, nil
case *clientQuery:
var r Roster
for _, item := range v.Item {
r = append(r, Contact{item.Jid, item.Name, item.Group})
}
return Chat{Type: "roster", Roster: r}, nil
case *clientPresence:
return Presence{v.From, v.To, v.Type, v.Show, v.Status}, nil
case *clientIQ:
return IQ{ID: v.ID, From: v.From, To: v.To, Type: v.Type, Query: v.Query}, nil
}
}
}
// Send sends the message wrapped inside an XMPP message stanza body.
func (c *Client) Send(chat Chat) (n int, err error) {
return fmt.Fprintf(c.conn, "<message to='%s' type='%s' xml:lang='en'>"+"<body>%s</body></message>",
xmlEscape(chat.Remote), xmlEscape(chat.Type), xmlEscape(chat.Text))
}
// SendOrg sends the original text without being wrapped in an XMPP message stanza.
func (c *Client) SendOrg(org string) (n int, err error) {
return fmt.Fprint(c.conn, org)
}
func (c *Client) SendPresence(presence Presence) (n int, err error) {
return fmt.Fprintf(c.conn, "<presence from='%s' to='%s'/>", xmlEscape(presence.From), xmlEscape(presence.To))
}
// SendHtml sends the message as HTML as defined by XEP-0071
func (c *Client) SendHtml(chat Chat) (n int, err error) {
return fmt.Fprintf(c.conn, "<message to='%s' type='%s' xml:lang='en'>"+
"<body>%s</body>"+
"<html xmlns='http://jabber.org/protocol/xhtml-im'><body xmlns='http://www.w3.org/1999/xhtml'>%s</body></html></message>",
xmlEscape(chat.Remote), xmlEscape(chat.Type), xmlEscape(chat.Text), chat.Text)
}
// Roster asks for the chat roster.
func (c *Client) Roster() error {
fmt.Fprintf(c.conn, "<iq from='%s' type='get' id='roster1'><query xmlns='jabber:iq:roster'/></iq>\n", xmlEscape(c.jid))
return nil
}
// RFC 3920 C.1 Streams name space
type streamFeatures struct {
XMLName xml.Name `xml:"http://etherx.jabber.org/streams features"`
StartTLS *tlsStartTLS
Mechanisms saslMechanisms
Bind bindBind
Session bool
}
type streamError struct {
XMLName xml.Name `xml:"http://etherx.jabber.org/streams error"`
Any xml.Name
Text string
}
// RFC 3920 C.3 TLS name space
type tlsStartTLS struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls starttls"`
Required *string `xml:"required"`
}
type tlsProceed struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls proceed"`
}
type tlsFailure struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls failure"`
}
// RFC 3920 C.4 SASL name space
type saslMechanisms struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl mechanisms"`
Mechanism []string `xml:"mechanism"`
}
type saslAuth struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl auth"`
Mechanism string `xml:",attr"`
}
type saslChallenge string
type saslRspAuth string
type saslResponse string
type saslAbort struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl abort"`
}
type saslSuccess struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl success"`
}
type saslFailure struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl failure"`
Any xml.Name `xml:",any"`
}
// RFC 3920 C.5 Resource binding name space
type bindBind struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-bind bind"`
Resource string
Jid string `xml:"jid"`
}
// RFC 3921 B.1 jabber:client
type clientMessage struct {
XMLName xml.Name `xml:"jabber:client message"`
From string `xml:"from,attr"`
ID string `xml:"id,attr"`
To string `xml:"to,attr"`
Type string `xml:"type,attr"` // chat, error, groupchat, headline, or normal
// These should technically be []clientText, but string is much more convenient.
Subject string `xml:"subject"`
Body string `xml:"body"`
Thread string `xml:"thread"`
// Any hasn't matched element
Other []string `xml:",any"`
Delay Delay `xml:"delay"`
}
type Delay struct {
Stamp string `xml:"stamp,attr"`
}
type clientText struct {
Lang string `xml:",attr"`
Body string `xml:"chardata"`
}
type clientPresence struct {
XMLName xml.Name `xml:"jabber:client presence"`
From string `xml:"from,attr"`
ID string `xml:"id,attr"`
To string `xml:"to,attr"`
Type string `xml:"type,attr"` // error, probe, subscribe, subscribed, unavailable, unsubscribe, unsubscribed
Lang string `xml:"lang,attr"`
Show string `xml:"show"` // away, chat, dnd, xa
Status string `xml:"status"` // sb []clientText
Priority string `xml:"priority,attr"`
Error *clientError
}
type clientIQ struct { // info/query
XMLName xml.Name `xml:"jabber:client iq"`
From string `xml:"from,attr"`
ID string `xml:"id,attr"`
To string `xml:"to,attr"`
Type string `xml:"type,attr"` // error, get, result, set
Query []byte `xml:",innerxml"`
Error clientError
Bind bindBind
}
type clientError struct {
XMLName xml.Name `xml:"jabber:client error"`
Code string `xml:",attr"`
Type string `xml:",attr"`
Any xml.Name
Text string
}
type clientQuery struct {
Item []rosterItem
}
type rosterItem struct {
XMLName xml.Name `xml:"jabber:iq:roster item"`
Jid string `xml:",attr"`
Name string `xml:",attr"`
Subscription string `xml:",attr"`
Group []string
}
// Scan XML token stream to find next StartElement.
func nextStart(p *xml.Decoder) (xml.StartElement, error) {
for {
t, err := p.Token()
if err != nil && err != io.EOF || t == nil {
return xml.StartElement{}, err
}
switch t := t.(type) {
case xml.StartElement:
return t, nil
}
}
}
// Scan XML token stream for next element and save into val.
// If val == nil, allocate new element based on proto map.
// Either way, return val.
func next(p *xml.Decoder) (xml.Name, interface{}, error) {
// Read start element to find out what type we want.
se, err := nextStart(p)
if err != nil {
return xml.Name{}, nil, err
}
// Put it in an interface and allocate one.
var nv interface{}
switch se.Name.Space + " " + se.Name.Local {
case nsStream + " features":
nv = &streamFeatures{}
case nsStream + " error":
nv = &streamError{}
case nsTLS + " starttls":
nv = &tlsStartTLS{}
case nsTLS + " proceed":
nv = &tlsProceed{}
case nsTLS + " failure":
nv = &tlsFailure{}
case nsSASL + " mechanisms":
nv = &saslMechanisms{}
case nsSASL + " challenge":
nv = ""
case nsSASL + " response":
nv = ""
case nsSASL + " abort":
nv = &saslAbort{}
case nsSASL + " success":
nv = &saslSuccess{}
case nsSASL + " failure":
nv = &saslFailure{}
case nsBind + " bind":
nv = &bindBind{}
case nsClient + " message":
nv = &clientMessage{}
case nsClient + " presence":
nv = &clientPresence{}
case nsClient + " iq":
nv = &clientIQ{}
case nsClient + " error":
nv = &clientError{}
default:
return xml.Name{}, nil, errors.New("unexpected XMPP message " +
se.Name.Space + " <" + se.Name.Local + "/>")
}
// Unmarshal into that storage.
if err = p.DecodeElement(nv, &se); err != nil {
return xml.Name{}, nil, err
}
return se.Name, nv, err
}
var xmlSpecial = map[byte]string{
'<': "&lt;",
'>': "&gt;",
'"': "&quot;",
'\'': "&apos;",
'&': "&amp;",
}
func xmlEscape(s string) string {
var b bytes.Buffer
for i := 0; i < len(s); i++ {
c := s[i]
if s, ok := xmlSpecial[c]; ok {
b.WriteString(s)
} else {
b.WriteByte(c)
}
}
return b.String()
}
type tee struct {
r io.Reader
w io.Writer
}
func (t tee) Read(p []byte) (n int, err error) {
n, err = t.r.Read(p)
if n > 0 {
t.w.Write(p[0:n])
t.w.Write([]byte("\n"))
}
return
}

View File

@ -0,0 +1,24 @@
package xmpp
import (
"fmt"
"strconv"
)
const IQTypeGet = "get"
const IQTypeSet = "set"
const IQTypeResult = "result"
func (c *Client) Discovery() (string, error) {
const namespace = "http://jabber.org/protocol/disco#items"
// use getCookie for a pseudo random id.
reqID := strconv.FormatUint(uint64(getCookie()), 10)
return c.RawInformationQuery(c.jid, c.domain, reqID, IQTypeGet, namespace, "")
}
// RawInformationQuery sends an information query request to the server.
func (c *Client) RawInformationQuery(from, to, id, iqType, requestNamespace, body string) (string, error) {
const xmlIQ = "<iq from='%s' to='%s' id='%s' type='%s'><query xmlns='%s'>%s</query></iq>"
_, err := fmt.Fprintf(c.conn, xmlIQ, xmlEscape(from), xmlEscape(to), id, iqType, requestNamespace, body)
return id, err
}

134
vendor/github.com/mattn/go-xmpp/xmpp_muc.go generated vendored Normal file
View File

@ -0,0 +1,134 @@
// Copyright 2013 Flo Lauber <dev@qatfy.at>. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// TODO(flo):
// - support password protected MUC rooms
// - cleanup signatures of join/leave functions
package xmpp
import (
"fmt"
"time"
"errors"
)
const (
nsMUC = "http://jabber.org/protocol/muc"
nsMUCUser = "http://jabber.org/protocol/muc#user"
NoHistory = 0
CharHistory = 1
StanzaHistory = 2
SecondsHistory = 3
SinceHistory = 4
)
// Send sends room topic wrapped inside an XMPP message stanza body.
func (c *Client) SendTopic(chat Chat) (n int, err error) {
return fmt.Fprintf(c.conn, "<message to='%s' type='%s' xml:lang='en'>"+"<subject>%s</subject></message>",
xmlEscape(chat.Remote), xmlEscape(chat.Type), xmlEscape(chat.Text))
}
func (c *Client) JoinMUCNoHistory(jid, nick string) (n int, err error) {
if nick == "" {
nick = c.jid
}
return fmt.Fprintf(c.conn, "<presence to='%s/%s'>\n"+
"<x xmlns='%s'>"+
"<history maxchars='0'/></x>\n"+
"</presence>",
xmlEscape(jid), xmlEscape(nick), nsMUC)
}
// xep-0045 7.2
func (c *Client) JoinMUC(jid, nick string, history_type, history int, history_date *time.Time) (n int, err error) {
if nick == "" {
nick = c.jid
}
switch history_type {
case NoHistory:
return fmt.Fprintf(c.conn, "<presence to='%s/%s'>\n" +
"<x xmlns='%s' />\n" +
"</presence>",
xmlEscape(jid), xmlEscape(nick), nsMUC)
case CharHistory:
return fmt.Fprintf(c.conn, "<presence to='%s/%s'>\n" +
"<x xmlns='%s'>\n" +
"<history maxchars='%d'/></x>\n"+
"</presence>",
xmlEscape(jid), xmlEscape(nick), nsMUC, history)
case StanzaHistory:
return fmt.Fprintf(c.conn, "<presence to='%s/%s'>\n" +
"<x xmlns='%s'>\n" +
"<history maxstanzas='%d'/></x>\n"+
"</presence>",
xmlEscape(jid), xmlEscape(nick), nsMUC, history)
case SecondsHistory:
return fmt.Fprintf(c.conn, "<presence to='%s/%s'>\n" +
"<x xmlns='%s'>\n" +
"<history seconds='%d'/></x>\n"+
"</presence>",
xmlEscape(jid), xmlEscape(nick), nsMUC, history)
case SinceHistory:
if history_date != nil {
return fmt.Fprintf(c.conn, "<presence to='%s/%s'>\n" +
"<x xmlns='%s'>\n" +
"<history since='%s'/></x>\n" +
"</presence>",
xmlEscape(jid), xmlEscape(nick), nsMUC, history_date.Format(time.RFC3339))
}
}
return 0, errors.New("Unknown history option")
}
// xep-0045 7.2.6
func (c *Client) JoinProtectedMUC(jid, nick string, password string, history_type, history int, history_date *time.Time) (n int, err error) {
if nick == "" {
nick = c.jid
}
switch history_type {
case NoHistory:
return fmt.Fprintf(c.conn, "<presence to='%s/%s'>\n" +
"<x xmlns='%s'>\n" +
"<password>%s</password>\n"+
"</presence>",
xmlEscape(jid), xmlEscape(nick), nsMUC, xmlEscape(password))
case CharHistory:
return fmt.Fprintf(c.conn, "<presence to='%s/%s'>\n" +
"<x xmlns='%s'>\n" +
"<password>%s</password>\n"+
"<history maxchars='%d'/></x>\n"+
"</presence>",
xmlEscape(jid), xmlEscape(nick), nsMUC, xmlEscape(password), history)
case StanzaHistory:
return fmt.Fprintf(c.conn, "<presence to='%s/%s'>\n" +
"<x xmlns='%s'>\n" +
"<password>%s</password>\n"+
"<history maxstanzas='%d'/></x>\n"+
"</presence>",
xmlEscape(jid), xmlEscape(nick), nsMUC, xmlEscape(password), history)
case SecondsHistory:
return fmt.Fprintf(c.conn, "<presence to='%s/%s'>\n" +
"<x xmlns='%s'>\n" +
"<password>%s</password>\n"+
"<history seconds='%d'/></x>\n"+
"</presence>",
xmlEscape(jid), xmlEscape(nick), nsMUC, xmlEscape(password), history)
case SinceHistory:
if history_date != nil {
return fmt.Fprintf(c.conn, "<presence to='%s/%s'>\n" +
"<x xmlns='%s'>\n" +
"<password>%s</password>\n"+
"<history since='%s'/></x>\n" +
"</presence>",
xmlEscape(jid), xmlEscape(nick), nsMUC, xmlEscape(password), history_date.Format(time.RFC3339))
}
}
return 0, errors.New("Unknown history option")
}
// xep-0045 7.14
func (c *Client) LeaveMUC(jid string) (n int, err error) {
return fmt.Fprintf(c.conn, "<presence from='%s' to='%s' type='unavailable' />",
c.jid, xmlEscape(jid))
}

27
vendor/github.com/mattn/go-xmpp/xmpp_ping.go generated vendored Normal file
View File

@ -0,0 +1,27 @@
package xmpp
import (
"fmt"
)
func (c *Client) PingC2S(jid, server string) error {
if jid == "" {
jid = c.jid
}
if server == "" {
server = c.domain
}
_, err := fmt.Fprintf(c.conn, "<iq from='%s' to='%s' id='c2s1' type='get'>\n"+
"<ping xmlns='urn:xmpp:ping'/>\n"+
"</iq>",
xmlEscape(jid), xmlEscape(server))
return err
}
func (c *Client) PingS2S(fromServer, toServer string) error {
_, err := fmt.Fprintf(c.conn, "<iq from='%s' to='%s' id='s2s1' type='get'>\n"+
"<ping xmlns='urn:xmpp:ping'/>\n"+
"</iq>",
xmlEscape(fromServer), xmlEscape(toServer))
return err
}

20
vendor/github.com/mattn/go-xmpp/xmpp_subscription.go generated vendored Normal file
View File

@ -0,0 +1,20 @@
package xmpp
import (
"fmt"
)
func (c *Client) ApproveSubscription(jid string) {
fmt.Fprintf(c.conn, "<presence to='%s' type='subscribed'/>",
xmlEscape(jid))
}
func (c *Client) RevokeSubscription(jid string) {
fmt.Fprintf(c.conn, "<presence to='%s' type='unsubscribed'/>",
xmlEscape(jid))
}
func (c *Client) RequestSubscription(jid string) {
fmt.Fprintf(c.conn, "<presence to='%s' type='subscribe'/>",
xmlEscape(jid))
}

View File

@ -152,7 +152,6 @@ func (irc *Connection) writeLoop() {
}
}
}
return
}
// Pings the server if we have not received any messages for 5 minutes
@ -439,6 +438,25 @@ func (irc *Connection) Connect(server string) error {
if len(irc.Password) > 0 {
irc.pwrite <- fmt.Sprintf("PASS %s\r\n", irc.Password)
}
resChan := make(chan *SASLResult)
if irc.UseSASL {
irc.setupSASLCallbacks(resChan)
irc.pwrite <- fmt.Sprintf("CAP LS\r\n")
// request SASL
irc.pwrite <- fmt.Sprintf("CAP REQ :sasl\r\n")
// if sasl request doesn't complete in 15 seconds, close chan and timeout
select {
case res := <-resChan:
if res.Failed {
close(resChan)
return res.Err
}
case <-time.After(time.Second * 15):
close(resChan)
return errors.New("SASL setup timed out. This shouldn't happen.")
}
}
irc.pwrite <- fmt.Sprintf("NICK %s\r\n", irc.nick)
irc.pwrite <- fmt.Sprintf("USER %s 0.0.0.0 0.0.0.0 :%s\r\n", irc.user, irc.user)
return nil
@ -466,6 +484,7 @@ func IRC(nick, user string) *Connection {
KeepAlive: 4 * time.Minute,
Timeout: 1 * time.Minute,
PingFreq: 15 * time.Minute,
SASLMech: "PLAIN",
QuitMessage: "",
}
irc.setupCallbacks()

View File

@ -33,7 +33,7 @@ func (irc *Connection) RemoveCallback(eventcode string, i int) bool {
delete(irc.events[eventcode], i)
return true
}
irc.Log.Printf("Event found, but no callback found at id %s\n", i)
irc.Log.Printf("Event found, but no callback found at id %d\n", i)
return false
}
@ -64,7 +64,7 @@ func (irc *Connection) ReplaceCallback(eventcode string, i int, callback func(*E
event[i] = callback
return
}
irc.Log.Printf("Event found, but no callback found at id %s\n", i)
irc.Log.Printf("Event found, but no callback found at id %d\n", i)
}
irc.Log.Printf("Event not found. Use AddCallBack\n")
}

54
vendor/github.com/thoj/go-ircevent/irc_sasl.go generated vendored Normal file
View File

@ -0,0 +1,54 @@
package irc
import (
"encoding/base64"
"errors"
"fmt"
"strings"
)
type SASLResult struct {
Failed bool
Err error
}
func (irc *Connection) setupSASLCallbacks(result chan<- *SASLResult) {
irc.AddCallback("CAP", func(e *Event) {
if len(e.Arguments) == 3 {
if e.Arguments[1] == "LS" {
if !strings.Contains(e.Arguments[2], "sasl") {
result <- &SASLResult{true, errors.New("no SASL capability " + e.Arguments[2])}
}
}
if e.Arguments[1] == "ACK" {
if irc.SASLMech != "PLAIN" {
result <- &SASLResult{true, errors.New("only PLAIN is supported")}
}
irc.SendRaw("AUTHENTICATE " + irc.SASLMech)
}
}
})
irc.AddCallback("AUTHENTICATE", func(e *Event) {
str := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s\x00%s\x00%s", irc.SASLLogin, irc.SASLLogin, irc.SASLPassword)))
irc.SendRaw("AUTHENTICATE " + str)
})
irc.AddCallback("901", func(e *Event) {
irc.SendRaw("CAP END")
irc.SendRaw("QUIT")
result <- &SASLResult{true, errors.New(e.Arguments[1])}
})
irc.AddCallback("902", func(e *Event) {
irc.SendRaw("CAP END")
irc.SendRaw("QUIT")
result <- &SASLResult{true, errors.New(e.Arguments[1])}
})
irc.AddCallback("903", func(e *Event) {
irc.SendRaw("CAP END")
result <- &SASLResult{false, nil}
})
irc.AddCallback("904", func(e *Event) {
irc.SendRaw("CAP END")
irc.SendRaw("QUIT")
result <- &SASLResult{true, errors.New(e.Arguments[1])}
})
}

View File

@ -14,16 +14,20 @@ import (
type Connection struct {
sync.WaitGroup
Debug bool
Error chan error
Password string
UseTLS bool
TLSConfig *tls.Config
Version string
Timeout time.Duration
PingFreq time.Duration
KeepAlive time.Duration
Server string
Debug bool
Error chan error
Password string
UseTLS bool
UseSASL bool
SASLLogin string
SASLPassword string
SASLMech string
TLSConfig *tls.Config
Version string
Timeout time.Duration
PingFreq time.Duration
KeepAlive time.Duration
Server string
socket net.Conn
pwrite chan string

27
vendor/golang.org/x/net/lex/httplex/LICENSE generated vendored Normal file
View File

@ -0,0 +1,27 @@
Copyright (c) 2009 The Go Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* 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.
* Neither the name of Google Inc. 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.

312
vendor/golang.org/x/net/lex/httplex/httplex.go generated vendored Normal file
View File

@ -0,0 +1,312 @@
// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package httplex contains rules around lexical matters of various
// HTTP-related specifications.
//
// This package is shared by the standard library (which vendors it)
// and x/net/http2. It comes with no API stability promise.
package httplex
import (
"strings"
"unicode/utf8"
)
var isTokenTable = [127]bool{
'!': true,
'#': true,
'$': true,
'%': true,
'&': true,
'\'': true,
'*': true,
'+': true,
'-': true,
'.': true,
'0': true,
'1': true,
'2': true,
'3': true,
'4': true,
'5': true,
'6': true,
'7': true,
'8': true,
'9': true,
'A': true,
'B': true,
'C': true,
'D': true,
'E': true,
'F': true,
'G': true,
'H': true,
'I': true,
'J': true,
'K': true,
'L': true,
'M': true,
'N': true,
'O': true,
'P': true,
'Q': true,
'R': true,
'S': true,
'T': true,
'U': true,
'W': true,
'V': true,
'X': true,
'Y': true,
'Z': true,
'^': true,
'_': true,
'`': true,
'a': true,
'b': true,
'c': true,
'd': true,
'e': true,
'f': true,
'g': true,
'h': true,
'i': true,
'j': true,
'k': true,
'l': true,
'm': true,
'n': true,
'o': true,
'p': true,
'q': true,
'r': true,
's': true,
't': true,
'u': true,
'v': true,
'w': true,
'x': true,
'y': true,
'z': true,
'|': true,
'~': true,
}
func IsTokenRune(r rune) bool {
i := int(r)
return i < len(isTokenTable) && isTokenTable[i]
}
func isNotToken(r rune) bool {
return !IsTokenRune(r)
}
// HeaderValuesContainsToken reports whether any string in values
// contains the provided token, ASCII case-insensitively.
func HeaderValuesContainsToken(values []string, token string) bool {
for _, v := range values {
if headerValueContainsToken(v, token) {
return true
}
}
return false
}
// isOWS reports whether b is an optional whitespace byte, as defined
// by RFC 7230 section 3.2.3.
func isOWS(b byte) bool { return b == ' ' || b == '\t' }
// trimOWS returns x with all optional whitespace removes from the
// beginning and end.
func trimOWS(x string) string {
// TODO: consider using strings.Trim(x, " \t") instead,
// if and when it's fast enough. See issue 10292.
// But this ASCII-only code will probably always beat UTF-8
// aware code.
for len(x) > 0 && isOWS(x[0]) {
x = x[1:]
}
for len(x) > 0 && isOWS(x[len(x)-1]) {
x = x[:len(x)-1]
}
return x
}
// headerValueContainsToken reports whether v (assumed to be a
// 0#element, in the ABNF extension described in RFC 7230 section 7)
// contains token amongst its comma-separated tokens, ASCII
// case-insensitively.
func headerValueContainsToken(v string, token string) bool {
v = trimOWS(v)
if comma := strings.IndexByte(v, ','); comma != -1 {
return tokenEqual(trimOWS(v[:comma]), token) || headerValueContainsToken(v[comma+1:], token)
}
return tokenEqual(v, token)
}
// lowerASCII returns the ASCII lowercase version of b.
func lowerASCII(b byte) byte {
if 'A' <= b && b <= 'Z' {
return b + ('a' - 'A')
}
return b
}
// tokenEqual reports whether t1 and t2 are equal, ASCII case-insensitively.
func tokenEqual(t1, t2 string) bool {
if len(t1) != len(t2) {
return false
}
for i, b := range t1 {
if b >= utf8.RuneSelf {
// No UTF-8 or non-ASCII allowed in tokens.
return false
}
if lowerASCII(byte(b)) != lowerASCII(t2[i]) {
return false
}
}
return true
}
// isLWS reports whether b is linear white space, according
// to http://www.w3.org/Protocols/rfc2616/rfc2616-sec2.html#sec2.2
// LWS = [CRLF] 1*( SP | HT )
func isLWS(b byte) bool { return b == ' ' || b == '\t' }
// isCTL reports whether b is a control byte, according
// to http://www.w3.org/Protocols/rfc2616/rfc2616-sec2.html#sec2.2
// CTL = <any US-ASCII control character
// (octets 0 - 31) and DEL (127)>
func isCTL(b byte) bool {
const del = 0x7f // a CTL
return b < ' ' || b == del
}
// ValidHeaderFieldName reports whether v is a valid HTTP/1.x header name.
// HTTP/2 imposes the additional restriction that uppercase ASCII
// letters are not allowed.
//
// RFC 7230 says:
// header-field = field-name ":" OWS field-value OWS
// field-name = token
// token = 1*tchar
// tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." /
// "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA
func ValidHeaderFieldName(v string) bool {
if len(v) == 0 {
return false
}
for _, r := range v {
if !IsTokenRune(r) {
return false
}
}
return true
}
// ValidHostHeader reports whether h is a valid host header.
func ValidHostHeader(h string) bool {
// The latest spec is actually this:
//
// http://tools.ietf.org/html/rfc7230#section-5.4
// Host = uri-host [ ":" port ]
//
// Where uri-host is:
// http://tools.ietf.org/html/rfc3986#section-3.2.2
//
// But we're going to be much more lenient for now and just
// search for any byte that's not a valid byte in any of those
// expressions.
for i := 0; i < len(h); i++ {
if !validHostByte[h[i]] {
return false
}
}
return true
}
// See the validHostHeader comment.
var validHostByte = [256]bool{
'0': true, '1': true, '2': true, '3': true, '4': true, '5': true, '6': true, '7': true,
'8': true, '9': true,
'a': true, 'b': true, 'c': true, 'd': true, 'e': true, 'f': true, 'g': true, 'h': true,
'i': true, 'j': true, 'k': true, 'l': true, 'm': true, 'n': true, 'o': true, 'p': true,
'q': true, 'r': true, 's': true, 't': true, 'u': true, 'v': true, 'w': true, 'x': true,
'y': true, 'z': true,
'A': true, 'B': true, 'C': true, 'D': true, 'E': true, 'F': true, 'G': true, 'H': true,
'I': true, 'J': true, 'K': true, 'L': true, 'M': true, 'N': true, 'O': true, 'P': true,
'Q': true, 'R': true, 'S': true, 'T': true, 'U': true, 'V': true, 'W': true, 'X': true,
'Y': true, 'Z': true,
'!': true, // sub-delims
'$': true, // sub-delims
'%': true, // pct-encoded (and used in IPv6 zones)
'&': true, // sub-delims
'(': true, // sub-delims
')': true, // sub-delims
'*': true, // sub-delims
'+': true, // sub-delims
',': true, // sub-delims
'-': true, // unreserved
'.': true, // unreserved
':': true, // IPv6address + Host expression's optional port
';': true, // sub-delims
'=': true, // sub-delims
'[': true,
'\'': true, // sub-delims
']': true,
'_': true, // unreserved
'~': true, // unreserved
}
// ValidHeaderFieldValue reports whether v is a valid "field-value" according to
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2 :
//
// message-header = field-name ":" [ field-value ]
// field-value = *( field-content | LWS )
// field-content = <the OCTETs making up the field-value
// and consisting of either *TEXT or combinations
// of token, separators, and quoted-string>
//
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec2.html#sec2.2 :
//
// TEXT = <any OCTET except CTLs,
// but including LWS>
// LWS = [CRLF] 1*( SP | HT )
// CTL = <any US-ASCII control character
// (octets 0 - 31) and DEL (127)>
//
// RFC 7230 says:
// field-value = *( field-content / obs-fold )
// obj-fold = N/A to http2, and deprecated
// field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
// field-vchar = VCHAR / obs-text
// obs-text = %x80-FF
// VCHAR = "any visible [USASCII] character"
//
// http2 further says: "Similarly, HTTP/2 allows header field values
// that are not valid. While most of the values that can be encoded
// will not alter header field parsing, carriage return (CR, ASCII
// 0xd), line feed (LF, ASCII 0xa), and the zero character (NUL, ASCII
// 0x0) might be exploited by an attacker if they are translated
// verbatim. Any request or response that contains a character not
// permitted in a header field value MUST be treated as malformed
// (Section 8.1.2.6). Valid characters are defined by the
// field-content ABNF rule in Section 3.2 of [RFC7230]."
//
// This function does not (yet?) properly handle the rejection of
// strings that begin or end with SP or HTAB.
func ValidHeaderFieldValue(v string) bool {
for i := 0; i < len(v); i++ {
b := v[i]
if isCTL(b) && !isLWS(b) {
return false
}
}
return true
}

42
vendor/manifest vendored
View File

@ -5,7 +5,7 @@
"importpath": "github.com/42wim/matterbridge-plus/bridge",
"repository": "https://github.com/42wim/matterbridge-plus",
"vcs": "git",
"revision": "e48ce1d820967506c7ea21539f6c88f7a1377d0f",
"revision": "63c7caabf4b92fa47060cf37beb0ed814cefbaea",
"branch": "master",
"path": "/bridge",
"notests": true
@ -14,20 +14,11 @@
"importpath": "github.com/42wim/matterbridge-plus/matterclient",
"repository": "https://github.com/42wim/matterbridge-plus",
"vcs": "git",
"revision": "e48ce1d820967506c7ea21539f6c88f7a1377d0f",
"revision": "63c7caabf4b92fa47060cf37beb0ed814cefbaea",
"branch": "master",
"path": "/matterclient",
"notests": true
},
{
"importpath": "github.com/42wim/matterbridge/matterhook",
"repository": "https://github.com/42wim/matterbridge",
"vcs": "",
"revision": "6b18257185b1830bd2eff83fae30bdd2055f78b0",
"branch": "master",
"path": "/matterhook",
"notests": true
},
{
"importpath": "github.com/Sirupsen/logrus",
"repository": "https://github.com/Sirupsen/logrus",
@ -72,8 +63,8 @@
"importpath": "github.com/mattermost/platform/einterfaces",
"repository": "https://github.com/mattermost/platform",
"vcs": "git",
"revision": "9d94869cc6a0fb9f051879437c104ccd76094380",
"branch": "HEAD",
"revision": "20735470185e0b0ac1d15b975041ed9a2e0e43bc",
"branch": "release-3.3",
"path": "/einterfaces",
"notests": true
},
@ -81,11 +72,19 @@
"importpath": "github.com/mattermost/platform/model",
"repository": "https://github.com/mattermost/platform",
"vcs": "git",
"revision": "9d94869cc6a0fb9f051879437c104ccd76094380",
"branch": "HEAD",
"revision": "20735470185e0b0ac1d15b975041ed9a2e0e43bc",
"branch": "release-3.3",
"path": "/model",
"notests": true
},
{
"importpath": "github.com/mattn/go-xmpp",
"repository": "https://github.com/mattn/go-xmpp",
"vcs": "git",
"revision": "e44d1877bb457f5c3991903e9934a31e55c3a2ad",
"branch": "master",
"notests": true
},
{
"importpath": "github.com/nicksnyder/go-i18n/i18n",
"repository": "https://github.com/nicksnyder/go-i18n",
@ -122,8 +121,8 @@
{
"importpath": "github.com/thoj/go-ircevent",
"repository": "https://github.com/thoj/go-ircevent",
"vcs": "",
"revision": "da78ed515c0f0833e7a92c7cc52898176198e2c1",
"vcs": "git",
"revision": "98c1902dd2097f38142384167e60206ba26f1585",
"branch": "master",
"notests": true
},
@ -154,6 +153,15 @@
"path": "/http2/hpack",
"notests": true
},
{
"importpath": "golang.org/x/net/lex/httplex",
"repository": "https://go.googlesource.com/net",
"vcs": "git",
"revision": "bc3663df0ac92f928d419e31e0d2af22e683a5a2",
"branch": "master",
"path": "/lex/httplex",
"notests": true
},
{
"importpath": "gopkg.in/gcfg.v1",
"repository": "https://gopkg.in/gcfg.v1",