4
0
mirror of https://github.com/cwinfo/matterbridge.git synced 2025-06-26 23:29:25 +00:00

Compare commits

...

71 Commits

Author SHA1 Message Date
Wim
3f79da84d5 Release v0.14.0 2017-06-15 01:44:46 +02:00
Wim
d540638223 Remove debug 2017-06-15 01:30:58 +02:00
Wim
4ec9b6dd4e Add 3.10.0 support (mattermost) 2017-06-15 01:30:05 +02:00
Wim
3bc219167a Remove need for channel when using api. Closes #195 2017-06-15 00:40:23 +02:00
Wim
8a55c97b4e Fix utf-8 issues #193 2017-06-15 00:07:12 +02:00
9e34162a09 remove second flag.Parse() (#196)
flag.Parse() is already being called on line 28 https://github.com/42wim/matterbridge/blob/master/matterbridge.go#L28
and there is no need for calling it again
2017-06-14 17:15:35 +02:00
Wim
860a371eeb Use cache for teamid 2017-06-12 20:30:30 +02:00
Wim
41a46526a1 Add note about file permissions 2017-06-08 23:42:00 +02:00
Wim
46b798ac1b Update documentation (api) 2017-06-08 00:03:06 +02:00
Wim
359d0f2910 Allow reuse of api in different gateways. See #189 2017-06-07 23:54:50 +02:00
Wim
ad3cb0386b Add token authentication (api) 2017-06-06 00:05:32 +02:00
Wim
3a183cb218 Update vendor 2017-06-06 00:04:18 +02:00
Wim
2eecaccd1c Change to lowercase JSON keys (api) 2017-06-05 23:18:13 +02:00
Wim
5f30a98bc1 Add gateway name to messages 2017-06-05 23:12:19 +02:00
Wim
b8a2fcbaff Post valid JSON (api). See #185 2017-06-05 23:08:36 +02:00
Wim
01496cd080 Fix panic (mattermost). Closes #186 2017-06-05 21:35:38 +02:00
Wim
6a968ab82a Bump version 2017-06-03 18:22:09 +02:00
Wim
c0c4890887 Add hashtag to channel (discord) 2017-06-03 18:21:47 +02:00
Wim
171a53592d Add note about lowercase channel (irc) 2017-06-01 21:00:58 +02:00
Wim
7811c330db Release v0.13.0 2017-05-31 23:32:38 +02:00
Wim
9bcd131e66 Reset variables each loop (telegram). Closes #181 2017-05-30 21:14:03 +02:00
Wim
c791423dd5 Add NOPINGNICK option. Closes #175 2017-05-30 00:11:53 +02:00
Wim
80bdf38388 Bump version 2017-05-29 23:54:43 +02:00
Wim
9d9cb32f4e Limit message length (irc). Closes #179 2017-05-29 21:54:34 +02:00
Wim
87229bab13 Fix sending to different channels on same account (slack). Closes #177 2017-05-24 22:10:21 +02:00
Wim
f065e9e4d5 Release v0.12.1 2017-05-23 22:48:05 +02:00
Wim
3812693111 Replace long ids in channel metions (discord). Fixes #174 2017-05-23 22:26:37 +02:00
Wim
dd3c572256 Fix possible crash on nil (discord) 2017-05-22 21:57:19 +02:00
Wim
c5dfe40326 Update documentation about encrypted rooms in matrix 2017-05-21 15:36:40 +02:00
ef278301e3 Fix JoinChannel argument to use IRC channel key (#172) 2017-05-21 15:23:56 +02:00
Wim
2888fd64b0 Add UseFirstName option (telegram). Closes #144 2017-05-15 23:23:10 +02:00
Wim
27c0f37e49 Update matterbridge.toml.sample about NoHomeServerSuffix 2017-05-15 23:11:27 +02:00
Wim
0774f6a5e7 Bump version 2017-05-12 23:20:22 +02:00
Wim
4036d4459b Add NoHomeServerSuffix. Option to disable homeserver on username (matrix). Closes #160. 2017-05-12 23:04:58 +02:00
ee643de5b6 Add Compatibility for Cisco Jabber (xmpp) (#166) 2017-05-11 20:10:53 +02:00
Wim
8c7549a09e Update changelog 2017-05-09 23:46:20 +02:00
Wim
7a16146304 Release v0.12.0 2017-05-09 23:31:26 +02:00
Wim
3d3809a21b Add 3.9.0 support (mattermost) 2017-05-09 23:30:53 +02:00
29465397dd Add support for HTTP{S}_PROXY env variables (#162) 2017-05-08 21:20:52 +02:00
Wim
d300bb1735 Relay messages starting with ! (irc). Closes #164 2017-05-08 21:15:01 +02:00
Wim
2e703472f1 Fix crash on reconnects when server is down. Closes #163 2017-05-08 20:44:36 +02:00
Wim
8fede90b9e Remove examples (vendor issues) 2017-05-05 20:45:11 +02:00
Wim
d128f157c4 Update doc 2017-04-19 21:57:40 +02:00
Wim
4fcedabfd0 Revert "Add support for edited messages (gitter)"
This reverts commit 17b8b86d68.
Reverted because of lingering file descriptors (memory leak)
2017-04-19 19:51:33 +02:00
Wim
246c8e4f74 Ignore error on private channel join (slack) Fixes #150 2017-04-17 18:01:24 +02:00
Wim
4d2207aba7 Add support for edited messages (slack) 2017-04-16 00:16:24 +02:00
Wim
17b8b86d68 Add support for edited messages (gitter) 2017-04-15 23:46:01 +02:00
Wim
fdb57230a3 Add support for edited messages (mattermost) 2017-04-15 20:21:57 +02:00
Wim
7469732bbc Add support for edited messages (telegram) 2017-04-15 19:07:35 +02:00
Wim
d1dd6c3440 Add support for edited messages (discord) 2017-04-15 19:00:15 +02:00
Wim
02612c0061 Add support for sending edited messages 2017-04-15 18:46:25 +02:00
Wim
a4db63a773 Bump version 2017-04-15 16:24:25 +02:00
Wim
035c2b906a Strip custom emoji metadata (discord). Closes #148 2017-04-15 16:23:34 +02:00
Wim
6ea8be5749 Release v0.11.0 2017-04-11 21:51:23 +02:00
Wim
36024d5439 Add 3.8.0 support (mattermost) 2017-04-09 23:15:11 +02:00
Wim
8d52c98373 Update README 2017-04-08 00:57:11 +02:00
Wim
b4a4eb0057 Update changelog 2017-04-08 00:50:17 +02:00
Wim
b469c8ddbd Rejoin channel when kicked (irc). Closes #146 2017-04-08 00:42:37 +02:00
Wim
eee0036c7f Modify iconurl correctly (mattermost). Closes #145 2017-04-08 00:16:46 +02:00
Wim
89c66b9430 Reconnect on session removal (mattermost) 2017-04-07 23:27:41 +02:00
Wim
bd38319d83 Add support for showing/hiding join/leave messages from mattermost. Closes #147 2017-04-07 22:27:36 +02:00
Wim
33dffd5ea8 Fix join/leave regression (irc) 2017-04-03 22:18:29 +02:00
Wim
57176dadd4 Support edited messages (telegram). See #141 2017-04-01 18:18:38 +02:00
Wim
dd449a8705 Remove debug info (irc) 2017-04-01 18:10:11 +02:00
Wim
587ad9f41d Remove space after nick (mattermost). Closes #142 2017-04-01 17:44:17 +02:00
Wim
a16ad8bf3b Reuse connection when using same bridge with another gateway. See #87 2017-04-01 17:24:19 +02:00
Wim
1e0490bd36 Merge branch 'channelinfo' 2017-03-28 23:58:22 +02:00
Wim
8afc641f0c Bump version 2017-03-28 23:58:07 +02:00
Wim
2e4d58cb92 Refactor 2017-03-28 23:56:58 +02:00
Wim
02d7e2db65 Release v0.10.3 2017-03-27 20:40:57 +02:00
Wim
f935c573e9 Allow bot tokens for now without warning (slack). Closes #140 2017-03-27 20:15:05 +02:00
72 changed files with 1195 additions and 1817 deletions

View File

@ -28,7 +28,7 @@ Simple bridge between Mattermost, IRC, XMPP, Gitter, Slack, Discord, Telegram, R
# Requirements # Requirements
Accounts to one of the supported bridges Accounts to one of the supported bridges
* [Mattermost](https://github.com/mattermost/platform/) 3.5.x - 3.7.x * [Mattermost](https://github.com/mattermost/platform/) 3.5.x - 3.10.x
* [IRC](http://www.mirc.com/servers.html) * [IRC](http://www.mirc.com/servers.html)
* [XMPP](https://jabber.org) * [XMPP](https://jabber.org)
* [Gitter](https://gitter.im) * [Gitter](https://gitter.im)
@ -42,7 +42,7 @@ Accounts to one of the supported bridges
# Installing # Installing
## Binaries ## Binaries
Binaries can be found [here] (https://github.com/42wim/matterbridge/releases/) Binaries can be found [here] (https://github.com/42wim/matterbridge/releases/)
* Latest release [v0.10.2](https://github.com/42wim/matterbridge/releases/latest) * Latest stable release [v0.14.0](https://github.com/42wim/matterbridge/releases/latest)
## Building ## 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) 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)

View File

@ -4,6 +4,7 @@ import (
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
log "github.com/Sirupsen/logrus" log "github.com/Sirupsen/logrus"
"github.com/labstack/echo" "github.com/labstack/echo"
"github.com/labstack/echo/middleware"
"github.com/zfjagann/golang-ring" "github.com/zfjagann/golang-ring"
"net/http" "net/http"
"sync" "sync"
@ -21,6 +22,7 @@ type ApiMessage struct {
Text string `json:"text"` Text string `json:"text"`
Username string `json:"username"` Username string `json:"username"`
Avatar string `json:"avatar"` Avatar string `json:"avatar"`
Gateway string `json:"gateway"`
} }
var flog *log.Entry var flog *log.Entry
@ -38,6 +40,11 @@ func New(cfg config.Protocol, account string, c chan config.Message) *Api {
b.Config = &cfg b.Config = &cfg
b.Account = account b.Account = account
b.Remote = c b.Remote = c
if b.Config.Token != "" {
e.Use(middleware.KeyAuth(func(key string, c echo.Context) (bool, error) {
return key == b.Config.Token, nil
}))
}
e.GET("/api/messages", b.handleMessages) e.GET("/api/messages", b.handleMessages)
e.POST("/api/message", b.handlePostMessage) e.POST("/api/message", b.handlePostMessage)
go func() { go func() {
@ -70,12 +77,15 @@ func (b *Api) handlePostMessage(c echo.Context) error {
if err := c.Bind(message); err != nil { if err := c.Bind(message); err != nil {
return err return err
} }
flog.Debugf("Sending message from %s on %s to gateway", message.Username, "api")
b.Remote <- config.Message{ b.Remote <- config.Message{
Text: message.Text, Text: message.Text,
Username: message.Username, Username: message.Username,
Channel: "api", Channel: "api",
Avatar: message.Avatar, Avatar: message.Avatar,
Account: b.Account, Account: b.Account,
Gateway: message.Gateway,
Protocol: "api",
} }
return c.JSON(http.StatusOK, message) return c.JSON(http.StatusOK, message)
} }
@ -83,9 +93,7 @@ func (b *Api) handlePostMessage(c echo.Context) error {
func (b *Api) handleMessages(c echo.Context) error { func (b *Api) handleMessages(c echo.Context) error {
b.Lock() b.Lock()
defer b.Unlock() defer b.Unlock()
for _, msg := range b.Messages.Values() { c.JSONPretty(http.StatusOK, b.Messages.Values(), " ")
c.JSONPretty(http.StatusOK, msg, " ")
}
b.Messages = ring.Ring{} b.Messages = ring.Ring{}
return nil return nil
} }

View File

@ -27,23 +27,23 @@ type Bridger interface {
type Bridge struct { type Bridge struct {
Config config.Protocol Config config.Protocol
Bridger Bridger
Name string Name string
Account string Account string
Protocol string Protocol string
ChannelsIn map[string]config.ChannelOptions Channels map[string]config.ChannelInfo
ChannelsOut map[string]config.ChannelOptions Joined map[string]bool
} }
func New(cfg *config.Config, bridge *config.Bridge, c chan config.Message) *Bridge { func New(cfg *config.Config, bridge *config.Bridge, c chan config.Message) *Bridge {
b := new(Bridge) b := new(Bridge)
b.ChannelsIn = make(map[string]config.ChannelOptions) b.Channels = make(map[string]config.ChannelInfo)
b.ChannelsOut = make(map[string]config.ChannelOptions)
accInfo := strings.Split(bridge.Account, ".") accInfo := strings.Split(bridge.Account, ".")
protocol := accInfo[0] protocol := accInfo[0]
name := accInfo[1] name := accInfo[1]
b.Name = name b.Name = name
b.Protocol = protocol b.Protocol = protocol
b.Account = bridge.Account b.Account = bridge.Account
b.Joined = make(map[string]bool)
// override config from environment // override config from environment
config.OverrideCfgFromEnv(cfg, protocol, name) config.OverrideCfgFromEnv(cfg, protocol, name)
@ -83,33 +83,28 @@ func New(cfg *config.Config, bridge *config.Bridge, c chan config.Message) *Brid
} }
func (b *Bridge) JoinChannels() error { func (b *Bridge) JoinChannels() error {
exists := make(map[string]bool) err := b.joinChannels(b.Channels, b.Joined)
err := b.joinChannels(b.ChannelsIn, exists)
if err != nil {
return err
}
err = b.joinChannels(b.ChannelsOut, exists)
if err != nil { if err != nil {
return err return err
} }
return nil return nil
} }
func (b *Bridge) joinChannels(cMap map[string]config.ChannelOptions, exists map[string]bool) error { func (b *Bridge) joinChannels(channels map[string]config.ChannelInfo, exists map[string]bool) error {
mychannel := "" mychannel := ""
for channel, info := range cMap { for ID, channel := range channels {
if !exists[channel] { if !exists[ID] {
mychannel = channel mychannel = channel.Name
log.Infof("%s: joining %s", b.Account, channel) log.Infof("%s: joining %s (%s)", b.Account, channel.Name, ID)
if b.Protocol == "irc" && info.Key != "" { if b.Protocol == "irc" && channel.Options.Key != "" {
log.Debugf("using key %s for channel %s", info.Key, channel) log.Debugf("using key %s for channel %s", channel.Options.Key, channel.Name)
mychannel = mychannel + " " + info.Key mychannel = mychannel + " " + channel.Options.Key
} }
err := b.JoinChannel(mychannel) err := b.JoinChannel(mychannel)
if err != nil { if err != nil {
return err return err
} }
exists[channel] = true exists[ID] = true
} }
} }
return nil return nil

View File

@ -10,24 +10,38 @@ import (
) )
const ( const (
EVENT_JOIN_LEAVE = "join_leave" EVENT_JOIN_LEAVE = "join_leave"
EVENT_FAILURE = "failure" EVENT_FAILURE = "failure"
EVENT_REJOIN_CHANNELS = "rejoin_channels"
) )
type Message struct { type Message struct {
Text string Text string `json:"text"`
Channel string Channel string `json:"channel"`
Username string Username string `json:"username"`
Avatar string Avatar string `json:"avatar"`
Account string Account string `json:"account"`
Event string Event string `json:"event"`
Protocol string Protocol string `json:"protocol"`
Timestamp time.Time Gateway string `json:"gateway"`
Timestamp time.Time `json:"timestamp"`
}
type ChannelInfo struct {
Name string
Account string
Direction string
ID string
GID map[string]bool
SameChannel map[string]bool
Options ChannelOptions
} }
type Protocol struct { type Protocol struct {
BindAddress string // mattermost, slack BindAddress string // mattermost, slack
Buffer int // api Buffer int // api
EditSuffix string // mattermost, slack, discord, telegram, gitter
EditDisable bool // mattermost, slack, discord, telegram, gitter
IconURL string // mattermost, slack IconURL string // mattermost, slack
IgnoreNicks string // all protocols IgnoreNicks string // all protocols
Jid string // xmpp Jid string // xmpp
@ -39,23 +53,26 @@ type Protocol struct {
NickServNick string // IRC NickServNick string // IRC
NickServPassword string // IRC NickServPassword string // IRC
NicksPerRow int // mattermost, slack NicksPerRow int // mattermost, slack
NoHomeServerSuffix bool // matrix
NoTLS bool // mattermost NoTLS bool // mattermost
Password string // IRC,mattermost,XMPP,matrix Password string // IRC,mattermost,XMPP,matrix
PrefixMessagesWithNick bool // mattemost, slack PrefixMessagesWithNick bool // mattemost, slack
Protocol string //all protocols Protocol string //all protocols
MessageQueue int // IRC, size of message queue for flood control MessageQueue int // IRC, size of message queue for flood control
MessageDelay int // IRC, time in millisecond to wait between messages MessageDelay int // IRC, time in millisecond to wait between messages
MessageLength int // IRC, max length of a message allowed
MessageFormat string // telegram MessageFormat string // telegram
RemoteNickFormat string // all protocols RemoteNickFormat string // all protocols
Server string // IRC,mattermost,XMPP,discord Server string // IRC,mattermost,XMPP,discord
ShowJoinPart bool // all protocols ShowJoinPart bool // all protocols
SkipTLSVerify bool // IRC, mattermost SkipTLSVerify bool // IRC, mattermost
Team string // mattermost Team string // mattermost
Token string // gitter, slack, discord Token string // gitter, slack, discord, api
URL string // mattermost, slack, matrix URL string // mattermost, slack, matrix
UseAPI bool // mattermost, slack UseAPI bool // mattermost, slack
UseSASL bool // IRC UseSASL bool // IRC
UseTLS bool // IRC UseTLS bool // IRC
UseFirstName bool // telegram
} }
type ChannelOptions struct { type ChannelOptions struct {
@ -63,9 +80,10 @@ type ChannelOptions struct {
} }
type Bridge struct { type Bridge struct {
Account string Account string
Channel string Channel string
Options ChannelOptions Options ChannelOptions
SameChannel bool
} }
type Gateway struct { type Gateway struct {

View File

@ -4,6 +4,7 @@ import (
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
log "github.com/Sirupsen/logrus" log "github.com/Sirupsen/logrus"
"github.com/bwmarrin/discordgo" "github.com/bwmarrin/discordgo"
"regexp"
"strings" "strings"
"sync" "sync"
) )
@ -51,6 +52,7 @@ func (b *bdiscord) Connect() error {
flog.Info("Connection succeeded") flog.Info("Connection succeeded")
b.c.AddHandler(b.messageCreate) b.c.AddHandler(b.messageCreate)
b.c.AddHandler(b.memberUpdate) b.c.AddHandler(b.memberUpdate)
b.c.AddHandler(b.messageUpdate)
err = b.c.Open() err = b.c.Open()
if err != nil { if err != nil {
flog.Debugf("%#v", err) flog.Debugf("%#v", err)
@ -103,6 +105,18 @@ func (b *bdiscord) Send(msg config.Message) error {
return nil return nil
} }
func (b *bdiscord) messageUpdate(s *discordgo.Session, m *discordgo.MessageUpdate) {
if b.Config.EditDisable {
return
}
// only when message is actually edited
if m.Message.EditedTimestamp != "" {
flog.Debugf("Sending edit message")
m.Content = m.Content + b.Config.EditSuffix
b.messageCreate(s, (*discordgo.MessageCreate)(m))
}
}
func (b *bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) { func (b *bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) {
// not relay our own messages // not relay our own messages
if m.Author.Username == b.Nick { if m.Author.Username == b.Nick {
@ -125,6 +139,8 @@ func (b *bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreat
if len(m.MentionRoles) > 0 { if len(m.MentionRoles) > 0 {
m.Message.Content = b.replaceRoleMentions(m.Message.Content) m.Message.Content = b.replaceRoleMentions(m.Message.Content)
} }
m.Message.Content = b.stripCustomoji(m.Message.Content)
m.Message.Content = b.replaceChannelMentions(m.Message.Content)
b.Remote <- config.Message{Username: username, Text: m.ContentWithMentionsReplaced(), Channel: channelName, b.Remote <- config.Message{Username: username, Text: m.ContentWithMentionsReplaced(), Channel: channelName,
Account: b.Account, Avatar: "https://cdn.discordapp.com/avatars/" + m.Author.ID + "/" + m.Author.Avatar + ".jpg"} Account: b.Account, Avatar: "https://cdn.discordapp.com/avatars/" + m.Author.ID + "/" + m.Author.Avatar + ".jpg"}
} }
@ -143,12 +159,14 @@ func (b *bdiscord) getNick(user *discordgo.User) string {
b.Lock() b.Lock()
defer b.Unlock() defer b.Unlock()
if _, ok := b.userMemberMap[user.ID]; ok { if _, ok := b.userMemberMap[user.ID]; ok {
if b.userMemberMap[user.ID].Nick != "" { if b.userMemberMap[user.ID] != nil {
// only return if nick is set if b.userMemberMap[user.ID].Nick != "" {
return b.userMemberMap[user.ID].Nick // only return if nick is set
return b.userMemberMap[user.ID].Nick
}
// otherwise return username
return user.Username
} }
// otherwise return username
return user.Username
} }
// if we didn't find nick, search for it // if we didn't find nick, search for it
b.userMemberMap[user.ID], err = b.c.GuildMember(b.guildID, user.ID) b.userMemberMap[user.ID], err = b.c.GuildMember(b.guildID, user.ID)
@ -195,3 +213,28 @@ func (b *bdiscord) replaceRoleMentions(text string) string {
} }
return text return text
} }
func (b *bdiscord) replaceChannelMentions(text string) string {
var err error
re := regexp.MustCompile("<#[0-9]+>")
text = re.ReplaceAllStringFunc(text, func(m string) string {
channel := b.getChannelName(m[2 : len(m)-1])
// if at first don't succeed, try again
if channel == "" {
b.Channels, err = b.c.GuildChannels(b.guildID)
if err != nil {
return "#unknownchannel"
}
channel = b.getChannelName(m[2 : len(m)-1])
return "#" + channel
}
return "#" + channel
})
return text
}
func (b *bdiscord) stripCustomoji(text string) string {
// <:doge:302803592035958784>
re := regexp.MustCompile("<(:.*?:)[0-9]+>")
return re.ReplaceAllString(text, `$1`)
}

View File

@ -46,6 +46,9 @@ func New(cfg config.Protocol, account string, c chan config.Message) *Birc {
if b.Config.MessageQueue == 0 { if b.Config.MessageQueue == 0 {
b.Config.MessageQueue = 30 b.Config.MessageQueue = 30
} }
if b.Config.MessageLength == 0 {
b.Config.MessageLength = 400
}
return b return b
} }
@ -92,7 +95,7 @@ func (b *Birc) Connect() error {
} }
func (b *Birc) Disconnect() error { func (b *Birc) Disconnect() error {
b.i.Disconnect() //b.i.Disconnect()
close(b.Local) close(b.Local)
return nil return nil
} }
@ -109,9 +112,11 @@ func (b *Birc) Send(msg config.Message) error {
} }
if strings.HasPrefix(msg.Text, "!") { if strings.HasPrefix(msg.Text, "!") {
b.Command(&msg) b.Command(&msg)
return nil
} }
for _, text := range strings.Split(msg.Text, "\n") { for _, text := range strings.Split(msg.Text, "\n") {
if len(text) > b.Config.MessageLength {
text = text[:b.Config.MessageLength] + " <message clipped>"
}
if len(b.Local) < b.Config.MessageQueue { if len(b.Local) < b.Config.MessageQueue {
if len(b.Local) == b.Config.MessageQueue-1 { if len(b.Local) == b.Config.MessageQueue-1 {
text = text + " <message clipped>" text = text + " <message clipped>"
@ -167,14 +172,19 @@ func (b *Birc) handleNewConnection(event *irc.Event) {
i.AddCallback("JOIN", b.handleJoinPart) i.AddCallback("JOIN", b.handleJoinPart)
i.AddCallback("PART", b.handleJoinPart) i.AddCallback("PART", b.handleJoinPart)
i.AddCallback("QUIT", b.handleJoinPart) i.AddCallback("QUIT", b.handleJoinPart)
i.AddCallback("KICK", b.handleJoinPart)
i.AddCallback("*", b.handleOther) i.AddCallback("*", b.handleOther)
// we are now fully connected // we are now fully connected
b.connected <- struct{}{} b.connected <- struct{}{}
} }
func (b *Birc) handleJoinPart(event *irc.Event) { func (b *Birc) handleJoinPart(event *irc.Event) {
flog.Debugf("Sending JOIN_LEAVE event from %s to gateway", b.Account)
channel := event.Arguments[0] channel := event.Arguments[0]
if event.Code == "KICK" {
flog.Infof("Got kicked from %s by %s", channel, event.Nick)
b.Remote <- config.Message{Username: "system", Text: "rejoin", Channel: channel, Account: b.Account, Event: config.EVENT_REJOIN_CHANNELS}
return
}
if event.Code == "QUIT" { if event.Code == "QUIT" {
if event.Nick == b.Nick && strings.Contains(event.Raw, "Ping timeout") { if event.Nick == b.Nick && strings.Contains(event.Raw, "Ping timeout") {
flog.Infof("%s reconnecting ..", b.Account) flog.Infof("%s reconnecting ..", b.Account)
@ -182,6 +192,7 @@ func (b *Birc) handleJoinPart(event *irc.Event) {
return return
} }
} }
flog.Debugf("Sending JOIN_LEAVE event from %s to gateway", b.Account)
b.Remote <- config.Message{Username: "system", Text: event.Nick + " " + strings.ToLower(event.Code) + "s", Channel: channel, Account: b.Account, Event: config.EVENT_JOIN_LEAVE} b.Remote <- config.Message{Username: "system", Text: event.Nick + " " + strings.ToLower(event.Code) + "s", Channel: channel, Account: b.Account, Event: config.EVENT_JOIN_LEAVE}
flog.Debugf("handle %#v", event) flog.Debugf("handle %#v", event)
} }

View File

@ -1,10 +1,12 @@
package bmatrix package bmatrix
import ( import (
"regexp"
"sync"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
log "github.com/Sirupsen/logrus" log "github.com/Sirupsen/logrus"
matrix "github.com/matrix-org/gomatrix" matrix "github.com/matrix-org/gomatrix"
"sync"
) )
type Bmatrix struct { type Bmatrix struct {
@ -101,8 +103,13 @@ func (b *Bmatrix) handlematrix() error {
flog.Debugf("Unknown room %s", ev.RoomID) flog.Debugf("Unknown room %s", ev.RoomID)
return return
} }
username := ev.Sender[1:]
if b.Config.NoHomeServerSuffix {
re := regexp.MustCompile("(.*?):.*")
username = re.ReplaceAllString(username, `$1`)
}
flog.Debugf("Sending message from %s on %s to gateway", ev.Sender, b.Account) flog.Debugf("Sending message from %s on %s to gateway", ev.Sender, b.Account)
b.Remote <- config.Message{Username: ev.Sender, Text: ev.Content["body"].(string), Channel: channel, Account: b.Account} b.Remote <- config.Message{Username: username, Text: ev.Content["body"].(string), Channel: channel, Account: b.Account}
} }
flog.Debugf("Received: %#v", ev) flog.Debugf("Received: %#v", ev)
}) })

View File

@ -72,6 +72,7 @@ func (b *Bmattermost) Connect() error {
flog.Info("Connection succeeded") flog.Info("Connection succeeded")
b.TeamId = b.mc.GetTeamId() b.TeamId = b.mc.GetTeamId()
go b.mc.WsReceiver() go b.mc.WsReceiver()
go b.mc.StatusLoop()
} }
go b.handleMatter() go b.handleMatter()
return nil return nil
@ -96,15 +97,11 @@ func (b *Bmattermost) Send(msg config.Message) error {
channel := msg.Channel channel := msg.Channel
if b.Config.PrefixMessagesWithNick { if b.Config.PrefixMessagesWithNick {
/*if IsMarkup(message) { message = nick + message
message = nick + "\n\n" + message
} else {
*/
message = nick + " " + message
//}
} }
if !b.Config.UseAPI { if !b.Config.UseAPI {
matterMessage := matterhook.OMessage{IconURL: b.Config.IconURL} matterMessage := matterhook.OMessage{IconURL: b.Config.IconURL}
matterMessage.IconURL = msg.Avatar
matterMessage.Channel = channel matterMessage.Channel = channel
matterMessage.UserName = nick matterMessage.UserName = nick
matterMessage.Type = "" matterMessage.Type = ""
@ -136,14 +133,26 @@ func (b *Bmattermost) handleMatter() {
func (b *Bmattermost) handleMatterClient(mchan chan *MMMessage) { func (b *Bmattermost) handleMatterClient(mchan chan *MMMessage) {
for message := range b.mc.MessageChan { for message := range b.mc.MessageChan {
flog.Debugf("%#v", message.Raw.Data)
if message.Type == "system_join_leave" ||
message.Type == "system_join_channel" ||
message.Type == "system_leave_channel" {
flog.Debugf("Sending JOIN_LEAVE event from %s to gateway", b.Account)
b.Remote <- config.Message{Username: "system", Text: message.Text, Channel: message.Channel, Account: b.Account, Event: config.EVENT_JOIN_LEAVE}
continue
}
// do not post our own messages back to irc // do not post our own messages back to irc
// only listen to message from our team // only listen to message from our team
if message.Raw.Event == "posted" && b.mc.User.Username != message.Username && message.Raw.Data["team_id"].(string) == b.TeamId { if (message.Raw.Event == "posted" || message.Raw.Event == "post_edited") &&
b.mc.User.Username != message.Username && message.Raw.Data["team_id"].(string) == b.TeamId {
flog.Debugf("Receiving from matterclient %#v", message) flog.Debugf("Receiving from matterclient %#v", message)
m := &MMMessage{} m := &MMMessage{}
m.Username = message.Username m.Username = message.Username
m.Channel = message.Channel m.Channel = message.Channel
m.Text = message.Text m.Text = message.Text
if message.Raw.Event == "post_edited" && !b.Config.EditDisable {
m.Text = message.Text + b.Config.EditSuffix
}
if len(message.Post.FileIds) > 0 { if len(message.Post.FileIds) > 0 {
for _, link := range b.mc.GetPublicLinks(message.Post.FileIds) { for _, link := range b.mc.GetPublicLinks(message.Post.FileIds) {
m.Text = m.Text + "\n" + link m.Text = m.Text + "\n" + link

View File

@ -73,9 +73,15 @@ func (b *Bslack) Disconnect() error {
func (b *Bslack) JoinChannel(channel string) error { func (b *Bslack) JoinChannel(channel string) error {
// we can only join channels using the API // we can only join channels using the API
if b.Config.UseAPI { if b.Config.UseAPI {
if strings.HasPrefix(b.Config.Token, "xoxb") {
// TODO check if bot has already joined channel
return nil
}
_, err := b.sc.JoinChannel(channel) _, err := b.sc.JoinChannel(channel)
if err != nil { if err != nil {
return err if err.Error() != "name_taken" {
return err
}
} }
} }
return nil return nil
@ -83,9 +89,6 @@ func (b *Bslack) JoinChannel(channel string) error {
func (b *Bslack) Send(msg config.Message) error { func (b *Bslack) Send(msg config.Message) error {
flog.Debugf("Receiving %#v", msg) flog.Debugf("Receiving %#v", msg)
if msg.Account == b.Account {
return nil
}
nick := msg.Username nick := msg.Username
message := msg.Text message := msg.Text
channel := msg.Channel channel := msg.Channel
@ -195,6 +198,11 @@ func (b *Bslack) handleSlackClient(mchan chan *MMMessage) {
// ignore first message // ignore first message
if count > 0 { if count > 0 {
flog.Debugf("Receiving from slackclient %#v", ev) flog.Debugf("Receiving from slackclient %#v", ev)
if !b.Config.EditDisable && ev.SubMessage != nil {
flog.Debugf("SubMessage %#v", ev.SubMessage)
ev.User = ev.SubMessage.User
ev.Text = ev.SubMessage.Text + b.Config.EditSuffix
}
// use our own func because rtm.GetChannelInfo doesn't work for private channels // use our own func because rtm.GetChannelInfo doesn't work for private channels
channel, err := b.getChannelByID(ev.Channel) channel, err := b.getChannelByID(ev.Channel)
if err != nil { if err != nil {

View File

@ -76,32 +76,41 @@ func (b *Btelegram) Send(msg config.Message) error {
} }
func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) { func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
username := ""
text := ""
channel := ""
for update := range updates { for update := range updates {
var message *tgbotapi.Message
username := ""
channel := ""
text := ""
// handle channels // handle channels
if update.ChannelPost != nil { if update.ChannelPost != nil {
if update.ChannelPost.From != nil { message = update.ChannelPost
username = update.ChannelPost.From.FirstName }
if username == "" { if update.EditedChannelPost != nil && !b.Config.EditDisable {
username = update.ChannelPost.From.UserName message = update.EditedChannelPost
} message.Text = message.Text + b.Config.EditSuffix
}
text = update.ChannelPost.Text
channel = strconv.FormatInt(update.ChannelPost.Chat.ID, 10)
} }
// handle groups // handle groups
if update.Message != nil { if update.Message != nil {
if update.Message.From != nil { message = update.Message
username = update.Message.From.FirstName }
if update.EditedMessage != nil && !b.Config.EditDisable {
message = update.EditedMessage
message.Text = message.Text + b.Config.EditSuffix
}
if message.From != nil {
if b.Config.UseFirstName {
username = message.From.FirstName
}
if username == "" {
username = message.From.UserName
if username == "" { if username == "" {
username = update.Message.From.UserName username = message.From.FirstName
} }
} }
text = update.Message.Text text = message.Text
channel = strconv.FormatInt(update.Message.Chat.ID, 10) channel = strconv.FormatInt(message.Chat.ID, 10)
} }
if username == "" { if username == "" {
username = "unknown" username = "unknown"
} }

View File

@ -119,7 +119,7 @@ func (b *Bxmpp) handleXmpp() error {
var channel, nick string var channel, nick string
if v.Type == "groupchat" { if v.Type == "groupchat" {
s := strings.Split(v.Remote, "@") s := strings.Split(v.Remote, "@")
if len(s) == 2 { if len(s) >= 2 {
channel = s[0] channel = s[0]
} }
s = strings.Split(s[1], "/") s = strings.Split(s[1], "/")

View File

@ -1,3 +1,82 @@
# v0.14.0
## New features
* api: add token authentication
* mattermost: add support for mattermost 3.10.0
## Changes
* api: gateway name is added in JSON messages
* api: lowercase JSON keys
* api: channel name isn't needed in config #195
## Bugfix
* discord: Add hashtag to channelname (when translating from id) (discord)
* mattermost: Fix a panic. #186
* mattermost: use teamid cache if possible. Fixes a panic
* api: post valid json. #185
* api: allow reuse of api in different gateways. #189
* general: Fix utf-8 issues for {NOPINGNICK}. #193
# v0.13.0
## New features
* irc: Limit message length. ```MessageLength=400```
Maximum length of message sent to irc server. If it exceeds <message clipped> will be add to the message.
* irc: Add NOPINGNICK option.
The string "{NOPINGNICK}" (case sensitive) will be replaced by the actual nick / username, but with a ZWSP inside the nick, so the irc user with the same nick won't get pinged.
See https://github.com/42wim/matterbridge/issues/175 for more information
## Bugfix
* slack: Fix sending to different channels on same account (slack). Closes #177
* telegram: Fix incorrect usernames being sent. Closes #181
# v0.12.1
## New features
* telegram: Add UseFirstName option (telegram). Closes #144
* matrix: Add NoHomeServerSuffix. Option to disable homeserver on username (matrix). Closes #160.
## Bugfix
* xmpp: Add Compatibility for Cisco Jabber (xmpp) (#166)
* irc: Fix JoinChannel argument to use IRC channel key (#172)
* discord: Fix possible crash on nil (discord)
* discord: Replace long ids in channel metions (discord). Fixes #174
# v0.12.0
## Changes
* general: edited messages are now being sent by default on discord/mattermost/telegram/slack. See "New Features"
## New features
* general: add support for edited messages.
Add new keyword EditDisable (false/true), default false. Which means by default edited messages will be sent to other bridges.
Add new keyword EditSuffix , default "". You can change this eg to "(edited)", this will be appended to every edit message.
* mattermost: support mattermost v3.9.x
* general: Add support for HTTP{S}_PROXY env variables (#162)
* discord: Strip custom emoji metadata (discord). Closes #148
## Bugfix
* slack: Ignore error on private channel join (slack) Fixes #150
* mattermost: fix crash on reconnects when server is down. Closes #163
* irc: Relay messages starting with ! (irc). Closes #164
# v0.11.0
## New features
* general: reusing the same account on multiple gateways now also reuses the connection.
This is particuarly useful for irc. See #87
* general: the Name is now REQUIRED and needs to be UNIQUE for each gateway configuration
* telegram: Support edited messages (telegram). See #141
* mattermost: Add support for showing/hiding join/leave messages from mattermost. Closes #147
* mattermost: Reconnect on session removal/timeout (mattermost)
* mattermost: Support mattermost v3.8.x
* irc: Rejoin channel when kicked (irc).
## Bugfix
* mattermost: Remove space after nick (mattermost). Closes #142
* mattermost: Modify iconurl correctly (mattermost).
* irc: Fix join/leave regression (irc)
# v0.10.3
## Bugfix
* slack: Allow bot tokens for now without warning (slack). Closes #140 (fixes user_is_bot message on channel join)
# v0.10.2 # v0.10.2
## New features ## New features
* general: gops agent added. Allows for more debugging. See #134 * general: gops agent added. Allows for more debugging. See #134

View File

@ -5,7 +5,6 @@ import (
"github.com/42wim/matterbridge/bridge" "github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
log "github.com/Sirupsen/logrus" log "github.com/Sirupsen/logrus"
"reflect"
"strings" "strings"
"time" "time"
) )
@ -14,21 +13,21 @@ type Gateway struct {
*config.Config *config.Config
MyConfig *config.Gateway MyConfig *config.Gateway
Bridges map[string]*bridge.Bridge Bridges map[string]*bridge.Bridge
ChannelsOut map[string][]string Channels map[string]*config.ChannelInfo
ChannelsIn map[string][]string
ChannelOptions map[string]config.ChannelOptions ChannelOptions map[string]config.ChannelOptions
Names map[string]bool
Name string Name string
Message chan config.Message Message chan config.Message
DestChannelFunc func(msg *config.Message, dest string) []string DestChannelFunc func(msg *config.Message, dest bridge.Bridge) []config.ChannelInfo
} }
func New(cfg *config.Config, gateway *config.Gateway) *Gateway { func New(cfg *config.Config) *Gateway {
gw := &Gateway{} gw := &Gateway{}
gw.Name = gateway.Name
gw.Config = cfg gw.Config = cfg
gw.MyConfig = gateway gw.Channels = make(map[string]*config.ChannelInfo)
gw.Message = make(chan config.Message) gw.Message = make(chan config.Message)
gw.Bridges = make(map[string]*bridge.Bridge) gw.Bridges = make(map[string]*bridge.Bridge)
gw.Names = make(map[string]bool)
gw.DestChannelFunc = gw.getDestChannel gw.DestChannelFunc = gw.getDestChannel
return gw return gw
} }
@ -36,13 +35,17 @@ func New(cfg *config.Config, gateway *config.Gateway) *Gateway {
func (gw *Gateway) AddBridge(cfg *config.Bridge) error { func (gw *Gateway) AddBridge(cfg *config.Bridge) error {
for _, br := range gw.Bridges { for _, br := range gw.Bridges {
if br.Account == cfg.Account { if br.Account == cfg.Account {
gw.mapChannelsToBridge(br)
err := br.JoinChannels()
if err != nil {
return fmt.Errorf("Bridge %s failed to join channel: %v", br.Account, err)
}
return nil return nil
} }
} }
log.Infof("Starting bridge: %s ", cfg.Account) log.Infof("Starting bridge: %s ", cfg.Account)
br := bridge.New(gw.Config, cfg, gw.Message) br := bridge.New(gw.Config, cfg, gw.Message)
gw.mapChannelsToBridge(br, gw.ChannelsOut) gw.mapChannelsToBridge(br)
gw.mapChannelsToBridge(br, gw.ChannelsIn)
gw.Bridges[cfg.Account] = br gw.Bridges[cfg.Account] = br
err := br.Connect() err := br.Connect()
if err != nil { if err != nil {
@ -55,17 +58,17 @@ func (gw *Gateway) AddBridge(cfg *config.Bridge) error {
return nil return nil
} }
func (gw *Gateway) mapChannelsToBridge(br *bridge.Bridge, cMap map[string][]string) { func (gw *Gateway) AddConfig(cfg *config.Gateway) error {
for _, channel := range cMap[br.Account] { if gw.Names[cfg.Name] {
if _, ok := gw.ChannelOptions[br.Account+channel]; ok { return fmt.Errorf("Gateway with name %s already exists", cfg.Name)
br.ChannelsOut[channel] = gw.ChannelOptions[br.Account+channel]
} else {
br.ChannelsOut[channel] = config.ChannelOptions{}
}
} }
} if cfg.Name == "" {
return fmt.Errorf("%s", "Gateway without name found")
func (gw *Gateway) Start() error { }
log.Infof("Starting gateway: %s", cfg.Name)
gw.Names[cfg.Name] = true
gw.Name = cfg.Name
gw.MyConfig = cfg
gw.mapChannels() gw.mapChannels()
for _, br := range append(gw.MyConfig.In, append(gw.MyConfig.InOut, gw.MyConfig.Out...)...) { for _, br := range append(gw.MyConfig.In, append(gw.MyConfig.InOut, gw.MyConfig.Out...)...) {
err := gw.AddBridge(&br) err := gw.AddBridge(&br)
@ -73,6 +76,18 @@ func (gw *Gateway) Start() error {
return err return err
} }
} }
return nil
}
func (gw *Gateway) mapChannelsToBridge(br *bridge.Bridge) {
for ID, channel := range gw.Channels {
if br.Account == channel.Account {
br.Channels[ID] = *channel
}
}
}
func (gw *Gateway) Start() error {
go gw.handleReceive() go gw.handleReceive()
return nil return nil
} }
@ -88,6 +103,15 @@ func (gw *Gateway) handleReceive() {
} }
} }
} }
if msg.Event == config.EVENT_REJOIN_CHANNELS {
for _, br := range gw.Bridges {
if msg.Account == br.Account {
br.Joined = make(map[string]bool)
br.JoinChannels()
}
}
continue
}
if !gw.ignoreMessage(&msg) { if !gw.ignoreMessage(&msg) {
msg.Timestamp = time.Now() msg.Timestamp = time.Now()
for _, br := range gw.Bridges { for _, br := range gw.Bridges {
@ -109,45 +133,58 @@ RECONNECT:
time.Sleep(time.Second * 60) time.Sleep(time.Second * 60)
goto RECONNECT goto RECONNECT
} }
br.Joined = make(map[string]bool)
br.JoinChannels() br.JoinChannels()
} }
func (gw *Gateway) mapChannels() error { func (gw *Gateway) mapChannels() error {
options := make(map[string]config.ChannelOptions) for _, br := range append(gw.MyConfig.Out, gw.MyConfig.InOut...) {
m := make(map[string][]string) if isApi(br.Account) {
for _, br := range gw.MyConfig.Out { br.Channel = "api"
m[br.Account] = append(m[br.Account], br.Channel) }
options[br.Account+br.Channel] = br.Options ID := br.Channel + br.Account
_, ok := gw.Channels[ID]
if !ok {
channel := &config.ChannelInfo{Name: br.Channel, Direction: "out", ID: ID, Options: br.Options, Account: br.Account,
GID: make(map[string]bool), SameChannel: make(map[string]bool)}
channel.GID[gw.Name] = true
channel.SameChannel[gw.Name] = br.SameChannel
gw.Channels[channel.ID] = channel
}
gw.Channels[ID].GID[gw.Name] = true
gw.Channels[ID].SameChannel[gw.Name] = br.SameChannel
} }
gw.ChannelsOut = m
m = nil for _, br := range append(gw.MyConfig.In, gw.MyConfig.InOut...) {
m = make(map[string][]string) if isApi(br.Account) {
for _, br := range gw.MyConfig.In { br.Channel = "api"
m[br.Account] = append(m[br.Account], br.Channel) }
options[br.Account+br.Channel] = br.Options ID := br.Channel + br.Account
_, ok := gw.Channels[ID]
if !ok {
channel := &config.ChannelInfo{Name: br.Channel, Direction: "in", ID: ID, Options: br.Options, Account: br.Account,
GID: make(map[string]bool), SameChannel: make(map[string]bool)}
channel.GID[gw.Name] = true
channel.SameChannel[gw.Name] = br.SameChannel
gw.Channels[channel.ID] = channel
}
gw.Channels[ID].GID[gw.Name] = true
gw.Channels[ID].SameChannel[gw.Name] = br.SameChannel
} }
gw.ChannelsIn = m
for _, br := range gw.MyConfig.InOut {
gw.ChannelsIn[br.Account] = append(gw.ChannelsIn[br.Account], br.Channel)
gw.ChannelsOut[br.Account] = append(gw.ChannelsOut[br.Account], br.Channel)
options[br.Account+br.Channel] = br.Options
}
gw.ChannelOptions = options
return nil return nil
} }
func (gw *Gateway) getDestChannel(msg *config.Message, dest string) []string { func (gw *Gateway) getDestChannel(msg *config.Message, dest bridge.Bridge) []config.ChannelInfo {
channels := gw.ChannelsIn[msg.Account] var channels []config.ChannelInfo
// broadcast to every out channel (irc QUIT) for _, channel := range gw.Channels {
if msg.Event == config.EVENT_JOIN_LEAVE && msg.Channel == "" { if _, ok := gw.Channels[getChannelID(*msg)]; !ok {
return gw.ChannelsOut[dest] continue
} }
for _, channel := range channels { if channel.Direction == "out" && channel.Account == dest.Account && gw.validGatewayDest(msg, channel) {
if channel == msg.Channel { channels = append(channels, *channel)
return gw.ChannelsOut[dest]
} }
} }
return []string{} return channels
} }
func (gw *Gateway) handleMessage(msg config.Message, dest *bridge.Bridge) { func (gw *Gateway) handleMessage(msg config.Message, dest *bridge.Bridge) {
@ -155,19 +192,21 @@ func (gw *Gateway) handleMessage(msg config.Message, dest *bridge.Bridge) {
if msg.Event == config.EVENT_JOIN_LEAVE && !gw.Bridges[dest.Account].Config.ShowJoinPart { if msg.Event == config.EVENT_JOIN_LEAVE && !gw.Bridges[dest.Account].Config.ShowJoinPart {
return return
} }
// broadcast to every out channel (irc QUIT)
if msg.Channel == "" && msg.Event != config.EVENT_JOIN_LEAVE {
log.Debug("empty channel")
return
}
originchannel := msg.Channel originchannel := msg.Channel
channels := gw.DestChannelFunc(&msg, dest.Account) origmsg := msg
for _, channel := range channels { for _, channel := range gw.DestChannelFunc(&msg, *dest) {
// do not send the message to the bridge we come from if also the channel is the same // do not send to ourself
if msg.Account == dest.Account && channel == originchannel { if channel.ID == getChannelID(origmsg) {
continue continue
} }
msg.Channel = channel log.Debugf("Sending %#v from %s (%s) to %s (%s)", msg, msg.Account, originchannel, dest.Account, channel.Name)
if msg.Channel == "" { msg.Channel = channel.Name
log.Debug("empty channel") gw.modifyAvatar(&msg, dest)
return
}
log.Debugf("Sending %#v from %s (%s) to %s (%s)", msg, msg.Account, originchannel, dest.Account, channel)
gw.modifyUsername(&msg, dest) gw.modifyUsername(&msg, dest)
// for api we need originchannel as channel // for api we need originchannel as channel
if dest.Protocol == "api" { if dest.Protocol == "api" {
@ -194,21 +233,6 @@ func (gw *Gateway) ignoreMessage(msg *config.Message) bool {
return false return false
} }
func (gw *Gateway) modifyMessage(msg *config.Message, dest *bridge.Bridge) {
val := reflect.ValueOf(gw.Config).Elem()
for i := 0; i < val.NumField(); i++ {
typeField := val.Type().Field(i)
// look for the protocol map (both lowercase)
if strings.ToLower(typeField.Name) == dest.Protocol {
// get the Protocol struct from the map
protoCfg := val.Field(i).MapIndex(reflect.ValueOf(dest.Name))
//config.SetNickFormat(msg, protoCfg.Interface().(config.Protocol))
val.Field(i).SetMapIndex(reflect.ValueOf(dest.Name), protoCfg)
break
}
}
}
func (gw *Gateway) modifyUsername(msg *config.Message, dest *bridge.Bridge) { func (gw *Gateway) modifyUsername(msg *config.Message, dest *bridge.Bridge) {
br := gw.Bridges[msg.Account] br := gw.Bridges[msg.Account]
msg.Protocol = br.Protocol msg.Protocol = br.Protocol
@ -216,8 +240,74 @@ func (gw *Gateway) modifyUsername(msg *config.Message, dest *bridge.Bridge) {
if nick == "" { if nick == "" {
nick = dest.Config.RemoteNickFormat nick = dest.Config.RemoteNickFormat
} }
if len(msg.Username) > 0 {
// fix utf-8 issue #193
i := 0
for index := range msg.Username {
if i == 1 {
i = index
break
}
i++
}
nick = strings.Replace(nick, "{NOPINGNICK}", msg.Username[:i]+""+msg.Username[i:], -1)
}
nick = strings.Replace(nick, "{NICK}", msg.Username, -1) nick = strings.Replace(nick, "{NICK}", msg.Username, -1)
nick = strings.Replace(nick, "{BRIDGE}", br.Name, -1) nick = strings.Replace(nick, "{BRIDGE}", br.Name, -1)
nick = strings.Replace(nick, "{PROTOCOL}", br.Protocol, -1) nick = strings.Replace(nick, "{PROTOCOL}", br.Protocol, -1)
msg.Username = nick msg.Username = nick
} }
func (gw *Gateway) modifyAvatar(msg *config.Message, dest *bridge.Bridge) {
iconurl := gw.Config.General.IconURL
if iconurl == "" {
iconurl = dest.Config.IconURL
}
iconurl = strings.Replace(iconurl, "{NICK}", msg.Username, -1)
if msg.Avatar == "" {
msg.Avatar = iconurl
}
}
func getChannelID(msg config.Message) string {
return msg.Channel + msg.Account
}
func (gw *Gateway) validGatewayDest(msg *config.Message, channel *config.ChannelInfo) bool {
GIDmap := gw.Channels[getChannelID(*msg)].GID
// gateway is specified in message (probably from api)
if msg.Gateway != "" {
return channel.GID[msg.Gateway]
}
// check if we are running a samechannelgateway.
// if it is and the channel name matches it's ok, otherwise we shouldn't use this channel.
for k, _ := range GIDmap {
if channel.SameChannel[k] == true {
if msg.Channel == channel.Name {
// add the gateway to our message
msg.Gateway = k
return true
} else {
return false
}
}
}
// check if we are in the correct gateway
for k, _ := range GIDmap {
if channel.GID[k] == true {
// add the gateway to our message
msg.Gateway = k
return true
}
}
return false
}
func isApi(account string) bool {
if strings.HasPrefix(account, "api.") {
return true
}
return false
}

View File

@ -2,48 +2,27 @@ package samechannelgateway
import ( import (
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/gateway"
) )
type SameChannelGateway struct { type SameChannelGateway struct {
*config.Config *config.Config
MyConfig *config.SameChannelGateway
Channels []string
Name string
} }
func New(cfg *config.Config, gatewayCfg *config.SameChannelGateway) *SameChannelGateway { func New(cfg *config.Config) *SameChannelGateway {
return &SameChannelGateway{ return &SameChannelGateway{Config: cfg}
MyConfig: gatewayCfg,
Channels: gatewayCfg.Channels,
Name: gatewayCfg.Name,
Config: cfg}
} }
func (sgw *SameChannelGateway) Start() error { func (sgw *SameChannelGateway) GetConfig() []config.Gateway {
gw := gateway.New(sgw.Config, &config.Gateway{Name: sgw.Name}) var gwconfigs []config.Gateway
gw.DestChannelFunc = sgw.getDestChannel cfg := sgw.Config
for _, account := range sgw.MyConfig.Accounts { for _, gw := range cfg.SameChannelGateway {
for _, channel := range sgw.Channels { gwconfig := config.Gateway{Name: gw.Name, Enable: gw.Enable}
br := config.Bridge{Account: account, Channel: channel} for _, account := range gw.Accounts {
gw.MyConfig.InOut = append(gw.MyConfig.InOut, br) for _, channel := range gw.Channels {
gwconfig.InOut = append(gwconfig.InOut, config.Bridge{Account: account, Channel: channel, SameChannel: true})
}
} }
gwconfigs = append(gwconfigs, gwconfig)
} }
return gw.Start() return gwconfigs
}
func (sgw *SameChannelGateway) validChannel(channel string) bool {
for _, c := range sgw.Channels {
if c == channel {
return true
}
}
return false
}
func (sgw *SameChannelGateway) getDestChannel(msg *config.Message, dest string) []string {
if sgw.validChannel(msg.Channel) {
return []string{msg.Channel}
}
return []string{}
} }

View File

@ -8,10 +8,11 @@ import (
"github.com/42wim/matterbridge/gateway/samechannel" "github.com/42wim/matterbridge/gateway/samechannel"
log "github.com/Sirupsen/logrus" log "github.com/Sirupsen/logrus"
"github.com/google/gops/agent" "github.com/google/gops/agent"
"strings"
) )
var ( var (
version = "0.10.2" version = "0.14.0"
githash string githash string
) )
@ -33,37 +34,31 @@ func main() {
fmt.Printf("version: %s %s\n", version, githash) fmt.Printf("version: %s %s\n", version, githash)
return return
} }
flag.Parse()
if *flagDebug { if *flagDebug {
log.Info("Enabling debug") log.Info("Enabling debug")
log.SetLevel(log.DebugLevel) log.SetLevel(log.DebugLevel)
} }
log.Printf("Running version %s %s", version, githash) log.Printf("Running version %s %s", version, githash)
cfg := config.NewConfig(*flagConfig) if strings.Contains(version, "-dev") {
for _, gw := range cfg.SameChannelGateway { log.Println("WARNING: THIS IS A DEVELOPMENT VERSION. Things may break.")
if !gw.Enable {
continue
}
log.Printf("Starting samechannel gateway %#v", gw.Name)
g := samechannelgateway.New(cfg, &gw)
err := g.Start()
if err != nil {
log.Fatalf("Starting gateway failed %#v", err)
}
log.Printf("Started samechannel gateway %#v", gw.Name)
} }
cfg := config.NewConfig(*flagConfig)
for _, gw := range cfg.Gateway { g := gateway.New(cfg)
sgw := samechannelgateway.New(cfg)
gwconfigs := sgw.GetConfig()
for _, gw := range append(gwconfigs, cfg.Gateway...) {
if !gw.Enable { if !gw.Enable {
continue continue
} }
log.Printf("Starting gateway %#v", gw.Name) err := g.AddConfig(&gw)
g := gateway.New(cfg, &gw)
err := g.Start()
if err != nil { if err != nil {
log.Fatalf("Starting gateway failed %#v", err) log.Fatalf("Starting gateway failed: %s", err)
} }
log.Printf("Started gateway %#v", gw.Name) }
err := g.Start()
if err != nil {
log.Fatalf("Starting gateway failed: %s", err)
} }
log.Printf("Gateway(s) started succesfully. Now relaying messages") log.Printf("Gateway(s) started succesfully. Now relaying messages")
select {} select {}

View File

@ -1,4 +1,5 @@
#This is configuration for matterbridge. #This is configuration for matterbridge.
#WARNING: as this file contains credentials, be sure to set correct file permissions
################################################################### ###################################################################
#IRC section #IRC section
################################################################### ###################################################################
@ -48,10 +49,15 @@ MessageDelay=1300
#Maximum amount of messages to hold in queue. If queue is full #Maximum amount of messages to hold in queue. If queue is full
#messages will be dropped. #messages will be dropped.
#<clipped> will be add to the message that fills the queue. #<message clipped> will be add to the message that fills the queue.
#OPTIONAL (default 30) #OPTIONAL (default 30)
MessageQueue=30 MessageQueue=30
#Maximum length of message sent to irc server. If it exceeds
#<message clipped> will be add to the message.
#OPTIONAL (default 400)
MessageLength=400
#Nicks you want to ignore. #Nicks you want to ignore.
#Messages from those users will not be sent to other bridges. #Messages from those users will not be sent to other bridges.
#OPTIONAL #OPTIONAL
@ -61,10 +67,12 @@ IgnoreNicks="ircspammer1 ircspammer2"
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username. #The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge #The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
#The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge #The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
#The string "{NOPINGNICK}" (case sensitive) will be replaced by the actual nick / username, but with a ZWSP inside the nick, so the irc user with the same nick won't get pinged. See https://github.com/42wim/matterbridge/issues/175 for more information
#OPTIONAL (default empty) #OPTIONAL (default empty)
RemoteNickFormat="[{PROTOCOL}] <{NICK}> " RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#Enable to show users joins/parts from other bridges (only from irc-bridge at the moment) #Enable to show users joins/parts from other bridges
#Only works hiding/show messages from irc and mattermost bridge for now
#OPTIONAL (default false) #OPTIONAL (default false)
ShowJoinPart=false ShowJoinPart=false
@ -114,7 +122,8 @@ IgnoreNicks="ircspammer1 ircspammer2"
#OPTIONAL (default empty) #OPTIONAL (default empty)
RemoteNickFormat="[{PROTOCOL}] <{NICK}> " RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#Enable to show users joins/parts from other bridges (only from irc-bridge at the moment) #Enable to show users joins/parts from other bridges
#Only works hiding/show messages from irc and mattermost bridge for now
#OPTIONAL (default false) #OPTIONAL (default false)
ShowJoinPart=false ShowJoinPart=false
@ -157,7 +166,8 @@ IgnoreNicks="spammer1 spammer2"
#OPTIONAL (default empty) #OPTIONAL (default empty)
RemoteNickFormat="[{PROTOCOL}/{BRIDGE}] <{NICK}> " RemoteNickFormat="[{PROTOCOL}/{BRIDGE}] <{NICK}> "
#Enable to show users joins/parts from other bridges (only from irc-bridge at the moment) #Enable to show users joins/parts from other bridges
#Only works hiding/show messages from irc and mattermost bridge for now
#OPTIONAL (default false) #OPTIONAL (default false)
ShowJoinPart=false ShowJoinPart=false
@ -197,9 +207,9 @@ IconURL="http://youricon.png"
#OPTIONAL #OPTIONAL
useAPI=false useAPI=false
#The mattermost hostname. #The mattermost hostname. (do not prefix it with http or https)
#REQUIRED (when useAPI=true) #REQUIRED (when useAPI=true)
Server="yourmattermostserver.domain" Server="yourmattermostserver.domain"
#Your team on mattermost. #Your team on mattermost.
#REQUIRED (when useAPI=true) #REQUIRED (when useAPI=true)
@ -238,6 +248,14 @@ NicksPerRow=4
#OPTIONAL (default false) #OPTIONAL (default false)
PrefixMessagesWithNick=false PrefixMessagesWithNick=false
#Disable sending of edits to other bridges
#OPTIONAL (default false)
EditDisable=false
#Message to be appended to every edited message
#OPTIONAL (default empty)
EditSuffix=" (edited)"
#Nicks you want to ignore. #Nicks you want to ignore.
#Messages from those users will not be sent to other bridges. #Messages from those users will not be sent to other bridges.
#OPTIONAL #OPTIONAL
@ -250,7 +268,8 @@ IgnoreNicks="ircspammer1 ircspammer2"
#OPTIONAL (default empty) #OPTIONAL (default empty)
RemoteNickFormat="[{PROTOCOL}] <{NICK}> " RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#Enable to show users joins/parts from other bridges (only from irc-bridge at the moment) #Enable to show users joins/parts from other bridges
#Only works hiding/show messages from irc and mattermost bridge for now
#OPTIONAL (default false) #OPTIONAL (default false)
ShowJoinPart=false ShowJoinPart=false
@ -282,7 +301,8 @@ IgnoreNicks="ircspammer1 ircspammer2"
#OPTIONAL (default empty) #OPTIONAL (default empty)
RemoteNickFormat="[{PROTOCOL}] <{NICK}> " RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#Enable to show users joins/parts from other bridges (only from irc-bridge at the moment) #Enable to show users joins/parts from other bridges
#Only works hiding/show messages from irc and mattermost bridge for now
#OPTIONAL (default false) #OPTIONAL (default false)
ShowJoinPart=false ShowJoinPart=false
@ -342,6 +362,14 @@ NickFormatter="plain"
#OPTIONAL (default 4) #OPTIONAL (default 4)
NicksPerRow=4 NicksPerRow=4
#Disable sending of edits to other bridges
#OPTIONAL (default false)
EditDisable=true
#Message to be appended to every edited message
#OPTIONAL (default empty)
EditSuffix=" (edited)"
#Whether to prefix messages from other bridges to mattermost with RemoteNickFormat #Whether to prefix messages from other bridges to mattermost with RemoteNickFormat
#Useful if username overrides for incoming webhooks isn't enabled on the #Useful if username overrides for incoming webhooks isn't enabled on the
#slack server. If you set PrefixMessagesWithNick to true, each message #slack server. If you set PrefixMessagesWithNick to true, each message
@ -362,7 +390,8 @@ IgnoreNicks="ircspammer1 ircspammer2"
#OPTIONAL (default empty) #OPTIONAL (default empty)
RemoteNickFormat="[{PROTOCOL}] <{NICK}> " RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#Enable to show users joins/parts from other bridges (only from irc-bridge at the moment) #Enable to show users joins/parts from other bridges
#Only works hiding/show messages from irc and mattermost bridge for now
#OPTIONAL (default false) #OPTIONAL (default false)
ShowJoinPart=false ShowJoinPart=false
@ -385,6 +414,14 @@ Token="Yourtokenhere"
#REQUIRED #REQUIRED
Server="yourservername" Server="yourservername"
#Disable sending of edits to other bridges
#OPTIONAL (default false)
EditDisable=false
#Message to be appended to every edited message
#OPTIONAL (default empty)
EditSuffix=" (edited)"
#Nicks you want to ignore. #Nicks you want to ignore.
#Messages from those users will not be sent to other bridges. #Messages from those users will not be sent to other bridges.
#OPTIONAL #OPTIONAL
@ -397,7 +434,8 @@ IgnoreNicks="ircspammer1 ircspammer2"
#OPTIONAL (default empty) #OPTIONAL (default empty)
RemoteNickFormat="[{PROTOCOL}] <{NICK}> " RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#Enable to show users joins/parts from other bridges (only from irc-bridge at the moment) #Enable to show users joins/parts from other bridges
#Only works hiding/show messages from irc and mattermost bridge for now
#OPTIONAL (default false) #OPTIONAL (default false)
ShowJoinPart=false ShowJoinPart=false
@ -420,6 +458,20 @@ Token="Yourtokenhere"
#See https://core.telegram.org/bots/api#html-style #See https://core.telegram.org/bots/api#html-style
MessageFormat="" MessageFormat=""
#If enabled use the "First Name" as username. If this is empty use the Username
#If disabled use the "Username" as username. If this is empty use the First Name
#If all names are empty, username will be "unknown"
#OPTIONAL (default false)
UseFirstName=false
#Disable sending of edits to other bridges
#OPTIONAL (default false)
EditDisable=false
#Message to be appended to every edited message
#OPTIONAL (default empty)
EditSuffix=" (edited)"
#Nicks you want to ignore. #Nicks you want to ignore.
#Messages from those users will not be sent to other bridges. #Messages from those users will not be sent to other bridges.
#OPTIONAL #OPTIONAL
@ -432,7 +484,8 @@ IgnoreNicks="spammer1 spammer2"
#OPTIONAL (default empty) #OPTIONAL (default empty)
RemoteNickFormat="[{PROTOCOL}] <{NICK}> " RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#Enable to show users joins/parts from other bridges (only from irc-bridge at the moment) #Enable to show users joins/parts from other bridges
#Only works hiding/show messages from irc and mattermost bridge for now
#OPTIONAL (default false) #OPTIONAL (default false)
ShowJoinPart=false ShowJoinPart=false
@ -489,7 +542,8 @@ IgnoreNicks="ircspammer1 ircspammer2"
#OPTIONAL (default empty) #OPTIONAL (default empty)
RemoteNickFormat="[{PROTOCOL}] <{NICK}> " RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#Enable to show users joins/parts from other bridges (only from irc-bridge at the moment) #Enable to show users joins/parts from other bridges
#Only works hiding/show messages from irc and mattermost bridge for now
#OPTIONAL (default false) #OPTIONAL (default false)
ShowJoinPart=false ShowJoinPart=false
@ -513,6 +567,11 @@ Server="https://matrix.org"
Login="yourlogin" Login="yourlogin"
Password="yourpass" Password="yourpass"
#Whether to send the homeserver suffix. eg ":matrix.org" in @username:matrix.org
#to other bridges, or only send "username".(true only sends username)
#OPTIONAL (default false)
NoHomeServerSuffix=false
#Whether to prefix messages from other bridges to matrix with the sender's nick. #Whether to prefix messages from other bridges to matrix with the sender's nick.
#Useful if username overrides for incoming webhooks isn't enabled on the #Useful if username overrides for incoming webhooks isn't enabled on the
#matrix server. If you set PrefixMessagesWithNick to true, each message #matrix server. If you set PrefixMessagesWithNick to true, each message
@ -532,7 +591,8 @@ IgnoreNicks="spammer1 spammer2"
#OPTIONAL (default empty) #OPTIONAL (default empty)
RemoteNickFormat="[{PROTOCOL}] <{NICK}> " RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#Enable to show users joins/parts from other bridges (only from irc-bridge at the moment) #Enable to show users joins/parts from other bridges
#Only works hiding/show messages from irc and mattermost bridge for now
#OPTIONAL (default false) #OPTIONAL (default false)
ShowJoinPart=false ShowJoinPart=false
@ -553,6 +613,11 @@ BindAddress="127.0.0.1:4242"
#Amount of messages to keep in memory #Amount of messages to keep in memory
Buffer=1000 Buffer=1000
#Bearer token used for authentication
#curl -H "Authorization: Bearer token" http://localhost:4242/api/messages
#OPTIONAL (no authorization if token is empty)
Token="mytoken"
#RemoteNickFormat defines how remote users appear on this bridge #RemoteNickFormat defines how remote users appear on this bridge
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username. #The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge #The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
@ -560,6 +625,8 @@ Buffer=1000
#OPTIONAL (default empty) #OPTIONAL (default empty)
RemoteNickFormat="{NICK}" RemoteNickFormat="{NICK}"
################################################################### ###################################################################
#General configuration #General configuration
################################################################### ###################################################################
@ -587,7 +654,7 @@ RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
# #
[[gateway]] [[gateway]]
#OPTIONAL (not used for now) #REQUIRED and UNIQUE
name="gateway1" name="gateway1"
#Enable enables this gateway #Enable enables this gateway
##OPTIONAL (default false) ##OPTIONAL (default false)
@ -603,7 +670,7 @@ enable=true
#channel to connect on that account #channel to connect on that account
#How to specify them for the different bridges: #How to specify them for the different bridges:
# #
#irc - #channel (# is required) #irc - #channel (# is required) (this needs to be lowercase!)
#mattermost - channel (the channel name as seen in the URL, not the displayname) #mattermost - channel (the channel name as seen in the URL, not the displayname)
#gitter - username/room #gitter - username/room
#xmpp - channel #xmpp - channel
@ -615,7 +682,8 @@ enable=true
# see (https://www.linkedin.com/pulse/telegram-bots-beginners-marco-frau) # see (https://www.linkedin.com/pulse/telegram-bots-beginners-marco-frau)
#hipchat - id_channel (see https://www.hipchat.com/account/xmpp for the correct channel) #hipchat - id_channel (see https://www.hipchat.com/account/xmpp for the correct channel)
#rocketchat - #channel (# is required) #rocketchat - #channel (# is required)
#matrix - #channel:server (eg #yourchannel:matrix.org) #matrix - #channel:server (eg #yourchannel:matrix.org)
# - encrypted rooms are not supported in matrix
#REQUIRED #REQUIRED
channel="#testing" channel="#testing"
@ -651,7 +719,9 @@ enable=true
#account="api.local" #account="api.local"
#channel="api" #channel="api"
#To send data to the api: #To send data to the api:
#curl -XPOST -H 'Content-Type: application/json' -d '{"text":"test","username":"randomuser"}' http://localhost:4242/api/message #curl -XPOST -H 'Content-Type: application/json' -d '{"text":"test","username":"randomuser","gateway":"gateway1"}' http://localhost:4242/api/message
#To read from the api:
#curl http://localhost:4242/api/messages
#If you want to do a 1:1 mapping between protocols where the channelnames are the same #If you want to do a 1:1 mapping between protocols where the channelnames are the same
#e.g. slack and mattermost you can use the samechannelgateway configuration #e.g. slack and mattermost you can use the samechannelgateway configuration
@ -659,6 +729,7 @@ enable=true
#channel testing on slack and vice versa. (and for the channel testing2 and testing3) #channel testing on slack and vice versa. (and for the channel testing2 and testing3)
[[samechannelgateway]] [[samechannelgateway]]
name="samechannel1"
enable = false enable = false
accounts = [ "mattermost.work","slack.hobby" ] accounts = [ "mattermost.work","slack.hobby" ]
channels = [ "testing","testing2","testing3"] channels = [ "testing","testing2","testing3"]

View File

@ -1,3 +1,4 @@
#WARNING: as this file contains credentials, be sure to set correct file permissions
[irc] [irc]
[irc.freenode] [irc.freenode]
Server="irc.freenode.net:6667" Server="irc.freenode.net:6667"
@ -6,7 +7,8 @@
[mattermost] [mattermost]
[mattermost.work] [mattermost.work]
useAPI=true useAPI=true
Server="yourmattermostserver.domain" #do not prefix it wit http:// or https://
Server="yourmattermostserver.domain"
Team="yourteam" Team="yourteam"
Login="yourlogin" Login="yourlogin"
Password="yourpass" Password="yourpass"
@ -15,22 +17,14 @@
[[gateway]] [[gateway]]
name="gateway1" name="gateway1"
enable=true enable=true
[[gateway.in]] [[gateway.inout]]
account="irc.freenode" account="irc.freenode"
channel="#testing" channel="#testing"
[[gateway.out]] [[gateway.inout]]
account="irc.freenode"
channel="#testing"
[[gateway.in]]
account="mattermost.work" account="mattermost.work"
channel="off-topic" channel="off-topic"
[[gateway.out]]
account="mattermost.work"
channel="off-topic"
#simpler config possible since v0.10.2 #simpler config possible since v0.10.2
#[[gateway]] #[[gateway]]
#name="gateway2" #name="gateway2"

View File

@ -4,9 +4,11 @@ import (
"crypto/tls" "crypto/tls"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"net/http" "net/http"
"net/http/cookiejar" "net/http/cookiejar"
"net/url" "net/url"
"strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -34,6 +36,7 @@ type Message struct {
Channel string Channel string
Username string Username string
Text string Text string
Type string
} }
type Team struct { type Team struct {
@ -47,19 +50,20 @@ type Team struct {
type MMClient struct { type MMClient struct {
sync.RWMutex sync.RWMutex
*Credentials *Credentials
Team *Team Team *Team
OtherTeams []*Team OtherTeams []*Team
Client *model.Client Client *model.Client
User *model.User User *model.User
Users map[string]*model.User Users map[string]*model.User
MessageChan chan *Message MessageChan chan *Message
log *log.Entry log *log.Entry
WsClient *websocket.Conn WsClient *websocket.Conn
WsQuit bool WsQuit bool
WsAway bool WsAway bool
WsConnected bool WsConnected bool
WsSequence int64 WsSequence int64
WsPingChan chan *model.WebSocketResponse WsPingChan chan *model.WebSocketResponse
ServerVersion string
} }
func New(login, pass, team, server string) *MMClient { func New(login, pass, team, server string) *MMClient {
@ -102,8 +106,27 @@ func (m *MMClient) Login() error {
} }
// login to mattermost // login to mattermost
m.Client = model.NewClient(uriScheme + m.Credentials.Server) m.Client = model.NewClient(uriScheme + m.Credentials.Server)
m.Client.HttpClient.Transport = &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: m.SkipTLSVerify}} m.Client.HttpClient.Transport = &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: m.SkipTLSVerify}, Proxy: http.ProxyFromEnvironment}
m.Client.HttpClient.Timeout = time.Second * 10 m.Client.HttpClient.Timeout = time.Second * 10
for {
d := b.Duration()
// bogus call to get the serverversion
m.Client.GetClientProperties()
if firstConnection && !supportedVersion(m.Client.ServerVersion) {
return fmt.Errorf("unsupported mattermost version: %s", m.Client.ServerVersion)
}
m.ServerVersion = m.Client.ServerVersion
if m.ServerVersion == "" {
m.log.Debugf("Server not up yet, reconnecting in %s", d)
time.Sleep(d)
} else {
m.log.Infof("Found version %s", m.ServerVersion)
break
}
}
b.Reset()
var myinfo *model.Result var myinfo *model.Result
var appErr *model.AppError var appErr *model.AppError
var logmsg = "trying login" var logmsg = "trying login"
@ -177,6 +200,7 @@ func (m *MMClient) Login() error {
} }
b.Reset() b.Reset()
m.log.Debug("WsClient: connected")
m.WsSequence = 1 m.WsSequence = 1
m.WsPingChan = make(chan *model.WebSocketResponse) m.WsPingChan = make(chan *model.WebSocketResponse)
// only start to parse WS messages when login is completely done // only start to parse WS messages when login is completely done
@ -238,7 +262,7 @@ func (m *MMClient) WsReceiver() {
func (m *MMClient) parseMessage(rmsg *Message) { func (m *MMClient) parseMessage(rmsg *Message) {
switch rmsg.Raw.Event { switch rmsg.Raw.Event {
case model.WEBSOCKET_EVENT_POSTED: case model.WEBSOCKET_EVENT_POSTED, model.WEBSOCKET_EVENT_POST_EDITED:
m.parseActionPost(rmsg) m.parseActionPost(rmsg)
/* /*
case model.ACTION_USER_REMOVED: case model.ACTION_USER_REMOVED:
@ -264,9 +288,19 @@ func (m *MMClient) parseActionPost(rmsg *Message) {
if m.GetUser(data.UserId) == nil { if m.GetUser(data.UserId) == nil {
m.UpdateUsers() m.UpdateUsers()
} }
rmsg.Username = m.GetUser(data.UserId).Username rmsg.Username = m.GetUserName(data.UserId)
rmsg.Channel = m.GetChannelName(data.ChannelId) rmsg.Channel = m.GetChannelName(data.ChannelId)
rmsg.Team = m.GetTeamName(rmsg.Raw.Data["team_id"].(string)) rmsg.Type = data.Type
teamid, _ := rmsg.Raw.Data["team_id"].(string)
// edit messsages have no team_id for some reason
if teamid == "" {
// we can find the team_id from the channelid
teamid = m.GetChannelTeamId(data.ChannelId)
rmsg.Raw.Data["team_id"] = teamid
}
if teamid != "" {
rmsg.Team = m.GetTeamName(teamid)
}
// direct message // direct message
if rmsg.Raw.Data["channel_type"] == "D" { if rmsg.Raw.Data["channel_type"] == "D" {
rmsg.Channel = m.GetUser(data.UserId).Username rmsg.Channel = m.GetUser(data.UserId).Username
@ -292,7 +326,12 @@ func (m *MMClient) UpdateChannels() error {
if err != nil { if err != nil {
return errors.New(err.DetailedError) return errors.New(err.DetailedError)
} }
mmchannels2, err := m.Client.GetMoreChannels("") var mmchannels2 *model.Result
if m.mmVersion() >= 3.08 {
mmchannels2, err = m.Client.GetMoreChannelsPage(0, 5000)
} else {
mmchannels2, err = m.Client.GetMoreChannels("")
}
if err != nil { if err != nil {
return errors.New(err.DetailedError) return errors.New(err.DetailedError)
} }
@ -334,6 +373,19 @@ func (m *MMClient) GetChannelId(name string, teamId string) string {
return "" return ""
} }
func (m *MMClient) GetChannelTeamId(id string) string {
m.RLock()
defer m.RUnlock()
for _, t := range append(m.OtherTeams, m.Team) {
for _, channel := range append(*t.Channels, *t.MoreChannels...) {
if channel.Id == id {
return channel.TeamId
}
}
}
return ""
}
func (m *MMClient) GetChannelHeader(channelId string) string { func (m *MMClient) GetChannelHeader(channelId string) string {
m.RLock() m.RLock()
defer m.RUnlock() defer m.RUnlock()
@ -427,6 +479,14 @@ func (m *MMClient) UpdateChannelHeader(channelId string, header string) {
func (m *MMClient) UpdateLastViewed(channelId string) { func (m *MMClient) UpdateLastViewed(channelId string) {
m.log.Debugf("posting lastview %#v", channelId) m.log.Debugf("posting lastview %#v", channelId)
if m.mmVersion() >= 3.08 {
view := model.ChannelView{ChannelId: channelId}
res, _ := m.Client.ViewChannel(view)
if res == false {
m.log.Errorf("ChannelView update for %s failed", channelId)
}
return
}
_, err := m.Client.UpdateLastViewedAt(channelId, true) _, err := m.Client.UpdateLastViewedAt(channelId, true)
if err != nil { if err != nil {
m.log.Error(err) m.log.Error(err)
@ -573,6 +633,14 @@ func (m *MMClient) GetUser(userId string) *model.User {
return m.Users[userId] return m.Users[userId]
} }
func (m *MMClient) GetUserName(userId string) string {
user := m.GetUser(userId)
if user != nil {
return user.Username
}
return ""
}
func (m *MMClient) GetStatus(userId string) string { func (m *MMClient) GetStatus(userId string) string {
res, err := m.Client.GetStatuses() res, err := m.Client.GetStatuses()
if err != nil { if err != nil {
@ -628,6 +696,7 @@ func (m *MMClient) StatusLoop() {
m.Logout() m.Logout()
m.WsQuit = false m.WsQuit = false
m.Login() m.Login()
go m.WsReceiver()
} }
} }
time.Sleep(time.Second * 60) time.Sleep(time.Second * 60)
@ -659,7 +728,11 @@ func (m *MMClient) initUser() error {
return errors.New(err.DetailedError) return errors.New(err.DetailedError)
} }
t.Channels = mmchannels.Data.(*model.ChannelList) t.Channels = mmchannels.Data.(*model.ChannelList)
mmchannels, err = m.Client.GetMoreChannels("") if m.mmVersion() >= 3.08 {
mmchannels, err = m.Client.GetMoreChannelsPage(0, 5000)
} else {
mmchannels, err = m.Client.GetMoreChannels("")
}
if err != nil { if err != nil {
return errors.New(err.DetailedError) return errors.New(err.DetailedError)
} }
@ -687,3 +760,23 @@ func (m *MMClient) sendWSRequest(action string, data map[string]interface{}) err
m.WsClient.WriteJSON(req) m.WsClient.WriteJSON(req)
return nil return nil
} }
func (m *MMClient) mmVersion() float64 {
v, _ := strconv.ParseFloat(string(m.ServerVersion[0:2])+"0"+string(m.ServerVersion[2]), 64)
if string(m.ServerVersion[4]) == "." {
v, _ = strconv.ParseFloat(m.ServerVersion[0:4], 64)
}
return v
}
func supportedVersion(version string) bool {
if strings.HasPrefix(version, "3.5.0") ||
strings.HasPrefix(version, "3.6.0") ||
strings.HasPrefix(version, "3.7.0") ||
strings.HasPrefix(version, "3.8.0") ||
strings.HasPrefix(version, "3.9.0") ||
strings.HasPrefix(version, "3.10.0") {
return true
}
return false
}

View File

@ -30,16 +30,16 @@ type (
// Bind implements the `Binder#Bind` function. // Bind implements the `Binder#Bind` function.
func (b *DefaultBinder) Bind(i interface{}, c Context) (err error) { func (b *DefaultBinder) Bind(i interface{}, c Context) (err error) {
req := c.Request() req := c.Request()
if req.Method == GET {
if err = b.bindData(i, c.QueryParams(), "query"); err != nil {
return NewHTTPError(http.StatusBadRequest, err.Error())
}
return
}
ctype := req.Header.Get(HeaderContentType)
if req.ContentLength == 0 { if req.ContentLength == 0 {
if req.Method == GET || req.Method == DELETE {
if err = b.bindData(i, c.QueryParams(), "query"); err != nil {
return NewHTTPError(http.StatusBadRequest, err.Error())
}
return
}
return NewHTTPError(http.StatusBadRequest, "Request body can't be empty") return NewHTTPError(http.StatusBadRequest, "Request body can't be empty")
} }
ctype := req.Header.Get(HeaderContentType)
switch { switch {
case strings.HasPrefix(ctype, MIMEApplicationJSON): case strings.HasPrefix(ctype, MIMEApplicationJSON):
if err = json.NewDecoder(req.Body).Decode(i); err != nil { if err = json.NewDecoder(req.Body).Decode(i); err != nil {
@ -51,7 +51,7 @@ func (b *DefaultBinder) Bind(i interface{}, c Context) (err error) {
return NewHTTPError(http.StatusBadRequest, err.Error()) return NewHTTPError(http.StatusBadRequest, err.Error())
} }
} }
case strings.HasPrefix(ctype, MIMEApplicationXML): case strings.HasPrefix(ctype, MIMEApplicationXML), strings.HasPrefix(ctype, MIMETextXML):
if err = xml.NewDecoder(req.Body).Decode(i); err != nil { if err = xml.NewDecoder(req.Body).Decode(i); err != nil {
if ute, ok := err.(*xml.UnsupportedTypeError); ok { if ute, ok := err.(*xml.UnsupportedTypeError); ok {
return NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Unsupported type error: type=%v, error=%v", ute.Type, ute.Error())) return NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Unsupported type error: type=%v, error=%v", ute.Type, ute.Error()))
@ -142,6 +142,8 @@ func setWithProperType(valueKind reflect.Kind, val string, structField reflect.V
} }
switch valueKind { switch valueKind {
case reflect.Ptr:
return setWithProperType(structField.Elem().Kind(), val, structField.Elem())
case reflect.Int: case reflect.Int:
return setIntField(val, 0, structField) return setIntField(val, 0, structField)
case reflect.Int8: case reflect.Int8:

View File

@ -31,6 +31,9 @@ type (
// IsTLS returns true if HTTP connection is TLS otherwise false. // IsTLS returns true if HTTP connection is TLS otherwise false.
IsTLS() bool IsTLS() bool
// IsWebSocket returns true if HTTP connection is WebSocket otherwise false.
IsWebSocket() bool
// Scheme returns the HTTP protocol scheme, `http` or `https`. // Scheme returns the HTTP protocol scheme, `http` or `https`.
Scheme() string Scheme() string
@ -219,19 +222,36 @@ func (c *context) IsTLS() bool {
return c.request.TLS != nil return c.request.TLS != nil
} }
func (c *context) IsWebSocket() bool {
upgrade := c.request.Header.Get(HeaderUpgrade)
return upgrade == "websocket" || upgrade == "Websocket"
}
func (c *context) Scheme() string { func (c *context) Scheme() string {
// Can't use `r.Request.URL.Scheme` // Can't use `r.Request.URL.Scheme`
// See: https://groups.google.com/forum/#!topic/golang-nuts/pMUkBlQBDF0 // See: https://groups.google.com/forum/#!topic/golang-nuts/pMUkBlQBDF0
if c.IsTLS() { if c.IsTLS() {
return "https" return "https"
} }
if scheme := c.request.Header.Get(HeaderXForwardedProto); scheme != "" {
return scheme
}
if scheme := c.request.Header.Get(HeaderXForwardedProtocol); scheme != "" {
return scheme
}
if ssl := c.request.Header.Get(HeaderXForwardedSsl); ssl == "on" {
return "https"
}
if scheme := c.request.Header.Get(HeaderXUrlScheme); scheme != "" {
return scheme
}
return "http" return "http"
} }
func (c *context) RealIP() string { func (c *context) RealIP() string {
ra := c.request.RemoteAddr ra := c.request.RemoteAddr
if ip := c.request.Header.Get(HeaderXForwardedFor); ip != "" { if ip := c.request.Header.Get(HeaderXForwardedFor); ip != "" {
ra = ip ra = strings.Split(ip, ", ")[0]
} else if ip := c.request.Header.Get(HeaderXRealIP); ip != "" { } else if ip := c.request.Header.Get(HeaderXRealIP); ip != "" {
ra = ip ra = ip
} else { } else {
@ -275,7 +295,7 @@ func (c *context) SetParamNames(names ...string) {
} }
func (c *context) ParamValues() []string { func (c *context) ParamValues() []string {
return c.pvalues return c.pvalues[:len(c.pnames)]
} }
func (c *context) SetParamValues(values ...string) { func (c *context) SetParamValues(values ...string) {
@ -385,7 +405,8 @@ func (c *context) String(code int, s string) (err error) {
} }
func (c *context) JSON(code int, i interface{}) (err error) { func (c *context) JSON(code int, i interface{}) (err error) {
if c.echo.Debug { _, pretty := c.QueryParams()["pretty"]
if c.echo.Debug || pretty {
return c.JSONPretty(code, i, " ") return c.JSONPretty(code, i, " ")
} }
b, err := json.Marshal(i) b, err := json.Marshal(i)
@ -429,7 +450,8 @@ func (c *context) JSONPBlob(code int, callback string, b []byte) (err error) {
} }
func (c *context) XML(code int, i interface{}) (err error) { func (c *context) XML(code int, i interface{}) (err error) {
if c.echo.Debug { _, pretty := c.QueryParams()["pretty"]
if c.echo.Debug || pretty {
return c.XMLPretty(code, i, " ") return c.XMLPretty(code, i, " ")
} }
b, err := xml.Marshal(i) b, err := xml.Marshal(i)
@ -471,7 +493,12 @@ func (c *context) Stream(code int, contentType string, r io.Reader) (err error)
return return
} }
func (c *context) File(file string) error { func (c *context) File(file string) (err error) {
file, err = url.QueryUnescape(file) // Issue #839
if err != nil {
return
}
f, err := os.Open(file) f, err := os.Open(file)
if err != nil { if err != nil {
return ErrNotFound return ErrNotFound
@ -487,11 +514,11 @@ func (c *context) File(file string) error {
} }
defer f.Close() defer f.Close()
if fi, err = f.Stat(); err != nil { if fi, err = f.Stat(); err != nil {
return err return
} }
} }
http.ServeContent(c.Response(), c.Request(), fi.Name(), fi.ModTime(), f) http.ServeContent(c.Response(), c.Request(), fi.Name(), fi.ModTime(), f)
return nil return
} }
func (c *context) Attachment(file, name string) (err error) { func (c *context) Attachment(file, name string) (err error) {
@ -514,7 +541,7 @@ func (c *context) NoContent(code int) error {
} }
func (c *context) Redirect(code int, url string) error { func (c *context) Redirect(code int, url string) error {
if code < http.StatusMultipleChoices || code > http.StatusTemporaryRedirect { if code < 300 || code > 308 {
return ErrInvalidRedirectCode return ErrInvalidRedirectCode
} }
c.response.Header().Set(HeaderLocation, url) c.response.Header().Set(HeaderLocation, url)
@ -548,4 +575,8 @@ func (c *context) Reset(r *http.Request, w http.ResponseWriter) {
c.query = nil c.query = nil
c.handler = NotFoundHandler c.handler = NotFoundHandler
c.store = nil c.store = nil
c.path = ""
c.pnames = nil
// NOTE: Don't reset because it has to have length c.echo.maxParam at all times
// c.pvalues = nil
} }

View File

@ -1,26 +0,0 @@
package main
import (
"net/http"
"golang.org/x/crypto/acme/autocert"
"github.com/labstack/echo"
"github.com/labstack/echo/middleware"
)
func main() {
e := echo.New()
// e.AutoTLSManager.HostPolicy = autocert.HostWhitelist("<DOMAIN>")
// Cache certificates
e.AutoTLSManager.Cache = autocert.DirCache("/var/www/.cache")
e.Use(middleware.Recover())
e.Use(middleware.Logger())
e.GET("/", func(c echo.Context) error {
return c.HTML(http.StatusOK, `
<h1>Welcome to Echo!</h1>
<h3>TLS certificates automatically installed from Let's Encrypt :)</h3>
`)
})
e.Logger.Fatal(e.StartAutoTLS(":443"))
}

View File

@ -1,38 +0,0 @@
package main
import (
"net/http"
"github.com/labstack/echo"
"github.com/labstack/echo/middleware"
)
var (
users = []string{"Joe", "Veer", "Zion"}
)
func getUsers(c echo.Context) error {
return c.JSON(http.StatusOK, users)
}
func main() {
e := echo.New()
e.Use(middleware.Logger())
e.Use(middleware.Recover())
// CORS default
// Allows requests from any origin wth GET, HEAD, PUT, POST or DELETE method.
// e.Use(middleware.CORS())
// CORS restricted
// Allows requests from any `https://labstack.com` or `https://labstack.net` origin
// wth GET, PUT, POST or DELETE method.
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: []string{"https://labstack.com", "https://labstack.net"},
AllowMethods: []string{echo.GET, echo.PUT, echo.POST, echo.DELETE},
}))
e.GET("/api/users", getUsers)
e.Logger.Fatal(e.Start(":1323"))
}

View File

@ -1,75 +0,0 @@
package main
import (
"net/http"
"strconv"
"github.com/labstack/echo"
"github.com/labstack/echo/middleware"
)
type (
user struct {
ID int `json:"id"`
Name string `json:"name"`
}
)
var (
users = map[int]*user{}
seq = 1
)
//----------
// Handlers
//----------
func createUser(c echo.Context) error {
u := &user{
ID: seq,
}
if err := c.Bind(u); err != nil {
return err
}
users[u.ID] = u
seq++
return c.JSON(http.StatusCreated, u)
}
func getUser(c echo.Context) error {
id, _ := strconv.Atoi(c.Param("id"))
return c.JSON(http.StatusOK, users[id])
}
func updateUser(c echo.Context) error {
u := new(user)
if err := c.Bind(u); err != nil {
return err
}
id, _ := strconv.Atoi(c.Param("id"))
users[id].Name = u.Name
return c.JSON(http.StatusOK, users[id])
}
func deleteUser(c echo.Context) error {
id, _ := strconv.Atoi(c.Param("id"))
delete(users, id)
return c.NoContent(http.StatusNoContent)
}
func main() {
e := echo.New()
// Middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())
// Routes
e.POST("/users", createUser)
e.GET("/users/:id", getUser)
e.PUT("/users/:id", updateUser)
e.DELETE("/users/:id", deleteUser)
// Start server
e.Logger.Fatal(e.Start(":1323"))
}

View File

@ -1,21 +0,0 @@
package main
import (
"net/http"
rice "github.com/GeertJohan/go.rice"
"github.com/labstack/echo"
)
func main() {
e := echo.New()
// the file server for rice. "app" is the folder where the files come from.
assetHandler := http.FileServer(rice.MustFindBox("app").HTTPBox())
// serves the index.html from rice
e.GET("/", echo.WrapHandler(assetHandler))
// servers other static files
e.GET("/static/*", echo.WrapHandler(http.StripPrefix("/static/", assetHandler)))
e.Logger.Fatal(e.Start(":1323"))
}

View File

@ -1,65 +0,0 @@
package main
import (
"fmt"
"io"
"os"
"net/http"
"github.com/labstack/echo"
"github.com/labstack/echo/middleware"
)
func upload(c echo.Context) error {
// Read form fields
name := c.FormValue("name")
email := c.FormValue("email")
//------------
// Read files
//------------
// Multipart form
form, err := c.MultipartForm()
if err != nil {
return err
}
files := form.File["files"]
for _, file := range files {
// Source
src, err := file.Open()
if err != nil {
return err
}
defer src.Close()
// Destination
dst, err := os.Create(file.Filename)
if err != nil {
return err
}
defer dst.Close()
// Copy
if _, err = io.Copy(dst, src); err != nil {
return err
}
}
return c.HTML(http.StatusOK, fmt.Sprintf("<p>Uploaded successfully %d files with fields name=%s and email=%s.</p>", len(files), name, email))
}
func main() {
e := echo.New()
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.Static("/", "public")
e.POST("/upload", upload)
e.Logger.Fatal(e.Start(":1323"))
}

View File

@ -1,59 +0,0 @@
package main
import (
"fmt"
"io"
"os"
"net/http"
"github.com/labstack/echo"
"github.com/labstack/echo/middleware"
)
func upload(c echo.Context) error {
// Read form fields
name := c.FormValue("name")
email := c.FormValue("email")
//-----------
// Read file
//-----------
// Source
file, err := c.FormFile("file")
if err != nil {
return err
}
src, err := file.Open()
if err != nil {
return err
}
defer src.Close()
// Destination
dst, err := os.Create(file.Filename)
if err != nil {
return err
}
defer dst.Close()
// Copy
if _, err = io.Copy(dst, src); err != nil {
return err
}
return c.HTML(http.StatusOK, fmt.Sprintf("<p>File %s uploaded successfully with fields name=%s and email=%s.</p>", file.Filename, name, email))
}
func main() {
e := echo.New()
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.Static("/", "public")
e.POST("/upload", upload)
e.Logger.Fatal(e.Start(":1323"))
}

View File

@ -1,17 +0,0 @@
// +build appengine
package main
import (
"net/http"
"github.com/labstack/echo"
)
func createMux() *echo.Echo {
e := echo.New()
// note: we don't need to provide the middleware or static handlers, that's taken care of by the platform
// app engine has it's own "main" wrapper - we just need to hook echo into the default handler
http.Handle("/", e)
return e
}

View File

@ -1,25 +0,0 @@
// +build appenginevm
package main
import (
"net/http"
"github.com/labstack/echo"
"google.golang.org/appengine"
)
func createMux() *echo.Echo {
e := echo.New()
// note: we don't need to provide the middleware or static handlers
// for the appengine vm version - that's taken care of by the platform
return e
}
func main() {
// the appengine package provides a convenient method to handle the health-check requests
// and also run the app on the correct port. We just need to add Echo to the default handler
e := echo.New(":8080")
http.Handle("/", e)
appengine.Main()
}

View File

@ -1,24 +0,0 @@
// +build !appengine,!appenginevm
package main
import (
"github.com/labstack/echo"
"github.com/labstack/echo/middleware"
)
func createMux() *echo.Echo {
e := echo.New()
e.Use(middleware.Recover())
e.Use(middleware.Logger())
e.Use(middleware.Gzip())
e.Static("/", "public")
return e
}
func main() {
e.Logger.Fatal(e.Start(":8080"))
}

View File

@ -1,4 +0,0 @@
package main
// reference our echo instance and create it early
var e = createMux()

View File

@ -1,54 +0,0 @@
package main
import (
"net/http"
"github.com/labstack/echo"
"github.com/labstack/echo/middleware"
)
type (
user struct {
ID string `json:"id"`
Name string `json:"name"`
}
)
var (
users map[string]user
)
func init() {
users = map[string]user{
"1": user{
ID: "1",
Name: "Wreck-It Ralph",
},
}
// hook into the echo instance to create an endpoint group
// and add specific middleware to it plus handlers
g := e.Group("/users")
g.Use(middleware.CORS())
g.POST("", createUser)
g.GET("", getUsers)
g.GET("/:id", getUser)
}
func createUser(c echo.Context) error {
u := new(user)
if err := c.Bind(u); err != nil {
return err
}
users[u.ID] = *u
return c.JSON(http.StatusCreated, u)
}
func getUsers(c echo.Context) error {
return c.JSON(http.StatusOK, users)
}
func getUser(c echo.Context) error {
return c.JSON(http.StatusOK, users[c.Param("id")])
}

View File

@ -1,31 +0,0 @@
package main
import (
"html/template"
"io"
"net/http"
"github.com/labstack/echo"
)
type (
Template struct {
templates *template.Template
}
)
func init() {
t := &Template{
templates: template.Must(template.ParseFiles("templates/welcome.html")),
}
e.Renderer = t
e.GET("/welcome", welcome)
}
func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
return t.templates.ExecuteTemplate(w, name, data)
}
func welcome(c echo.Context) error {
return c.Render(http.StatusOK, "welcome", "Joe")
}

View File

@ -1,20 +0,0 @@
package main
import (
"net/http"
"github.com/facebookgo/grace/gracehttp"
"github.com/labstack/echo"
)
func main() {
// Setup
e := echo.New()
e.GET("/", func(c echo.Context) error {
return c.String(http.StatusOK, "Six sick bricks tick")
})
e.Server.Addr = ":1323"
// Serve it like a boss
e.Logger.Fatal(gracehttp.Serve(e.Server))
}

View File

@ -1,21 +0,0 @@
package main
import (
"net/http"
"time"
"github.com/labstack/echo"
"github.com/tylerb/graceful"
)
func main() {
// Setup
e := echo.New()
e.GET("/", func(c echo.Context) error {
return c.String(http.StatusOK, "Sue sews rose on slow joe crows nose")
})
e.Server.Addr = ":1323"
// Serve it like a boss
graceful.ListenAndServe(e.Server, 5*time.Second)
}

View File

@ -1,25 +0,0 @@
package main
import (
"net/http"
"github.com/labstack/echo"
"github.com/labstack/echo/middleware"
)
func main() {
// Echo instance
e := echo.New()
// Middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())
// Route => handler
e.GET("/", func(c echo.Context) error {
return c.String(http.StatusOK, "Hello, World!\n")
})
// Start server
e.Logger.Fatal(e.Start(":1323"))
}

View File

@ -1,42 +0,0 @@
package main
import (
"fmt"
"net/http"
"time"
"github.com/labstack/echo"
)
func request(c echo.Context) error {
req := c.Request()
format := "<pre><strong>Request Information</strong>\n\n<code>Protocol: %s\nHost: %s\nRemote Address: %s\nMethod: %s\nPath: %s\n</code></pre>"
return c.HTML(http.StatusOK, fmt.Sprintf(format, req.Proto, req.Host, req.RemoteAddr, req.Method, req.URL.Path))
}
func stream(c echo.Context) error {
res := c.Response()
gone := res.CloseNotify()
res.Header().Set(echo.HeaderContentType, echo.MIMETextHTMLCharsetUTF8)
res.WriteHeader(http.StatusOK)
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
fmt.Fprint(res, "<pre><strong>Clock Stream</strong>\n\n<code>")
for {
fmt.Fprintf(res, "%v\n", time.Now())
res.Flush()
select {
case <-ticker.C:
case <-gone:
break
}
}
}
func main() {
e := echo.New()
e.GET("/request", request)
e.GET("/stream", stream)
e.Logger.Fatal(e.StartTLS(":1323", "cert.pem", "key.pem"))
}

View File

@ -1,35 +0,0 @@
package main
import (
"math/rand"
"net/http"
"time"
"github.com/labstack/echo"
"github.com/labstack/echo/middleware"
)
func main() {
e := echo.New()
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.Static("/", "public")
// JSONP
e.GET("/jsonp", func(c echo.Context) error {
callback := c.QueryParam("callback")
var content struct {
Response string `json:"response"`
Timestamp time.Time `json:"timestamp"`
Random int `json:"random"`
}
content.Response = "Sent via JSONP"
content.Timestamp = time.Now().UTC()
content.Random = rand.Intn(1000)
return c.JSONP(http.StatusOK, callback, &content)
})
// Start server
e.Logger.Fatal(e.Start(":1323"))
}

View File

@ -1,86 +0,0 @@
package main
import (
"net/http"
"time"
jwt "github.com/dgrijalva/jwt-go"
"github.com/labstack/echo"
"github.com/labstack/echo/middleware"
)
// jwtCustomClaims are custom claims extending default ones.
type jwtCustomClaims struct {
Name string `json:"name"`
Admin bool `json:"admin"`
jwt.StandardClaims
}
func login(c echo.Context) error {
username := c.FormValue("username")
password := c.FormValue("password")
if username == "jon" && password == "shhh!" {
// Set custom claims
claims := &jwtCustomClaims{
"Jon Snow",
true,
jwt.StandardClaims{
ExpiresAt: time.Now().Add(time.Hour * 72).Unix(),
},
}
// Create token with claims
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
// Generate encoded token and send it as response.
t, err := token.SignedString([]byte("secret"))
if err != nil {
return err
}
return c.JSON(http.StatusOK, echo.Map{
"token": t,
})
}
return echo.ErrUnauthorized
}
func accessible(c echo.Context) error {
return c.String(http.StatusOK, "Accessible")
}
func restricted(c echo.Context) error {
user := c.Get("user").(*jwt.Token)
claims := user.Claims.(*jwtCustomClaims)
name := claims.Name
return c.String(http.StatusOK, "Welcome "+name+"!")
}
func main() {
e := echo.New()
// Middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())
// Login route
e.POST("/login", login)
// Unauthenticated route
e.GET("/", accessible)
// Restricted group
r := e.Group("/restricted")
// Configure middleware with the custom claims type
config := middleware.JWTConfig{
Claims: &jwtCustomClaims{},
SigningKey: []byte("secret"),
}
r.Use(middleware.JWTWithConfig(config))
r.GET("", restricted)
e.Logger.Fatal(e.Start(":1323"))
}

View File

@ -1,69 +0,0 @@
package main
import (
"net/http"
"time"
jwt "github.com/dgrijalva/jwt-go"
"github.com/labstack/echo"
"github.com/labstack/echo/middleware"
)
func login(c echo.Context) error {
username := c.FormValue("username")
password := c.FormValue("password")
if username == "jon" && password == "shhh!" {
// Create token
token := jwt.New(jwt.SigningMethodHS256)
// Set claims
claims := token.Claims.(jwt.MapClaims)
claims["name"] = "Jon Snow"
claims["admin"] = true
claims["exp"] = time.Now().Add(time.Hour * 72).Unix()
// Generate encoded token and send it as response.
t, err := token.SignedString([]byte("secret"))
if err != nil {
return err
}
return c.JSON(http.StatusOK, map[string]string{
"token": t,
})
}
return echo.ErrUnauthorized
}
func accessible(c echo.Context) error {
return c.String(http.StatusOK, "Accessible")
}
func restricted(c echo.Context) error {
user := c.Get("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
name := claims["name"].(string)
return c.String(http.StatusOK, "Welcome "+name+"!")
}
func main() {
e := echo.New()
// Middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())
// Login route
e.POST("/login", login)
// Unauthenticated route
e.GET("/", accessible)
// Restricted group
r := e.Group("/restricted")
r.Use(middleware.JWT([]byte("secret")))
r.GET("", restricted)
e.Logger.Fatal(e.Start(":1323"))
}

View File

@ -1,82 +0,0 @@
package main
import (
"net/http"
"strconv"
"sync"
"time"
"github.com/labstack/echo"
)
type (
Stats struct {
Uptime time.Time `json:"uptime"`
RequestCount uint64 `json:"requestCount"`
Statuses map[string]int `json:"statuses"`
mutex sync.RWMutex
}
)
func NewStats() *Stats {
return &Stats{
Uptime: time.Now(),
Statuses: make(map[string]int),
}
}
// Process is the middleware function.
func (s *Stats) Process(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
if err := next(c); err != nil {
c.Error(err)
}
s.mutex.Lock()
defer s.mutex.Unlock()
s.RequestCount++
status := strconv.Itoa(c.Response().Status)
s.Statuses[status]++
return nil
}
}
// Handle is the endpoint to get stats.
func (s *Stats) Handle(c echo.Context) error {
s.mutex.RLock()
defer s.mutex.RUnlock()
return c.JSON(http.StatusOK, s)
}
// ServerHeader middleware adds a `Server` header to the response.
func ServerHeader(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
c.Response().Header().Set(echo.HeaderServer, "Echo/3.0")
return next(c)
}
}
func main() {
e := echo.New()
// Debug mode
e.Debug = true
//-------------------
// Custom middleware
//-------------------
// Stats
s := NewStats()
e.Use(s.Process)
e.GET("/stats", s.Handle) // Endpoint to get stats
// Server header
e.Use(ServerHeader)
// Handler
e.GET("/", func(c echo.Context) error {
return c.String(http.StatusOK, "Hello, World!")
})
// Start server
e.Logger.Fatal(e.Start(":1323"))
}

View File

@ -1,45 +0,0 @@
package main
import (
"net/http"
"time"
"encoding/json"
"github.com/labstack/echo"
)
type (
Geolocation struct {
Altitude float64
Latitude float64
Longitude float64
}
)
var (
locations = []Geolocation{
{-97, 37.819929, -122.478255},
{1899, 39.096849, -120.032351},
{2619, 37.865101, -119.538329},
{42, 33.812092, -117.918974},
{15, 37.77493, -122.419416},
}
)
func main() {
e := echo.New()
e.GET("/", func(c echo.Context) error {
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
c.Response().WriteHeader(http.StatusOK)
for _, l := range locations {
if err := json.NewEncoder(c.Response()).Encode(l); err != nil {
return err
}
c.Response().Flush()
time.Sleep(1 * time.Second)
}
return nil
})
e.Logger.Fatal(e.Start(":1323"))
}

View File

@ -1,78 +0,0 @@
package main
import (
"net/http"
"github.com/labstack/echo"
"github.com/labstack/echo/middleware"
)
type (
Host struct {
Echo *echo.Echo
}
)
func main() {
// Hosts
hosts := make(map[string]*Host)
//-----
// API
//-----
api := echo.New()
api.Use(middleware.Logger())
api.Use(middleware.Recover())
hosts["api.localhost:1323"] = &Host{api}
api.GET("/", func(c echo.Context) error {
return c.String(http.StatusOK, "API")
})
//------
// Blog
//------
blog := echo.New()
blog.Use(middleware.Logger())
blog.Use(middleware.Recover())
hosts["blog.localhost:1323"] = &Host{blog}
blog.GET("/", func(c echo.Context) error {
return c.String(http.StatusOK, "Blog")
})
//---------
// Website
//---------
site := echo.New()
site.Use(middleware.Logger())
site.Use(middleware.Recover())
hosts["localhost:1323"] = &Host{site}
site.GET("/", func(c echo.Context) error {
return c.String(http.StatusOK, "Website")
})
// Server
e := echo.New()
e.Any("/*", func(c echo.Context) (err error) {
req := c.Request()
res := c.Response()
host := hosts[req.Host]
if host == nil {
err = echo.ErrNotFound
} else {
host.Echo.ServeHTTP(res, req)
}
return
})
e.Logger.Fatal(e.Start(":1323"))
}

View File

@ -1,14 +0,0 @@
package handler
import mgo "gopkg.in/mgo.v2"
type (
Handler struct {
DB *mgo.Session
}
)
const (
// Key (Should come from somewhere else).
Key = "secret"
)

View File

@ -1,73 +0,0 @@
package handler
import (
"net/http"
"strconv"
"github.com/labstack/echo"
"github.com/labstack/echo/cookbook/twitter/model"
mgo "gopkg.in/mgo.v2"
"gopkg.in/mgo.v2/bson"
)
func (h *Handler) CreatePost(c echo.Context) (err error) {
u := &model.User{
ID: bson.ObjectIdHex(userIDFromToken(c)),
}
p := &model.Post{
ID: bson.NewObjectId(),
From: u.ID.Hex(),
}
if err = c.Bind(p); err != nil {
return
}
// Validation
if p.To == "" || p.Message == "" {
return &echo.HTTPError{Code: http.StatusBadRequest, Message: "invalid to or message fields"}
}
// Find user from database
db := h.DB.Clone()
defer db.Close()
if err = db.DB("twitter").C("users").FindId(u.ID).One(u); err != nil {
if err == mgo.ErrNotFound {
return echo.ErrNotFound
}
return
}
// Save post in database
if err = db.DB("twitter").C("posts").Insert(p); err != nil {
return
}
return c.JSON(http.StatusCreated, p)
}
func (h *Handler) FetchPost(c echo.Context) (err error) {
userID := userIDFromToken(c)
page, _ := strconv.Atoi(c.QueryParam("page"))
limit, _ := strconv.Atoi(c.QueryParam("limit"))
// Defaults
if page == 0 {
page = 1
}
if limit == 0 {
limit = 100
}
// Retrieve posts from database
posts := []*model.Post{}
db := h.DB.Clone()
if err = db.DB("twitter").C("posts").
Find(bson.M{"to": userID}).
Skip((page - 1) * limit).
Limit(limit).
All(&posts); err != nil {
return
}
defer db.Close()
return c.JSON(http.StatusOK, posts)
}

View File

@ -1,97 +0,0 @@
package handler
import (
"net/http"
"time"
jwt "github.com/dgrijalva/jwt-go"
"github.com/labstack/echo"
"github.com/labstack/echo/cookbook/twitter/model"
mgo "gopkg.in/mgo.v2"
"gopkg.in/mgo.v2/bson"
)
func (h *Handler) Signup(c echo.Context) (err error) {
// Bind
u := &model.User{ID: bson.NewObjectId()}
if err = c.Bind(u); err != nil {
return
}
// Validate
if u.Email == "" || u.Password == "" {
return &echo.HTTPError{Code: http.StatusBadRequest, Message: "invalid email or password"}
}
// Save user
db := h.DB.Clone()
defer db.Close()
if err = db.DB("twitter").C("users").Insert(u); err != nil {
return
}
return c.JSON(http.StatusCreated, u)
}
func (h *Handler) Login(c echo.Context) (err error) {
// Bind
u := new(model.User)
if err = c.Bind(u); err != nil {
return
}
// Find user
db := h.DB.Clone()
defer db.Close()
if err = db.DB("twitter").C("users").
Find(bson.M{"email": u.Email, "password": u.Password}).One(u); err != nil {
if err == mgo.ErrNotFound {
return &echo.HTTPError{Code: http.StatusUnauthorized, Message: "invalid email or password"}
}
return
}
//-----
// JWT
//-----
// Create token
token := jwt.New(jwt.SigningMethodHS256)
// Set claims
claims := token.Claims.(jwt.MapClaims)
claims["id"] = u.ID
claims["exp"] = time.Now().Add(time.Hour * 72).Unix()
// Generate encoded token and send it as response
u.Token, err = token.SignedString([]byte(Key))
if err != nil {
return err
}
u.Password = "" // Don't send password
return c.JSON(http.StatusOK, u)
}
func (h *Handler) Follow(c echo.Context) (err error) {
userID := userIDFromToken(c)
id := c.Param("id")
// Add a follower to user
db := h.DB.Clone()
defer db.Close()
if err = db.DB("twitter").C("users").
UpdateId(bson.ObjectIdHex(id), bson.M{"$addToSet": bson.M{"followers": userID}}); err != nil {
if err == mgo.ErrNotFound {
return echo.ErrNotFound
}
}
return
}
func userIDFromToken(c echo.Context) string {
user := c.Get("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
return claims["id"].(string)
}

View File

@ -1,12 +0,0 @@
package model
import "gopkg.in/mgo.v2/bson"
type (
Post struct {
ID bson.ObjectId `json:"id" bson:"_id,omitempty"`
To string `json:"to" bson:"to"`
From string `json:"from" bson:"from"`
Message string `json:"message" bson:"message"`
}
)

View File

@ -1,13 +0,0 @@
package model
import "gopkg.in/mgo.v2/bson"
type (
User struct {
ID bson.ObjectId `json:"id" bson:"_id,omitempty"`
Email string `json:"email" bson:"email"`
Password string `json:"password,omitempty" bson:"password"`
Token string `json:"token,omitempty" bson:"-"`
Followers []string `json:"followers,omitempty" bson:"followers,omitempty"`
}
)

View File

@ -1,52 +0,0 @@
package main
import (
"github.com/labstack/echo"
"github.com/labstack/echo/cookbook/twitter/handler"
"github.com/labstack/echo/middleware"
"github.com/labstack/gommon/log"
mgo "gopkg.in/mgo.v2"
)
func main() {
e := echo.New()
e.Logger.SetLevel(log.ERROR)
e.Use(middleware.Logger())
e.Use(middleware.JWTWithConfig(middleware.JWTConfig{
SigningKey: []byte(handler.Key),
Skipper: func(c echo.Context) bool {
// Skip authentication for and signup login requests
if c.Path() == "/login" || c.Path() == "/signup" {
return true
}
return false
},
}))
// Database connection
db, err := mgo.Dial("localhost")
if err != nil {
e.Logger.Fatal(err)
}
// Create indices
if err = db.Copy().DB("twitter").C("users").EnsureIndex(mgo.Index{
Key: []string{"email"},
Unique: true,
}); err != nil {
log.Fatal(err)
}
// Initialize handler
h := &handler.Handler{DB: db}
// Routes
e.POST("/signup", h.Signup)
e.POST("/login", h.Login)
e.POST("/follow/:id", h.Follow)
e.POST("/posts", h.CreatePost)
e.GET("/feed", h.FetchPost)
// Start server
e.Logger.Fatal(e.Start(":1323"))
}

View File

@ -1,47 +0,0 @@
package main
import (
"fmt"
"log"
"github.com/labstack/echo"
"github.com/gorilla/websocket"
"github.com/labstack/echo/middleware"
)
var (
upgrader = websocket.Upgrader{}
)
func hello(c echo.Context) error {
ws, err := upgrader.Upgrade(c.Response(), c.Request(), nil)
if err != nil {
return err
}
defer ws.Close()
for {
// Write
err := ws.WriteMessage(websocket.TextMessage, []byte("Hello, Client!"))
if err != nil {
log.Fatal(err)
}
// Read
_, msg, err := ws.ReadMessage()
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s\n", msg)
}
}
func main() {
e := echo.New()
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.Static("/", "../public")
e.GET("/ws", hello)
e.Logger.Fatal(e.Start(":1323"))
}

View File

@ -1,41 +0,0 @@
package main
import (
"fmt"
"log"
"github.com/labstack/echo"
"github.com/labstack/echo/middleware"
"golang.org/x/net/websocket"
)
func hello(c echo.Context) error {
websocket.Handler(func(ws *websocket.Conn) {
defer ws.Close()
for {
// Write
err := websocket.Message.Send(ws, "Hello, Client!")
if err != nil {
log.Fatal(err)
}
// Read
msg := ""
err = websocket.Message.Receive(ws, &msg)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s\n", msg)
}
}).ServeHTTP(c.Response(), c.Request())
return nil
}
func main() {
e := echo.New()
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.Static("/", "../public")
e.GET("/ws", hello)
e.Logger.Fatal(e.Start(":1323"))
}

View File

@ -42,10 +42,11 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
slog "log" stdLog "log"
"net" "net"
"net/http" "net/http"
"path" "path"
"path/filepath"
"reflect" "reflect"
"runtime" "runtime"
"sync" "sync"
@ -59,7 +60,7 @@ import (
type ( type (
// Echo is the top-level framework instance. // Echo is the top-level framework instance.
Echo struct { Echo struct {
stdLogger *slog.Logger stdLogger *stdLog.Logger
colorer *color.Color colorer *color.Color
premiddleware []MiddlewareFunc premiddleware []MiddlewareFunc
middleware []MiddlewareFunc middleware []MiddlewareFunc
@ -73,20 +74,21 @@ type (
TLSListener net.Listener TLSListener net.Listener
DisableHTTP2 bool DisableHTTP2 bool
Debug bool Debug bool
HideBanner bool
HTTPErrorHandler HTTPErrorHandler HTTPErrorHandler HTTPErrorHandler
Binder Binder Binder Binder
Validator Validator Validator Validator
Renderer Renderer Renderer Renderer
AutoTLSManager autocert.Manager AutoTLSManager autocert.Manager
Mutex sync.RWMutex // Mutex sync.RWMutex
Logger Logger Logger Logger
} }
// Route contains a handler and information for matching against requests. // Route contains a handler and information for matching against requests.
Route struct { Route struct {
Method string Method string `json:"method"`
Path string Path string `json:"path"`
Handler string Handler string `json:"handler"`
} }
// HTTPError represents an error that occurred while handling a request. // HTTPError represents an error that occurred while handling a request.
@ -144,6 +146,8 @@ const (
MIMEApplicationJavaScriptCharsetUTF8 = MIMEApplicationJavaScript + "; " + charsetUTF8 MIMEApplicationJavaScriptCharsetUTF8 = MIMEApplicationJavaScript + "; " + charsetUTF8
MIMEApplicationXML = "application/xml" MIMEApplicationXML = "application/xml"
MIMEApplicationXMLCharsetUTF8 = MIMEApplicationXML + "; " + charsetUTF8 MIMEApplicationXMLCharsetUTF8 = MIMEApplicationXML + "; " + charsetUTF8
MIMETextXML = "text/xml"
MIMETextXMLCharsetUTF8 = MIMETextXML + "; " + charsetUTF8
MIMEApplicationForm = "application/x-www-form-urlencoded" MIMEApplicationForm = "application/x-www-form-urlencoded"
MIMEApplicationProtobuf = "application/protobuf" MIMEApplicationProtobuf = "application/protobuf"
MIMEApplicationMsgpack = "application/msgpack" MIMEApplicationMsgpack = "application/msgpack"
@ -161,27 +165,34 @@ const (
// Headers // Headers
const ( const (
HeaderAcceptEncoding = "Accept-Encoding" HeaderAccept = "Accept"
HeaderAllow = "Allow" HeaderAcceptEncoding = "Accept-Encoding"
HeaderAuthorization = "Authorization" HeaderAllow = "Allow"
HeaderContentDisposition = "Content-Disposition" HeaderAuthorization = "Authorization"
HeaderContentEncoding = "Content-Encoding" HeaderContentDisposition = "Content-Disposition"
HeaderContentLength = "Content-Length" HeaderContentEncoding = "Content-Encoding"
HeaderContentType = "Content-Type" HeaderContentLength = "Content-Length"
HeaderCookie = "Cookie" HeaderContentType = "Content-Type"
HeaderSetCookie = "Set-Cookie" HeaderCookie = "Cookie"
HeaderIfModifiedSince = "If-Modified-Since" HeaderSetCookie = "Set-Cookie"
HeaderLastModified = "Last-Modified" HeaderIfModifiedSince = "If-Modified-Since"
HeaderLocation = "Location" HeaderLastModified = "Last-Modified"
HeaderUpgrade = "Upgrade" HeaderLocation = "Location"
HeaderVary = "Vary" HeaderUpgrade = "Upgrade"
HeaderWWWAuthenticate = "WWW-Authenticate" HeaderVary = "Vary"
HeaderXForwardedProto = "X-Forwarded-Proto" HeaderWWWAuthenticate = "WWW-Authenticate"
HeaderXHTTPMethodOverride = "X-HTTP-Method-Override" HeaderXForwardedFor = "X-Forwarded-For"
HeaderXForwardedFor = "X-Forwarded-For" HeaderXForwardedProto = "X-Forwarded-Proto"
HeaderXRealIP = "X-Real-IP" HeaderXForwardedProtocol = "X-Forwarded-Protocol"
HeaderServer = "Server" HeaderXForwardedSsl = "X-Forwarded-Ssl"
HeaderOrigin = "Origin" HeaderXUrlScheme = "X-Url-Scheme"
HeaderXHTTPMethodOverride = "X-HTTP-Method-Override"
HeaderXRealIP = "X-Real-IP"
HeaderXRequestID = "X-Request-ID"
HeaderServer = "Server"
HeaderOrigin = "Origin"
// Access control
HeaderAccessControlRequestMethod = "Access-Control-Request-Method" HeaderAccessControlRequestMethod = "Access-Control-Request-Method"
HeaderAccessControlRequestHeaders = "Access-Control-Request-Headers" HeaderAccessControlRequestHeaders = "Access-Control-Request-Headers"
HeaderAccessControlAllowOrigin = "Access-Control-Allow-Origin" HeaderAccessControlAllowOrigin = "Access-Control-Allow-Origin"
@ -200,6 +211,22 @@ const (
HeaderXCSRFToken = "X-CSRF-Token" HeaderXCSRFToken = "X-CSRF-Token"
) )
const (
version = "3.1.0"
website = "https://echo.labstack.com"
// http://patorjk.com/software/taag/#p=display&f=Small%20Slant&t=Echo
banner = `
____ __
/ __/___/ / ___
/ _// __/ _ \/ _ \
/___/\__/_//_/\___/ %s
High performance, minimalist Go web framework
%s
____________________________________O/_______
O\
`
)
var ( var (
methods = [...]string{ methods = [...]string{
CONNECT, CONNECT,
@ -219,6 +246,7 @@ var (
ErrUnsupportedMediaType = NewHTTPError(http.StatusUnsupportedMediaType) ErrUnsupportedMediaType = NewHTTPError(http.StatusUnsupportedMediaType)
ErrNotFound = NewHTTPError(http.StatusNotFound) ErrNotFound = NewHTTPError(http.StatusNotFound)
ErrUnauthorized = NewHTTPError(http.StatusUnauthorized) ErrUnauthorized = NewHTTPError(http.StatusUnauthorized)
ErrForbidden = NewHTTPError(http.StatusForbidden)
ErrMethodNotAllowed = NewHTTPError(http.StatusMethodNotAllowed) ErrMethodNotAllowed = NewHTTPError(http.StatusMethodNotAllowed)
ErrStatusRequestEntityTooLarge = NewHTTPError(http.StatusRequestEntityTooLarge) ErrStatusRequestEntityTooLarge = NewHTTPError(http.StatusRequestEntityTooLarge)
ErrValidatorNotRegistered = errors.New("Validator not registered") ErrValidatorNotRegistered = errors.New("Validator not registered")
@ -255,7 +283,7 @@ func New() (e *Echo) {
e.HTTPErrorHandler = e.DefaultHTTPErrorHandler e.HTTPErrorHandler = e.DefaultHTTPErrorHandler
e.Binder = &DefaultBinder{} e.Binder = &DefaultBinder{}
e.Logger.SetLevel(log.OFF) e.Logger.SetLevel(log.OFF)
e.stdLogger = slog.New(e.Logger.Output(), e.Logger.Prefix()+": ", 0) e.stdLogger = stdLog.New(e.Logger.Output(), e.Logger.Prefix()+": ", 0)
e.pool.New = func() interface{} { e.pool.New = func() interface{} {
return e.NewContext(nil, nil) return e.NewContext(nil, nil)
} }
@ -398,12 +426,16 @@ func (e *Echo) Match(methods []string, path string, handler HandlerFunc, middlew
// Static registers a new route with path prefix to serve static files from the // Static registers a new route with path prefix to serve static files from the
// provided root directory. // provided root directory.
func (e *Echo) Static(prefix, root string) { func (e *Echo) Static(prefix, root string) {
if root == "" {
root = "." // For security we want to restrict to CWD.
}
static(e, prefix, root) static(e, prefix, root)
} }
func static(i i, prefix, root string) { func static(i i, prefix, root string) {
h := func(c Context) error { h := func(c Context) error {
return c.File(path.Join(root, c.Param("*"))) name := filepath.Join(root, path.Clean("/"+c.Param("*"))) // "/"+ for security
return c.File(name)
} }
i.GET(prefix, h) i.GET(prefix, h)
if prefix == "/" { if prefix == "/" {
@ -430,7 +462,7 @@ func (e *Echo) add(method, path string, handler HandlerFunc, middleware ...Middl
} }
return h(c) return h(c)
}) })
r := Route{ r := &Route{
Method: method, Method: method,
Path: path, Path: path,
Handler: name, Handler: name,
@ -476,8 +508,8 @@ func (e *Echo) URL(h HandlerFunc, params ...interface{}) string {
} }
// Routes returns the registered routes. // Routes returns the registered routes.
func (e *Echo) Routes() []Route { func (e *Echo) Routes() []*Route {
routes := []Route{} routes := []*Route{}
for _, v := range e.router.routes { for _, v := range e.router.routes {
routes = append(routes, v) routes = append(routes, v)
} }
@ -499,8 +531,8 @@ func (e *Echo) ReleaseContext(c Context) {
// ServeHTTP implements `http.Handler` interface, which serves HTTP requests. // ServeHTTP implements `http.Handler` interface, which serves HTTP requests.
func (e *Echo) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (e *Echo) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Acquire lock // Acquire lock
e.Mutex.RLock() // e.Mutex.RLock()
defer e.Mutex.RUnlock() // defer e.Mutex.RUnlock()
// Acquire context // Acquire context
c := e.pool.Get().(*context) c := e.pool.Get().(*context)
@ -510,7 +542,10 @@ func (e *Echo) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Middleware // Middleware
h := func(c Context) error { h := func(c Context) error {
method := r.Method method := r.Method
path := r.URL.EscapedPath() path := r.URL.RawPath
if path == "" {
path = r.URL.Path
}
e.router.Find(method, path, c) e.router.Find(method, path, c)
h := c.Handler() h := c.Handler()
for i := len(e.middleware) - 1; i >= 0; i-- { for i := len(e.middleware) - 1; i >= 0; i-- {
@ -572,8 +607,15 @@ func (e *Echo) startTLS(address string) error {
func (e *Echo) StartServer(s *http.Server) (err error) { func (e *Echo) StartServer(s *http.Server) (err error) {
// Setup // Setup
e.colorer.SetOutput(e.Logger.Output()) e.colorer.SetOutput(e.Logger.Output())
s.Handler = e
s.ErrorLog = e.stdLogger s.ErrorLog = e.stdLogger
s.Handler = e
if e.Debug {
e.Logger.SetLevel(log.DEBUG)
}
if !e.HideBanner {
e.colorer.Printf(banner, e.colorer.Red("v"+version), e.colorer.Blue(website))
}
if s.TLSConfig == nil { if s.TLSConfig == nil {
if e.Listener == nil { if e.Listener == nil {
@ -582,7 +624,9 @@ func (e *Echo) StartServer(s *http.Server) (err error) {
return err return err
} }
} }
e.colorer.Printf("⇛ http server started on %s\n", e.colorer.Green(e.Listener.Addr())) if !e.HideBanner {
e.colorer.Printf("⇨ http server started on %s\n", e.colorer.Green(e.Listener.Addr()))
}
return s.Serve(e.Listener) return s.Serve(e.Listener)
} }
if e.TLSListener == nil { if e.TLSListener == nil {
@ -592,7 +636,9 @@ func (e *Echo) StartServer(s *http.Server) (err error) {
} }
e.TLSListener = tls.NewListener(l, s.TLSConfig) e.TLSListener = tls.NewListener(l, s.TLSConfig)
} }
e.colorer.Printf("⇛ https server started on %s\n", e.colorer.Green(e.TLSListener.Addr())) if !e.HideBanner {
e.colorer.Printf("⇨ https server started on %s\n", e.colorer.Green(e.TLSListener.Addr()))
}
return s.Serve(e.TLSListener) return s.Serve(e.TLSListener)
} }

25
vendor/github.com/labstack/echo/echo_go1.8.go generated vendored Normal file
View File

@ -0,0 +1,25 @@
// +build go1.8
package echo
import (
stdContext "context"
)
// Close immediately stops the server.
// It internally calls `http.Server#Close()`.
func (e *Echo) Close() error {
if err := e.TLSServer.Close(); err != nil {
return err
}
return e.Server.Close()
}
// Shutdown stops server the gracefully.
// It internally calls `http.Server#Shutdown()`.
func (e *Echo) Shutdown(ctx stdContext.Context) error {
if err := e.TLSServer.Shutdown(ctx); err != nil {
return err
}
return e.Server.Shutdown(ctx)
}

View File

@ -1,8 +1,12 @@
package echo package echo
import (
"path"
)
type ( type (
// Group is a set of sub-routes for a specified route. It can be used for inner // Group is a set of sub-routes for a specified route. It can be used for inner
// routes that share a common middlware or functionality that should be separate // routes that share a common middleware or functionality that should be separate
// from the parent echo instance while still inheriting from it. // from the parent echo instance while still inheriting from it.
Group struct { Group struct {
prefix string prefix string
@ -14,6 +18,11 @@ type (
// Use implements `Echo#Use()` for sub-routes within the Group. // Use implements `Echo#Use()` for sub-routes within the Group.
func (g *Group) Use(middleware ...MiddlewareFunc) { func (g *Group) Use(middleware ...MiddlewareFunc) {
g.middleware = append(g.middleware, middleware...) g.middleware = append(g.middleware, middleware...)
// Allow all requests to reach the group as they might get dropped if router
// doesn't find a match, making none of the group middleware process.
g.echo.Any(path.Clean(g.prefix+"/*"), func(c Context) error {
return ErrNotFound
}, g.middleware...)
} }
// CONNECT implements `Echo#CONNECT()` for sub-routes within the Group. // CONNECT implements `Echo#CONNECT()` for sub-routes within the Group.

View File

@ -2,6 +2,7 @@ package middleware
import ( import (
"encoding/base64" "encoding/base64"
"strconv"
"github.com/labstack/echo" "github.com/labstack/echo"
) )
@ -15,20 +16,26 @@ type (
// Validator is a function to validate BasicAuth credentials. // Validator is a function to validate BasicAuth credentials.
// Required. // Required.
Validator BasicAuthValidator Validator BasicAuthValidator
// Realm is a string to define realm attribute of BasicAuth.
// Default value "Restricted".
Realm string
} }
// BasicAuthValidator defines a function to validate BasicAuth credentials. // BasicAuthValidator defines a function to validate BasicAuth credentials.
BasicAuthValidator func(string, string, echo.Context) bool BasicAuthValidator func(string, string, echo.Context) (bool, error)
) )
const ( const (
basic = "Basic" basic = "Basic"
defaultRealm = "Restricted"
) )
var ( var (
// DefaultBasicAuthConfig is the default BasicAuth middleware config. // DefaultBasicAuthConfig is the default BasicAuth middleware config.
DefaultBasicAuthConfig = BasicAuthConfig{ DefaultBasicAuthConfig = BasicAuthConfig{
Skipper: DefaultSkipper, Skipper: DefaultSkipper,
Realm: defaultRealm,
} }
) )
@ -52,6 +59,9 @@ func BasicAuthWithConfig(config BasicAuthConfig) echo.MiddlewareFunc {
if config.Skipper == nil { if config.Skipper == nil {
config.Skipper = DefaultBasicAuthConfig.Skipper config.Skipper = DefaultBasicAuthConfig.Skipper
} }
if config.Realm == "" {
config.Realm = defaultRealm
}
return func(next echo.HandlerFunc) echo.HandlerFunc { return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) error {
@ -71,15 +81,25 @@ func BasicAuthWithConfig(config BasicAuthConfig) echo.MiddlewareFunc {
for i := 0; i < len(cred); i++ { for i := 0; i < len(cred); i++ {
if cred[i] == ':' { if cred[i] == ':' {
// Verify credentials // Verify credentials
if config.Validator(cred[:i], cred[i+1:], c) { valid, err := config.Validator(cred[:i], cred[i+1:], c)
if err != nil {
return err
} else if valid {
return next(c) return next(c)
} }
} }
} }
} }
realm := ""
if config.Realm == defaultRealm {
realm = defaultRealm
} else {
realm = strconv.Quote(config.Realm)
}
// Need to return `401` for browsers to pop-up login box. // Need to return `401` for browsers to pop-up login box.
c.Response().Header().Set(echo.HeaderWWWAuthenticate, basic+" realm=Restricted") c.Response().Header().Set(echo.HeaderWWWAuthenticate, basic+" realm="+realm)
return echo.ErrUnauthorized return echo.ErrUnauthorized
} }
} }

View File

@ -108,8 +108,8 @@ func (w *gzipResponseWriter) Write(b []byte) (int, error) {
return w.Writer.Write(b) return w.Writer.Write(b)
} }
func (w *gzipResponseWriter) Flush() error { func (w *gzipResponseWriter) Flush() {
return w.Writer.(*gzip.Writer).Flush() w.Writer.(*gzip.Writer).Flush()
} }
func (w *gzipResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { func (w *gzipResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {

View File

@ -91,7 +91,7 @@ func JWTWithConfig(config JWTConfig) echo.MiddlewareFunc {
config.Skipper = DefaultJWTConfig.Skipper config.Skipper = DefaultJWTConfig.Skipper
} }
if config.SigningKey == nil { if config.SigningKey == nil {
panic("jwt middleware requires signing key") panic("echo: jwt middleware requires signing key")
} }
if config.SigningMethod == "" { if config.SigningMethod == "" {
config.SigningMethod = DefaultJWTConfig.SigningMethod config.SigningMethod = DefaultJWTConfig.SigningMethod

View File

@ -32,7 +32,7 @@ type (
} }
// KeyAuthValidator defines a function to validate KeyAuth credentials. // KeyAuthValidator defines a function to validate KeyAuth credentials.
KeyAuthValidator func(string, echo.Context) bool KeyAuthValidator func(string, echo.Context) (bool, error)
keyExtractor func(echo.Context) (string, error) keyExtractor func(echo.Context) (string, error)
) )
@ -94,7 +94,10 @@ func KeyAuthWithConfig(config KeyAuthConfig) echo.MiddlewareFunc {
if err != nil { if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error()) return echo.NewHTTPError(http.StatusBadRequest, err.Error())
} }
if config.Validator(key, c) { valid, err := config.Validator(key, c)
if err != nil {
return err
} else if valid {
return next(c) return next(c)
} }

View File

@ -26,7 +26,7 @@ type (
// - time_unix_nano // - time_unix_nano
// - time_rfc3339 // - time_rfc3339
// - time_rfc3339_nano // - time_rfc3339_nano
// - id (Request ID - Not implemented) // - id (Request ID)
// - remote_ip // - remote_ip
// - uri // - uri
// - host // - host
@ -62,7 +62,7 @@ var (
// DefaultLoggerConfig is the default Logger middleware config. // DefaultLoggerConfig is the default Logger middleware config.
DefaultLoggerConfig = LoggerConfig{ DefaultLoggerConfig = LoggerConfig{
Skipper: DefaultSkipper, Skipper: DefaultSkipper,
Format: `{"time":"${time_rfc3339_nano}","remote_ip":"${remote_ip}","host":"${host}",` + Format: `{"time":"${time_rfc3339_nano}","id":"${id}","remote_ip":"${remote_ip}","host":"${host}",` +
`"method":"${method}","uri":"${uri}","status":${status}, "latency":${latency},` + `"method":"${method}","uri":"${uri}","status":${status}, "latency":${latency},` +
`"latency_human":"${latency_human}","bytes_in":${bytes_in},` + `"latency_human":"${latency_human}","bytes_in":${bytes_in},` +
`"bytes_out":${bytes_out}}` + "\n", `"bytes_out":${bytes_out}}` + "\n",
@ -126,6 +126,12 @@ func LoggerWithConfig(config LoggerConfig) echo.MiddlewareFunc {
return buf.WriteString(time.Now().Format(time.RFC3339)) return buf.WriteString(time.Now().Format(time.RFC3339))
case "time_rfc3339_nano": case "time_rfc3339_nano":
return buf.WriteString(time.Now().Format(time.RFC3339Nano)) return buf.WriteString(time.Now().Format(time.RFC3339Nano))
case "id":
id := req.Header.Get(echo.HeaderXRequestID)
if id == "" {
id = res.Header().Get(echo.HeaderXRequestID)
}
return buf.WriteString(id)
case "remote_ip": case "remote_ip":
return buf.WriteString(c.RealIP()) return buf.WriteString(c.RealIP())
case "host": case "host":
@ -177,6 +183,11 @@ func LoggerWithConfig(config LoggerConfig) echo.MiddlewareFunc {
return buf.Write([]byte(c.QueryParam(tag[6:]))) return buf.Write([]byte(c.QueryParam(tag[6:])))
case strings.HasPrefix(tag, "form:"): case strings.HasPrefix(tag, "form:"):
return buf.Write([]byte(c.FormValue(tag[5:]))) return buf.Write([]byte(c.FormValue(tag[5:])))
case strings.HasPrefix(tag, "cookie:"):
cookie, err := c.Cookie(tag[7:])
if err == nil {
return buf.Write([]byte(cookie.Value))
}
} }
} }
return 0, nil return 0, nil

View File

@ -9,6 +9,6 @@ type (
) )
// DefaultSkipper returns false which processes the middleware. // DefaultSkipper returns false which processes the middleware.
func DefaultSkipper(c echo.Context) bool { func DefaultSkipper(echo.Context) bool {
return false return false
} }

160
vendor/github.com/labstack/echo/middleware/proxy.go generated vendored Normal file
View File

@ -0,0 +1,160 @@
package middleware
import (
"errors"
"fmt"
"io"
"math/rand"
"net"
"net/http"
"net/http/httputil"
"net/url"
"sync/atomic"
"time"
"github.com/labstack/echo"
)
// TODO: Handle TLS proxy
type (
// ProxyConfig defines the config for Proxy middleware.
ProxyConfig struct {
// Skipper defines a function to skip middleware.
Skipper Skipper
// Balancer defines a load balancing technique.
// Required.
// Possible values:
// - RandomBalancer
// - RoundRobinBalancer
Balancer ProxyBalancer
}
// ProxyTarget defines the upstream target.
ProxyTarget struct {
URL *url.URL
}
// RandomBalancer implements a random load balancing technique.
RandomBalancer struct {
Targets []*ProxyTarget
random *rand.Rand
}
// RoundRobinBalancer implements a round-robin load balancing technique.
RoundRobinBalancer struct {
Targets []*ProxyTarget
i uint32
}
// ProxyBalancer defines an interface to implement a load balancing technique.
ProxyBalancer interface {
Next() *ProxyTarget
}
)
func proxyHTTP(t *ProxyTarget) http.Handler {
return httputil.NewSingleHostReverseProxy(t.URL)
}
func proxyRaw(t *ProxyTarget, c echo.Context) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h, ok := w.(http.Hijacker)
if !ok {
c.Error(errors.New("proxy raw, not a hijacker"))
return
}
in, _, err := h.Hijack()
if err != nil {
c.Error(fmt.Errorf("proxy raw, hijack error=%v, url=%s", r.URL, err))
return
}
defer in.Close()
out, err := net.Dial("tcp", t.URL.Host)
if err != nil {
he := echo.NewHTTPError(http.StatusBadGateway, fmt.Sprintf("proxy raw, dial error=%v, url=%s", r.URL, err))
c.Error(he)
return
}
defer out.Close()
err = r.Write(out)
if err != nil {
he := echo.NewHTTPError(http.StatusBadGateway, fmt.Sprintf("proxy raw, request copy error=%v, url=%s", r.URL, err))
c.Error(he)
return
}
errc := make(chan error, 2)
cp := func(dst io.Writer, src io.Reader) {
_, err := io.Copy(dst, src)
errc <- err
}
go cp(out, in)
go cp(in, out)
err = <-errc
if err != nil && err != io.EOF {
c.Logger().Errorf("proxy raw, error=%v, url=%s", r.URL, err)
}
})
}
// Next randomly returns an upstream target.
func (r *RandomBalancer) Next() *ProxyTarget {
if r.random == nil {
r.random = rand.New(rand.NewSource(int64(time.Now().Nanosecond())))
}
return r.Targets[r.random.Intn(len(r.Targets))]
}
// Next returns an upstream target using round-robin technique.
func (r *RoundRobinBalancer) Next() *ProxyTarget {
r.i = r.i % uint32(len(r.Targets))
t := r.Targets[r.i]
atomic.AddUint32(&r.i, 1)
return t
}
// Proxy returns an HTTP/WebSocket reverse proxy middleware.
func Proxy(config ProxyConfig) echo.MiddlewareFunc {
// Defaults
if config.Skipper == nil {
config.Skipper = DefaultLoggerConfig.Skipper
}
if config.Balancer == nil {
panic("echo: proxy middleware requires balancer")
}
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) (err error) {
req := c.Request()
res := c.Response()
tgt := config.Balancer.Next()
// Fix header
if req.Header.Get(echo.HeaderXRealIP) == "" {
req.Header.Set(echo.HeaderXRealIP, c.RealIP())
}
if req.Header.Get(echo.HeaderXForwardedProto) == "" {
req.Header.Set(echo.HeaderXForwardedProto, c.Scheme())
}
if c.IsWebSocket() && req.Header.Get(echo.HeaderXForwardedFor) == "" { // For HTTP, it is automatically set by Go HTTP reverse proxy.
req.Header.Set(echo.HeaderXForwardedFor, c.RealIP())
}
// Proxy
switch {
case c.IsWebSocket():
proxyRaw(tgt, c).ServeHTTP(res, req)
case req.Header.Get(echo.HeaderAccept) == "text/event-stream":
default:
proxyHTTP(tgt).ServeHTTP(res, req)
}
return
}
}
}

View File

@ -0,0 +1,64 @@
package middleware
import (
"github.com/labstack/echo"
"github.com/labstack/gommon/random"
)
type (
// RequestIDConfig defines the config for RequestID middleware.
RequestIDConfig struct {
// Skipper defines a function to skip middleware.
Skipper Skipper
// Generator defines a function to generate an ID.
// Optional. Default value random.String(32).
Generator func() string
}
)
var (
// DefaultRequestIDConfig is the default RequestID middleware config.
DefaultRequestIDConfig = RequestIDConfig{
Skipper: DefaultSkipper,
Generator: generator,
}
)
// RequestID returns a X-Request-ID middleware.
func RequestID() echo.MiddlewareFunc {
return RequestIDWithConfig(DefaultRequestIDConfig)
}
// RequestIDWithConfig returns a X-Request-ID middleware with config.
func RequestIDWithConfig(config RequestIDConfig) echo.MiddlewareFunc {
// Defaults
if config.Skipper == nil {
config.Skipper = DefaultRequestIDConfig.Skipper
}
if config.Generator == nil {
config.Generator = generator
}
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
if config.Skipper(c) {
return next(c)
}
req := c.Request()
res := c.Response()
rid := req.Header.Get(echo.HeaderXRequestID)
if rid == "" {
rid = config.Generator()
}
res.Header().Set(echo.HeaderXRequestID, rid)
return next(c)
}
}
}
func generator() string {
return random.String(32)
}

View File

@ -3,7 +3,9 @@ package middleware
import ( import (
"fmt" "fmt"
"os" "os"
"path"
"path/filepath" "path/filepath"
"strings"
"github.com/labstack/echo" "github.com/labstack/echo"
) )
@ -53,6 +55,9 @@ func Static(root string) echo.MiddlewareFunc {
// See `Static()`. // See `Static()`.
func StaticWithConfig(config StaticConfig) echo.MiddlewareFunc { func StaticWithConfig(config StaticConfig) echo.MiddlewareFunc {
// Defaults // Defaults
if config.Root == "" {
config.Root = "." // For security we want to restrict to CWD.
}
if config.Skipper == nil { if config.Skipper == nil {
config.Skipper = DefaultStaticConfig.Skipper config.Skipper = DefaultStaticConfig.Skipper
} }
@ -62,26 +67,44 @@ func StaticWithConfig(config StaticConfig) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc { return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) error {
p := c.Param("*") if config.Skipper(c) {
name := filepath.Join(config.Root, p) return next(c)
fi, err := os.Stat(name) }
p := c.Request().URL.Path
if strings.HasSuffix(c.Path(), "*") { // When serving from a group, e.g. `/static*`.
p = c.Param("*")
}
name := filepath.Join(config.Root, path.Clean("/"+p)) // "/"+ for security
fi, err := os.Stat(name)
if err != nil { if err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
if config.HTML5 { if config.HTML5 && path.Ext(p) == "" {
return c.File(filepath.Join(config.Root, config.Index)) return c.File(filepath.Join(config.Root, config.Index))
} }
return echo.ErrNotFound return next(c)
} }
return err return err
} }
if fi.IsDir() { if fi.IsDir() {
if config.Browse { index := filepath.Join(name, config.Index)
return listDir(name, c.Response()) fi, err = os.Stat(index)
if err != nil {
if config.Browse {
return listDir(name, c.Response())
}
if os.IsNotExist(err) {
return next(c)
}
return err
} }
return c.File(filepath.Join(name, config.Index))
return c.File(index)
} }
return c.File(name) return c.File(name)
} }
} }

View File

@ -7,7 +7,7 @@ type (
// request matching and URL path parameter parsing. // request matching and URL path parameter parsing.
Router struct { Router struct {
tree *node tree *node
routes map[string]Route routes map[string]*Route
echo *Echo echo *Echo
} }
node struct { node struct {
@ -47,7 +47,7 @@ func NewRouter(e *Echo) *Router {
tree: &node{ tree: &node{
methodHandler: new(methodHandler), methodHandler: new(methodHandler),
}, },
routes: make(map[string]Route), routes: map[string]*Route{},
echo: e, echo: e,
} }
} }
@ -101,7 +101,7 @@ func (r *Router) insert(method, path string, h HandlerFunc, t kind, ppath string
cn := r.tree // Current node as root cn := r.tree // Current node as root
if cn == nil { if cn == nil {
panic("echo invalid method") panic("echo: invalid method")
} }
search := path search := path
@ -296,18 +296,19 @@ func (n *node) checkMethodNotAllowed() HandlerFunc {
// - Get context from `Echo#AcquireContext()` // - Get context from `Echo#AcquireContext()`
// - Reset it `Context#Reset()` // - Reset it `Context#Reset()`
// - Return it `Echo#ReleaseContext()`. // - Return it `Echo#ReleaseContext()`.
func (r *Router) Find(method, path string, context Context) { func (r *Router) Find(method, path string, c Context) {
context.SetPath(path) ctx := c.(*context)
ctx.path = path
cn := r.tree // Current node as root cn := r.tree // Current node as root
var ( var (
search = path search = path
c *node // Child node child *node // Child node
n int // Param counter n int // Param counter
nk kind // Next kind nk kind // Next kind
nn *node // Next node nn *node // Next node
ns string // Next search ns string // Next search
pvalues = context.ParamValues() pvalues = ctx.pvalues // Use the internal slice so the interface can keep the illusion of a dynamic slice
) )
// Search order static > param > any // Search order static > param > any
@ -352,20 +353,20 @@ func (r *Router) Find(method, path string, context Context) {
} }
// Static node // Static node
if c = cn.findChild(search[0], skind); c != nil { if child = cn.findChild(search[0], skind); child != nil {
// Save next // Save next
if cn.prefix[len(cn.prefix)-1] == '/' { // Issue #623 if cn.prefix[len(cn.prefix)-1] == '/' { // Issue #623
nk = pkind nk = pkind
nn = cn nn = cn
ns = search ns = search
} }
cn = c cn = child
continue continue
} }
// Param node // Param node
Param: Param:
if c = cn.findChildByKind(pkind); c != nil { if child = cn.findChildByKind(pkind); child != nil {
// Issue #378 // Issue #378
if len(pvalues) == n { if len(pvalues) == n {
continue continue
@ -378,7 +379,7 @@ func (r *Router) Find(method, path string, context Context) {
ns = search ns = search
} }
cn = c cn = child
i, l := 0, len(search) i, l := 0, len(search)
for ; i < l && search[i] != '/'; i++ { for ; i < l && search[i] != '/'; i++ {
} }
@ -409,13 +410,13 @@ func (r *Router) Find(method, path string, context Context) {
} }
End: End:
context.SetHandler(cn.findHandler(method)) ctx.handler = cn.findHandler(method)
context.SetPath(cn.ppath) ctx.path = cn.ppath
context.SetParamNames(cn.pnames...) ctx.pnames = cn.pnames
// NOTE: Slow zone... // NOTE: Slow zone...
if context.Handler() == nil { if ctx.handler == nil {
context.SetHandler(cn.checkMethodNotAllowed()) ctx.handler = cn.checkMethodNotAllowed()
// Dig further for any, might have an empty value for *, e.g. // Dig further for any, might have an empty value for *, e.g.
// serving a directory. Issue #207. // serving a directory. Issue #207.
@ -423,12 +424,12 @@ End:
return return
} }
if h := cn.findHandler(method); h != nil { if h := cn.findHandler(method); h != nil {
context.SetHandler(h) ctx.handler = h
} else { } else {
context.SetHandler(cn.checkMethodNotAllowed()) ctx.handler = cn.checkMethodNotAllowed()
} }
context.SetPath(cn.ppath) ctx.path = cn.ppath
context.SetParamNames(cn.pnames...) ctx.pnames = cn.pnames
pvalues[len(cn.pnames)-1] = "" pvalues[len(cn.pnames)-1] = ""
} }

View File

@ -2,22 +2,24 @@ package random
import ( import (
"math/rand" "math/rand"
"strings"
"time" "time"
) )
type ( type (
Random struct { Random struct {
charset Charset
} }
Charset string
) )
// Charsets
const ( const (
Alphanumeric Charset = Alphabetic + Numeric Uppercase string = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
Alphabetic Charset = "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" Lowercase = "abcdefghijklmnopqrstuvwxyz"
Numeric Charset = "0123456789" Alphabetic = Uppercase + Lowercase
Hex Charset = Numeric + "abcdef" Numeric = "0123456789"
Alphanumeric = Alphabetic + Numeric
Symbols = "`" + `~!@#$%^&*()-_+={}[]|\;:"<>,./?`
Hex = Numeric + "abcdef"
) )
var ( var (
@ -26,27 +28,21 @@ var (
func New() *Random { func New() *Random {
rand.Seed(time.Now().UnixNano()) rand.Seed(time.Now().UnixNano())
return &Random{ return new(Random)
charset: Alphanumeric, }
func (r *Random) String(length uint8, charsets ...string) string {
charset := strings.Join(charsets, "")
if charset == "" {
charset = Alphanumeric
} }
}
func (r *Random) SetCharset(c Charset) {
r.charset = c
}
func (r *Random) String(length uint8) string {
b := make([]byte, length) b := make([]byte, length)
for i := range b { for i := range b {
b[i] = r.charset[rand.Int63()%int64(len(r.charset))] b[i] = charset[rand.Int63()%int64(len(charset))]
} }
return string(b) return string(b)
} }
func SetCharset(c Charset) { func String(length uint8, charsets ...string) string {
global.SetCharset(c) return global.String(length, charsets...)
}
func String(length uint8) string {
return global.String(length)
} }

View File

@ -1,14 +0,0 @@
package main
import (
"time"
)
import l4g "code.google.com/p/log4go"
func main() {
log := l4g.NewLogger()
defer log.Close()
log.AddFilter("stdout", l4g.DEBUG, l4g.NewConsoleLogWriter())
log.Info("The time is now: %s", time.Now().Format("15:04:05 MST 2006/01/02"))
}

View File

@ -1,57 +0,0 @@
package main
import (
"bufio"
"fmt"
"io"
"os"
"time"
)
import l4g "code.google.com/p/log4go"
const (
filename = "flw.log"
)
func main() {
// Get a new logger instance
log := l4g.NewLogger()
// Create a default logger that is logging messages of FINE or higher
log.AddFilter("file", l4g.FINE, l4g.NewFileLogWriter(filename, false))
log.Close()
/* Can also specify manually via the following: (these are the defaults) */
flw := l4g.NewFileLogWriter(filename, false)
flw.SetFormat("[%D %T] [%L] (%S) %M")
flw.SetRotate(false)
flw.SetRotateSize(0)
flw.SetRotateLines(0)
flw.SetRotateDaily(false)
log.AddFilter("file", l4g.FINE, flw)
// Log some experimental messages
log.Finest("Everything is created now (notice that I will not be printing to the file)")
log.Info("The time is now: %s", time.Now().Format("15:04:05 MST 2006/01/02"))
log.Critical("Time to close out!")
// Close the log
log.Close()
// Print what was logged to the file (yes, I know I'm skipping error checking)
fd, _ := os.Open(filename)
in := bufio.NewReader(fd)
fmt.Print("Messages logged to file were: (line numbers not included)\n")
for lineno := 1; ; lineno++ {
line, err := in.ReadString('\n')
if err == io.EOF {
break
}
fmt.Printf("%3d:\t%s", lineno, line)
}
fd.Close()
// Remove the file so it's not lying around
os.Remove(filename)
}

View File

@ -1,42 +0,0 @@
package main
import (
"flag"
"fmt"
"net"
"os"
)
var (
port = flag.String("p", "12124", "Port number to listen on")
)
func e(err error) {
if err != nil {
fmt.Printf("Erroring out: %s\n", err)
os.Exit(1)
}
}
func main() {
flag.Parse()
// Bind to the port
bind, err := net.ResolveUDPAddr("0.0.0.0:" + *port)
e(err)
// Create listener
listener, err := net.ListenUDP("udp", bind)
e(err)
fmt.Printf("Listening to port %s...\n", *port)
for {
// read into a new buffer
buffer := make([]byte, 1024)
_, _, err := listener.ReadFrom(buffer)
e(err)
// log to standard output
fmt.Println(string(buffer))
}
}

View File

@ -1,18 +0,0 @@
package main
import (
"time"
)
import l4g "code.google.com/p/log4go"
func main() {
log := l4g.NewLogger()
log.AddFilter("network", l4g.FINEST, l4g.NewSocketLogWriter("udp", "192.168.1.255:12124"))
// Run `nc -u -l -p 12124` or similar before you run this to see the following message
log.Info("The time is now: %s", time.Now().Format("15:04:05 MST 2006/01/02"))
// This makes sure the output stream buffer is written
log.Close()
}

View File

@ -1,13 +0,0 @@
package main
import l4g "code.google.com/p/log4go"
func main() {
// Load the configuration (isn't this easy?)
l4g.LoadConfiguration("example.xml")
// And now we're ready!
l4g.Finest("This will only go to those of you really cool UDP kids! If you change enabled=true.")
l4g.Debug("Oh no! %d + %d = %d!", 2, 2, 2+2)
l4g.Info("About that time, eh chaps?")
}

View File

@ -1,3 +1,5 @@
// +build !appengine
package fasttemplate package fasttemplate
import ( import (

11
vendor/github.com/valyala/fasttemplate/unsafe_gae.go generated vendored Normal file
View File

@ -0,0 +1,11 @@
// +build appengine
package fasttemplate
func unsafeBytes2String(b []byte) string {
return string(b)
}
func unsafeString2Bytes(s string) []byte {
return []byte(s)
}

14
vendor/manifest vendored
View File

@ -53,7 +53,7 @@
"importpath": "github.com/dgrijalva/jwt-go", "importpath": "github.com/dgrijalva/jwt-go",
"repository": "https://github.com/dgrijalva/jwt-go", "repository": "https://github.com/dgrijalva/jwt-go",
"vcs": "git", "vcs": "git",
"revision": "2268707a8f0843315e2004ee4f1d021dc08baedf", "revision": "6c8dedd55f8a2e41f605de6d5d66e51ed1f299fc",
"branch": "master", "branch": "master",
"notests": true "notests": true
}, },
@ -170,7 +170,7 @@
"importpath": "github.com/labstack/echo", "importpath": "github.com/labstack/echo",
"repository": "https://github.com/labstack/echo", "repository": "https://github.com/labstack/echo",
"vcs": "git", "vcs": "git",
"revision": "0b53f397ad7709a27d37500a67735c0a639b5c38", "revision": "c3887ebb131d996411cf13a9688ab02c8dba599e",
"branch": "master", "branch": "master",
"notests": true "notests": true
}, },
@ -178,7 +178,7 @@
"importpath": "github.com/labstack/gommon/bytes", "importpath": "github.com/labstack/gommon/bytes",
"repository": "https://github.com/labstack/gommon", "repository": "https://github.com/labstack/gommon",
"vcs": "git", "vcs": "git",
"revision": "f72d3c883f8ea180da8f085dd320804c41332ad1", "revision": "1121fd3e243c202482226a7afe4dcd07ffc4139a",
"branch": "master", "branch": "master",
"path": "/bytes", "path": "/bytes",
"notests": true "notests": true
@ -187,7 +187,7 @@
"importpath": "github.com/labstack/gommon/color", "importpath": "github.com/labstack/gommon/color",
"repository": "https://github.com/labstack/gommon", "repository": "https://github.com/labstack/gommon",
"vcs": "git", "vcs": "git",
"revision": "f72d3c883f8ea180da8f085dd320804c41332ad1", "revision": "1121fd3e243c202482226a7afe4dcd07ffc4139a",
"branch": "master", "branch": "master",
"path": "/color", "path": "/color",
"notests": true "notests": true
@ -196,7 +196,7 @@
"importpath": "github.com/labstack/gommon/log", "importpath": "github.com/labstack/gommon/log",
"repository": "https://github.com/labstack/gommon", "repository": "https://github.com/labstack/gommon",
"vcs": "git", "vcs": "git",
"revision": "f72d3c883f8ea180da8f085dd320804c41332ad1", "revision": "1121fd3e243c202482226a7afe4dcd07ffc4139a",
"branch": "master", "branch": "master",
"path": "/log", "path": "/log",
"notests": true "notests": true
@ -205,7 +205,7 @@
"importpath": "github.com/labstack/gommon/random", "importpath": "github.com/labstack/gommon/random",
"repository": "https://github.com/labstack/gommon", "repository": "https://github.com/labstack/gommon",
"vcs": "git", "vcs": "git",
"revision": "f72d3c883f8ea180da8f085dd320804c41332ad1", "revision": "1121fd3e243c202482226a7afe4dcd07ffc4139a",
"branch": "master", "branch": "master",
"path": "/random", "path": "/random",
"notests": true "notests": true
@ -424,7 +424,7 @@
"importpath": "github.com/valyala/fasttemplate", "importpath": "github.com/valyala/fasttemplate",
"repository": "https://github.com/valyala/fasttemplate", "repository": "https://github.com/valyala/fasttemplate",
"vcs": "git", "vcs": "git",
"revision": "d090d65668a286d9a180d43a19dfdc5dcad8fe88", "revision": "dcecefd839c4193db0d35b88ec65b4c12d360ab0",
"branch": "master", "branch": "master",
"notests": true "notests": true
}, },