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

Compare commits

..

89 Commits

Author SHA1 Message Date
Wim
822605c157 Release v0.15.0 2017-06-19 20:47:41 +02:00
Wim
e49266ae43 Update gitter vendor. (fixes crash) 2017-06-19 20:27:14 +02:00
Wim
62e9de1a3b Use the last (and biggest) photo to relay (telegram). Closes #184 2017-06-18 23:59:52 +02:00
Wim
2ddc4f7ae9 Add UserID to each message. Closes #200 2017-06-18 15:44:54 +02:00
Wim
2dd402675d Sent only the biggest picture to bridges (telegram) 2017-06-18 01:23:15 +02:00
Wim
25b1af1e11 Add option IgnoreMessages to ignore messages based on regexp. (all). Closes #70 2017-06-18 01:08:11 +02:00
Wim
75fb2b8156 Make reconnection more robust (irc). #153 2017-06-18 00:13:10 +02:00
Wim
2a403f8b85 Add initial sticker/video/photo/document support (telegram). #184 2017-06-17 18:25:17 +02:00
Wim
c3d45a9f06 Do not relay join/part of ourselves (irc). Closes #190 2017-06-17 17:58:56 +02:00
Wim
c07b85b625 Add note about private channels (rocketchat). See #180 2017-06-15 23:05:59 +02:00
Wim
511f653e6e Fix incorrect behaviour of EditDisable (mattermost). Fixes #197 2017-06-15 22:45:34 +02:00
Wim
5636eaca6d Bump version 2017-06-15 22:45:23 +02:00
Wim
4b839b9958 Avoid nil in usermembermap (discord). See #198 2017-06-15 22:29:01 +02:00
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
Wim
4a25e66c00 Release v0.10.2 2017-03-25 21:35:13 +01:00
Wim
95f4e3448e Use API_URL_SUFFIX_V3 (mattermost) 2017-03-25 21:05:02 +01:00
Wim
eacb1c1771 Update vendor (mattermost) 2017-03-25 21:04:10 +01:00
Wim
07fd825349 Update vendor 2017-03-25 20:45:10 +01:00
Wim
be15cc8a36 Update vendored toml. Adds support for inline tables 2017-03-25 19:13:47 +01:00
424 changed files with 152831 additions and 2693 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.1](https://github.com/42wim/matterbridge/releases/latest) * Latest stable release [v0.15.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)
@ -122,9 +122,11 @@ RemoteNickFormat="[{PROTOCOL}/{BRIDGE}] <{NICK}> "
``` ```
# Running # Running
1) Copy the matterbridge.toml.sample to matterbridge.toml in the same directory as the matterbridge binary. 1) Copy the matterbridge.toml.sample to matterbridge.toml
2) Edit matterbridge.toml with the settings for your environment. 2) Edit matterbridge.toml with the settings for your environment.
3) Now you can run matterbridge. (```./matterbridge```) 3) Now you can run matterbridge. (```./matterbridge```)
(Matterbridge will only look for the config file in your current directory, if it isn't there specify -conf "/path/toyour/matterbridge.toml")
``` ```
Usage of ./matterbridge: Usage of ./matterbridge:
@ -132,6 +134,8 @@ Usage of ./matterbridge:
config file (default "matterbridge.toml") config file (default "matterbridge.toml")
-debug -debug
enable debug enable debug
-gops
enable gops agent
-version -version
show version show version
``` ```
@ -165,6 +169,7 @@ Matterbridge wouldn't exist without these libraries:
* discord - https://github.com/bwmarrin/discordgo * discord - https://github.com/bwmarrin/discordgo
* echo - https://github.com/labstack/echo * echo - https://github.com/labstack/echo
* gitter - https://github.com/sromku/go-gitter * gitter - https://github.com/sromku/go-gitter
* gops - https://github.com/google/gops
* irc - https://github.com/thoj/go-ircevent * irc - https://github.com/thoj/go-ircevent
* mattermost - https://github.com/mattermost/platform * mattermost - https://github.com/mattermost/platform
* matrix - https://github.com/matrix-org/gomatrix * matrix - https://github.com/matrix-org/gomatrix

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"
@ -20,7 +21,9 @@ type Api struct {
type ApiMessage struct { type ApiMessage struct {
Text string `json:"text"` Text string `json:"text"`
Username string `json:"username"` Username string `json:"username"`
UserID string `json:"userid"`
Avatar string `json:"avatar"` Avatar string `json:"avatar"`
Gateway string `json:"gateway"`
} }
var flog *log.Entry var flog *log.Entry
@ -38,6 +41,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 +78,16 @@ 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,
UserID: message.UserID,
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 +95,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,26 +10,42 @@ 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 UserID string `json:"userid"` // userid on the bridge
Account string Avatar string `json:"avatar"`
Event string Account string `json:"account"`
Protocol string Event string `json:"event"`
Timestamp time.Time Protocol string `json:"protocol"`
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
IgnoreMessages string // all protocols
Jid string // xmpp Jid string // xmpp
Login string // mattermost, matrix Login string // mattermost, matrix
Muc string // xmpp Muc string // xmpp
@ -39,23 +55,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 +82,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,8 +139,11 @@ 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",
UserID: m.Author.ID}
} }
func (b *bdiscord) memberUpdate(s *discordgo.Session, m *discordgo.GuildMemberUpdate) { func (b *bdiscord) memberUpdate(s *discordgo.Session, m *discordgo.GuildMemberUpdate) {
@ -143,18 +160,21 @@ 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) member, err := b.c.GuildMember(b.guildID, user.ID)
if err != nil { if err != nil {
return user.Username return user.Username
} }
b.userMemberMap[user.ID] = member
// only return if nick is set // only return if nick is set
if b.userMemberMap[user.ID].Nick != "" { if b.userMemberMap[user.ID].Nick != "" {
return b.userMemberMap[user.ID].Nick return b.userMemberMap[user.ID].Nick
@ -195,3 +215,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

@ -82,7 +82,7 @@ func (b *Bgitter) JoinChannel(channel string) error {
if !strings.HasSuffix(ev.Message.Text, "") { if !strings.HasSuffix(ev.Message.Text, "") {
flog.Debugf("Sending message from %s on %s to gateway", ev.Message.From.Username, b.Account) flog.Debugf("Sending message from %s on %s to gateway", ev.Message.From.Username, b.Account)
b.Remote <- config.Message{Username: ev.Message.From.Username, Text: ev.Message.Text, Channel: room, b.Remote <- config.Message{Username: ev.Message.From.Username, Text: ev.Message.Text, Channel: room,
Account: b.Account, Avatar: b.getAvatar(ev.Message.From.Username)} Account: b.Account, Avatar: b.getAvatar(ev.Message.From.Username), UserID: ev.Message.From.ID}
} }
case *gitter.GitterConnectionClosed: case *gitter.GitterConnectionClosed:
flog.Errorf("connection with gitter closed for room %s", room) flog.Errorf("connection with gitter closed for room %s", room)

View File

@ -15,14 +15,15 @@ import (
) )
type Birc struct { type Birc struct {
i *irc.Connection i *irc.Connection
Nick string Nick string
names map[string][]string names map[string][]string
Config *config.Protocol Config *config.Protocol
Remote chan config.Message Remote chan config.Message
connected chan struct{} connected chan struct{}
Local chan config.Message // local queue for flood control Local chan config.Message // local queue for flood control
Account string Account string
FirstConnection bool
} }
var flog *log.Entry var flog *log.Entry
@ -46,6 +47,10 @@ 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
}
b.FirstConnection = true
return b return b
} }
@ -71,6 +76,8 @@ func (b *Birc) Connect() error {
i.SASLLogin = b.Config.NickServNick i.SASLLogin = b.Config.NickServNick
i.SASLPassword = b.Config.NickServPassword i.SASLPassword = b.Config.NickServPassword
i.TLSConfig = &tls.Config{InsecureSkipVerify: b.Config.SkipTLSVerify} i.TLSConfig = &tls.Config{InsecureSkipVerify: b.Config.SkipTLSVerify}
i.KeepAlive = time.Minute
i.PingFreq = time.Minute
if b.Config.Password != "" { if b.Config.Password != "" {
i.Password = b.Config.Password i.Password = b.Config.Password
} }
@ -87,12 +94,20 @@ func (b *Birc) Connect() error {
return fmt.Errorf("connection timed out") return fmt.Errorf("connection timed out")
} }
i.Debug = false i.Debug = false
// clear on reconnects
i.ClearCallback(ircm.RPL_WELCOME)
i.AddCallback(ircm.RPL_WELCOME, func(event *irc.Event) {
b.Remote <- config.Message{Username: "system", Text: "rejoin", Channel: "", Account: b.Account, Event: config.EVENT_REJOIN_CHANNELS}
// set our correct nick on reconnect if necessary
b.Nick = event.Nick
})
go i.Loop()
go b.doSend() go b.doSend()
return nil return nil
} }
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 +124,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 +184,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,7 +204,11 @@ func (b *Birc) handleJoinPart(event *irc.Event) {
return return
} }
} }
b.Remote <- config.Message{Username: "system", Text: event.Nick + " " + strings.ToLower(event.Code) + "s", Channel: channel, Account: b.Account, Event: config.EVENT_JOIN_LEAVE} if event.Nick != b.Nick {
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}
return
}
flog.Debugf("handle %#v", event) flog.Debugf("handle %#v", event)
} }
@ -203,6 +229,11 @@ func (b *Birc) handleOther(event *irc.Event) {
} }
func (b *Birc) handlePrivMsg(event *irc.Event) { func (b *Birc) handlePrivMsg(event *irc.Event) {
b.Nick = b.i.GetNick()
// freenode doesn't send 001 as first reply
if event.Code == "NOTICE" {
return
}
// don't forward queries to the bot // don't forward queries to the bot
if event.Arguments[0] == b.Nick { if event.Arguments[0] == b.Nick {
return return
@ -221,7 +252,7 @@ func (b *Birc) handlePrivMsg(event *irc.Event) {
re := regexp.MustCompile(`[[:cntrl:]](\d+,|)\d+`) re := regexp.MustCompile(`[[:cntrl:]](\d+,|)\d+`)
msg = re.ReplaceAllString(msg, "") msg = re.ReplaceAllString(msg, "")
flog.Debugf("Sending message from %s on %s to gateway", event.Arguments[0], b.Account) flog.Debugf("Sending message from %s on %s to gateway", event.Arguments[0], b.Account)
b.Remote <- config.Message{Username: event.Nick, Text: msg, Channel: event.Arguments[0], Account: b.Account} b.Remote <- config.Message{Username: event.Nick, Text: msg, Channel: event.Arguments[0], Account: b.Account, UserID: event.User + "@" + event.Host}
} }
func (b *Birc) handleTopicWhoTime(event *irc.Event) { func (b *Birc) handleTopicWhoTime(event *irc.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, UserID: ev.Sender}
} }
flog.Debugf("Received: %#v", ev) flog.Debugf("Received: %#v", ev)
}) })

View File

@ -21,6 +21,7 @@ type MMMessage struct {
Text string Text string
Channel string Channel string
Username string Username string
UserID string
} }
type Bmattermost struct { type Bmattermost struct {
@ -72,6 +73,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 +98,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 = ""
@ -130,20 +128,36 @@ func (b *Bmattermost) handleMatter() {
} }
for message := range mchan { for message := range mchan {
flog.Debugf("Sending message from %s on %s to gateway", message.Username, b.Account) flog.Debugf("Sending message from %s on %s to gateway", message.Username, b.Account)
b.Remote <- config.Message{Text: message.Text, Username: message.Username, Channel: message.Channel, Account: b.Account} b.Remote <- config.Message{Text: message.Text, Username: message.Username, Channel: message.Channel, Account: b.Account, UserID: message.UserID}
} }
} }
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
}
if (message.Raw.Event == "post_edited") && b.Config.EditDisable {
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.UserID = message.UserID
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
@ -159,6 +173,7 @@ func (b *Bmattermost) handleMatterHook(mchan chan *MMMessage) {
message := b.mh.Receive() message := b.mh.Receive()
flog.Debugf("Receiving from matterhook %#v", message) flog.Debugf("Receiving from matterhook %#v", message)
m := &MMMessage{} m := &MMMessage{}
m.UserID = message.UserID
m.Username = message.UserName m.Username = message.UserName
m.Text = message.Text m.Text = message.Text
m.Channel = message.ChannelName m.Channel = message.ChannelName

View File

@ -82,6 +82,6 @@ func (b *Brocketchat) handleRocketHook() {
continue continue
} }
flog.Debugf("Sending message from %s on %s to gateway", message.UserName, b.Account) flog.Debugf("Sending message from %s on %s to gateway", message.UserName, b.Account)
b.Remote <- config.Message{Text: message.Text, Username: message.UserName, Channel: message.ChannelName, Account: b.Account} b.Remote <- config.Message{Text: message.Text, Username: message.UserName, Channel: message.ChannelName, Account: b.Account, UserID: message.UserID}
} }
} }

View File

@ -15,6 +15,7 @@ type MMMessage struct {
Text string Text string
Channel string Channel string
Username string Username string
UserID string
Raw *slack.MessageEvent Raw *slack.MessageEvent
} }
@ -73,9 +74,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 +90,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
@ -182,7 +186,7 @@ func (b *Bslack) handleSlack() {
texts := strings.Split(message.Text, "\n") texts := strings.Split(message.Text, "\n")
for _, text := range texts { for _, text := range texts {
flog.Debugf("Sending message from %s on %s to gateway", message.Username, b.Account) flog.Debugf("Sending message from %s on %s to gateway", message.Username, b.Account)
b.Remote <- config.Message{Text: text, Username: message.Username, Channel: message.Channel, Account: b.Account, Avatar: b.getAvatar(message.Username)} b.Remote <- config.Message{Text: text, Username: message.Username, Channel: message.Channel, Account: b.Account, Avatar: b.getAvatar(message.Username), UserID: message.UserID}
} }
} }
} }
@ -195,6 +199,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 {
@ -205,6 +214,7 @@ func (b *Bslack) handleSlackClient(mchan chan *MMMessage) {
continue continue
} }
m := &MMMessage{} m := &MMMessage{}
m.UserID = user.ID
m.Username = user.Name m.Username = user.Name
m.Channel = channel.Name m.Channel = channel.Name
m.Text = ev.Text m.Text = ev.Text

View File

@ -76,38 +76,69 @@ 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"
} }
if message.Sticker != nil {
text = text + " " + b.getFileDirectURL(message.Sticker.FileID)
}
if message.Video != nil {
text = text + " " + b.getFileDirectURL(message.Video.FileID)
}
if message.Photo != nil {
photos := *message.Photo
// last photo is the biggest
text = text + " " + b.getFileDirectURL(photos[len(photos)-1].FileID)
}
if message.Document != nil {
text = text + " " + message.Document.FileName + " : " + b.getFileDirectURL(message.Document.FileID)
}
if text != "" { if text != "" {
flog.Debugf("Sending message from %s on %s to gateway", username, b.Account) flog.Debugf("Sending message from %s on %s to gateway", username, b.Account)
b.Remote <- config.Message{Username: username, Text: text, Channel: channel, Account: b.Account} b.Remote <- config.Message{Username: username, Text: text, Channel: channel, Account: b.Account, UserID: strconv.Itoa(message.From.ID)}
} }
} }
} }
func (b *Btelegram) getFileDirectURL(id string) string {
res, err := b.c.GetFileDirectURL(id)
if err != nil {
return ""
}
return res
}

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], "/")
@ -128,7 +128,7 @@ func (b *Bxmpp) handleXmpp() error {
} }
if nick != b.Config.Nick && v.Stamp == nodelay && v.Text != "" { if nick != b.Config.Nick && v.Stamp == nodelay && v.Text != "" {
flog.Debugf("Sending message from %s on %s to gateway", nick, b.Account) flog.Debugf("Sending message from %s on %s to gateway", nick, b.Account)
b.Remote <- config.Message{Username: nick, Text: v.Text, Channel: channel, Account: b.Account} b.Remote <- config.Message{Username: nick, Text: v.Text, Channel: channel, Account: b.Account, UserID: v.Remote}
} }
} }
case xmpp.Presence: case xmpp.Presence:

View File

@ -1,3 +1,110 @@
# v0.15.0
## New features
* general: add option IgnoreMessages for all protocols (see mattebridge.toml.sample)
Messages matching these regexp will be ignored and not sent to other bridges
e.g. IgnoreMessages="^~~ badword"
* telegram: add support for sticker/video/photo/document #184
## Changes
* api: add userid to each message #200
## Bugfix
* discord: fix crash in memberupdate #198
* mattermost: Fix incorrect behaviour of EditDisable (mattermost). Fixes #197
* irc: Do not relay join/part of ourselves (irc). Closes #190
* irc: make reconnections more robust. #153
* gitter: update library, fixes possible crash
# 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
## New features
* general: gops agent added. Allows for more debugging. See #134
* general: toml inline table support added for config file
## Bugfix
* all: vendored libs updated
## Changes
* general: add more informative messages on startup
# v0.10.1 # v0.10.1
## Bugfix ## Bugfix
* gitter: Fix sending messages on new channel join. * gitter: Fix sending messages on new channel join.

View File

@ -5,7 +5,7 @@ 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" "regexp"
"strings" "strings"
"time" "time"
) )
@ -14,21 +14,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 +36,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 +59,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 +77,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 +104,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 +134,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 +193,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" {
@ -191,22 +231,21 @@ func (gw *Gateway) ignoreMessage(msg *config.Message) bool {
return true return true
} }
} }
return false // TODO do not compile regexps everytime
} for _, entry := range strings.Fields(gw.Bridges[msg.Account].Config.IgnoreMessages) {
if entry != "" {
func (gw *Gateway) modifyMessage(msg *config.Message, dest *bridge.Bridge) { re, err := regexp.Compile(entry)
val := reflect.ValueOf(gw.Config).Elem() if err != nil {
for i := 0; i < val.NumField(); i++ { log.Errorf("incorrect regexp %s for %s", entry, msg.Account)
typeField := val.Type().Field(i) continue
// look for the protocol map (both lowercase) }
if strings.ToLower(typeField.Name) == dest.Protocol { if re.MatchString(msg.Text) {
// get the Protocol struct from the map log.Debugf("matching %s. ignoring %s from %s", entry, msg.Text, msg.Account)
protoCfg := val.Field(i).MapIndex(reflect.ValueOf(dest.Name)) return true
//config.SetNickFormat(msg, protoCfg.Interface().(config.Protocol)) }
val.Field(i).SetMapIndex(reflect.ValueOf(dest.Name), protoCfg)
break
} }
} }
return false
} }
func (gw *Gateway) modifyUsername(msg *config.Message, dest *bridge.Bridge) { func (gw *Gateway) modifyUsername(msg *config.Message, dest *bridge.Bridge) {
@ -216,8 +255,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-dev" version = "0.15.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,23 +49,36 @@ 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
IgnoreNicks="ircspammer1 ircspammer2" IgnoreNicks="ircspammer1 ircspammer2"
#Messages you want to ignore.
#Messages matching these regexp will be ignored and not sent to other bridges
#See https://regex-golang.appspot.com/assets/html/index.html for more regex info
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
IgnoreMessages="^~~ badword"
#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
#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
@ -107,6 +121,12 @@ SkipTLSVerify=true
#OPTIONAL #OPTIONAL
IgnoreNicks="ircspammer1 ircspammer2" IgnoreNicks="ircspammer1 ircspammer2"
#Messages you want to ignore.
#Messages matching these regexp will be ignored and not sent to other bridges
#See https://regex-golang.appspot.com/assets/html/index.html for more regex info
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
IgnoreMessages="^~~ badword"
#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
@ -114,7 +134,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
@ -150,6 +171,12 @@ Nick="yourlogin"
#OPTIONAL #OPTIONAL
IgnoreNicks="spammer1 spammer2" IgnoreNicks="spammer1 spammer2"
#Messages you want to ignore.
#Messages matching these regexp will be ignored and not sent to other bridges
#See https://regex-golang.appspot.com/assets/html/index.html for more regex info
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
IgnoreMessages="^~~ badword"
#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
@ -157,7 +184,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 +225,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,11 +266,25 @@ 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
IgnoreNicks="ircspammer1 ircspammer2" IgnoreNicks="ircspammer1 ircspammer2"
#Messages you want to ignore.
#Messages matching these regexp will be ignored and not sent to other bridges
#See https://regex-golang.appspot.com/assets/html/index.html for more regex info
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
IgnoreMessages="^~~ badword"
#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
@ -250,7 +292,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
@ -275,6 +318,12 @@ Token="Yourtokenhere"
#OPTIONAL #OPTIONAL
IgnoreNicks="ircspammer1 ircspammer2" IgnoreNicks="ircspammer1 ircspammer2"
#Messages you want to ignore.
#Messages matching these regexp will be ignored and not sent to other bridges
#See https://regex-golang.appspot.com/assets/html/index.html for more regex info
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
IgnoreMessages="^~~ badword"
#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
@ -282,7 +331,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 +392,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
@ -355,6 +413,12 @@ PrefixMessagesWithNick=false
#OPTIONAL #OPTIONAL
IgnoreNicks="ircspammer1 ircspammer2" IgnoreNicks="ircspammer1 ircspammer2"
#Messages you want to ignore.
#Messages matching these regexp will be ignored and not sent to other bridges
#See https://regex-golang.appspot.com/assets/html/index.html for more regex info
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
IgnoreMessages="^~~ badword"
#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
@ -362,7 +426,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,11 +450,25 @@ 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
IgnoreNicks="ircspammer1 ircspammer2" IgnoreNicks="ircspammer1 ircspammer2"
#Messages you want to ignore.
#Messages matching these regexp will be ignored and not sent to other bridges
#See https://regex-golang.appspot.com/assets/html/index.html for more regex info
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
IgnoreMessages="^~~ badword"
#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
@ -397,7 +476,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,11 +500,31 @@ 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
IgnoreNicks="spammer1 spammer2" IgnoreNicks="spammer1 spammer2"
#Messages you want to ignore.
#Messages matching these regexp will be ignored and not sent to other bridges
#See https://regex-golang.appspot.com/assets/html/index.html for more regex info
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
IgnoreMessages="^~~ badword"
#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
@ -432,7 +532,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
@ -482,6 +583,12 @@ PrefixMessagesWithNick=false
#OPTIONAL #OPTIONAL
IgnoreNicks="ircspammer1 ircspammer2" IgnoreNicks="ircspammer1 ircspammer2"
#Messages you want to ignore.
#Messages matching these regexp will be ignored and not sent to other bridges
#See https://regex-golang.appspot.com/assets/html/index.html for more regex info
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
IgnoreMessages="^~~ badword"
#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
@ -489,7 +596,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 +621,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
@ -525,6 +638,12 @@ PrefixMessagesWithNick=false
#OPTIONAL #OPTIONAL
IgnoreNicks="spammer1 spammer2" IgnoreNicks="spammer1 spammer2"
#Messages you want to ignore.
#Messages matching these regexp will be ignored and not sent to other bridges
#See https://regex-golang.appspot.com/assets/html/index.html for more regex info
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
IgnoreMessages="^~~ badword"
#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
@ -532,7 +651,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 +673,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 +685,8 @@ Buffer=1000
#OPTIONAL (default empty) #OPTIONAL (default empty)
RemoteNickFormat="{NICK}" RemoteNickFormat="{NICK}"
################################################################### ###################################################################
#General configuration #General configuration
################################################################### ###################################################################
@ -587,7 +714,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 +730,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
@ -614,8 +741,9 @@ enable=true
#telegram - chatid (a large negative number, eg -123456789) #telegram - chatid (a large negative number, eg -123456789)
# 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 (also needed for private channels!)
#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 +779,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 +789,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,18 +17,19 @@
[[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]] #simpler config possible since v0.10.2
account="mattermost.work" #[[gateway]]
channel="off-topic" #name="gateway2"
#enable=true
#inout = [
# { account="irc.freenode", channel="#testing", options={key="channelkey"}},
# { account="mattermost.work", channel="off-topic" },
#]

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,8 @@ type Message struct {
Channel string Channel string
Username string Username string
Text string Text string
Type string
UserID string
} }
type Team struct { type Team struct {
@ -47,19 +51,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 +107,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"
@ -159,11 +183,11 @@ func (m *MMClient) Login() error {
m.Client.SetTeamId(m.Team.Id) m.Client.SetTeamId(m.Team.Id)
// setup websocket connection // setup websocket connection
wsurl := wsScheme + m.Credentials.Server + model.API_URL_SUFFIX + "/users/websocket" wsurl := wsScheme + m.Credentials.Server + model.API_URL_SUFFIX_V3 + "/users/websocket"
header := http.Header{} header := http.Header{}
header.Set(model.HEADER_AUTH, "BEARER "+m.Client.AuthToken) header.Set(model.HEADER_AUTH, "BEARER "+m.Client.AuthToken)
m.log.Debug("WsClient: making connection") m.log.Debugf("WsClient: making connection: %s", wsurl)
for { for {
wsDialer := &websocket.Dialer{Proxy: http.ProxyFromEnvironment, TLSClientConfig: &tls.Config{InsecureSkipVerify: m.SkipTLSVerify}} wsDialer := &websocket.Dialer{Proxy: http.ProxyFromEnvironment, TLSClientConfig: &tls.Config{InsecureSkipVerify: m.SkipTLSVerify}}
m.WsClient, _, err = wsDialer.Dial(wsurl, header) m.WsClient, _, err = wsDialer.Dial(wsurl, header)
@ -177,6 +201,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 +263,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 +289,20 @@ 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.UserID = data.UserId
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 +328,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 +375,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 +481,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 +635,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 +698,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 +730,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 +762,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

@ -4,7 +4,7 @@ files via reflection. There is also support for delaying decoding with
the Primitive type, and querying the set of keys in a TOML document with the the Primitive type, and querying the set of keys in a TOML document with the
MetaData type. MetaData type.
The specification implemented: https://github.com/mojombo/toml The specification implemented: https://github.com/toml-lang/toml
The sub-command github.com/BurntSushi/toml/cmd/tomlv can be used to verify The sub-command github.com/BurntSushi/toml/cmd/tomlv can be used to verify
whether a file is a valid TOML document. It can also be used to print the whether a file is a valid TOML document. It can also be used to print the

View File

@ -241,7 +241,7 @@ func (enc *Encoder) eArrayOfTables(key Key, rv reflect.Value) {
func (enc *Encoder) eTable(key Key, rv reflect.Value) { func (enc *Encoder) eTable(key Key, rv reflect.Value) {
panicIfInvalidKey(key) panicIfInvalidKey(key)
if len(key) == 1 { if len(key) == 1 {
// Output an extra new line between top-level tables. // Output an extra newline between top-level tables.
// (The newline isn't written if nothing else has been written though.) // (The newline isn't written if nothing else has been written though.)
enc.newline() enc.newline()
} }

View File

@ -30,24 +30,28 @@ const (
itemArrayTableEnd itemArrayTableEnd
itemKeyStart itemKeyStart
itemCommentStart itemCommentStart
itemInlineTableStart
itemInlineTableEnd
) )
const ( const (
eof = 0 eof = 0
tableStart = '[' comma = ','
tableEnd = ']' tableStart = '['
arrayTableStart = '[' tableEnd = ']'
arrayTableEnd = ']' arrayTableStart = '['
tableSep = '.' arrayTableEnd = ']'
keySep = '=' tableSep = '.'
arrayStart = '[' keySep = '='
arrayEnd = ']' arrayStart = '['
arrayValTerm = ',' arrayEnd = ']'
commentStart = '#' commentStart = '#'
stringStart = '"' stringStart = '"'
stringEnd = '"' stringEnd = '"'
rawStringStart = '\'' rawStringStart = '\''
rawStringEnd = '\'' rawStringEnd = '\''
inlineTableStart = '{'
inlineTableEnd = '}'
) )
type stateFn func(lx *lexer) stateFn type stateFn func(lx *lexer) stateFn
@ -56,11 +60,18 @@ type lexer struct {
input string input string
start int start int
pos int pos int
width int
line int line int
state stateFn state stateFn
items chan item items chan item
// Allow for backing up up to three runes.
// This is necessary because TOML contains 3-rune tokens (""" and ''').
prevWidths [3]int
nprev int // how many of prevWidths are in use
// If we emit an eof, we can still back up, but it is not OK to call
// next again.
atEOF bool
// A stack of state functions used to maintain context. // A stack of state functions used to maintain context.
// The idea is to reuse parts of the state machine in various places. // The idea is to reuse parts of the state machine in various places.
// For example, values can appear at the top level or within arbitrarily // For example, values can appear at the top level or within arbitrarily
@ -88,7 +99,7 @@ func (lx *lexer) nextItem() item {
func lex(input string) *lexer { func lex(input string) *lexer {
lx := &lexer{ lx := &lexer{
input: input + "\n", input: input,
state: lexTop, state: lexTop,
line: 1, line: 1,
items: make(chan item, 10), items: make(chan item, 10),
@ -103,7 +114,7 @@ func (lx *lexer) push(state stateFn) {
func (lx *lexer) pop() stateFn { func (lx *lexer) pop() stateFn {
if len(lx.stack) == 0 { if len(lx.stack) == 0 {
return lx.errorf("BUG in lexer: no states to pop.") return lx.errorf("BUG in lexer: no states to pop")
} }
last := lx.stack[len(lx.stack)-1] last := lx.stack[len(lx.stack)-1]
lx.stack = lx.stack[0 : len(lx.stack)-1] lx.stack = lx.stack[0 : len(lx.stack)-1]
@ -125,16 +136,25 @@ func (lx *lexer) emitTrim(typ itemType) {
} }
func (lx *lexer) next() (r rune) { func (lx *lexer) next() (r rune) {
if lx.atEOF {
panic("next called after EOF")
}
if lx.pos >= len(lx.input) { if lx.pos >= len(lx.input) {
lx.width = 0 lx.atEOF = true
return eof return eof
} }
if lx.input[lx.pos] == '\n' { if lx.input[lx.pos] == '\n' {
lx.line++ lx.line++
} }
r, lx.width = utf8.DecodeRuneInString(lx.input[lx.pos:]) lx.prevWidths[2] = lx.prevWidths[1]
lx.pos += lx.width lx.prevWidths[1] = lx.prevWidths[0]
if lx.nprev < 3 {
lx.nprev++
}
r, w := utf8.DecodeRuneInString(lx.input[lx.pos:])
lx.prevWidths[0] = w
lx.pos += w
return r return r
} }
@ -143,9 +163,20 @@ func (lx *lexer) ignore() {
lx.start = lx.pos lx.start = lx.pos
} }
// backup steps back one rune. Can be called only once per call of next. // backup steps back one rune. Can be called only twice between calls to next.
func (lx *lexer) backup() { func (lx *lexer) backup() {
lx.pos -= lx.width if lx.atEOF {
lx.atEOF = false
return
}
if lx.nprev < 1 {
panic("backed up too far")
}
w := lx.prevWidths[0]
lx.prevWidths[0] = lx.prevWidths[1]
lx.prevWidths[1] = lx.prevWidths[2]
lx.nprev--
lx.pos -= w
if lx.pos < len(lx.input) && lx.input[lx.pos] == '\n' { if lx.pos < len(lx.input) && lx.input[lx.pos] == '\n' {
lx.line-- lx.line--
} }
@ -182,7 +213,7 @@ func (lx *lexer) skip(pred func(rune) bool) {
// errorf stops all lexing by emitting an error and returning `nil`. // errorf stops all lexing by emitting an error and returning `nil`.
// Note that any value that is a character is escaped if it's a special // Note that any value that is a character is escaped if it's a special
// character (new lines, tabs, etc.). // character (newlines, tabs, etc.).
func (lx *lexer) errorf(format string, values ...interface{}) stateFn { func (lx *lexer) errorf(format string, values ...interface{}) stateFn {
lx.items <- item{ lx.items <- item{
itemError, itemError,
@ -198,7 +229,6 @@ func lexTop(lx *lexer) stateFn {
if isWhitespace(r) || isNL(r) { if isWhitespace(r) || isNL(r) {
return lexSkip(lx, lexTop) return lexSkip(lx, lexTop)
} }
switch r { switch r {
case commentStart: case commentStart:
lx.push(lexTop) lx.push(lexTop)
@ -207,7 +237,7 @@ func lexTop(lx *lexer) stateFn {
return lexTableStart return lexTableStart
case eof: case eof:
if lx.pos > lx.start { if lx.pos > lx.start {
return lx.errorf("Unexpected EOF.") return lx.errorf("unexpected EOF")
} }
lx.emit(itemEOF) lx.emit(itemEOF)
return nil return nil
@ -222,12 +252,12 @@ func lexTop(lx *lexer) stateFn {
// lexTopEnd is entered whenever a top-level item has been consumed. (A value // lexTopEnd is entered whenever a top-level item has been consumed. (A value
// or a table.) It must see only whitespace, and will turn back to lexTop // or a table.) It must see only whitespace, and will turn back to lexTop
// upon a new line. If it sees EOF, it will quit the lexer successfully. // upon a newline. If it sees EOF, it will quit the lexer successfully.
func lexTopEnd(lx *lexer) stateFn { func lexTopEnd(lx *lexer) stateFn {
r := lx.next() r := lx.next()
switch { switch {
case r == commentStart: case r == commentStart:
// a comment will read to a new line for us. // a comment will read to a newline for us.
lx.push(lexTop) lx.push(lexTop)
return lexCommentStart return lexCommentStart
case isWhitespace(r): case isWhitespace(r):
@ -236,11 +266,11 @@ func lexTopEnd(lx *lexer) stateFn {
lx.ignore() lx.ignore()
return lexTop return lexTop
case r == eof: case r == eof:
lx.ignore() lx.emit(itemEOF)
return lexTop return nil
} }
return lx.errorf("Expected a top-level item to end with a new line, "+ return lx.errorf("expected a top-level item to end with a newline, "+
"comment or EOF, but got %q instead.", r) "comment, or EOF, but got %q instead", r)
} }
// lexTable lexes the beginning of a table. Namely, it makes sure that // lexTable lexes the beginning of a table. Namely, it makes sure that
@ -267,8 +297,8 @@ func lexTableEnd(lx *lexer) stateFn {
func lexArrayTableEnd(lx *lexer) stateFn { func lexArrayTableEnd(lx *lexer) stateFn {
if r := lx.next(); r != arrayTableEnd { if r := lx.next(); r != arrayTableEnd {
return lx.errorf("Expected end of table array name delimiter %q, "+ return lx.errorf("expected end of table array name delimiter %q, "+
"but got %q instead.", arrayTableEnd, r) "but got %q instead", arrayTableEnd, r)
} }
lx.emit(itemArrayTableEnd) lx.emit(itemArrayTableEnd)
return lexTopEnd return lexTopEnd
@ -278,11 +308,11 @@ func lexTableNameStart(lx *lexer) stateFn {
lx.skip(isWhitespace) lx.skip(isWhitespace)
switch r := lx.peek(); { switch r := lx.peek(); {
case r == tableEnd || r == eof: case r == tableEnd || r == eof:
return lx.errorf("Unexpected end of table name. (Table names cannot " + return lx.errorf("unexpected end of table name " +
"be empty.)") "(table names cannot be empty)")
case r == tableSep: case r == tableSep:
return lx.errorf("Unexpected table separator. (Table names cannot " + return lx.errorf("unexpected table separator " +
"be empty.)") "(table names cannot be empty)")
case r == stringStart || r == rawStringStart: case r == stringStart || r == rawStringStart:
lx.ignore() lx.ignore()
lx.push(lexTableNameEnd) lx.push(lexTableNameEnd)
@ -317,8 +347,8 @@ func lexTableNameEnd(lx *lexer) stateFn {
case r == tableEnd: case r == tableEnd:
return lx.pop() return lx.pop()
default: default:
return lx.errorf("Expected '.' or ']' to end table name, but got %q "+ return lx.errorf("expected '.' or ']' to end table name, "+
"instead.", r) "but got %q instead", r)
} }
} }
@ -328,7 +358,7 @@ func lexKeyStart(lx *lexer) stateFn {
r := lx.peek() r := lx.peek()
switch { switch {
case r == keySep: case r == keySep:
return lx.errorf("Unexpected key separator %q.", keySep) return lx.errorf("unexpected key separator %q", keySep)
case isWhitespace(r) || isNL(r): case isWhitespace(r) || isNL(r):
lx.next() lx.next()
return lexSkip(lx, lexKeyStart) return lexSkip(lx, lexKeyStart)
@ -359,7 +389,7 @@ func lexBareKey(lx *lexer) stateFn {
lx.emit(itemText) lx.emit(itemText)
return lexKeyEnd return lexKeyEnd
default: default:
return lx.errorf("Bare keys cannot contain %q.", r) return lx.errorf("bare keys cannot contain %q", r)
} }
} }
@ -372,7 +402,7 @@ func lexKeyEnd(lx *lexer) stateFn {
case isWhitespace(r): case isWhitespace(r):
return lexSkip(lx, lexKeyEnd) return lexSkip(lx, lexKeyEnd)
default: default:
return lx.errorf("Expected key separator %q, but got %q instead.", return lx.errorf("expected key separator %q, but got %q instead",
keySep, r) keySep, r)
} }
} }
@ -381,9 +411,8 @@ func lexKeyEnd(lx *lexer) stateFn {
// lexValue will ignore whitespace. // lexValue will ignore whitespace.
// After a value is lexed, the last state on the next is popped and returned. // After a value is lexed, the last state on the next is popped and returned.
func lexValue(lx *lexer) stateFn { func lexValue(lx *lexer) stateFn {
// We allow whitespace to precede a value, but NOT new lines. // We allow whitespace to precede a value, but NOT newlines.
// In array syntax, the array states are responsible for ignoring new // In array syntax, the array states are responsible for ignoring newlines.
// lines.
r := lx.next() r := lx.next()
switch { switch {
case isWhitespace(r): case isWhitespace(r):
@ -397,6 +426,10 @@ func lexValue(lx *lexer) stateFn {
lx.ignore() lx.ignore()
lx.emit(itemArray) lx.emit(itemArray)
return lexArrayValue return lexArrayValue
case inlineTableStart:
lx.ignore()
lx.emit(itemInlineTableStart)
return lexInlineTableValue
case stringStart: case stringStart:
if lx.accept(stringStart) { if lx.accept(stringStart) {
if lx.accept(stringStart) { if lx.accept(stringStart) {
@ -420,7 +453,7 @@ func lexValue(lx *lexer) stateFn {
case '+', '-': case '+', '-':
return lexNumberStart return lexNumberStart
case '.': // special error case, be kind to users case '.': // special error case, be kind to users
return lx.errorf("Floats must start with a digit, not '.'.") return lx.errorf("floats must start with a digit, not '.'")
} }
if unicode.IsLetter(r) { if unicode.IsLetter(r) {
// Be permissive here; lexBool will give a nice error if the // Be permissive here; lexBool will give a nice error if the
@ -430,11 +463,11 @@ func lexValue(lx *lexer) stateFn {
lx.backup() lx.backup()
return lexBool return lexBool
} }
return lx.errorf("Expected value but found %q instead.", r) return lx.errorf("expected value but found %q instead", r)
} }
// lexArrayValue consumes one value in an array. It assumes that '[' or ',' // lexArrayValue consumes one value in an array. It assumes that '[' or ','
// have already been consumed. All whitespace and new lines are ignored. // have already been consumed. All whitespace and newlines are ignored.
func lexArrayValue(lx *lexer) stateFn { func lexArrayValue(lx *lexer) stateFn {
r := lx.next() r := lx.next()
switch { switch {
@ -443,10 +476,11 @@ func lexArrayValue(lx *lexer) stateFn {
case r == commentStart: case r == commentStart:
lx.push(lexArrayValue) lx.push(lexArrayValue)
return lexCommentStart return lexCommentStart
case r == arrayValTerm: case r == comma:
return lx.errorf("Unexpected array value terminator %q.", return lx.errorf("unexpected comma")
arrayValTerm)
case r == arrayEnd: case r == arrayEnd:
// NOTE(caleb): The spec isn't clear about whether you can have
// a trailing comma or not, so we'll allow it.
return lexArrayEnd return lexArrayEnd
} }
@ -455,8 +489,9 @@ func lexArrayValue(lx *lexer) stateFn {
return lexValue return lexValue
} }
// lexArrayValueEnd consumes the cruft between values of an array. Namely, // lexArrayValueEnd consumes everything between the end of an array value and
// it ignores whitespace and expects either a ',' or a ']'. // the next value (or the end of the array): it ignores whitespace and newlines
// and expects either a ',' or a ']'.
func lexArrayValueEnd(lx *lexer) stateFn { func lexArrayValueEnd(lx *lexer) stateFn {
r := lx.next() r := lx.next()
switch { switch {
@ -465,31 +500,88 @@ func lexArrayValueEnd(lx *lexer) stateFn {
case r == commentStart: case r == commentStart:
lx.push(lexArrayValueEnd) lx.push(lexArrayValueEnd)
return lexCommentStart return lexCommentStart
case r == arrayValTerm: case r == comma:
lx.ignore() lx.ignore()
return lexArrayValue // move on to the next value return lexArrayValue // move on to the next value
case r == arrayEnd: case r == arrayEnd:
return lexArrayEnd return lexArrayEnd
} }
return lx.errorf("Expected an array value terminator %q or an array "+ return lx.errorf(
"terminator %q, but got %q instead.", arrayValTerm, arrayEnd, r) "expected a comma or array terminator %q, but got %q instead",
arrayEnd, r,
)
} }
// lexArrayEnd finishes the lexing of an array. It assumes that a ']' has // lexArrayEnd finishes the lexing of an array.
// just been consumed. // It assumes that a ']' has just been consumed.
func lexArrayEnd(lx *lexer) stateFn { func lexArrayEnd(lx *lexer) stateFn {
lx.ignore() lx.ignore()
lx.emit(itemArrayEnd) lx.emit(itemArrayEnd)
return lx.pop() return lx.pop()
} }
// lexInlineTableValue consumes one key/value pair in an inline table.
// It assumes that '{' or ',' have already been consumed. Whitespace is ignored.
func lexInlineTableValue(lx *lexer) stateFn {
r := lx.next()
switch {
case isWhitespace(r):
return lexSkip(lx, lexInlineTableValue)
case isNL(r):
return lx.errorf("newlines not allowed within inline tables")
case r == commentStart:
lx.push(lexInlineTableValue)
return lexCommentStart
case r == comma:
return lx.errorf("unexpected comma")
case r == inlineTableEnd:
return lexInlineTableEnd
}
lx.backup()
lx.push(lexInlineTableValueEnd)
return lexKeyStart
}
// lexInlineTableValueEnd consumes everything between the end of an inline table
// key/value pair and the next pair (or the end of the table):
// it ignores whitespace and expects either a ',' or a '}'.
func lexInlineTableValueEnd(lx *lexer) stateFn {
r := lx.next()
switch {
case isWhitespace(r):
return lexSkip(lx, lexInlineTableValueEnd)
case isNL(r):
return lx.errorf("newlines not allowed within inline tables")
case r == commentStart:
lx.push(lexInlineTableValueEnd)
return lexCommentStart
case r == comma:
lx.ignore()
return lexInlineTableValue
case r == inlineTableEnd:
return lexInlineTableEnd
}
return lx.errorf("expected a comma or an inline table terminator %q, "+
"but got %q instead", inlineTableEnd, r)
}
// lexInlineTableEnd finishes the lexing of an inline table.
// It assumes that a '}' has just been consumed.
func lexInlineTableEnd(lx *lexer) stateFn {
lx.ignore()
lx.emit(itemInlineTableEnd)
return lx.pop()
}
// lexString consumes the inner contents of a string. It assumes that the // lexString consumes the inner contents of a string. It assumes that the
// beginning '"' has already been consumed and ignored. // beginning '"' has already been consumed and ignored.
func lexString(lx *lexer) stateFn { func lexString(lx *lexer) stateFn {
r := lx.next() r := lx.next()
switch { switch {
case r == eof:
return lx.errorf("unexpected EOF")
case isNL(r): case isNL(r):
return lx.errorf("Strings cannot contain new lines.") return lx.errorf("strings cannot contain newlines")
case r == '\\': case r == '\\':
lx.push(lexString) lx.push(lexString)
return lexStringEscape return lexStringEscape
@ -506,11 +598,12 @@ func lexString(lx *lexer) stateFn {
// lexMultilineString consumes the inner contents of a string. It assumes that // lexMultilineString consumes the inner contents of a string. It assumes that
// the beginning '"""' has already been consumed and ignored. // the beginning '"""' has already been consumed and ignored.
func lexMultilineString(lx *lexer) stateFn { func lexMultilineString(lx *lexer) stateFn {
r := lx.next() switch lx.next() {
switch { case eof:
case r == '\\': return lx.errorf("unexpected EOF")
case '\\':
return lexMultilineStringEscape return lexMultilineStringEscape
case r == stringEnd: case stringEnd:
if lx.accept(stringEnd) { if lx.accept(stringEnd) {
if lx.accept(stringEnd) { if lx.accept(stringEnd) {
lx.backup() lx.backup()
@ -534,8 +627,10 @@ func lexMultilineString(lx *lexer) stateFn {
func lexRawString(lx *lexer) stateFn { func lexRawString(lx *lexer) stateFn {
r := lx.next() r := lx.next()
switch { switch {
case r == eof:
return lx.errorf("unexpected EOF")
case isNL(r): case isNL(r):
return lx.errorf("Strings cannot contain new lines.") return lx.errorf("strings cannot contain newlines")
case r == rawStringEnd: case r == rawStringEnd:
lx.backup() lx.backup()
lx.emit(itemRawString) lx.emit(itemRawString)
@ -547,12 +642,13 @@ func lexRawString(lx *lexer) stateFn {
} }
// lexMultilineRawString consumes a raw string. Nothing can be escaped in such // lexMultilineRawString consumes a raw string. Nothing can be escaped in such
// a string. It assumes that the beginning "'" has already been consumed and // a string. It assumes that the beginning "'''" has already been consumed and
// ignored. // ignored.
func lexMultilineRawString(lx *lexer) stateFn { func lexMultilineRawString(lx *lexer) stateFn {
r := lx.next() switch lx.next() {
switch { case eof:
case r == rawStringEnd: return lx.errorf("unexpected EOF")
case rawStringEnd:
if lx.accept(rawStringEnd) { if lx.accept(rawStringEnd) {
if lx.accept(rawStringEnd) { if lx.accept(rawStringEnd) {
lx.backup() lx.backup()
@ -605,10 +701,9 @@ func lexStringEscape(lx *lexer) stateFn {
case 'U': case 'U':
return lexLongUnicodeEscape return lexLongUnicodeEscape
} }
return lx.errorf("Invalid escape character %q. Only the following "+ return lx.errorf("invalid escape character %q; only the following "+
"escape characters are allowed: "+ "escape characters are allowed: "+
"\\b, \\t, \\n, \\f, \\r, \\\", \\/, \\\\, "+ `\b, \t, \n, \f, \r, \", \\, \uXXXX, and \UXXXXXXXX`, r)
"\\uXXXX and \\UXXXXXXXX.", r)
} }
func lexShortUnicodeEscape(lx *lexer) stateFn { func lexShortUnicodeEscape(lx *lexer) stateFn {
@ -616,8 +711,8 @@ func lexShortUnicodeEscape(lx *lexer) stateFn {
for i := 0; i < 4; i++ { for i := 0; i < 4; i++ {
r = lx.next() r = lx.next()
if !isHexadecimal(r) { if !isHexadecimal(r) {
return lx.errorf("Expected four hexadecimal digits after '\\u', "+ return lx.errorf(`expected four hexadecimal digits after '\u', `+
"but got '%s' instead.", lx.current()) "but got %q instead", lx.current())
} }
} }
return lx.pop() return lx.pop()
@ -628,8 +723,8 @@ func lexLongUnicodeEscape(lx *lexer) stateFn {
for i := 0; i < 8; i++ { for i := 0; i < 8; i++ {
r = lx.next() r = lx.next()
if !isHexadecimal(r) { if !isHexadecimal(r) {
return lx.errorf("Expected eight hexadecimal digits after '\\U', "+ return lx.errorf(`expected eight hexadecimal digits after '\U', `+
"but got '%s' instead.", lx.current()) "but got %q instead", lx.current())
} }
} }
return lx.pop() return lx.pop()
@ -647,9 +742,9 @@ func lexNumberOrDateStart(lx *lexer) stateFn {
case 'e', 'E': case 'e', 'E':
return lexFloat return lexFloat
case '.': case '.':
return lx.errorf("Floats must start with a digit, not '.'.") return lx.errorf("floats must start with a digit, not '.'")
} }
return lx.errorf("Expected a digit but got %q.", r) return lx.errorf("expected a digit but got %q", r)
} }
// lexNumberOrDate consumes either an integer, float or datetime. // lexNumberOrDate consumes either an integer, float or datetime.
@ -697,9 +792,9 @@ func lexNumberStart(lx *lexer) stateFn {
r := lx.next() r := lx.next()
if !isDigit(r) { if !isDigit(r) {
if r == '.' { if r == '.' {
return lx.errorf("Floats must start with a digit, not '.'.") return lx.errorf("floats must start with a digit, not '.'")
} }
return lx.errorf("Expected a digit but got %q.", r) return lx.errorf("expected a digit but got %q", r)
} }
return lexNumber return lexNumber
} }
@ -757,7 +852,7 @@ func lexBool(lx *lexer) stateFn {
lx.emit(itemBool) lx.emit(itemBool)
return lx.pop() return lx.pop()
} }
return lx.errorf("Expected value but found %q instead.", s) return lx.errorf("expected value but found %q instead", s)
} }
// lexCommentStart begins the lexing of a comment. It will emit // lexCommentStart begins the lexing of a comment. It will emit
@ -769,7 +864,7 @@ func lexCommentStart(lx *lexer) stateFn {
} }
// lexComment lexes an entire comment. It assumes that '#' has been consumed. // lexComment lexes an entire comment. It assumes that '#' has been consumed.
// It will consume *up to* the first new line character, and pass control // It will consume *up to* the first newline character, and pass control
// back to the last state on the stack. // back to the last state on the stack.
func lexComment(lx *lexer) stateFn { func lexComment(lx *lexer) stateFn {
r := lx.peek() r := lx.peek()

View File

@ -269,6 +269,41 @@ func (p *parser) value(it item) (interface{}, tomlType) {
types = append(types, typ) types = append(types, typ)
} }
return array, p.typeOfArray(types) return array, p.typeOfArray(types)
case itemInlineTableStart:
var (
hash = make(map[string]interface{})
outerContext = p.context
outerKey = p.currentKey
)
p.context = append(p.context, p.currentKey)
p.currentKey = ""
for it := p.next(); it.typ != itemInlineTableEnd; it = p.next() {
if it.typ != itemKeyStart {
p.bug("Expected key start but instead found %q, around line %d",
it.val, p.approxLine)
}
if it.typ == itemCommentStart {
p.expect(itemText)
continue
}
// retrieve key
k := p.next()
p.approxLine = k.line
kname := p.keyString(k)
// retrieve value
p.currentKey = kname
val, typ := p.value(p.next())
// make sure we keep metadata up to date
p.setType(kname, typ)
p.ordered = append(p.ordered, p.context.add(p.currentKey))
hash[kname] = val
}
p.context = outerContext
p.currentKey = outerKey
return hash, tomlHash
} }
p.bug("Unexpected value type: %s", it.typ) p.bug("Unexpected value type: %s", it.typ)
panic("unreachable") panic("unreachable")

64
vendor/github.com/Sirupsen/logrus/alt_exit.go generated vendored Normal file
View File

@ -0,0 +1,64 @@
package logrus
// The following code was sourced and modified from the
// https://github.com/tebeka/atexit package governed by the following license:
//
// Copyright (c) 2012 Miki Tebeka <miki.tebeka@gmail.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import (
"fmt"
"os"
)
var handlers = []func(){}
func runHandler(handler func()) {
defer func() {
if err := recover(); err != nil {
fmt.Fprintln(os.Stderr, "Error: Logrus exit handler error:", err)
}
}()
handler()
}
func runHandlers() {
for _, handler := range handlers {
runHandler(handler)
}
}
// Exit runs all the Logrus atexit handlers and then terminates the program using os.Exit(code)
func Exit(code int) {
runHandlers()
os.Exit(code)
}
// RegisterExitHandler adds a Logrus Exit handler, call logrus.Exit to invoke
// all handlers. The handlers will also be invoked when any Fatal log entry is
// made.
//
// This method is useful when a caller wishes to use logrus to log a fatal
// message but also needs to gracefully shutdown. An example usecase could be
// closing database connections, or sending a alert that the application is
// closing.
func RegisterExitHandler(handler func()) {
handlers = append(handlers, handler)
}

View File

@ -3,11 +3,21 @@ package logrus
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"io"
"os" "os"
"sync"
"time" "time"
) )
var bufferPool *sync.Pool
func init() {
bufferPool = &sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
}
// Defines the key when adding errors using WithError. // Defines the key when adding errors using WithError.
var ErrorKey = "error" var ErrorKey = "error"
@ -29,6 +39,9 @@ type Entry struct {
// Message passed to Debug, Info, Warn, Error, Fatal or Panic // Message passed to Debug, Info, Warn, Error, Fatal or Panic
Message string Message string
// When formatter is called in entry.log(), an Buffer may be set to entry
Buffer *bytes.Buffer
} }
func NewEntry(logger *Logger) *Entry { func NewEntry(logger *Logger) *Entry {
@ -39,21 +52,15 @@ func NewEntry(logger *Logger) *Entry {
} }
} }
// Returns a reader for the entry, which is a proxy to the formatter.
func (entry *Entry) Reader() (*bytes.Buffer, error) {
serialized, err := entry.Logger.Formatter.Format(entry)
return bytes.NewBuffer(serialized), err
}
// Returns the string representation from the reader and ultimately the // Returns the string representation from the reader and ultimately the
// formatter. // formatter.
func (entry *Entry) String() (string, error) { func (entry *Entry) String() (string, error) {
reader, err := entry.Reader() serialized, err := entry.Logger.Formatter.Format(entry)
if err != nil { if err != nil {
return "", err return "", err
} }
str := string(serialized)
return reader.String(), err return str, nil
} }
// Add an error as single field (using the key defined in ErrorKey) to the Entry. // Add an error as single field (using the key defined in ErrorKey) to the Entry.
@ -81,6 +88,7 @@ func (entry *Entry) WithFields(fields Fields) *Entry {
// This function is not declared with a pointer value because otherwise // This function is not declared with a pointer value because otherwise
// race conditions will occur when using multiple goroutines // race conditions will occur when using multiple goroutines
func (entry Entry) log(level Level, msg string) { func (entry Entry) log(level Level, msg string) {
var buffer *bytes.Buffer
entry.Time = time.Now() entry.Time = time.Now()
entry.Level = level entry.Level = level
entry.Message = msg entry.Message = msg
@ -90,20 +98,23 @@ func (entry Entry) log(level Level, msg string) {
fmt.Fprintf(os.Stderr, "Failed to fire hook: %v\n", err) fmt.Fprintf(os.Stderr, "Failed to fire hook: %v\n", err)
entry.Logger.mu.Unlock() entry.Logger.mu.Unlock()
} }
buffer = bufferPool.Get().(*bytes.Buffer)
reader, err := entry.Reader() buffer.Reset()
defer bufferPool.Put(buffer)
entry.Buffer = buffer
serialized, err := entry.Logger.Formatter.Format(&entry)
entry.Buffer = nil
if err != nil { if err != nil {
entry.Logger.mu.Lock() entry.Logger.mu.Lock()
fmt.Fprintf(os.Stderr, "Failed to obtain reader, %v\n", err) fmt.Fprintf(os.Stderr, "Failed to obtain reader, %v\n", err)
entry.Logger.mu.Unlock() entry.Logger.mu.Unlock()
} } else {
entry.Logger.mu.Lock()
entry.Logger.mu.Lock() _, err = entry.Logger.Out.Write(serialized)
defer entry.Logger.mu.Unlock() if err != nil {
fmt.Fprintf(os.Stderr, "Failed to write to log, %v\n", err)
_, err = io.Copy(entry.Logger.Out, reader) }
if err != nil { entry.Logger.mu.Unlock()
fmt.Fprintf(os.Stderr, "Failed to write to log, %v\n", err)
} }
// To avoid Entry#log() returning a value that only would make sense for // To avoid Entry#log() returning a value that only would make sense for
@ -150,7 +161,7 @@ func (entry *Entry) Fatal(args ...interface{}) {
if entry.Logger.Level >= FatalLevel { if entry.Logger.Level >= FatalLevel {
entry.log(FatalLevel, fmt.Sprint(args...)) entry.log(FatalLevel, fmt.Sprint(args...))
} }
os.Exit(1) Exit(1)
} }
func (entry *Entry) Panic(args ...interface{}) { func (entry *Entry) Panic(args ...interface{}) {
@ -198,7 +209,7 @@ func (entry *Entry) Fatalf(format string, args ...interface{}) {
if entry.Logger.Level >= FatalLevel { if entry.Logger.Level >= FatalLevel {
entry.Fatal(fmt.Sprintf(format, args...)) entry.Fatal(fmt.Sprintf(format, args...))
} }
os.Exit(1) Exit(1)
} }
func (entry *Entry) Panicf(format string, args ...interface{}) { func (entry *Entry) Panicf(format string, args ...interface{}) {
@ -245,7 +256,7 @@ func (entry *Entry) Fatalln(args ...interface{}) {
if entry.Logger.Level >= FatalLevel { if entry.Logger.Level >= FatalLevel {
entry.Fatal(entry.sprintlnn(args...)) entry.Fatal(entry.sprintlnn(args...))
} }
os.Exit(1) Exit(1)
} }
func (entry *Entry) Panicln(args ...interface{}) { func (entry *Entry) Panicln(args ...interface{}) {

View File

@ -2,6 +2,7 @@ package main
import ( import (
"github.com/Sirupsen/logrus" "github.com/Sirupsen/logrus"
// "os"
) )
var log = logrus.New() var log = logrus.New()
@ -9,6 +10,14 @@ var log = logrus.New()
func init() { func init() {
log.Formatter = new(logrus.JSONFormatter) log.Formatter = new(logrus.JSONFormatter)
log.Formatter = new(logrus.TextFormatter) // default log.Formatter = new(logrus.TextFormatter) // default
// file, err := os.OpenFile("logrus.log", os.O_CREATE|os.O_WRONLY, 0666)
// if err == nil {
// log.Out = file
// } else {
// log.Info("Failed to log to file, using default stderr")
// }
log.Level = logrus.DebugLevel log.Level = logrus.DebugLevel
} }

View File

@ -31,18 +31,15 @@ type Formatter interface {
// It's not exported because it's still using Data in an opinionated way. It's to // It's not exported because it's still using Data in an opinionated way. It's to
// avoid code duplication between the two default formatters. // avoid code duplication between the two default formatters.
func prefixFieldClashes(data Fields) { func prefixFieldClashes(data Fields) {
_, ok := data["time"] if t, ok := data["time"]; ok {
if ok { data["fields.time"] = t
data["fields.time"] = data["time"]
} }
_, ok = data["msg"] if m, ok := data["msg"]; ok {
if ok { data["fields.msg"] = m
data["fields.msg"] = data["msg"]
} }
_, ok = data["level"] if l, ok := data["level"]; ok {
if ok { data["fields.level"] = l
data["fields.level"] = data["level"]
} }
} }

View File

@ -1,61 +0,0 @@
package logstash
import (
"encoding/json"
"fmt"
"github.com/Sirupsen/logrus"
)
// Formatter generates json in logstash format.
// Logstash site: http://logstash.net/
type LogstashFormatter struct {
Type string // if not empty use for logstash type field.
// TimestampFormat sets the format used for timestamps.
TimestampFormat string
}
func (f *LogstashFormatter) Format(entry *logrus.Entry) ([]byte, error) {
fields := make(logrus.Fields)
for k, v := range entry.Data {
fields[k] = v
}
fields["@version"] = 1
if f.TimestampFormat == "" {
f.TimestampFormat = logrus.DefaultTimestampFormat
}
fields["@timestamp"] = entry.Time.Format(f.TimestampFormat)
// set message field
v, ok := entry.Data["message"]
if ok {
fields["fields.message"] = v
}
fields["message"] = entry.Message
// set level field
v, ok = entry.Data["level"]
if ok {
fields["fields.level"] = v
}
fields["level"] = entry.Level.String()
// set type field
if f.Type != "" {
v, ok = entry.Data["type"]
if ok {
fields["fields.type"] = v
}
fields["type"] = f.Type
}
serialized, err := json.Marshal(fields)
if err != nil {
return nil, fmt.Errorf("Failed to marshal fields to JSON, %v", err)
}
return append(serialized, '\n'), nil
}

View File

@ -5,9 +5,40 @@ import (
"fmt" "fmt"
) )
type fieldKey string
type FieldMap map[fieldKey]string
const (
FieldKeyMsg = "msg"
FieldKeyLevel = "level"
FieldKeyTime = "time"
)
func (f FieldMap) resolve(key fieldKey) string {
if k, ok := f[key]; ok {
return k
}
return string(key)
}
type JSONFormatter struct { type JSONFormatter struct {
// TimestampFormat sets the format used for marshaling timestamps. // TimestampFormat sets the format used for marshaling timestamps.
TimestampFormat string TimestampFormat string
// DisableTimestamp allows disabling automatic timestamps in output
DisableTimestamp bool
// FieldMap allows users to customize the names of keys for various fields.
// As an example:
// formatter := &JSONFormatter{
// FieldMap: FieldMap{
// FieldKeyTime: "@timestamp",
// FieldKeyLevel: "@level",
// FieldKeyLevel: "@message",
// },
// }
FieldMap FieldMap
} }
func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) { func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) {
@ -29,9 +60,11 @@ func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) {
timestampFormat = DefaultTimestampFormat timestampFormat = DefaultTimestampFormat
} }
data["time"] = entry.Time.Format(timestampFormat) if !f.DisableTimestamp {
data["msg"] = entry.Message data[f.FieldMap.resolve(FieldKeyTime)] = entry.Time.Format(timestampFormat)
data["level"] = entry.Level.String() }
data[f.FieldMap.resolve(FieldKeyMsg)] = entry.Message
data[f.FieldMap.resolve(FieldKeyLevel)] = entry.Level.String()
serialized, err := json.Marshal(data) serialized, err := json.Marshal(data)
if err != nil { if err != nil {

View File

@ -26,8 +26,31 @@ type Logger struct {
// to) `logrus.Info`, which allows Info(), Warn(), Error() and Fatal() to be // to) `logrus.Info`, which allows Info(), Warn(), Error() and Fatal() to be
// logged. `logrus.Debug` is useful in // logged. `logrus.Debug` is useful in
Level Level Level Level
// Used to sync writing to the log. // Used to sync writing to the log. Locking is enabled by Default
mu sync.Mutex mu MutexWrap
// Reusable empty entry
entryPool sync.Pool
}
type MutexWrap struct {
lock sync.Mutex
disabled bool
}
func (mw *MutexWrap) Lock() {
if !mw.disabled {
mw.lock.Lock()
}
}
func (mw *MutexWrap) Unlock() {
if !mw.disabled {
mw.lock.Unlock()
}
}
func (mw *MutexWrap) Disable() {
mw.disabled = true
} }
// Creates a new logger. Configuration should be set by changing `Formatter`, // Creates a new logger. Configuration should be set by changing `Formatter`,
@ -51,162 +74,235 @@ func New() *Logger {
} }
} }
// Adds a field to the log entry, note that you it doesn't log until you call func (logger *Logger) newEntry() *Entry {
entry, ok := logger.entryPool.Get().(*Entry)
if ok {
return entry
}
return NewEntry(logger)
}
func (logger *Logger) releaseEntry(entry *Entry) {
logger.entryPool.Put(entry)
}
// Adds a field to the log entry, note that it doesn't log until you call
// Debug, Print, Info, Warn, Fatal or Panic. It only creates a log entry. // Debug, Print, Info, Warn, Fatal or Panic. It only creates a log entry.
// If you want multiple fields, use `WithFields`. // If you want multiple fields, use `WithFields`.
func (logger *Logger) WithField(key string, value interface{}) *Entry { func (logger *Logger) WithField(key string, value interface{}) *Entry {
return NewEntry(logger).WithField(key, value) entry := logger.newEntry()
defer logger.releaseEntry(entry)
return entry.WithField(key, value)
} }
// Adds a struct of fields to the log entry. All it does is call `WithField` for // Adds a struct of fields to the log entry. All it does is call `WithField` for
// each `Field`. // each `Field`.
func (logger *Logger) WithFields(fields Fields) *Entry { func (logger *Logger) WithFields(fields Fields) *Entry {
return NewEntry(logger).WithFields(fields) entry := logger.newEntry()
defer logger.releaseEntry(entry)
return entry.WithFields(fields)
} }
// Add an error as single field to the log entry. All it does is call // Add an error as single field to the log entry. All it does is call
// `WithError` for the given `error`. // `WithError` for the given `error`.
func (logger *Logger) WithError(err error) *Entry { func (logger *Logger) WithError(err error) *Entry {
return NewEntry(logger).WithError(err) entry := logger.newEntry()
defer logger.releaseEntry(entry)
return entry.WithError(err)
} }
func (logger *Logger) Debugf(format string, args ...interface{}) { func (logger *Logger) Debugf(format string, args ...interface{}) {
if logger.Level >= DebugLevel { if logger.Level >= DebugLevel {
NewEntry(logger).Debugf(format, args...) entry := logger.newEntry()
entry.Debugf(format, args...)
logger.releaseEntry(entry)
} }
} }
func (logger *Logger) Infof(format string, args ...interface{}) { func (logger *Logger) Infof(format string, args ...interface{}) {
if logger.Level >= InfoLevel { if logger.Level >= InfoLevel {
NewEntry(logger).Infof(format, args...) entry := logger.newEntry()
entry.Infof(format, args...)
logger.releaseEntry(entry)
} }
} }
func (logger *Logger) Printf(format string, args ...interface{}) { func (logger *Logger) Printf(format string, args ...interface{}) {
NewEntry(logger).Printf(format, args...) entry := logger.newEntry()
entry.Printf(format, args...)
logger.releaseEntry(entry)
} }
func (logger *Logger) Warnf(format string, args ...interface{}) { func (logger *Logger) Warnf(format string, args ...interface{}) {
if logger.Level >= WarnLevel { if logger.Level >= WarnLevel {
NewEntry(logger).Warnf(format, args...) entry := logger.newEntry()
entry.Warnf(format, args...)
logger.releaseEntry(entry)
} }
} }
func (logger *Logger) Warningf(format string, args ...interface{}) { func (logger *Logger) Warningf(format string, args ...interface{}) {
if logger.Level >= WarnLevel { if logger.Level >= WarnLevel {
NewEntry(logger).Warnf(format, args...) entry := logger.newEntry()
entry.Warnf(format, args...)
logger.releaseEntry(entry)
} }
} }
func (logger *Logger) Errorf(format string, args ...interface{}) { func (logger *Logger) Errorf(format string, args ...interface{}) {
if logger.Level >= ErrorLevel { if logger.Level >= ErrorLevel {
NewEntry(logger).Errorf(format, args...) entry := logger.newEntry()
entry.Errorf(format, args...)
logger.releaseEntry(entry)
} }
} }
func (logger *Logger) Fatalf(format string, args ...interface{}) { func (logger *Logger) Fatalf(format string, args ...interface{}) {
if logger.Level >= FatalLevel { if logger.Level >= FatalLevel {
NewEntry(logger).Fatalf(format, args...) entry := logger.newEntry()
entry.Fatalf(format, args...)
logger.releaseEntry(entry)
} }
os.Exit(1) Exit(1)
} }
func (logger *Logger) Panicf(format string, args ...interface{}) { func (logger *Logger) Panicf(format string, args ...interface{}) {
if logger.Level >= PanicLevel { if logger.Level >= PanicLevel {
NewEntry(logger).Panicf(format, args...) entry := logger.newEntry()
entry.Panicf(format, args...)
logger.releaseEntry(entry)
} }
} }
func (logger *Logger) Debug(args ...interface{}) { func (logger *Logger) Debug(args ...interface{}) {
if logger.Level >= DebugLevel { if logger.Level >= DebugLevel {
NewEntry(logger).Debug(args...) entry := logger.newEntry()
entry.Debug(args...)
logger.releaseEntry(entry)
} }
} }
func (logger *Logger) Info(args ...interface{}) { func (logger *Logger) Info(args ...interface{}) {
if logger.Level >= InfoLevel { if logger.Level >= InfoLevel {
NewEntry(logger).Info(args...) entry := logger.newEntry()
entry.Info(args...)
logger.releaseEntry(entry)
} }
} }
func (logger *Logger) Print(args ...interface{}) { func (logger *Logger) Print(args ...interface{}) {
NewEntry(logger).Info(args...) entry := logger.newEntry()
entry.Info(args...)
logger.releaseEntry(entry)
} }
func (logger *Logger) Warn(args ...interface{}) { func (logger *Logger) Warn(args ...interface{}) {
if logger.Level >= WarnLevel { if logger.Level >= WarnLevel {
NewEntry(logger).Warn(args...) entry := logger.newEntry()
entry.Warn(args...)
logger.releaseEntry(entry)
} }
} }
func (logger *Logger) Warning(args ...interface{}) { func (logger *Logger) Warning(args ...interface{}) {
if logger.Level >= WarnLevel { if logger.Level >= WarnLevel {
NewEntry(logger).Warn(args...) entry := logger.newEntry()
entry.Warn(args...)
logger.releaseEntry(entry)
} }
} }
func (logger *Logger) Error(args ...interface{}) { func (logger *Logger) Error(args ...interface{}) {
if logger.Level >= ErrorLevel { if logger.Level >= ErrorLevel {
NewEntry(logger).Error(args...) entry := logger.newEntry()
entry.Error(args...)
logger.releaseEntry(entry)
} }
} }
func (logger *Logger) Fatal(args ...interface{}) { func (logger *Logger) Fatal(args ...interface{}) {
if logger.Level >= FatalLevel { if logger.Level >= FatalLevel {
NewEntry(logger).Fatal(args...) entry := logger.newEntry()
entry.Fatal(args...)
logger.releaseEntry(entry)
} }
os.Exit(1) Exit(1)
} }
func (logger *Logger) Panic(args ...interface{}) { func (logger *Logger) Panic(args ...interface{}) {
if logger.Level >= PanicLevel { if logger.Level >= PanicLevel {
NewEntry(logger).Panic(args...) entry := logger.newEntry()
entry.Panic(args...)
logger.releaseEntry(entry)
} }
} }
func (logger *Logger) Debugln(args ...interface{}) { func (logger *Logger) Debugln(args ...interface{}) {
if logger.Level >= DebugLevel { if logger.Level >= DebugLevel {
NewEntry(logger).Debugln(args...) entry := logger.newEntry()
entry.Debugln(args...)
logger.releaseEntry(entry)
} }
} }
func (logger *Logger) Infoln(args ...interface{}) { func (logger *Logger) Infoln(args ...interface{}) {
if logger.Level >= InfoLevel { if logger.Level >= InfoLevel {
NewEntry(logger).Infoln(args...) entry := logger.newEntry()
entry.Infoln(args...)
logger.releaseEntry(entry)
} }
} }
func (logger *Logger) Println(args ...interface{}) { func (logger *Logger) Println(args ...interface{}) {
NewEntry(logger).Println(args...) entry := logger.newEntry()
entry.Println(args...)
logger.releaseEntry(entry)
} }
func (logger *Logger) Warnln(args ...interface{}) { func (logger *Logger) Warnln(args ...interface{}) {
if logger.Level >= WarnLevel { if logger.Level >= WarnLevel {
NewEntry(logger).Warnln(args...) entry := logger.newEntry()
entry.Warnln(args...)
logger.releaseEntry(entry)
} }
} }
func (logger *Logger) Warningln(args ...interface{}) { func (logger *Logger) Warningln(args ...interface{}) {
if logger.Level >= WarnLevel { if logger.Level >= WarnLevel {
NewEntry(logger).Warnln(args...) entry := logger.newEntry()
entry.Warnln(args...)
logger.releaseEntry(entry)
} }
} }
func (logger *Logger) Errorln(args ...interface{}) { func (logger *Logger) Errorln(args ...interface{}) {
if logger.Level >= ErrorLevel { if logger.Level >= ErrorLevel {
NewEntry(logger).Errorln(args...) entry := logger.newEntry()
entry.Errorln(args...)
logger.releaseEntry(entry)
} }
} }
func (logger *Logger) Fatalln(args ...interface{}) { func (logger *Logger) Fatalln(args ...interface{}) {
if logger.Level >= FatalLevel { if logger.Level >= FatalLevel {
NewEntry(logger).Fatalln(args...) entry := logger.newEntry()
entry.Fatalln(args...)
logger.releaseEntry(entry)
} }
os.Exit(1) Exit(1)
} }
func (logger *Logger) Panicln(args ...interface{}) { func (logger *Logger) Panicln(args ...interface{}) {
if logger.Level >= PanicLevel { if logger.Level >= PanicLevel {
NewEntry(logger).Panicln(args...) entry := logger.newEntry()
entry.Panicln(args...)
logger.releaseEntry(entry)
} }
} }
//When file is opened with appending mode, it's safe to
//write concurrently to a file (within 4k message on Linux).
//In these cases user can choose to disable the lock.
func (logger *Logger) SetNoLock() {
logger.mu.Disable()
}

View File

@ -0,0 +1,10 @@
// +build appengine
package logrus
import "io"
// IsTerminal returns true if stderr's file descriptor is a terminal.
func IsTerminal(f io.Writer) bool {
return true
}

View File

@ -1,4 +1,5 @@
// +build darwin freebsd openbsd netbsd dragonfly // +build darwin freebsd openbsd netbsd dragonfly
// +build !appengine
package logrus package logrus

View File

@ -3,6 +3,8 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
// +build !appengine
package logrus package logrus
import "syscall" import "syscall"

View File

@ -4,18 +4,25 @@
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
// +build linux darwin freebsd openbsd netbsd dragonfly // +build linux darwin freebsd openbsd netbsd dragonfly
// +build !appengine
package logrus package logrus
import ( import (
"io"
"os"
"syscall" "syscall"
"unsafe" "unsafe"
) )
// IsTerminal returns true if stderr's file descriptor is a terminal. // IsTerminal returns true if stderr's file descriptor is a terminal.
func IsTerminal() bool { func IsTerminal(f io.Writer) bool {
fd := syscall.Stderr
var termios Termios var termios Termios
_, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlReadTermios, uintptr(unsafe.Pointer(&termios)), 0, 0, 0) switch v := f.(type) {
return err == 0 case *os.File:
_, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(v.Fd()), ioctlReadTermios, uintptr(unsafe.Pointer(&termios)), 0, 0, 0)
return err == 0
default:
return false
}
} }

View File

@ -1,15 +1,21 @@
// +build solaris // +build solaris,!appengine
package logrus package logrus
import ( import (
"io"
"os" "os"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
) )
// IsTerminal returns true if the given file descriptor is a terminal. // IsTerminal returns true if the given file descriptor is a terminal.
func IsTerminal() bool { func IsTerminal(f io.Writer) bool {
_, err := unix.IoctlGetTermios(int(os.Stdout.Fd()), unix.TCGETA) switch v := f.(type) {
return err == nil case *os.File:
_, err := unix.IoctlGetTermios(int(v.Fd()), unix.TCGETA)
return err == nil
default:
return false
}
} }

View File

@ -3,11 +3,13 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
// +build windows // +build windows,!appengine
package logrus package logrus
import ( import (
"io"
"os"
"syscall" "syscall"
"unsafe" "unsafe"
) )
@ -19,9 +21,13 @@ var (
) )
// IsTerminal returns true if stderr's file descriptor is a terminal. // IsTerminal returns true if stderr's file descriptor is a terminal.
func IsTerminal() bool { func IsTerminal(f io.Writer) bool {
fd := syscall.Stderr switch v := f.(type) {
var st uint32 case *os.File:
r, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, uintptr(fd), uintptr(unsafe.Pointer(&st)), 0) var st uint32
return r != 0 && e == 0 r, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, uintptr(v.Fd()), uintptr(unsafe.Pointer(&st)), 0)
return r != 0 && e == 0
default:
return false
}
} }

View File

@ -3,9 +3,9 @@ package logrus
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"runtime"
"sort" "sort"
"strings" "strings"
"sync"
"time" "time"
) )
@ -20,16 +20,10 @@ const (
var ( var (
baseTimestamp time.Time baseTimestamp time.Time
isTerminal bool
) )
func init() { func init() {
baseTimestamp = time.Now() baseTimestamp = time.Now()
isTerminal = IsTerminal()
}
func miniTS() int {
return int(time.Since(baseTimestamp) / time.Second)
} }
type TextFormatter struct { type TextFormatter struct {
@ -54,10 +48,32 @@ type TextFormatter struct {
// that log extremely frequently and don't use the JSON formatter this may not // that log extremely frequently and don't use the JSON formatter this may not
// be desired. // be desired.
DisableSorting bool DisableSorting bool
// QuoteEmptyFields will wrap empty fields in quotes if true
QuoteEmptyFields bool
// QuoteCharacter can be set to the override the default quoting character "
// with something else. For example: ', or `.
QuoteCharacter string
// Whether the logger's out is to a terminal
isTerminal bool
sync.Once
}
func (f *TextFormatter) init(entry *Entry) {
if len(f.QuoteCharacter) == 0 {
f.QuoteCharacter = "\""
}
if entry.Logger != nil {
f.isTerminal = IsTerminal(entry.Logger.Out)
}
} }
func (f *TextFormatter) Format(entry *Entry) ([]byte, error) { func (f *TextFormatter) Format(entry *Entry) ([]byte, error) {
var keys []string = make([]string, 0, len(entry.Data)) var b *bytes.Buffer
keys := make([]string, 0, len(entry.Data))
for k := range entry.Data { for k := range entry.Data {
keys = append(keys, k) keys = append(keys, k)
} }
@ -65,13 +81,17 @@ func (f *TextFormatter) Format(entry *Entry) ([]byte, error) {
if !f.DisableSorting { if !f.DisableSorting {
sort.Strings(keys) sort.Strings(keys)
} }
if entry.Buffer != nil {
b := &bytes.Buffer{} b = entry.Buffer
} else {
b = &bytes.Buffer{}
}
prefixFieldClashes(entry.Data) prefixFieldClashes(entry.Data)
isColorTerminal := isTerminal && (runtime.GOOS != "windows") f.Do(func() { f.init(entry) })
isColored := (f.ForceColors || isColorTerminal) && !f.DisableColors
isColored := (f.ForceColors || f.isTerminal) && !f.DisableColors
timestampFormat := f.TimestampFormat timestampFormat := f.TimestampFormat
if timestampFormat == "" { if timestampFormat == "" {
@ -111,51 +131,59 @@ func (f *TextFormatter) printColored(b *bytes.Buffer, entry *Entry, keys []strin
levelText := strings.ToUpper(entry.Level.String())[0:4] levelText := strings.ToUpper(entry.Level.String())[0:4]
if !f.FullTimestamp { if f.DisableTimestamp {
fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%04d] %-44s ", levelColor, levelText, miniTS(), entry.Message) fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m %-44s ", levelColor, levelText, entry.Message)
} else if !f.FullTimestamp {
fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%04d] %-44s ", levelColor, levelText, int(entry.Time.Sub(baseTimestamp)/time.Second), entry.Message)
} else { } else {
fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%s] %-44s ", levelColor, levelText, entry.Time.Format(timestampFormat), entry.Message) fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%s] %-44s ", levelColor, levelText, entry.Time.Format(timestampFormat), entry.Message)
} }
for _, k := range keys { for _, k := range keys {
v := entry.Data[k] v := entry.Data[k]
fmt.Fprintf(b, " \x1b[%dm%s\x1b[0m=%+v", levelColor, k, v) fmt.Fprintf(b, " \x1b[%dm%s\x1b[0m=", levelColor, k)
f.appendValue(b, v)
} }
} }
func needsQuoting(text string) bool { func (f *TextFormatter) needsQuoting(text string) bool {
if f.QuoteEmptyFields && len(text) == 0 {
return true
}
for _, ch := range text { for _, ch := range text {
if !((ch >= 'a' && ch <= 'z') || if !((ch >= 'a' && ch <= 'z') ||
(ch >= 'A' && ch <= 'Z') || (ch >= 'A' && ch <= 'Z') ||
(ch >= '0' && ch <= '9') || (ch >= '0' && ch <= '9') ||
ch == '-' || ch == '.') { ch == '-' || ch == '.') {
return false return true
} }
} }
return true return false
} }
func (f *TextFormatter) appendKeyValue(b *bytes.Buffer, key string, value interface{}) { func (f *TextFormatter) appendKeyValue(b *bytes.Buffer, key string, value interface{}) {
b.WriteString(key) b.WriteString(key)
b.WriteByte('=') b.WriteByte('=')
f.appendValue(b, value)
b.WriteByte(' ')
}
func (f *TextFormatter) appendValue(b *bytes.Buffer, value interface{}) {
switch value := value.(type) { switch value := value.(type) {
case string: case string:
if needsQuoting(value) { if !f.needsQuoting(value) {
b.WriteString(value) b.WriteString(value)
} else { } else {
fmt.Fprintf(b, "%q", value) fmt.Fprintf(b, "%s%v%s", f.QuoteCharacter, value, f.QuoteCharacter)
} }
case error: case error:
errmsg := value.Error() errmsg := value.Error()
if needsQuoting(errmsg) { if !f.needsQuoting(errmsg) {
b.WriteString(errmsg) b.WriteString(errmsg)
} else { } else {
fmt.Fprintf(b, "%q", value) fmt.Fprintf(b, "%s%v%s", f.QuoteCharacter, errmsg, f.QuoteCharacter)
} }
default: default:
fmt.Fprint(b, value) fmt.Fprint(b, value)
} }
b.WriteByte(' ')
} }

View File

@ -7,21 +7,52 @@ import (
) )
func (logger *Logger) Writer() *io.PipeWriter { func (logger *Logger) Writer() *io.PipeWriter {
return logger.WriterLevel(InfoLevel)
}
func (logger *Logger) WriterLevel(level Level) *io.PipeWriter {
return NewEntry(logger).WriterLevel(level)
}
func (entry *Entry) Writer() *io.PipeWriter {
return entry.WriterLevel(InfoLevel)
}
func (entry *Entry) WriterLevel(level Level) *io.PipeWriter {
reader, writer := io.Pipe() reader, writer := io.Pipe()
go logger.writerScanner(reader) var printFunc func(args ...interface{})
switch level {
case DebugLevel:
printFunc = entry.Debug
case InfoLevel:
printFunc = entry.Info
case WarnLevel:
printFunc = entry.Warn
case ErrorLevel:
printFunc = entry.Error
case FatalLevel:
printFunc = entry.Fatal
case PanicLevel:
printFunc = entry.Panic
default:
printFunc = entry.Print
}
go entry.writerScanner(reader, printFunc)
runtime.SetFinalizer(writer, writerFinalizer) runtime.SetFinalizer(writer, writerFinalizer)
return writer return writer
} }
func (logger *Logger) writerScanner(reader *io.PipeReader) { func (entry *Entry) writerScanner(reader *io.PipeReader, printFunc func(args ...interface{})) {
scanner := bufio.NewScanner(reader) scanner := bufio.NewScanner(reader)
for scanner.Scan() { for scanner.Scan() {
logger.Print(scanner.Text()) printFunc(scanner.Text())
} }
if err := scanner.Err(); err != nil { if err := scanner.Err(); err != nil {
logger.Errorf("Error while reading from Writer: %s", err) entry.Errorf("Error while reading from Writer: %s", err)
} }
reader.Close() reader.Close()
} }

View File

@ -21,8 +21,10 @@ import (
// BotAPI allows you to interact with the Telegram Bot API. // BotAPI allows you to interact with the Telegram Bot API.
type BotAPI struct { type BotAPI struct {
Token string `json:"token"` Token string `json:"token"`
Debug bool `json:"debug"` Debug bool `json:"debug"`
Buffer int `json:"buffer"`
Self User `json:"-"` Self User `json:"-"`
Client *http.Client `json:"-"` Client *http.Client `json:"-"`
} }
@ -42,11 +44,12 @@ func NewBotAPIWithClient(token string, client *http.Client) (*BotAPI, error) {
bot := &BotAPI{ bot := &BotAPI{
Token: token, Token: token,
Client: client, Client: client,
Buffer: 100,
} }
self, err := bot.GetMe() self, err := bot.GetMe()
if err != nil { if err != nil {
return &BotAPI{}, err return nil, err
} }
bot.Self = self bot.Self = self
@ -68,6 +71,10 @@ func (bot *BotAPI) MakeRequest(endpoint string, params url.Values) (APIResponse,
return APIResponse{}, errors.New(ErrAPIForbidden) return APIResponse{}, errors.New(ErrAPIForbidden)
} }
if resp.StatusCode != http.StatusOK {
return APIResponse{}, errors.New(http.StatusText(resp.StatusCode))
}
bytes, err := ioutil.ReadAll(resp.Body) bytes, err := ioutil.ReadAll(resp.Body)
if err != nil { if err != nil {
return APIResponse{}, err return APIResponse{}, err
@ -457,7 +464,7 @@ func (bot *BotAPI) GetWebhookInfo() (WebhookInfo, error) {
// GetUpdatesChan starts and returns a channel for getting updates. // GetUpdatesChan starts and returns a channel for getting updates.
func (bot *BotAPI) GetUpdatesChan(config UpdateConfig) (UpdatesChannel, error) { func (bot *BotAPI) GetUpdatesChan(config UpdateConfig) (UpdatesChannel, error) {
ch := make(chan Update, 100) ch := make(chan Update, bot.Buffer)
go func() { go func() {
for { for {
@ -484,7 +491,7 @@ func (bot *BotAPI) GetUpdatesChan(config UpdateConfig) (UpdatesChannel, error) {
// ListenForWebhook registers a http handler for a webhook. // ListenForWebhook registers a http handler for a webhook.
func (bot *BotAPI) ListenForWebhook(pattern string) UpdatesChannel { func (bot *BotAPI) ListenForWebhook(pattern string) UpdatesChannel {
ch := make(chan Update, 100) ch := make(chan Update, bot.Buffer)
http.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) { http.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {
bytes, _ := ioutil.ReadAll(r.Body) bytes, _ := ioutil.ReadAll(r.Body)

View File

@ -768,8 +768,8 @@ type UpdateConfig struct {
// WebhookConfig contains information about a SetWebhook request. // WebhookConfig contains information about a SetWebhook request.
type WebhookConfig struct { type WebhookConfig struct {
URL *url.URL URL *url.URL
Certificate interface{} Certificate interface{}
MaxConnections int MaxConnections int
} }

View File

@ -318,21 +318,6 @@ func NewWebhookWithCert(link string, file interface{}) WebhookConfig {
} }
} }
// NewWebhookWithCert creates a new webhook with a certificate and max_connections.
//
// link is the url you wish to get webhooks,
// file contains a string to a file, FileReader, or FileBytes.
// maxConnections defines maximum number of connections from telegram to your server
func NewWebhookWithCertAndMaxConnections(link string, file interface{}, maxConnections int) WebhookConfig {
u, _ := url.Parse(link)
return WebhookConfig{
URL: u,
Certificate: file,
MaxConnections: maxConnections,
}
}
// NewInlineQueryResultArticle creates a new inline query article. // NewInlineQueryResultArticle creates a new inline query article.
func NewInlineQueryResultArticle(id, title, messageText string) InlineQueryResultArticle { func NewInlineQueryResultArticle(id, title, messageText string) InlineQueryResultArticle {
return InlineQueryResultArticle{ return InlineQueryResultArticle{

View File

@ -194,7 +194,7 @@ func (m *Message) CommandArguments() string {
return "" return ""
} }
return strings.SplitN(m.Text, " ", 2)[1] return split[1]
} }
// MessageEntity contains information about data in a Message. // MessageEntity contains information about data in a Message.

View File

@ -138,7 +138,12 @@ func (c *cache) create(t reflect.Type, info *structInfo) *structInfo {
ft = ft.Elem() ft = ft.Elem()
} }
if ft.Kind() == reflect.Struct { if ft.Kind() == reflect.Struct {
bef := len(info.fields)
c.create(ft, info) c.create(ft, info)
for _, fi := range info.fields[bef:len(info.fields)] {
// exclude required check because duplicated to embedded field
fi.required = false
}
} }
} }
c.createField(field, info) c.createField(field, info)
@ -148,7 +153,7 @@ func (c *cache) create(t reflect.Type, info *structInfo) *structInfo {
// createField creates a fieldInfo for the given field. // createField creates a fieldInfo for the given field.
func (c *cache) createField(field reflect.StructField, info *structInfo) { func (c *cache) createField(field reflect.StructField, info *structInfo) {
alias := fieldAlias(field, c.tag) alias, options := fieldAlias(field, c.tag)
if alias == "-" { if alias == "-" {
// Ignore this field. // Ignore this field.
return return
@ -173,17 +178,19 @@ func (c *cache) createField(field reflect.StructField, info *structInfo) {
} }
} }
if isStruct = ft.Kind() == reflect.Struct; !isStruct { if isStruct = ft.Kind() == reflect.Struct; !isStruct {
if conv := c.conv[ft.Kind()]; conv == nil { if conv := c.converter(ft); conv == nil {
// Type is not supported. // Type is not supported.
return return
} }
} }
info.fields = append(info.fields, &fieldInfo{ info.fields = append(info.fields, &fieldInfo{
typ: field.Type, typ: field.Type,
name: field.Name, name: field.Name,
ss: isSlice && isStruct, ss: isSlice && isStruct,
alias: alias, alias: alias,
anon: field.Anonymous,
required: options.Contains("required"),
}) })
} }
@ -212,10 +219,12 @@ func (i *structInfo) get(alias string) *fieldInfo {
} }
type fieldInfo struct { type fieldInfo struct {
typ reflect.Type typ reflect.Type
name string // field name in the struct. name string // field name in the struct.
ss bool // true if this is a slice of structs. ss bool // true if this is a slice of structs.
alias string alias string
anon bool // is an embedded field
required bool // tag option
} }
type pathPart struct { type pathPart struct {
@ -227,19 +236,33 @@ type pathPart struct {
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// fieldAlias parses a field tag to get a field alias. // fieldAlias parses a field tag to get a field alias.
func fieldAlias(field reflect.StructField, tagName string) string { func fieldAlias(field reflect.StructField, tagName string) (alias string, options tagOptions) {
var alias string
if tag := field.Tag.Get(tagName); tag != "" { if tag := field.Tag.Get(tagName); tag != "" {
// For now tags only support the name but let's follow the alias, options = parseTag(tag)
// comma convention from encoding/json and others.
if idx := strings.Index(tag, ","); idx == -1 {
alias = tag
} else {
alias = tag[:idx]
}
} }
if alias == "" { if alias == "" {
alias = field.Name alias = field.Name
} }
return alias return alias, options
}
// tagOptions is the string following a comma in a struct field's tag, or
// the empty string. It does not include the leading comma.
type tagOptions []string
// parseTag splits a struct field's url tag into its name and comma-separated
// options.
func parseTag(tag string) (string, tagOptions) {
s := strings.Split(tag, ",")
return s[0], s[1:]
}
// Contains checks whether the tagOptions contains the specified option.
func (o tagOptions) Contains(option string) bool {
for _, s := range o {
if s == option {
return true
}
}
return false
} }

View File

@ -87,9 +87,60 @@ func (d *Decoder) Decode(dst interface{}, src map[string][]string) error {
if len(errors) > 0 { if len(errors) > 0 {
return errors return errors
} }
return d.checkRequired(t, src, "")
}
// checkRequired checks whether requred field empty
//
// check type t recursively if t has struct fields, and prefix is same as parsePath: in dotted notation
//
// src is the source map for decoding, we use it here to see if those required fields are included in src
func (d *Decoder) checkRequired(t reflect.Type, src map[string][]string, prefix string) error {
struc := d.cache.get(t)
if struc == nil {
// unexpect, cache.get never return nil
return errors.New("cache fail")
}
for _, f := range struc.fields {
if f.typ.Kind() == reflect.Struct {
err := d.checkRequired(f.typ, src, prefix+f.alias+".")
if err != nil {
if !f.anon {
return err
}
// check embedded parent field.
err2 := d.checkRequired(f.typ, src, prefix)
if err2 != nil {
return err
}
}
}
if f.required {
key := f.alias
if prefix != "" {
key = prefix + key
}
if isEmpty(f.typ, src[key]) {
return fmt.Errorf("%v is empty", key)
}
}
}
return nil return nil
} }
// isEmpty returns true if value is empty for specific type
func isEmpty(t reflect.Type, value []string) bool {
if len(value) == 0 {
return true
}
switch t.Kind() {
case boolType, float32Type, float64Type, intType, int8Type, int32Type, int64Type, stringType, uint8Type, uint16Type, uint32Type, uint64Type:
return len(value[0]) == 0
}
return false
}
// decode fills a struct field using a parsed path. // decode fills a struct field using a parsed path.
func (d *Decoder) decode(v reflect.Value, path string, parts []pathPart, values []string) error { func (d *Decoder) decode(v reflect.Value, path string, parts []pathPart, values []string) error {
// Get the field walking the struct fields by index. // Get the field walking the struct fields by index.

View File

@ -12,7 +12,7 @@ The basic usage is really simple. Given this struct:
Phone string Phone string
} }
...we can fill it passing a map to the Load() function: ...we can fill it passing a map to the Decode() function:
values := map[string][]string{ values := map[string][]string{
"Name": {"John"}, "Name": {"John"},

161
vendor/github.com/gorilla/schema/encoder.go generated vendored Normal file
View File

@ -0,0 +1,161 @@
package schema
import (
"errors"
"fmt"
"reflect"
"strconv"
)
type encoderFunc func(reflect.Value) string
// Encoder encodes values from a struct into url.Values.
type Encoder struct {
cache *cache
regenc map[reflect.Type]encoderFunc
}
// NewEncoder returns a new Encoder with defaults.
func NewEncoder() *Encoder {
return &Encoder{cache: newCache(), regenc: make(map[reflect.Type]encoderFunc)}
}
// Encode encodes a struct into map[string][]string.
//
// Intended for use with url.Values.
func (e *Encoder) Encode(src interface{}, dst map[string][]string) error {
v := reflect.ValueOf(src)
return e.encode(v, dst)
}
// RegisterEncoder registers a converter for encoding a custom type.
func (e *Encoder) RegisterEncoder(value interface{}, encoder func(reflect.Value) string) {
e.regenc[reflect.TypeOf(value)] = encoder
}
// SetAliasTag changes the tag used to locate custom field aliases.
// The default tag is "schema".
func (e *Encoder) SetAliasTag(tag string) {
e.cache.tag = tag
}
func (e *Encoder) encode(v reflect.Value, dst map[string][]string) error {
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
if v.Kind() != reflect.Struct {
return errors.New("schema: interface must be a struct")
}
t := v.Type()
errors := MultiError{}
for i := 0; i < v.NumField(); i++ {
name, opts := fieldAlias(t.Field(i), e.cache.tag)
if name == "-" {
continue
}
if v.Field(i).Type().Kind() == reflect.Struct {
e.encode(v.Field(i), dst)
continue
}
encFunc := typeEncoder(v.Field(i).Type(), e.regenc)
// Encode non-slice types and custom implementations immediately.
if encFunc != nil {
value := encFunc(v.Field(i))
if value == "" && opts.Contains("omitempty") {
continue
}
dst[name] = append(dst[name], value)
continue
}
if v.Field(i).Type().Kind() == reflect.Slice {
encFunc = typeEncoder(v.Field(i).Type().Elem(), e.regenc)
}
if encFunc == nil {
errors[v.Field(i).Type().String()] = fmt.Errorf("schema: encoder not found for %v", v.Field(i))
continue
}
// Encode a slice.
if v.Field(i).Len() == 0 && opts.Contains("omitempty") {
continue
}
dst[name] = []string{}
for j := 0; j < v.Field(i).Len(); j++ {
dst[name] = append(dst[name], encFunc(v.Field(i).Index(j)))
}
}
if len(errors) > 0 {
return errors
}
return nil
}
func typeEncoder(t reflect.Type, reg map[reflect.Type]encoderFunc) encoderFunc {
if f, ok := reg[t]; ok {
return f
}
switch t.Kind() {
case reflect.Bool:
return encodeBool
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return encodeInt
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return encodeUint
case reflect.Float32:
return encodeFloat32
case reflect.Float64:
return encodeFloat64
case reflect.Ptr:
f := typeEncoder(t.Elem(), reg)
return func(v reflect.Value) string {
if v.IsNil() {
return "null"
}
return f(v.Elem())
}
case reflect.String:
return encodeString
default:
return nil
}
}
func encodeBool(v reflect.Value) string {
return strconv.FormatBool(v.Bool())
}
func encodeInt(v reflect.Value) string {
return strconv.FormatInt(int64(v.Int()), 10)
}
func encodeUint(v reflect.Value) string {
return strconv.FormatUint(uint64(v.Uint()), 10)
}
func encodeFloat(v reflect.Value, bits int) string {
return strconv.FormatFloat(v.Float(), 'f', 6, bits)
}
func encodeFloat32(v reflect.Value) string {
return encodeFloat(v, 32)
}
func encodeFloat64(v reflect.Value) string {
return encodeFloat(v, 64)
}
func encodeString(v reflect.Value) string {
return v.String()
}

View File

@ -23,6 +23,8 @@ import (
// invalid. // invalid.
var ErrBadHandshake = errors.New("websocket: bad handshake") var ErrBadHandshake = errors.New("websocket: bad handshake")
var errInvalidCompression = errors.New("websocket: invalid compression negotiation")
// NewClient creates a new client connection using the given net connection. // NewClient creates a new client connection using the given net connection.
// The URL u specifies the host and request URI. Use requestHeader to specify // The URL u specifies the host and request URI. Use requestHeader to specify
// the origin (Origin), subprotocols (Sec-WebSocket-Protocol) and cookies // the origin (Origin), subprotocols (Sec-WebSocket-Protocol) and cookies
@ -64,12 +66,24 @@ type Dialer struct {
// HandshakeTimeout specifies the duration for the handshake to complete. // HandshakeTimeout specifies the duration for the handshake to complete.
HandshakeTimeout time.Duration HandshakeTimeout time.Duration
// Input and output buffer sizes. If the buffer size is zero, then a // ReadBufferSize and WriteBufferSize specify I/O buffer sizes. If a buffer
// default value of 4096 is used. // size is zero, then a useful default size is used. The I/O buffer sizes
// do not limit the size of the messages that can be sent or received.
ReadBufferSize, WriteBufferSize int ReadBufferSize, WriteBufferSize int
// Subprotocols specifies the client's requested subprotocols. // Subprotocols specifies the client's requested subprotocols.
Subprotocols []string Subprotocols []string
// EnableCompression specifies if the client should attempt to negotiate
// per message compression (RFC 7692). Setting this value to true does not
// guarantee that compression will be supported. Currently only "no context
// takeover" modes are supported.
EnableCompression bool
// Jar specifies the cookie jar.
// If Jar is nil, cookies are not sent in requests and ignored
// in responses.
Jar http.CookieJar
} }
var errMalformedURL = errors.New("malformed ws or wss URL") var errMalformedURL = errors.New("malformed ws or wss URL")
@ -83,7 +97,6 @@ func parseURL(s string) (*url.URL, error) {
// //
// ws-URI = "ws:" "//" host [ ":" port ] path [ "?" query ] // ws-URI = "ws:" "//" host [ ":" port ] path [ "?" query ]
// wss-URI = "wss:" "//" host [ ":" port ] path [ "?" query ] // wss-URI = "wss:" "//" host [ ":" port ] path [ "?" query ]
var u url.URL var u url.URL
switch { switch {
case strings.HasPrefix(s, "ws://"): case strings.HasPrefix(s, "ws://"):
@ -193,6 +206,13 @@ func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Re
Host: u.Host, Host: u.Host,
} }
// Set the cookies present in the cookie jar of the dialer
if d.Jar != nil {
for _, cookie := range d.Jar.Cookies(u) {
req.AddCookie(cookie)
}
}
// Set the request headers using the capitalization for names and values in // Set the request headers using the capitalization for names and values in
// RFC examples. Although the capitalization shouldn't matter, there are // RFC examples. Although the capitalization shouldn't matter, there are
// servers that depend on it. The Header.Set method is not used because the // servers that depend on it. The Header.Set method is not used because the
@ -214,6 +234,7 @@ func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Re
k == "Connection" || k == "Connection" ||
k == "Sec-Websocket-Key" || k == "Sec-Websocket-Key" ||
k == "Sec-Websocket-Version" || k == "Sec-Websocket-Version" ||
k == "Sec-Websocket-Extensions" ||
(k == "Sec-Websocket-Protocol" && len(d.Subprotocols) > 0): (k == "Sec-Websocket-Protocol" && len(d.Subprotocols) > 0):
return nil, nil, errors.New("websocket: duplicate header not allowed: " + k) return nil, nil, errors.New("websocket: duplicate header not allowed: " + k)
default: default:
@ -221,6 +242,10 @@ func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Re
} }
} }
if d.EnableCompression {
req.Header.Set("Sec-Websocket-Extensions", "permessage-deflate; server_no_context_takeover; client_no_context_takeover")
}
hostPort, hostNoPort := hostPortNoPort(u) hostPort, hostNoPort := hostPortNoPort(u)
var proxyURL *url.URL var proxyURL *url.URL
@ -298,12 +323,8 @@ func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Re
} }
if u.Scheme == "https" { if u.Scheme == "https" {
cfg := d.TLSClientConfig cfg := cloneTLSConfig(d.TLSClientConfig)
if cfg == nil { if cfg.ServerName == "" {
cfg = &tls.Config{ServerName: hostNoPort}
} else if cfg.ServerName == "" {
shallowCopy := *cfg
cfg = &shallowCopy
cfg.ServerName = hostNoPort cfg.ServerName = hostNoPort
} }
tlsConn := tls.Client(netConn, cfg) tlsConn := tls.Client(netConn, cfg)
@ -328,6 +349,13 @@ func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Re
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
if d.Jar != nil {
if rc := resp.Cookies(); len(rc) > 0 {
d.Jar.SetCookies(u, rc)
}
}
if resp.StatusCode != 101 || if resp.StatusCode != 101 ||
!strings.EqualFold(resp.Header.Get("Upgrade"), "websocket") || !strings.EqualFold(resp.Header.Get("Upgrade"), "websocket") ||
!strings.EqualFold(resp.Header.Get("Connection"), "upgrade") || !strings.EqualFold(resp.Header.Get("Connection"), "upgrade") ||
@ -341,6 +369,20 @@ func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Re
return nil, resp, ErrBadHandshake return nil, resp, ErrBadHandshake
} }
for _, ext := range parseExtensions(resp.Header) {
if ext[""] != "permessage-deflate" {
continue
}
_, snct := ext["server_no_context_takeover"]
_, cnct := ext["client_no_context_takeover"]
if !snct || !cnct {
return nil, resp, errInvalidCompression
}
conn.newCompressionWriter = compressNoContextTakeover
conn.newDecompressionReader = decompressNoContextTakeover
break
}
resp.Body = ioutil.NopCloser(bytes.NewReader([]byte{})) resp.Body = ioutil.NopCloser(bytes.NewReader([]byte{}))
conn.subprotocol = resp.Header.Get("Sec-Websocket-Protocol") conn.subprotocol = resp.Header.Get("Sec-Websocket-Protocol")

16
vendor/github.com/gorilla/websocket/client_clone.go generated vendored Normal file
View File

@ -0,0 +1,16 @@
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build go1.8
package websocket
import "crypto/tls"
func cloneTLSConfig(cfg *tls.Config) *tls.Config {
if cfg == nil {
return &tls.Config{}
}
return cfg.Clone()
}

View File

@ -0,0 +1,38 @@
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build !go1.8
package websocket
import "crypto/tls"
// cloneTLSConfig clones all public fields except the fields
// SessionTicketsDisabled and SessionTicketKey. This avoids copying the
// sync.Mutex in the sync.Once and makes it safe to call cloneTLSConfig on a
// config in active use.
func cloneTLSConfig(cfg *tls.Config) *tls.Config {
if cfg == nil {
return &tls.Config{}
}
return &tls.Config{
Rand: cfg.Rand,
Time: cfg.Time,
Certificates: cfg.Certificates,
NameToCertificate: cfg.NameToCertificate,
GetCertificate: cfg.GetCertificate,
RootCAs: cfg.RootCAs,
NextProtos: cfg.NextProtos,
ServerName: cfg.ServerName,
ClientAuth: cfg.ClientAuth,
ClientCAs: cfg.ClientCAs,
InsecureSkipVerify: cfg.InsecureSkipVerify,
CipherSuites: cfg.CipherSuites,
PreferServerCipherSuites: cfg.PreferServerCipherSuites,
ClientSessionCache: cfg.ClientSessionCache,
MinVersion: cfg.MinVersion,
MaxVersion: cfg.MaxVersion,
CurvePreferences: cfg.CurvePreferences,
}
}

148
vendor/github.com/gorilla/websocket/compression.go generated vendored Normal file
View File

@ -0,0 +1,148 @@
// Copyright 2017 The Gorilla WebSocket Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package websocket
import (
"compress/flate"
"errors"
"io"
"strings"
"sync"
)
const (
minCompressionLevel = -2 // flate.HuffmanOnly not defined in Go < 1.6
maxCompressionLevel = flate.BestCompression
defaultCompressionLevel = 1
)
var (
flateWriterPools [maxCompressionLevel - minCompressionLevel + 1]sync.Pool
flateReaderPool = sync.Pool{New: func() interface{} {
return flate.NewReader(nil)
}}
)
func decompressNoContextTakeover(r io.Reader) io.ReadCloser {
const tail =
// Add four bytes as specified in RFC
"\x00\x00\xff\xff" +
// Add final block to squelch unexpected EOF error from flate reader.
"\x01\x00\x00\xff\xff"
fr, _ := flateReaderPool.Get().(io.ReadCloser)
fr.(flate.Resetter).Reset(io.MultiReader(r, strings.NewReader(tail)), nil)
return &flateReadWrapper{fr}
}
func isValidCompressionLevel(level int) bool {
return minCompressionLevel <= level && level <= maxCompressionLevel
}
func compressNoContextTakeover(w io.WriteCloser, level int) io.WriteCloser {
p := &flateWriterPools[level-minCompressionLevel]
tw := &truncWriter{w: w}
fw, _ := p.Get().(*flate.Writer)
if fw == nil {
fw, _ = flate.NewWriter(tw, level)
} else {
fw.Reset(tw)
}
return &flateWriteWrapper{fw: fw, tw: tw, p: p}
}
// truncWriter is an io.Writer that writes all but the last four bytes of the
// stream to another io.Writer.
type truncWriter struct {
w io.WriteCloser
n int
p [4]byte
}
func (w *truncWriter) Write(p []byte) (int, error) {
n := 0
// fill buffer first for simplicity.
if w.n < len(w.p) {
n = copy(w.p[w.n:], p)
p = p[n:]
w.n += n
if len(p) == 0 {
return n, nil
}
}
m := len(p)
if m > len(w.p) {
m = len(w.p)
}
if nn, err := w.w.Write(w.p[:m]); err != nil {
return n + nn, err
}
copy(w.p[:], w.p[m:])
copy(w.p[len(w.p)-m:], p[len(p)-m:])
nn, err := w.w.Write(p[:len(p)-m])
return n + nn, err
}
type flateWriteWrapper struct {
fw *flate.Writer
tw *truncWriter
p *sync.Pool
}
func (w *flateWriteWrapper) Write(p []byte) (int, error) {
if w.fw == nil {
return 0, errWriteClosed
}
return w.fw.Write(p)
}
func (w *flateWriteWrapper) Close() error {
if w.fw == nil {
return errWriteClosed
}
err1 := w.fw.Flush()
w.p.Put(w.fw)
w.fw = nil
if w.tw.p != [4]byte{0, 0, 0xff, 0xff} {
return errors.New("websocket: internal error, unexpected bytes at end of flate stream")
}
err2 := w.tw.w.Close()
if err1 != nil {
return err1
}
return err2
}
type flateReadWrapper struct {
fr io.ReadCloser
}
func (r *flateReadWrapper) Read(p []byte) (int, error) {
if r.fr == nil {
return 0, io.ErrClosedPipe
}
n, err := r.fr.Read(p)
if err == io.EOF {
// Preemptively place the reader back in the pool. This helps with
// scenarios where the application does not call NextReader() soon after
// this final read.
r.Close()
}
return n, err
}
func (r *flateReadWrapper) Close() error {
if r.fr == nil {
return io.ErrClosedPipe
}
err := r.fr.Close()
flateReaderPool.Put(r.fr)
r.fr = nil
return err
}

View File

@ -13,15 +13,25 @@ import (
"math/rand" "math/rand"
"net" "net"
"strconv" "strconv"
"sync"
"time" "time"
"unicode/utf8"
) )
const ( const (
// Frame header byte 0 bits from Section 5.2 of RFC 6455
finalBit = 1 << 7
rsv1Bit = 1 << 6
rsv2Bit = 1 << 5
rsv3Bit = 1 << 4
// Frame header byte 1 bits from Section 5.2 of RFC 6455
maskBit = 1 << 7
maxFrameHeaderSize = 2 + 8 + 4 // Fixed header + length + mask maxFrameHeaderSize = 2 + 8 + 4 // Fixed header + length + mask
maxControlFramePayloadSize = 125 maxControlFramePayloadSize = 125
finalBit = 1 << 7
maskBit = 1 << 7 writeWait = time.Second
writeWait = time.Second
defaultReadBufferSize = 4096 defaultReadBufferSize = 4096
defaultWriteBufferSize = 4096 defaultWriteBufferSize = 4096
@ -43,6 +53,8 @@ const (
CloseMessageTooBig = 1009 CloseMessageTooBig = 1009
CloseMandatoryExtension = 1010 CloseMandatoryExtension = 1010
CloseInternalServerErr = 1011 CloseInternalServerErr = 1011
CloseServiceRestart = 1012
CloseTryAgainLater = 1013
CloseTLSHandshake = 1015 CloseTLSHandshake = 1015
) )
@ -169,6 +181,11 @@ var (
errInvalidControlFrame = errors.New("websocket: invalid control frame") errInvalidControlFrame = errors.New("websocket: invalid control frame")
) )
func newMaskKey() [4]byte {
n := rand.Uint32()
return [4]byte{byte(n), byte(n >> 8), byte(n >> 16), byte(n >> 24)}
}
func hideTempErr(err error) error { func hideTempErr(err error) error {
if e, ok := err.(net.Error); ok && e.Temporary() { if e, ok := err.(net.Error); ok && e.Temporary() {
err = &netError{msg: e.Error(), timeout: e.Timeout()} err = &netError{msg: e.Error(), timeout: e.Timeout()}
@ -184,74 +201,138 @@ func isData(frameType int) bool {
return frameType == TextMessage || frameType == BinaryMessage return frameType == TextMessage || frameType == BinaryMessage
} }
func maskBytes(key [4]byte, pos int, b []byte) int { var validReceivedCloseCodes = map[int]bool{
for i := range b { // see http://www.iana.org/assignments/websocket/websocket.xhtml#close-code-number
b[i] ^= key[pos&3]
pos++ CloseNormalClosure: true,
} CloseGoingAway: true,
return pos & 3 CloseProtocolError: true,
CloseUnsupportedData: true,
CloseNoStatusReceived: false,
CloseAbnormalClosure: false,
CloseInvalidFramePayloadData: true,
ClosePolicyViolation: true,
CloseMessageTooBig: true,
CloseMandatoryExtension: true,
CloseInternalServerErr: true,
CloseServiceRestart: true,
CloseTryAgainLater: true,
CloseTLSHandshake: false,
} }
func newMaskKey() [4]byte { func isValidReceivedCloseCode(code int) bool {
n := rand.Uint32() return validReceivedCloseCodes[code] || (code >= 3000 && code <= 4999)
return [4]byte{byte(n), byte(n >> 8), byte(n >> 16), byte(n >> 24)}
} }
// Conn represents a WebSocket connection. // The Conn type represents a WebSocket connection.
type Conn struct { type Conn struct {
conn net.Conn conn net.Conn
isServer bool isServer bool
subprotocol string subprotocol string
// Write fields // Write fields
mu chan bool // used as mutex to protect write to conn and closeSent mu chan bool // used as mutex to protect write to conn
closeSent bool // true if close message was sent writeBuf []byte // frame is constructed in this buffer.
writeDeadline time.Time
writer io.WriteCloser // the current writer returned to the application
isWriting bool // for best-effort concurrent write detection
// Message writer fields. writeErrMu sync.Mutex
writeErr error writeErr error
writeBuf []byte // frame is constructed in this buffer.
writePos int // end of data in writeBuf. enableWriteCompression bool
writeFrameType int // type of the current frame. compressionLevel int
writeSeq int // incremented to invalidate message writers. newCompressionWriter func(io.WriteCloser, int) io.WriteCloser
writeDeadline time.Time
isWriting bool // for best-effort concurrent write detection
// Read fields // Read fields
reader io.ReadCloser // the current reader returned to the application
readErr error readErr error
br *bufio.Reader br *bufio.Reader
readRemaining int64 // bytes remaining in current frame. readRemaining int64 // bytes remaining in current frame.
readFinal bool // true the current message has more frames. readFinal bool // true the current message has more frames.
readSeq int // incremented to invalidate message readers.
readLength int64 // Message size. readLength int64 // Message size.
readLimit int64 // Maximum message size. readLimit int64 // Maximum message size.
readMaskPos int readMaskPos int
readMaskKey [4]byte readMaskKey [4]byte
handlePong func(string) error handlePong func(string) error
handlePing func(string) error handlePing func(string) error
handleClose func(int, string) error
readErrCount int readErrCount int
messageReader *messageReader // the current low-level reader
readDecompress bool // whether last read frame had RSV1 set
newDecompressionReader func(io.Reader) io.ReadCloser
} }
func newConn(conn net.Conn, isServer bool, readBufferSize, writeBufferSize int) *Conn { func newConn(conn net.Conn, isServer bool, readBufferSize, writeBufferSize int) *Conn {
return newConnBRW(conn, isServer, readBufferSize, writeBufferSize, nil)
}
type writeHook struct {
p []byte
}
func (wh *writeHook) Write(p []byte) (int, error) {
wh.p = p
return len(p), nil
}
func newConnBRW(conn net.Conn, isServer bool, readBufferSize, writeBufferSize int, brw *bufio.ReadWriter) *Conn {
mu := make(chan bool, 1) mu := make(chan bool, 1)
mu <- true mu <- true
if readBufferSize == 0 { var br *bufio.Reader
readBufferSize = defaultReadBufferSize if readBufferSize == 0 && brw != nil && brw.Reader != nil {
// Reuse the supplied bufio.Reader if the buffer has a useful size.
// This code assumes that peek on a reader returns
// bufio.Reader.buf[:0].
brw.Reader.Reset(conn)
if p, err := brw.Reader.Peek(0); err == nil && cap(p) >= 256 {
br = brw.Reader
}
} }
if writeBufferSize == 0 { if br == nil {
writeBufferSize = defaultWriteBufferSize if readBufferSize == 0 {
readBufferSize = defaultReadBufferSize
}
if readBufferSize < maxControlFramePayloadSize {
readBufferSize = maxControlFramePayloadSize
}
br = bufio.NewReaderSize(conn, readBufferSize)
}
var writeBuf []byte
if writeBufferSize == 0 && brw != nil && brw.Writer != nil {
// Use the bufio.Writer's buffer if the buffer has a useful size. This
// code assumes that bufio.Writer.buf[:1] is passed to the
// bufio.Writer's underlying writer.
var wh writeHook
brw.Writer.Reset(&wh)
brw.Writer.WriteByte(0)
brw.Flush()
if cap(wh.p) >= maxFrameHeaderSize+256 {
writeBuf = wh.p[:cap(wh.p)]
}
}
if writeBuf == nil {
if writeBufferSize == 0 {
writeBufferSize = defaultWriteBufferSize
}
writeBuf = make([]byte, writeBufferSize+maxFrameHeaderSize)
} }
c := &Conn{ c := &Conn{
isServer: isServer, isServer: isServer,
br: bufio.NewReaderSize(conn, readBufferSize), br: br,
conn: conn, conn: conn,
mu: mu, mu: mu,
readFinal: true, readFinal: true,
writeBuf: make([]byte, writeBufferSize+maxFrameHeaderSize), writeBuf: writeBuf,
writeFrameType: noFrame, enableWriteCompression: true,
writePos: maxFrameHeaderSize, compressionLevel: defaultCompressionLevel,
} }
c.SetCloseHandler(nil)
c.SetPingHandler(nil) c.SetPingHandler(nil)
c.SetPongHandler(nil) c.SetPongHandler(nil)
return c return c
@ -279,29 +360,40 @@ func (c *Conn) RemoteAddr() net.Addr {
// Write methods // Write methods
func (c *Conn) writeFatal(err error) error {
err = hideTempErr(err)
c.writeErrMu.Lock()
if c.writeErr == nil {
c.writeErr = err
}
c.writeErrMu.Unlock()
return err
}
func (c *Conn) write(frameType int, deadline time.Time, bufs ...[]byte) error { func (c *Conn) write(frameType int, deadline time.Time, bufs ...[]byte) error {
<-c.mu <-c.mu
defer func() { c.mu <- true }() defer func() { c.mu <- true }()
if c.closeSent { c.writeErrMu.Lock()
return ErrCloseSent err := c.writeErr
} else if frameType == CloseMessage { c.writeErrMu.Unlock()
c.closeSent = true if err != nil {
return err
} }
c.conn.SetWriteDeadline(deadline) c.conn.SetWriteDeadline(deadline)
for _, buf := range bufs { for _, buf := range bufs {
if len(buf) > 0 { if len(buf) > 0 {
n, err := c.conn.Write(buf) _, err := c.conn.Write(buf)
if n != len(buf) {
// Close on partial write.
c.conn.Close()
}
if err != nil { if err != nil {
return err return c.writeFatal(err)
} }
} }
} }
if frameType == CloseMessage {
c.writeFatal(ErrCloseSent)
}
return nil return nil
} }
@ -350,60 +442,104 @@ func (c *Conn) WriteControl(messageType int, data []byte, deadline time.Time) er
} }
defer func() { c.mu <- true }() defer func() { c.mu <- true }()
if c.closeSent { c.writeErrMu.Lock()
return ErrCloseSent err := c.writeErr
} else if messageType == CloseMessage { c.writeErrMu.Unlock()
c.closeSent = true if err != nil {
return err
} }
c.conn.SetWriteDeadline(deadline) c.conn.SetWriteDeadline(deadline)
n, err := c.conn.Write(buf) _, err = c.conn.Write(buf)
if n != 0 && n != len(buf) { if err != nil {
c.conn.Close() return c.writeFatal(err)
} }
return hideTempErr(err) if messageType == CloseMessage {
c.writeFatal(ErrCloseSent)
}
return err
} }
// NextWriter returns a writer for the next message to send. The writer's func (c *Conn) prepWrite(messageType int) error {
// Close method flushes the complete message to the network. // Close previous writer if not already closed by the application. It's
// probably better to return an error in this situation, but we cannot
// change this without breaking existing applications.
if c.writer != nil {
c.writer.Close()
c.writer = nil
}
if !isControl(messageType) && !isData(messageType) {
return errBadWriteOpCode
}
c.writeErrMu.Lock()
err := c.writeErr
c.writeErrMu.Unlock()
return err
}
// NextWriter returns a writer for the next message to send. The writer's Close
// method flushes the complete message to the network.
// //
// There can be at most one open writer on a connection. NextWriter closes the // There can be at most one open writer on a connection. NextWriter closes the
// previous writer if the application has not already done so. // previous writer if the application has not already done so.
func (c *Conn) NextWriter(messageType int) (io.WriteCloser, error) { func (c *Conn) NextWriter(messageType int) (io.WriteCloser, error) {
if c.writeErr != nil { if err := c.prepWrite(messageType); err != nil {
return nil, c.writeErr return nil, err
} }
if c.writeFrameType != noFrame { mw := &messageWriter{
if err := c.flushFrame(true, nil); err != nil { c: c,
return nil, err frameType: messageType,
} pos: maxFrameHeaderSize,
} }
c.writer = mw
if !isControl(messageType) && !isData(messageType) { if c.newCompressionWriter != nil && c.enableWriteCompression && isData(messageType) {
return nil, errBadWriteOpCode w := c.newCompressionWriter(c.writer, c.compressionLevel)
mw.compress = true
c.writer = w
} }
return c.writer, nil
c.writeFrameType = messageType
return messageWriter{c, c.writeSeq}, nil
} }
func (c *Conn) flushFrame(final bool, extra []byte) error { type messageWriter struct {
length := c.writePos - maxFrameHeaderSize + len(extra) c *Conn
compress bool // whether next call to flushFrame should set RSV1
pos int // end of data in writeBuf.
frameType int // type of the current frame.
err error
}
func (w *messageWriter) fatal(err error) error {
if w.err != nil {
w.err = err
w.c.writer = nil
}
return err
}
// flushFrame writes buffered data and extra as a frame to the network. The
// final argument indicates that this is the last frame in the message.
func (w *messageWriter) flushFrame(final bool, extra []byte) error {
c := w.c
length := w.pos - maxFrameHeaderSize + len(extra)
// Check for invalid control frames. // Check for invalid control frames.
if isControl(c.writeFrameType) && if isControl(w.frameType) &&
(!final || length > maxControlFramePayloadSize) { (!final || length > maxControlFramePayloadSize) {
c.writeSeq++ return w.fatal(errInvalidControlFrame)
c.writeFrameType = noFrame
c.writePos = maxFrameHeaderSize
return errInvalidControlFrame
} }
b0 := byte(c.writeFrameType) b0 := byte(w.frameType)
if final { if final {
b0 |= finalBit b0 |= finalBit
} }
if w.compress {
b0 |= rsv1Bit
}
w.compress = false
b1 := byte(0) b1 := byte(0)
if !c.isServer { if !c.isServer {
b1 |= maskBit b1 |= maskBit
@ -435,10 +571,9 @@ func (c *Conn) flushFrame(final bool, extra []byte) error {
if !c.isServer { if !c.isServer {
key := newMaskKey() key := newMaskKey()
copy(c.writeBuf[maxFrameHeaderSize-4:], key[:]) copy(c.writeBuf[maxFrameHeaderSize-4:], key[:])
maskBytes(key, 0, c.writeBuf[maxFrameHeaderSize:c.writePos]) maskBytes(key, 0, c.writeBuf[maxFrameHeaderSize:w.pos])
if len(extra) > 0 { if len(extra) > 0 {
c.writeErr = errors.New("websocket: internal error, extra used in client mode") return c.writeFatal(errors.New("websocket: internal error, extra used in client mode"))
return c.writeErr
} }
} }
@ -451,46 +586,35 @@ func (c *Conn) flushFrame(final bool, extra []byte) error {
} }
c.isWriting = true c.isWriting = true
c.writeErr = c.write(c.writeFrameType, c.writeDeadline, c.writeBuf[framePos:c.writePos], extra) err := c.write(w.frameType, c.writeDeadline, c.writeBuf[framePos:w.pos], extra)
if !c.isWriting { if !c.isWriting {
panic("concurrent write to websocket connection") panic("concurrent write to websocket connection")
} }
c.isWriting = false c.isWriting = false
// Setup for next frame. if err != nil {
c.writePos = maxFrameHeaderSize return w.fatal(err)
c.writeFrameType = continuationFrame }
if final { if final {
c.writeSeq++ c.writer = nil
c.writeFrameType = noFrame return nil
} }
return c.writeErr
}
type messageWriter struct { // Setup for next frame.
c *Conn w.pos = maxFrameHeaderSize
seq int w.frameType = continuationFrame
}
func (w messageWriter) err() error {
c := w.c
if c.writeSeq != w.seq {
return errWriteClosed
}
if c.writeErr != nil {
return c.writeErr
}
return nil return nil
} }
func (w messageWriter) ncopy(max int) (int, error) { func (w *messageWriter) ncopy(max int) (int, error) {
n := len(w.c.writeBuf) - w.c.writePos n := len(w.c.writeBuf) - w.pos
if n <= 0 { if n <= 0 {
if err := w.c.flushFrame(false, nil); err != nil { if err := w.flushFrame(false, nil); err != nil {
return 0, err return 0, err
} }
n = len(w.c.writeBuf) - w.c.writePos n = len(w.c.writeBuf) - w.pos
} }
if n > max { if n > max {
n = max n = max
@ -498,14 +622,14 @@ func (w messageWriter) ncopy(max int) (int, error) {
return n, nil return n, nil
} }
func (w messageWriter) write(final bool, p []byte) (int, error) { func (w *messageWriter) Write(p []byte) (int, error) {
if err := w.err(); err != nil { if w.err != nil {
return 0, err return 0, w.err
} }
if len(p) > 2*len(w.c.writeBuf) && w.c.isServer { if len(p) > 2*len(w.c.writeBuf) && w.c.isServer {
// Don't buffer large messages. // Don't buffer large messages.
err := w.c.flushFrame(final, p) err := w.flushFrame(false, p)
if err != nil { if err != nil {
return 0, err return 0, err
} }
@ -518,20 +642,16 @@ func (w messageWriter) write(final bool, p []byte) (int, error) {
if err != nil { if err != nil {
return 0, err return 0, err
} }
copy(w.c.writeBuf[w.c.writePos:], p[:n]) copy(w.c.writeBuf[w.pos:], p[:n])
w.c.writePos += n w.pos += n
p = p[n:] p = p[n:]
} }
return nn, nil return nn, nil
} }
func (w messageWriter) Write(p []byte) (int, error) { func (w *messageWriter) WriteString(p string) (int, error) {
return w.write(false, p) if w.err != nil {
} return 0, w.err
func (w messageWriter) WriteString(p string) (int, error) {
if err := w.err(); err != nil {
return 0, err
} }
nn := len(p) nn := len(p)
@ -540,27 +660,27 @@ func (w messageWriter) WriteString(p string) (int, error) {
if err != nil { if err != nil {
return 0, err return 0, err
} }
copy(w.c.writeBuf[w.c.writePos:], p[:n]) copy(w.c.writeBuf[w.pos:], p[:n])
w.c.writePos += n w.pos += n
p = p[n:] p = p[n:]
} }
return nn, nil return nn, nil
} }
func (w messageWriter) ReadFrom(r io.Reader) (nn int64, err error) { func (w *messageWriter) ReadFrom(r io.Reader) (nn int64, err error) {
if err := w.err(); err != nil { if w.err != nil {
return 0, err return 0, w.err
} }
for { for {
if w.c.writePos == len(w.c.writeBuf) { if w.pos == len(w.c.writeBuf) {
err = w.c.flushFrame(false, nil) err = w.flushFrame(false, nil)
if err != nil { if err != nil {
break break
} }
} }
var n int var n int
n, err = r.Read(w.c.writeBuf[w.c.writePos:]) n, err = r.Read(w.c.writeBuf[w.pos:])
w.c.writePos += n w.pos += n
nn += int64(n) nn += int64(n)
if err != nil { if err != nil {
if err == io.EOF { if err == io.EOF {
@ -572,30 +692,64 @@ func (w messageWriter) ReadFrom(r io.Reader) (nn int64, err error) {
return nn, err return nn, err
} }
func (w messageWriter) Close() error { func (w *messageWriter) Close() error {
if err := w.err(); err != nil { if w.err != nil {
return w.err
}
if err := w.flushFrame(true, nil); err != nil {
return err return err
} }
return w.c.flushFrame(true, nil) w.err = errWriteClosed
return nil
}
// WritePreparedMessage writes prepared message into connection.
func (c *Conn) WritePreparedMessage(pm *PreparedMessage) error {
frameType, frameData, err := pm.frame(prepareKey{
isServer: c.isServer,
compress: c.newCompressionWriter != nil && c.enableWriteCompression && isData(pm.messageType),
compressionLevel: c.compressionLevel,
})
if err != nil {
return err
}
if c.isWriting {
panic("concurrent write to websocket connection")
}
c.isWriting = true
err = c.write(frameType, c.writeDeadline, frameData, nil)
if !c.isWriting {
panic("concurrent write to websocket connection")
}
c.isWriting = false
return err
} }
// WriteMessage is a helper method for getting a writer using NextWriter, // WriteMessage is a helper method for getting a writer using NextWriter,
// writing the message and closing the writer. // writing the message and closing the writer.
func (c *Conn) WriteMessage(messageType int, data []byte) error { func (c *Conn) WriteMessage(messageType int, data []byte) error {
wr, err := c.NextWriter(messageType)
if c.isServer && (c.newCompressionWriter == nil || !c.enableWriteCompression) {
// Fast path with no allocations and single frame.
if err := c.prepWrite(messageType); err != nil {
return err
}
mw := messageWriter{c: c, frameType: messageType, pos: maxFrameHeaderSize}
n := copy(c.writeBuf[mw.pos:], data)
mw.pos += n
data = data[n:]
return mw.flushFrame(true, data)
}
w, err := c.NextWriter(messageType)
if err != nil { if err != nil {
return err return err
} }
w := wr.(messageWriter) if _, err = w.Write(data); err != nil {
if _, err := w.write(true, data); err != nil {
return err return err
} }
if c.writeSeq == w.seq { return w.Close()
if err := c.flushFrame(true, nil); err != nil {
return err
}
}
return nil
} }
// SetWriteDeadline sets the write deadline on the underlying network // SetWriteDeadline sets the write deadline on the underlying network
@ -609,22 +763,6 @@ func (c *Conn) SetWriteDeadline(t time.Time) error {
// Read methods // Read methods
// readFull is like io.ReadFull except that io.EOF is never returned.
func (c *Conn) readFull(p []byte) (err error) {
var n int
for n < len(p) && err == nil {
var nn int
nn, err = c.br.Read(p[n:])
n += nn
}
if n == len(p) {
err = nil
} else if err == io.EOF {
err = errUnexpectedEOF
}
return
}
func (c *Conn) advanceFrame() (int, error) { func (c *Conn) advanceFrame() (int, error) {
// 1. Skip remainder of previous frame. // 1. Skip remainder of previous frame.
@ -637,19 +775,24 @@ func (c *Conn) advanceFrame() (int, error) {
// 2. Read and parse first two bytes of frame header. // 2. Read and parse first two bytes of frame header.
var b [8]byte p, err := c.read(2)
if err := c.readFull(b[:2]); err != nil { if err != nil {
return noFrame, err return noFrame, err
} }
final := b[0]&finalBit != 0 final := p[0]&finalBit != 0
frameType := int(b[0] & 0xf) frameType := int(p[0] & 0xf)
reserved := int((b[0] >> 4) & 0x7) mask := p[1]&maskBit != 0
mask := b[1]&maskBit != 0 c.readRemaining = int64(p[1] & 0x7f)
c.readRemaining = int64(b[1] & 0x7f)
if reserved != 0 { c.readDecompress = false
return noFrame, c.handleProtocolError("unexpected reserved bits " + strconv.Itoa(reserved)) if c.newDecompressionReader != nil && (p[0]&rsv1Bit) != 0 {
c.readDecompress = true
p[0] &^= rsv1Bit
}
if rsv := p[0] & (rsv1Bit | rsv2Bit | rsv3Bit); rsv != 0 {
return noFrame, c.handleProtocolError("unexpected reserved bits 0x" + strconv.FormatInt(int64(rsv), 16))
} }
switch frameType { switch frameType {
@ -678,15 +821,17 @@ func (c *Conn) advanceFrame() (int, error) {
switch c.readRemaining { switch c.readRemaining {
case 126: case 126:
if err := c.readFull(b[:2]); err != nil { p, err := c.read(2)
if err != nil {
return noFrame, err return noFrame, err
} }
c.readRemaining = int64(binary.BigEndian.Uint16(b[:2])) c.readRemaining = int64(binary.BigEndian.Uint16(p))
case 127: case 127:
if err := c.readFull(b[:8]); err != nil { p, err := c.read(8)
if err != nil {
return noFrame, err return noFrame, err
} }
c.readRemaining = int64(binary.BigEndian.Uint64(b[:8])) c.readRemaining = int64(binary.BigEndian.Uint64(p))
} }
// 4. Handle frame masking. // 4. Handle frame masking.
@ -697,9 +842,11 @@ func (c *Conn) advanceFrame() (int, error) {
if mask { if mask {
c.readMaskPos = 0 c.readMaskPos = 0
if err := c.readFull(c.readMaskKey[:]); err != nil { p, err := c.read(len(c.readMaskKey))
if err != nil {
return noFrame, err return noFrame, err
} }
copy(c.readMaskKey[:], p)
} }
// 5. For text and binary messages, enforce read limit and return. // 5. For text and binary messages, enforce read limit and return.
@ -719,9 +866,9 @@ func (c *Conn) advanceFrame() (int, error) {
var payload []byte var payload []byte
if c.readRemaining > 0 { if c.readRemaining > 0 {
payload = make([]byte, c.readRemaining) payload, err = c.read(int(c.readRemaining))
c.readRemaining = 0 c.readRemaining = 0
if err := c.readFull(payload); err != nil { if err != nil {
return noFrame, err return noFrame, err
} }
if c.isServer { if c.isServer {
@ -741,15 +888,21 @@ func (c *Conn) advanceFrame() (int, error) {
return noFrame, err return noFrame, err
} }
case CloseMessage: case CloseMessage:
echoMessage := []byte{}
closeCode := CloseNoStatusReceived closeCode := CloseNoStatusReceived
closeText := "" closeText := ""
if len(payload) >= 2 { if len(payload) >= 2 {
echoMessage = payload[:2]
closeCode = int(binary.BigEndian.Uint16(payload)) closeCode = int(binary.BigEndian.Uint16(payload))
if !isValidReceivedCloseCode(closeCode) {
return noFrame, c.handleProtocolError("invalid close code")
}
closeText = string(payload[2:]) closeText = string(payload[2:])
if !utf8.ValidString(closeText) {
return noFrame, c.handleProtocolError("invalid utf8 payload in close frame")
}
}
if err := c.handleClose(closeCode, closeText); err != nil {
return noFrame, err
} }
c.WriteControl(CloseMessage, echoMessage, time.Now().Add(writeWait))
return noFrame, &CloseError{Code: closeCode, Text: closeText} return noFrame, &CloseError{Code: closeCode, Text: closeText}
} }
@ -772,8 +925,13 @@ func (c *Conn) handleProtocolError(message string) error {
// permanent. Once this method returns a non-nil error, all subsequent calls to // permanent. Once this method returns a non-nil error, all subsequent calls to
// this method return the same error. // this method return the same error.
func (c *Conn) NextReader() (messageType int, r io.Reader, err error) { func (c *Conn) NextReader() (messageType int, r io.Reader, err error) {
// Close previous reader, only relevant for decompression.
if c.reader != nil {
c.reader.Close()
c.reader = nil
}
c.readSeq++ c.messageReader = nil
c.readLength = 0 c.readLength = 0
for c.readErr == nil { for c.readErr == nil {
@ -783,7 +941,12 @@ func (c *Conn) NextReader() (messageType int, r io.Reader, err error) {
break break
} }
if frameType == TextMessage || frameType == BinaryMessage { if frameType == TextMessage || frameType == BinaryMessage {
return frameType, messageReader{c, c.readSeq}, nil c.messageReader = &messageReader{c}
c.reader = c.messageReader
if c.readDecompress {
c.reader = c.newDecompressionReader(c.reader)
}
return frameType, c.reader, nil
} }
} }
@ -798,53 +961,57 @@ func (c *Conn) NextReader() (messageType int, r io.Reader, err error) {
return noFrame, nil, c.readErr return noFrame, nil, c.readErr
} }
type messageReader struct { type messageReader struct{ c *Conn }
c *Conn
seq int
}
func (r messageReader) Read(b []byte) (int, error) { func (r *messageReader) Read(b []byte) (int, error) {
c := r.c
if r.seq != r.c.readSeq { if c.messageReader != r {
return 0, io.EOF return 0, io.EOF
} }
for r.c.readErr == nil { for c.readErr == nil {
if r.c.readRemaining > 0 { if c.readRemaining > 0 {
if int64(len(b)) > r.c.readRemaining { if int64(len(b)) > c.readRemaining {
b = b[:r.c.readRemaining] b = b[:c.readRemaining]
} }
n, err := r.c.br.Read(b) n, err := c.br.Read(b)
r.c.readErr = hideTempErr(err) c.readErr = hideTempErr(err)
if r.c.isServer { if c.isServer {
r.c.readMaskPos = maskBytes(r.c.readMaskKey, r.c.readMaskPos, b[:n]) c.readMaskPos = maskBytes(c.readMaskKey, c.readMaskPos, b[:n])
} }
r.c.readRemaining -= int64(n) c.readRemaining -= int64(n)
return n, r.c.readErr if c.readRemaining > 0 && c.readErr == io.EOF {
c.readErr = errUnexpectedEOF
}
return n, c.readErr
} }
if r.c.readFinal { if c.readFinal {
r.c.readSeq++ c.messageReader = nil
return 0, io.EOF return 0, io.EOF
} }
frameType, err := r.c.advanceFrame() frameType, err := c.advanceFrame()
switch { switch {
case err != nil: case err != nil:
r.c.readErr = hideTempErr(err) c.readErr = hideTempErr(err)
case frameType == TextMessage || frameType == BinaryMessage: case frameType == TextMessage || frameType == BinaryMessage:
r.c.readErr = errors.New("websocket: internal error, unexpected text or binary in Reader") c.readErr = errors.New("websocket: internal error, unexpected text or binary in Reader")
} }
} }
err := r.c.readErr err := c.readErr
if err == io.EOF && r.seq == r.c.readSeq { if err == io.EOF && c.messageReader == r {
err = errUnexpectedEOF err = errUnexpectedEOF
} }
return 0, err return 0, err
} }
func (r *messageReader) Close() error {
return nil
}
// ReadMessage is a helper method for getting a reader using NextReader and // ReadMessage is a helper method for getting a reader using NextReader and
// reading from that reader to a buffer. // reading from that reader to a buffer.
func (c *Conn) ReadMessage() (messageType int, p []byte, err error) { func (c *Conn) ReadMessage() (messageType int, p []byte, err error) {
@ -872,9 +1039,49 @@ func (c *Conn) SetReadLimit(limit int64) {
c.readLimit = limit c.readLimit = limit
} }
// CloseHandler returns the current close handler
func (c *Conn) CloseHandler() func(code int, text string) error {
return c.handleClose
}
// SetCloseHandler sets the handler for close messages received from the peer.
// The code argument to h is the received close code or CloseNoStatusReceived
// if the close message is empty. The default close handler sends a close frame
// back to the peer.
//
// The application must read the connection to process close messages as
// described in the section on Control Frames above.
//
// The connection read methods return a CloseError when a close frame is
// received. Most applications should handle close messages as part of their
// normal error handling. Applications should only set a close handler when the
// application must perform some action before sending a close frame back to
// the peer.
func (c *Conn) SetCloseHandler(h func(code int, text string) error) {
if h == nil {
h = func(code int, text string) error {
message := []byte{}
if code != CloseNoStatusReceived {
message = FormatCloseMessage(code, "")
}
c.WriteControl(CloseMessage, message, time.Now().Add(writeWait))
return nil
}
}
c.handleClose = h
}
// PingHandler returns the current ping handler
func (c *Conn) PingHandler() func(appData string) error {
return c.handlePing
}
// SetPingHandler sets the handler for ping messages received from the peer. // SetPingHandler sets the handler for ping messages received from the peer.
// The appData argument to h is the PING frame application data. The default // The appData argument to h is the PING frame application data. The default
// ping handler sends a pong to the peer. // ping handler sends a pong to the peer.
//
// The application must read the connection to process ping messages as
// described in the section on Control Frames above.
func (c *Conn) SetPingHandler(h func(appData string) error) { func (c *Conn) SetPingHandler(h func(appData string) error) {
if h == nil { if h == nil {
h = func(message string) error { h = func(message string) error {
@ -890,9 +1097,17 @@ func (c *Conn) SetPingHandler(h func(appData string) error) {
c.handlePing = h c.handlePing = h
} }
// PongHandler returns the current pong handler
func (c *Conn) PongHandler() func(appData string) error {
return c.handlePong
}
// SetPongHandler sets the handler for pong messages received from the peer. // SetPongHandler sets the handler for pong messages received from the peer.
// The appData argument to h is the PONG frame application data. The default // The appData argument to h is the PONG frame application data. The default
// pong handler does nothing. // pong handler does nothing.
//
// The application must read the connection to process ping messages as
// described in the section on Control Frames above.
func (c *Conn) SetPongHandler(h func(appData string) error) { func (c *Conn) SetPongHandler(h func(appData string) error) {
if h == nil { if h == nil {
h = func(string) error { return nil } h = func(string) error { return nil }
@ -906,6 +1121,25 @@ func (c *Conn) UnderlyingConn() net.Conn {
return c.conn return c.conn
} }
// EnableWriteCompression enables and disables write compression of
// subsequent text and binary messages. This function is a noop if
// compression was not negotiated with the peer.
func (c *Conn) EnableWriteCompression(enable bool) {
c.enableWriteCompression = enable
}
// SetCompressionLevel sets the flate compression level for subsequent text and
// binary messages. This function is a noop if compression was not negotiated
// with the peer. See the compress/flate package for a description of
// compression levels.
func (c *Conn) SetCompressionLevel(level int) error {
if !isValidCompressionLevel(level) {
return errors.New("websocket: invalid compression level")
}
c.compressionLevel = level
return nil
}
// FormatCloseMessage formats closeCode and text as a WebSocket close message. // FormatCloseMessage formats closeCode and text as a WebSocket close message.
func FormatCloseMessage(closeCode int, text string) []byte { func FormatCloseMessage(closeCode int, text string) []byte {
buf := make([]byte, 2+len(text)) buf := make([]byte, 2+len(text))

18
vendor/github.com/gorilla/websocket/conn_read.go generated vendored Normal file
View File

@ -0,0 +1,18 @@
// Copyright 2016 The Gorilla WebSocket Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build go1.5
package websocket
import "io"
func (c *Conn) read(n int) ([]byte, error) {
p, err := c.br.Peek(n)
if err == io.EOF {
err = errUnexpectedEOF
}
c.br.Discard(len(p))
return p, err
}

View File

@ -0,0 +1,21 @@
// Copyright 2016 The Gorilla WebSocket Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build !go1.5
package websocket
import "io"
func (c *Conn) read(n int) ([]byte, error) {
p, err := c.br.Peek(n)
if err == io.EOF {
err = errUnexpectedEOF
}
if len(p) > 0 {
// advance over the bytes just read
io.ReadFull(c.br, p)
}
return p, err
}

View File

@ -118,9 +118,10 @@
// //
// Applications are responsible for ensuring that no more than one goroutine // Applications are responsible for ensuring that no more than one goroutine
// calls the write methods (NextWriter, SetWriteDeadline, WriteMessage, // calls the write methods (NextWriter, SetWriteDeadline, WriteMessage,
// WriteJSON) concurrently and that no more than one goroutine calls the read // WriteJSON, EnableWriteCompression, SetCompressionLevel) concurrently and
// methods (NextReader, SetReadDeadline, ReadMessage, ReadJSON, SetPongHandler, // that no more than one goroutine calls the read methods (NextReader,
// SetPingHandler) concurrently. // SetReadDeadline, ReadMessage, ReadJSON, SetPongHandler, SetPingHandler)
// concurrently.
// //
// The Close and WriteControl methods can be called concurrently with all other // The Close and WriteControl methods can be called concurrently with all other
// methods. // methods.
@ -149,4 +150,31 @@
// The deprecated Upgrade function does not enforce an origin policy. It's the // The deprecated Upgrade function does not enforce an origin policy. It's the
// application's responsibility to check the Origin header before calling // application's responsibility to check the Origin header before calling
// Upgrade. // Upgrade.
//
// Compression EXPERIMENTAL
//
// Per message compression extensions (RFC 7692) are experimentally supported
// by this package in a limited capacity. Setting the EnableCompression option
// to true in Dialer or Upgrader will attempt to negotiate per message deflate
// support.
//
// var upgrader = websocket.Upgrader{
// EnableCompression: true,
// }
//
// If compression was successfully negotiated with the connection's peer, any
// message received in compressed form will be automatically decompressed.
// All Read methods will return uncompressed bytes.
//
// Per message compression of messages written to a connection can be enabled
// or disabled by calling the corresponding Conn method:
//
// conn.EnableWriteCompression(false)
//
// Currently this package does not support compression with "context takeover".
// This means that messages must be compressed and decompressed in isolation,
// without retaining sliding window or dictionary state across messages. For
// more details refer to RFC 7692.
//
// Use of compression is experimental and may result in decreased performance.
package websocket package websocket

View File

@ -8,17 +8,19 @@ package main
import ( import (
"errors" "errors"
"flag" "flag"
"github.com/gorilla/websocket"
"io" "io"
"log" "log"
"net/http" "net/http"
"time" "time"
"unicode/utf8" "unicode/utf8"
"github.com/gorilla/websocket"
) )
var upgrader = websocket.Upgrader{ var upgrader = websocket.Upgrader{
ReadBufferSize: 4096, ReadBufferSize: 4096,
WriteBufferSize: 4096, WriteBufferSize: 4096,
EnableCompression: true,
CheckOrigin: func(r *http.Request) bool { CheckOrigin: func(r *http.Request) bool {
return true return true
}, },
@ -83,7 +85,7 @@ func echoCopyFull(w http.ResponseWriter, r *http.Request) {
// echoReadAll echoes messages from the client by reading the entire message // echoReadAll echoes messages from the client by reading the entire message
// with ioutil.ReadAll. // with ioutil.ReadAll.
func echoReadAll(w http.ResponseWriter, r *http.Request, writeMessage bool) { func echoReadAll(w http.ResponseWriter, r *http.Request, writeMessage, writePrepared bool) {
conn, err := upgrader.Upgrade(w, r, nil) conn, err := upgrader.Upgrade(w, r, nil)
if err != nil { if err != nil {
log.Println("Upgrade:", err) log.Println("Upgrade:", err)
@ -107,9 +109,21 @@ func echoReadAll(w http.ResponseWriter, r *http.Request, writeMessage bool) {
} }
} }
if writeMessage { if writeMessage {
err = conn.WriteMessage(mt, b) if !writePrepared {
if err != nil { err = conn.WriteMessage(mt, b)
log.Println("WriteMessage:", err) if err != nil {
log.Println("WriteMessage:", err)
}
} else {
pm, err := websocket.NewPreparedMessage(mt, b)
if err != nil {
log.Println("NewPreparedMessage:", err)
return
}
err = conn.WritePreparedMessage(pm)
if err != nil {
log.Println("WritePreparedMessage:", err)
}
} }
} else { } else {
w, err := conn.NextWriter(mt) w, err := conn.NextWriter(mt)
@ -130,11 +144,15 @@ func echoReadAll(w http.ResponseWriter, r *http.Request, writeMessage bool) {
} }
func echoReadAllWriter(w http.ResponseWriter, r *http.Request) { func echoReadAllWriter(w http.ResponseWriter, r *http.Request) {
echoReadAll(w, r, false) echoReadAll(w, r, false, false)
} }
func echoReadAllWriteMessage(w http.ResponseWriter, r *http.Request) { func echoReadAllWriteMessage(w http.ResponseWriter, r *http.Request) {
echoReadAll(w, r, true) echoReadAll(w, r, true, false)
}
func echoReadAllWritePreparedMessage(w http.ResponseWriter, r *http.Request) {
echoReadAll(w, r, true, true)
} }
func serveHome(w http.ResponseWriter, r *http.Request) { func serveHome(w http.ResponseWriter, r *http.Request) {
@ -159,6 +177,7 @@ func main() {
http.HandleFunc("/f", echoCopyFull) http.HandleFunc("/f", echoCopyFull)
http.HandleFunc("/r", echoReadAllWriter) http.HandleFunc("/r", echoReadAllWriter)
http.HandleFunc("/m", echoReadAllWriteMessage) http.HandleFunc("/m", echoReadAllWriteMessage)
http.HandleFunc("/p", echoReadAllWritePreparedMessage)
err := http.ListenAndServe(*addr, nil) err := http.ListenAndServe(*addr, nil)
if err != nil { if err != nil {
log.Fatal("ListenAndServe: ", err) log.Fatal("ListenAndServe: ", err)

View File

@ -0,0 +1,134 @@
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"bytes"
"log"
"net/http"
"time"
"github.com/gorilla/websocket"
)
const (
// Time allowed to write a message to the peer.
writeWait = 10 * time.Second
// Time allowed to read the next pong message from the peer.
pongWait = 60 * time.Second
// Send pings to peer with this period. Must be less than pongWait.
pingPeriod = (pongWait * 9) / 10
// Maximum message size allowed from peer.
maxMessageSize = 512
)
var (
newline = []byte{'\n'}
space = []byte{' '}
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
// Client is a middleman between the websocket connection and the hub.
type Client struct {
hub *Hub
// The websocket connection.
conn *websocket.Conn
// Buffered channel of outbound messages.
send chan []byte
}
// readPump pumps messages from the websocket connection to the hub.
//
// The application runs readPump in a per-connection goroutine. The application
// ensures that there is at most one reader on a connection by executing all
// reads from this goroutine.
func (c *Client) readPump() {
defer func() {
c.hub.unregister <- c
c.conn.Close()
}()
c.conn.SetReadLimit(maxMessageSize)
c.conn.SetReadDeadline(time.Now().Add(pongWait))
c.conn.SetPongHandler(func(string) error { c.conn.SetReadDeadline(time.Now().Add(pongWait)); return nil })
for {
_, message, err := c.conn.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway) {
log.Printf("error: %v", err)
}
break
}
message = bytes.TrimSpace(bytes.Replace(message, newline, space, -1))
c.hub.broadcast <- message
}
}
// writePump pumps messages from the hub to the websocket connection.
//
// A goroutine running writePump is started for each connection. The
// application ensures that there is at most one writer to a connection by
// executing all writes from this goroutine.
func (c *Client) writePump() {
ticker := time.NewTicker(pingPeriod)
defer func() {
ticker.Stop()
c.conn.Close()
}()
for {
select {
case message, ok := <-c.send:
c.conn.SetWriteDeadline(time.Now().Add(writeWait))
if !ok {
// The hub closed the channel.
c.conn.WriteMessage(websocket.CloseMessage, []byte{})
return
}
w, err := c.conn.NextWriter(websocket.TextMessage)
if err != nil {
return
}
w.Write(message)
// Add queued chat messages to the current websocket message.
n := len(c.send)
for i := 0; i < n; i++ {
w.Write(newline)
w.Write(<-c.send)
}
if err := w.Close(); err != nil {
return
}
case <-ticker.C:
c.conn.SetWriteDeadline(time.Now().Add(writeWait))
if err := c.conn.WriteMessage(websocket.PingMessage, []byte{}); err != nil {
return
}
}
}
}
// serveWs handles websocket requests from the peer.
func serveWs(hub *Hub, w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println(err)
return
}
client := &Client{hub: hub, conn: conn, send: make(chan []byte, 256)}
client.hub.register <- client
go client.writePump()
client.readPump()
}

View File

@ -1,105 +0,0 @@
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"github.com/gorilla/websocket"
"log"
"net/http"
"time"
)
const (
// Time allowed to write a message to the peer.
writeWait = 10 * time.Second
// Time allowed to read the next pong message from the peer.
pongWait = 60 * time.Second
// Send pings to peer with this period. Must be less than pongWait.
pingPeriod = (pongWait * 9) / 10
// Maximum message size allowed from peer.
maxMessageSize = 512
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
// connection is an middleman between the websocket connection and the hub.
type connection struct {
// The websocket connection.
ws *websocket.Conn
// Buffered channel of outbound messages.
send chan []byte
}
// readPump pumps messages from the websocket connection to the hub.
func (c *connection) readPump() {
defer func() {
h.unregister <- c
c.ws.Close()
}()
c.ws.SetReadLimit(maxMessageSize)
c.ws.SetReadDeadline(time.Now().Add(pongWait))
c.ws.SetPongHandler(func(string) error { c.ws.SetReadDeadline(time.Now().Add(pongWait)); return nil })
for {
_, message, err := c.ws.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway) {
log.Printf("error: %v", err)
}
break
}
h.broadcast <- message
}
}
// write writes a message with the given message type and payload.
func (c *connection) write(mt int, payload []byte) error {
c.ws.SetWriteDeadline(time.Now().Add(writeWait))
return c.ws.WriteMessage(mt, payload)
}
// writePump pumps messages from the hub to the websocket connection.
func (c *connection) writePump() {
ticker := time.NewTicker(pingPeriod)
defer func() {
ticker.Stop()
c.ws.Close()
}()
for {
select {
case message, ok := <-c.send:
if !ok {
c.write(websocket.CloseMessage, []byte{})
return
}
if err := c.write(websocket.TextMessage, message); err != nil {
return
}
case <-ticker.C:
if err := c.write(websocket.PingMessage, []byte{}); err != nil {
return
}
}
}
}
// serveWs handles websocket requests from the peer.
func serveWs(w http.ResponseWriter, r *http.Request) {
ws, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println(err)
return
}
c := &connection{send: make(chan []byte, 256), ws: ws}
h.register <- c
go c.writePump()
c.readPump()
}

View File

@ -4,46 +4,48 @@
package main package main
// hub maintains the set of active connections and broadcasts messages to the // hub maintains the set of active clients and broadcasts messages to the
// connections. // clients.
type hub struct { type Hub struct {
// Registered connections. // Registered clients.
connections map[*connection]bool clients map[*Client]bool
// Inbound messages from the connections. // Inbound messages from the clients.
broadcast chan []byte broadcast chan []byte
// Register requests from the connections. // Register requests from the clients.
register chan *connection register chan *Client
// Unregister requests from connections. // Unregister requests from clients.
unregister chan *connection unregister chan *Client
} }
var h = hub{ func newHub() *Hub {
broadcast: make(chan []byte), return &Hub{
register: make(chan *connection), broadcast: make(chan []byte),
unregister: make(chan *connection), register: make(chan *Client),
connections: make(map[*connection]bool), unregister: make(chan *Client),
clients: make(map[*Client]bool),
}
} }
func (h *hub) run() { func (h *Hub) run() {
for { for {
select { select {
case c := <-h.register: case client := <-h.register:
h.connections[c] = true h.clients[client] = true
case c := <-h.unregister: case client := <-h.unregister:
if _, ok := h.connections[c]; ok { if _, ok := h.clients[client]; ok {
delete(h.connections, c) delete(h.clients, client)
close(c.send) close(client.send)
} }
case m := <-h.broadcast: case message := <-h.broadcast:
for c := range h.connections { for client := range h.clients {
select { select {
case c.send <- m: case client.send <- message:
default: default:
close(c.send) close(client.send)
delete(h.connections, c) delete(h.clients, client)
} }
} }
} }

View File

@ -8,13 +8,12 @@ import (
"flag" "flag"
"log" "log"
"net/http" "net/http"
"text/template"
) )
var addr = flag.String("addr", ":8080", "http service address") var addr = flag.String("addr", ":8080", "http service address")
var homeTempl = template.Must(template.ParseFiles("home.html"))
func serveHome(w http.ResponseWriter, r *http.Request) { func serveHome(w http.ResponseWriter, r *http.Request) {
log.Println(r.URL)
if r.URL.Path != "/" { if r.URL.Path != "/" {
http.Error(w, "Not found", 404) http.Error(w, "Not found", 404)
return return
@ -23,15 +22,17 @@ func serveHome(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Method not allowed", 405) http.Error(w, "Method not allowed", 405)
return return
} }
w.Header().Set("Content-Type", "text/html; charset=utf-8") http.ServeFile(w, r, "home.html")
homeTempl.Execute(w, r.Host)
} }
func main() { func main() {
flag.Parse() flag.Parse()
go h.run() hub := newHub()
go hub.run()
http.HandleFunc("/", serveHome) http.HandleFunc("/", serveHome)
http.HandleFunc("/ws", serveWs) http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
serveWs(hub, w, r)
})
err := http.ListenAndServe(*addr, nil) err := http.ListenAndServe(*addr, nil)
if err != nil { if err != nil {
log.Fatal("ListenAndServe: ", err) log.Fatal("ListenAndServe: ", err)

View File

@ -12,16 +12,14 @@ import (
"net/http" "net/http"
"os" "os"
"os/exec" "os/exec"
"text/template"
"time" "time"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
) )
var ( var (
addr = flag.String("addr", "127.0.0.1:8080", "http service address") addr = flag.String("addr", "127.0.0.1:8080", "http service address")
cmdPath string cmdPath string
homeTempl = template.Must(template.ParseFiles("home.html"))
) )
const ( const (
@ -36,6 +34,9 @@ const (
// Send pings to peer with this period. Must be less than pongWait. // Send pings to peer with this period. Must be less than pongWait.
pingPeriod = (pongWait * 9) / 10 pingPeriod = (pongWait * 9) / 10
// Time to wait before force close on connection.
closeGracePeriod = 10 * time.Second
) )
func pumpStdin(ws *websocket.Conn, w io.Writer) { func pumpStdin(ws *websocket.Conn, w io.Writer) {
@ -57,19 +58,24 @@ func pumpStdin(ws *websocket.Conn, w io.Writer) {
func pumpStdout(ws *websocket.Conn, r io.Reader, done chan struct{}) { func pumpStdout(ws *websocket.Conn, r io.Reader, done chan struct{}) {
defer func() { defer func() {
ws.Close()
close(done)
}() }()
s := bufio.NewScanner(r) s := bufio.NewScanner(r)
for s.Scan() { for s.Scan() {
ws.SetWriteDeadline(time.Now().Add(writeWait)) ws.SetWriteDeadline(time.Now().Add(writeWait))
if err := ws.WriteMessage(websocket.TextMessage, s.Bytes()); err != nil { if err := ws.WriteMessage(websocket.TextMessage, s.Bytes()); err != nil {
ws.Close()
break break
} }
} }
if s.Err() != nil { if s.Err() != nil {
log.Println("scan:", s.Err()) log.Println("scan:", s.Err())
} }
close(done)
ws.SetWriteDeadline(time.Now().Add(writeWait))
ws.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
time.Sleep(closeGracePeriod)
ws.Close()
} }
func ping(ws *websocket.Conn, done chan struct{}) { func ping(ws *websocket.Conn, done chan struct{}) {
@ -168,8 +174,7 @@ func serveHome(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Method not allowed", 405) http.Error(w, "Method not allowed", 405)
return return
} }
w.Header().Set("Content-Type", "text/html; charset=utf-8") http.ServeFile(w, r, "home.html")
homeTempl.Execute(w, r.Host)
} }
func main() { func main() {

View File

@ -6,12 +6,12 @@ package main
import ( import (
"flag" "flag"
"html/template"
"io/ioutil" "io/ioutil"
"log" "log"
"net/http" "net/http"
"os" "os"
"strconv" "strconv"
"text/template"
"time" "time"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
@ -120,7 +120,7 @@ func serveWs(w http.ResponseWriter, r *http.Request) {
} }
var lastMod time.Time var lastMod time.Time
if n, err := strconv.ParseInt(r.FormValue("lastMod"), 16, 64); err != nil { if n, err := strconv.ParseInt(r.FormValue("lastMod"), 16, 64); err == nil {
lastMod = time.Unix(0, n) lastMod = time.Unix(0, n)
} }

55
vendor/github.com/gorilla/websocket/mask.go generated vendored Normal file
View File

@ -0,0 +1,55 @@
// Copyright 2016 The Gorilla WebSocket Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in the
// LICENSE file.
// +build !appengine
package websocket
import "unsafe"
const wordSize = int(unsafe.Sizeof(uintptr(0)))
func maskBytes(key [4]byte, pos int, b []byte) int {
// Mask one byte at a time for small buffers.
if len(b) < 2*wordSize {
for i := range b {
b[i] ^= key[pos&3]
pos++
}
return pos & 3
}
// Mask one byte at a time to word boundary.
if n := int(uintptr(unsafe.Pointer(&b[0]))) % wordSize; n != 0 {
n = wordSize - n
for i := range b[:n] {
b[i] ^= key[pos&3]
pos++
}
b = b[n:]
}
// Create aligned word size key.
var k [wordSize]byte
for i := range k {
k[i] = key[(pos+i)&3]
}
kw := *(*uintptr)(unsafe.Pointer(&k))
// Mask one word at a time.
n := (len(b) / wordSize) * wordSize
for i := 0; i < n; i += wordSize {
*(*uintptr)(unsafe.Pointer(uintptr(unsafe.Pointer(&b[0])) + uintptr(i))) ^= kw
}
// Mask one byte at a time for remaining bytes.
b = b[n:]
for i := range b {
b[i] ^= key[pos&3]
pos++
}
return pos & 3
}

15
vendor/github.com/gorilla/websocket/mask_safe.go generated vendored Normal file
View File

@ -0,0 +1,15 @@
// Copyright 2016 The Gorilla WebSocket Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in the
// LICENSE file.
// +build appengine
package websocket
func maskBytes(key [4]byte, pos int, b []byte) int {
for i := range b {
b[i] ^= key[pos&3]
pos++
}
return pos & 3
}

103
vendor/github.com/gorilla/websocket/prepared.go generated vendored Normal file
View File

@ -0,0 +1,103 @@
// Copyright 2017 The Gorilla WebSocket Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package websocket
import (
"bytes"
"net"
"sync"
"time"
)
// PreparedMessage caches on the wire representations of a message payload.
// Use PreparedMessage to efficiently send a message payload to multiple
// connections. PreparedMessage is especially useful when compression is used
// because the CPU and memory expensive compression operation can be executed
// once for a given set of compression options.
type PreparedMessage struct {
messageType int
data []byte
err error
mu sync.Mutex
frames map[prepareKey]*preparedFrame
}
// prepareKey defines a unique set of options to cache prepared frames in PreparedMessage.
type prepareKey struct {
isServer bool
compress bool
compressionLevel int
}
// preparedFrame contains data in wire representation.
type preparedFrame struct {
once sync.Once
data []byte
}
// NewPreparedMessage returns an initialized PreparedMessage. You can then send
// it to connection using WritePreparedMessage method. Valid wire
// representation will be calculated lazily only once for a set of current
// connection options.
func NewPreparedMessage(messageType int, data []byte) (*PreparedMessage, error) {
pm := &PreparedMessage{
messageType: messageType,
frames: make(map[prepareKey]*preparedFrame),
data: data,
}
// Prepare a plain server frame.
_, frameData, err := pm.frame(prepareKey{isServer: true, compress: false})
if err != nil {
return nil, err
}
// To protect against caller modifying the data argument, remember the data
// copied to the plain server frame.
pm.data = frameData[len(frameData)-len(data):]
return pm, nil
}
func (pm *PreparedMessage) frame(key prepareKey) (int, []byte, error) {
pm.mu.Lock()
frame, ok := pm.frames[key]
if !ok {
frame = &preparedFrame{}
pm.frames[key] = frame
}
pm.mu.Unlock()
var err error
frame.once.Do(func() {
// Prepare a frame using a 'fake' connection.
// TODO: Refactor code in conn.go to allow more direct construction of
// the frame.
mu := make(chan bool, 1)
mu <- true
var nc prepareConn
c := &Conn{
conn: &nc,
mu: mu,
isServer: key.isServer,
compressionLevel: key.compressionLevel,
enableWriteCompression: true,
writeBuf: make([]byte, defaultWriteBufferSize+maxFrameHeaderSize),
}
if key.compress {
c.newCompressionWriter = compressNoContextTakeover
}
err = c.WriteMessage(pm.messageType, pm.data)
frame.data = nc.buf.Bytes()
})
return pm.messageType, frame.data, err
}
type prepareConn struct {
buf bytes.Buffer
net.Conn
}
func (pc *prepareConn) Write(p []byte) (int, error) { return pc.buf.Write(p) }
func (pc *prepareConn) SetWriteDeadline(t time.Time) error { return nil }

View File

@ -28,8 +28,9 @@ type Upgrader struct {
HandshakeTimeout time.Duration HandshakeTimeout time.Duration
// ReadBufferSize and WriteBufferSize specify I/O buffer sizes. If a buffer // ReadBufferSize and WriteBufferSize specify I/O buffer sizes. If a buffer
// size is zero, then a default value of 4096 is used. The I/O buffer sizes // size is zero, then buffers allocated by the HTTP server are used. The
// do not limit the size of the messages that can be sent or received. // I/O buffer sizes do not limit the size of the messages that can be sent
// or received.
ReadBufferSize, WriteBufferSize int ReadBufferSize, WriteBufferSize int
// Subprotocols specifies the server's supported protocols in order of // Subprotocols specifies the server's supported protocols in order of
@ -46,6 +47,12 @@ type Upgrader struct {
// CheckOrigin is nil, the host in the Origin header must not be set or // CheckOrigin is nil, the host in the Origin header must not be set or
// must match the host of the request. // must match the host of the request.
CheckOrigin func(r *http.Request) bool CheckOrigin func(r *http.Request) bool
// EnableCompression specify if the server should attempt to negotiate per
// message compression (RFC 7692). Setting this value to true does not
// guarantee that compression will be supported. Currently only "no context
// takeover" modes are supported.
EnableCompression bool
} }
func (u *Upgrader) returnError(w http.ResponseWriter, r *http.Request, status int, reason string) (*Conn, error) { func (u *Upgrader) returnError(w http.ResponseWriter, r *http.Request, status int, reason string) (*Conn, error) {
@ -53,6 +60,7 @@ func (u *Upgrader) returnError(w http.ResponseWriter, r *http.Request, status in
if u.Error != nil { if u.Error != nil {
u.Error(w, r, status, err) u.Error(w, r, status, err)
} else { } else {
w.Header().Set("Sec-Websocket-Version", "13")
http.Error(w, http.StatusText(status), status) http.Error(w, http.StatusText(status), status)
} }
return nil, err return nil, err
@ -97,18 +105,23 @@ func (u *Upgrader) selectSubprotocol(r *http.Request, responseHeader http.Header
// response. // response.
func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeader http.Header) (*Conn, error) { func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeader http.Header) (*Conn, error) {
if r.Method != "GET" { if r.Method != "GET" {
return u.returnError(w, r, http.StatusMethodNotAllowed, "websocket: method not GET") return u.returnError(w, r, http.StatusMethodNotAllowed, "websocket: not a websocket handshake: request method is not GET")
} }
if values := r.Header["Sec-Websocket-Version"]; len(values) == 0 || values[0] != "13" {
return u.returnError(w, r, http.StatusBadRequest, "websocket: version != 13") if _, ok := responseHeader["Sec-Websocket-Extensions"]; ok {
return u.returnError(w, r, http.StatusInternalServerError, "websocket: application specific 'Sec-Websocket-Extensions' headers are unsupported")
} }
if !tokenListContainsValue(r.Header, "Connection", "upgrade") { if !tokenListContainsValue(r.Header, "Connection", "upgrade") {
return u.returnError(w, r, http.StatusBadRequest, "websocket: could not find connection header with token 'upgrade'") return u.returnError(w, r, http.StatusBadRequest, "websocket: not a websocket handshake: 'upgrade' token not found in 'Connection' header")
} }
if !tokenListContainsValue(r.Header, "Upgrade", "websocket") { if !tokenListContainsValue(r.Header, "Upgrade", "websocket") {
return u.returnError(w, r, http.StatusBadRequest, "websocket: could not find upgrade header with token 'websocket'") return u.returnError(w, r, http.StatusBadRequest, "websocket: not a websocket handshake: 'websocket' token not found in 'Upgrade' header")
}
if !tokenListContainsValue(r.Header, "Sec-Websocket-Version", "13") {
return u.returnError(w, r, http.StatusBadRequest, "websocket: unsupported version: 13 not found in 'Sec-Websocket-Version' header")
} }
checkOrigin := u.CheckOrigin checkOrigin := u.CheckOrigin
@ -116,19 +129,30 @@ func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeade
checkOrigin = checkSameOrigin checkOrigin = checkSameOrigin
} }
if !checkOrigin(r) { if !checkOrigin(r) {
return u.returnError(w, r, http.StatusForbidden, "websocket: origin not allowed") return u.returnError(w, r, http.StatusForbidden, "websocket: 'Origin' header value not allowed")
} }
challengeKey := r.Header.Get("Sec-Websocket-Key") challengeKey := r.Header.Get("Sec-Websocket-Key")
if challengeKey == "" { if challengeKey == "" {
return u.returnError(w, r, http.StatusBadRequest, "websocket: key missing or blank") return u.returnError(w, r, http.StatusBadRequest, "websocket: not a websocket handshake: `Sec-Websocket-Key' header is missing or blank")
} }
subprotocol := u.selectSubprotocol(r, responseHeader) subprotocol := u.selectSubprotocol(r, responseHeader)
// Negotiate PMCE
var compress bool
if u.EnableCompression {
for _, ext := range parseExtensions(r.Header) {
if ext[""] != "permessage-deflate" {
continue
}
compress = true
break
}
}
var ( var (
netConn net.Conn netConn net.Conn
br *bufio.Reader
err error err error
) )
@ -136,21 +160,25 @@ func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeade
if !ok { if !ok {
return u.returnError(w, r, http.StatusInternalServerError, "websocket: response does not implement http.Hijacker") return u.returnError(w, r, http.StatusInternalServerError, "websocket: response does not implement http.Hijacker")
} }
var rw *bufio.ReadWriter var brw *bufio.ReadWriter
netConn, rw, err = h.Hijack() netConn, brw, err = h.Hijack()
if err != nil { if err != nil {
return u.returnError(w, r, http.StatusInternalServerError, err.Error()) return u.returnError(w, r, http.StatusInternalServerError, err.Error())
} }
br = rw.Reader
if br.Buffered() > 0 { if brw.Reader.Buffered() > 0 {
netConn.Close() netConn.Close()
return nil, errors.New("websocket: client sent data before handshake is complete") return nil, errors.New("websocket: client sent data before handshake is complete")
} }
c := newConn(netConn, true, u.ReadBufferSize, u.WriteBufferSize) c := newConnBRW(netConn, true, u.ReadBufferSize, u.WriteBufferSize, brw)
c.subprotocol = subprotocol c.subprotocol = subprotocol
if compress {
c.newCompressionWriter = compressNoContextTakeover
c.newDecompressionReader = decompressNoContextTakeover
}
p := c.writeBuf[:0] p := c.writeBuf[:0]
p = append(p, "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: "...) p = append(p, "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: "...)
p = append(p, computeAcceptKey(challengeKey)...) p = append(p, computeAcceptKey(challengeKey)...)
@ -160,6 +188,9 @@ func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeade
p = append(p, c.subprotocol...) p = append(p, c.subprotocol...)
p = append(p, "\r\n"...) p = append(p, "\r\n"...)
} }
if compress {
p = append(p, "Sec-Websocket-Extensions: permessage-deflate; server_no_context_takeover; client_no_context_takeover\r\n"...)
}
for k, vs := range responseHeader { for k, vs := range responseHeader {
if k == "Sec-Websocket-Protocol" { if k == "Sec-Websocket-Protocol" {
continue continue

View File

@ -13,19 +13,6 @@ import (
"strings" "strings"
) )
// tokenListContainsValue returns true if the 1#token header with the given
// name contains token.
func tokenListContainsValue(header http.Header, name string, value string) bool {
for _, v := range header[name] {
for _, s := range strings.Split(v, ",") {
if strings.EqualFold(value, strings.TrimSpace(s)) {
return true
}
}
}
return false
}
var keyGUID = []byte("258EAFA5-E914-47DA-95CA-C5AB0DC85B11") var keyGUID = []byte("258EAFA5-E914-47DA-95CA-C5AB0DC85B11")
func computeAcceptKey(challengeKey string) string { func computeAcceptKey(challengeKey string) string {
@ -42,3 +29,186 @@ func generateChallengeKey() (string, error) {
} }
return base64.StdEncoding.EncodeToString(p), nil return base64.StdEncoding.EncodeToString(p), nil
} }
// Octet types from RFC 2616.
var octetTypes [256]byte
const (
isTokenOctet = 1 << iota
isSpaceOctet
)
func init() {
// From RFC 2616
//
// OCTET = <any 8-bit sequence of data>
// CHAR = <any US-ASCII character (octets 0 - 127)>
// CTL = <any US-ASCII control character (octets 0 - 31) and DEL (127)>
// CR = <US-ASCII CR, carriage return (13)>
// LF = <US-ASCII LF, linefeed (10)>
// SP = <US-ASCII SP, space (32)>
// HT = <US-ASCII HT, horizontal-tab (9)>
// <"> = <US-ASCII double-quote mark (34)>
// CRLF = CR LF
// LWS = [CRLF] 1*( SP | HT )
// TEXT = <any OCTET except CTLs, but including LWS>
// separators = "(" | ")" | "<" | ">" | "@" | "," | ";" | ":" | "\" | <">
// | "/" | "[" | "]" | "?" | "=" | "{" | "}" | SP | HT
// token = 1*<any CHAR except CTLs or separators>
// qdtext = <any TEXT except <">>
for c := 0; c < 256; c++ {
var t byte
isCtl := c <= 31 || c == 127
isChar := 0 <= c && c <= 127
isSeparator := strings.IndexRune(" \t\"(),/:;<=>?@[]\\{}", rune(c)) >= 0
if strings.IndexRune(" \t\r\n", rune(c)) >= 0 {
t |= isSpaceOctet
}
if isChar && !isCtl && !isSeparator {
t |= isTokenOctet
}
octetTypes[c] = t
}
}
func skipSpace(s string) (rest string) {
i := 0
for ; i < len(s); i++ {
if octetTypes[s[i]]&isSpaceOctet == 0 {
break
}
}
return s[i:]
}
func nextToken(s string) (token, rest string) {
i := 0
for ; i < len(s); i++ {
if octetTypes[s[i]]&isTokenOctet == 0 {
break
}
}
return s[:i], s[i:]
}
func nextTokenOrQuoted(s string) (value string, rest string) {
if !strings.HasPrefix(s, "\"") {
return nextToken(s)
}
s = s[1:]
for i := 0; i < len(s); i++ {
switch s[i] {
case '"':
return s[:i], s[i+1:]
case '\\':
p := make([]byte, len(s)-1)
j := copy(p, s[:i])
escape := true
for i = i + 1; i < len(s); i++ {
b := s[i]
switch {
case escape:
escape = false
p[j] = b
j += 1
case b == '\\':
escape = true
case b == '"':
return string(p[:j]), s[i+1:]
default:
p[j] = b
j += 1
}
}
return "", ""
}
}
return "", ""
}
// tokenListContainsValue returns true if the 1#token header with the given
// name contains token.
func tokenListContainsValue(header http.Header, name string, value string) bool {
headers:
for _, s := range header[name] {
for {
var t string
t, s = nextToken(skipSpace(s))
if t == "" {
continue headers
}
s = skipSpace(s)
if s != "" && s[0] != ',' {
continue headers
}
if strings.EqualFold(t, value) {
return true
}
if s == "" {
continue headers
}
s = s[1:]
}
}
return false
}
// parseExtensiosn parses WebSocket extensions from a header.
func parseExtensions(header http.Header) []map[string]string {
// From RFC 6455:
//
// Sec-WebSocket-Extensions = extension-list
// extension-list = 1#extension
// extension = extension-token *( ";" extension-param )
// extension-token = registered-token
// registered-token = token
// extension-param = token [ "=" (token | quoted-string) ]
// ;When using the quoted-string syntax variant, the value
// ;after quoted-string unescaping MUST conform to the
// ;'token' ABNF.
var result []map[string]string
headers:
for _, s := range header["Sec-Websocket-Extensions"] {
for {
var t string
t, s = nextToken(skipSpace(s))
if t == "" {
continue headers
}
ext := map[string]string{"": t}
for {
s = skipSpace(s)
if !strings.HasPrefix(s, ";") {
break
}
var k string
k, s = nextToken(skipSpace(s[1:]))
if k == "" {
continue headers
}
s = skipSpace(s)
var v string
if strings.HasPrefix(s, "=") {
v, s = nextTokenOrQuoted(skipSpace(s[1:]))
s = skipSpace(s)
}
if s != "" && s[0] != ',' && s[0] != ';' {
continue headers
}
ext[k] = v
}
if s != "" && s[0] != ',' {
continue headers
}
result = append(result, ext)
if s == "" {
continue headers
}
s = s[1:]
}
}
return result
}

21
vendor/github.com/jpillora/backoff/LICENSE generated vendored Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2017 Jaime Pillora
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,3 +1,4 @@
// Package backoff provides an exponential-backoff implementation.
package backoff package backoff
import ( import (
@ -6,64 +7,82 @@ import (
"time" "time"
) )
//Backoff is a time.Duration counter. It starts at Min. // Backoff is a time.Duration counter, starting at Min. After every call to
//After every call to Duration() it is multiplied by Factor. // the Duration method the current timing is multiplied by Factor, but it
//It is capped at Max. It returns to Min on every call to Reset(). // never exceeds Max.
//Used in conjunction with the time package.
// //
// Backoff is not threadsafe, but the ForAttempt method can be // Backoff is not generally concurrent-safe, but the ForAttempt method can
// used concurrently if non-zero values for Factor, Max, and Min // be used concurrently.
// are set on the Backoff shared among threads.
type Backoff struct { type Backoff struct {
//Factor is the multiplying factor for each increment step //Factor is the multiplying factor for each increment step
attempts, Factor float64 attempt, Factor float64
//Jitter eases contention by randomizing backoff steps //Jitter eases contention by randomizing backoff steps
Jitter bool Jitter bool
//Min and Max are the minimum and maximum values of the counter //Min and Max are the minimum and maximum values of the counter
Min, Max time.Duration Min, Max time.Duration
} }
//Returns the current value of the counter and then // Duration returns the duration for the current attempt before incrementing
//multiplies it Factor // the attempt counter. See ForAttempt.
func (b *Backoff) Duration() time.Duration { func (b *Backoff) Duration() time.Duration {
d := b.ForAttempt(b.attempts) d := b.ForAttempt(b.attempt)
b.attempts++ b.attempt++
return d return d
} }
const maxInt64 = float64(math.MaxInt64 - 512)
// ForAttempt returns the duration for a specific attempt. This is useful if // ForAttempt returns the duration for a specific attempt. This is useful if
// you have a large number of independent Backoffs, but don't want use // you have a large number of independent Backoffs, but don't want use
// unnecessary memory storing the Backoff parameters per Backoff. The first // unnecessary memory storing the Backoff parameters per Backoff. The first
// attempt should be 0. // attempt should be 0.
// //
// ForAttempt is threadsafe iff non-zero values for Factor, Max, and Min // ForAttempt is concurrent-safe.
// are set before any calls to ForAttempt are made.
func (b *Backoff) ForAttempt(attempt float64) time.Duration { func (b *Backoff) ForAttempt(attempt float64) time.Duration {
//Zero-values are nonsensical, so we use // Zero-values are nonsensical, so we use
//them to apply defaults // them to apply defaults
if b.Min == 0 { min := b.Min
b.Min = 100 * time.Millisecond if min <= 0 {
min = 100 * time.Millisecond
} }
if b.Max == 0 { max := b.Max
b.Max = 10 * time.Second if max <= 0 {
max = 10 * time.Second
} }
if b.Factor == 0 { if min >= max {
b.Factor = 2 // short-circuit
return max
}
factor := b.Factor
if factor <= 0 {
factor = 2
} }
//calculate this duration //calculate this duration
dur := float64(b.Min) * math.Pow(b.Factor, attempt) minf := float64(min)
if b.Jitter == true { durf := minf * math.Pow(factor, attempt)
dur = rand.Float64()*(dur-float64(b.Min)) + float64(b.Min) if b.Jitter {
durf = rand.Float64()*(durf-minf) + minf
} }
//cap! //ensure float64 wont overflow int64
if dur > float64(b.Max) { if durf > maxInt64 {
return b.Max return max
} }
//return as a time.Duration dur := time.Duration(durf)
return time.Duration(dur) //keep within bounds
if dur < min {
return min
} else if dur > max {
return max
}
return dur
} }
//Resets the current value of the counter back to Min // Reset restarts the current attempt counter at zero.
func (b *Backoff) Reset() { func (b *Backoff) Reset() {
b.attempts = 0 b.attempt = 0
}
// Attempt returns the current attempt counter value.
func (b *Backoff) Attempt() float64 {
return b.attempt
} }

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"))
}

Some files were not shown because too many files have changed in this diff Show More