3
0
mirror of https://github.com/ergochat/ergo.git synced 2024-11-25 13:29:27 +01:00

Merge branch 'persistent.14'

This commit is contained in:
Shivaram Lingamneni 2020-02-22 23:10:52 -05:00
commit fb8b73e29a
76 changed files with 9725 additions and 772 deletions

View File

@ -25,6 +25,7 @@ test:
cd irc/history && go test . && go vet .
cd irc/isupport && go test . && go vet .
cd irc/modes && go test . && go vet .
cd irc/mysql && go test . && go vet .
cd irc/passwd && go test . && go vet .
cd irc/utils && go test . && go vet .
./.check-gofmt.sh

View File

@ -135,12 +135,6 @@ CAPDEFS = [
url="https://ircv3.net/specs/extensions/userhost-in-names-3.2.html",
standard="IRCv3",
),
CapDef(
identifier="Bouncer",
name="oragono.io/bnc",
url="https://oragono.io/bnc",
standard="Oragono-specific",
),
CapDef(
identifier="ZNCSelfMessage",
name="znc.in/self-message",
@ -171,6 +165,12 @@ CAPDEFS = [
url="https://github.com/ircv3/ircv3-specifications/pull/398",
standard="proposed IRCv3",
),
CapDef(
identifier="Chathistory",
name="draft/chathistory",
url="https://github.com/ircv3/ircv3-specifications/pull/393",
standard="proposed IRCv3",
),
]
def validate_defs():

12
go.mod
View File

@ -6,23 +6,19 @@ require (
code.cloudfoundry.org/bytefmt v0.0.0-20190819182555-854d396b647c
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815
github.com/go-ldap/ldap/v3 v3.1.6
github.com/go-sql-driver/mysql v1.5.0
github.com/goshuirc/e-nfa v0.0.0-20160917075329-7071788e3940 // indirect
github.com/goshuirc/irc-go v0.0.0-20190713001546-05ecc95249a0
github.com/mattn/go-colorable v0.1.4
github.com/mattn/go-isatty v0.0.10 // indirect
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b
github.com/onsi/ginkgo v1.12.0 // indirect
github.com/onsi/gomega v1.9.0 // indirect
github.com/oragono/confusables v0.0.0-20190624102032-fe1cf31a24b0
github.com/oragono/go-ident v0.0.0-20170110123031-337fed0fd21a
github.com/tidwall/btree v0.0.0-20191029221954-400434d76274 // indirect
github.com/stretchr/testify v1.4.0 // indirect
github.com/tidwall/buntdb v1.1.2
github.com/tidwall/gjson v1.3.4 // indirect
github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb // indirect
github.com/tidwall/match v1.0.1 // indirect
github.com/tidwall/pretty v1.0.0 // indirect
github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e // indirect
github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563 // indirect
golang.org/x/crypto v0.0.0-20191112222119-e1110fd1c708
golang.org/x/sys v0.0.0-20191115151921-52ab43148777 // indirect
golang.org/x/text v0.3.2
gopkg.in/yaml.v2 v2.2.5
)

91
go.sum Normal file
View File

@ -0,0 +1,91 @@
code.cloudfoundry.org/bytefmt v0.0.0-20190819182555-854d396b647c h1:2RuXx1+tSNWRjxhY0Bx52kjV2odJQ0a6MTbfTPhGAkg=
code.cloudfoundry.org/bytefmt v0.0.0-20190819182555-854d396b647c/go.mod h1:wN/zk7mhREp/oviagqUXY3EwuHhWyOvAdsn5Y4CzOrc=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ=
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/go-asn1-ber/asn1-ber v1.3.1 h1:gvPdv/Hr++TRFCl0UbPFHC54P9N9jgsRPnmnr419Uck=
github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-ldap/ldap/v3 v3.1.6 h1:VTihvB7egSAvU6KOagaiA/EvgJMR2jsjRAVIho2ydBo=
github.com/go-ldap/ldap/v3 v3.1.6/go.mod h1:5Zun81jBTabRaI8lzN7E1JjyEl1g6zI6u9pd8luAK4Q=
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/goshuirc/e-nfa v0.0.0-20160917075329-7071788e3940 h1:KmRLPRstEJiE/9OjumKqI8Rccip8Qmyw2FwyTFxtVqs=
github.com/goshuirc/e-nfa v0.0.0-20160917075329-7071788e3940/go.mod h1:VOmrX6cmj7zwUeexC9HzznUdTIObHqIXUrWNYS+Ik7w=
github.com/goshuirc/irc-go v0.0.0-20190713001546-05ecc95249a0 h1:unxsR0de0MIS708eZI7lKa6HiP8FS0PhGCWwwEt9+vQ=
github.com/goshuirc/irc-go v0.0.0-20190713001546-05ecc95249a0/go.mod h1:rhIkxdehxNqK9iwJXWzQnxlGuuUR4cHu7PN64VryKXk=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10=
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.0 h1:Iw5WCbBcaAAd0fpRb1c9r5YCylv4XDoCSigm1zLevwU=
github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.9.0 h1:R1uwffexN6Pr340GtYRIdZmAiN4J+iw6WG4wog1DUXg=
github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=
github.com/oragono/confusables v0.0.0-20190624102032-fe1cf31a24b0 h1:4qw57EiWD2MhnmXoQus2ClSyPpGRd8/UxcAmmNe2FCg=
github.com/oragono/confusables v0.0.0-20190624102032-fe1cf31a24b0/go.mod h1:+uesPRay9e5tW6zhw4CJkRV3QOEbbZIJcsuo9ZnC+hE=
github.com/oragono/go-ident v0.0.0-20170110123031-337fed0fd21a h1:tZApUffT5QuX4XhJLz2KfBJT8JgdwjLUBWtvmRwgFu4=
github.com/oragono/go-ident v0.0.0-20170110123031-337fed0fd21a/go.mod h1:r5Fk840a4eu3ii1kxGDNSJupQu9Z1UC1nfJOZZXC24c=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/tidwall/btree v0.0.0-20191029221954-400434d76274 h1:G6Z6HvJuPjG6XfNGi/feOATzeJrfgTNJY+rGrHbA04E=
github.com/tidwall/btree v0.0.0-20191029221954-400434d76274/go.mod h1:huei1BkDWJ3/sLXmO+bsCNELL+Bp2Kks9OLyQFkzvA8=
github.com/tidwall/buntdb v1.1.2 h1:noCrqQXL9EKMtcdwJcmuVKSEjqu1ua99RHHgbLTEHRo=
github.com/tidwall/buntdb v1.1.2/go.mod h1:xAzi36Hir4FarpSHyfuZ6JzPJdjRZ8QlLZSntE2mqlI=
github.com/tidwall/gjson v1.3.4 h1:On5waDnyKKk3SWE4EthbjjirAWXp43xx5cKCUZY1eZw=
github.com/tidwall/gjson v1.3.4/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls=
github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb h1:5NSYaAdrnblKByzd7XByQEJVT8+9v0W/tIY0Oo4OwrE=
github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb/go.mod h1:lKYYLFIr9OIgdgrtgkZ9zgRxRdvPYsExnYBsEAd8W5M=
github.com/tidwall/match v1.0.1 h1:PnKP62LPNxHKTwvHHZZzdOAOCtsJTjo6dZLCwpKm5xc=
github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E=
github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e h1:+NL1GDIUOKxVfbp2KoJQD9cTQ6dyP2co9q4yzmT9FZo=
github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e/go.mod h1:/h+UnNGt0IhNNJLkGikcdcJqm66zGD/uJGMRxK/9+Ao=
github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563 h1:Otn9S136ELckZ3KKDyCkxapfufrqDqwmGjcHfAyXRrE=
github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563/go.mod h1:mLqSmt7Dv/CNneF2wfcChfN1rvapyQr01LGKnKex0DQ=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191112222119-e1110fd1c708 h1:pXVtWnwHkrWD9ru3sDxY/qFK/bfc0egRovX91EjWjf4=
golang.org/x/crypto v0.0.0-20191112222119-e1110fd1c708/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e h1:N7DeIrjYszNmSW409R3frPPwglRwMkXSBzwVbkOjLLA=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5 h1:ymVxjfMaHvXD8RqPRmzHHsB3VvucivSkIAvJFDI5O3c=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@ -33,7 +33,9 @@ const (
keyAccountSettings = "account.settings %s"
keyAccountVHost = "account.vhost %s"
keyCertToAccount = "account.creds.certfp %s"
keyAccountChannels = "account.channels %s"
keyAccountChannels = "account.channels %s" // channels registered to the account
keyAccountJoinedChannels = "account.joinedto %s" // channels a persistent client has joined
keyAccountLastSignoff = "account.lastsignoff %s"
keyVHostQueueAcctToId = "vhostQueue %s"
vhostRequestIdx = "vhostQueue"
@ -71,6 +73,40 @@ func (am *AccountManager) Initialize(server *Server) {
config := server.Config()
am.buildNickToAccountIndex(config)
am.initVHostRequestQueue(config)
am.createAlwaysOnClients(config)
}
func (am *AccountManager) createAlwaysOnClients(config *Config) {
if config.Accounts.Multiclient.AlwaysOn == PersistentDisabled {
return
}
verifiedPrefix := fmt.Sprintf(keyAccountVerified, "")
am.serialCacheUpdateMutex.Lock()
defer am.serialCacheUpdateMutex.Unlock()
var accounts []string
am.server.store.View(func(tx *buntdb.Tx) error {
err := tx.AscendGreaterOrEqual("", verifiedPrefix, func(key, value string) bool {
if !strings.HasPrefix(key, verifiedPrefix) {
return false
}
account := strings.TrimPrefix(key, verifiedPrefix)
accounts = append(accounts, account)
return true
})
return err
})
for _, accountName := range accounts {
account, err := am.LoadAccount(accountName)
if err == nil && account.Verified &&
persistenceEnabled(config.Accounts.Multiclient.AlwaysOn, account.Settings.AlwaysOn) {
am.server.AddAlwaysOnClient(account, am.loadChannels(accountName), am.loadLastSignoff(accountName))
}
}
}
func (am *AccountManager) buildNickToAccountIndex(config *Config) {
@ -346,7 +382,7 @@ func (am *AccountManager) Register(client *Client, account string, callbackNames
callbackSpec := fmt.Sprintf("%s:%s", callbackNamespace, callbackValue)
var setOptions *buntdb.SetOptions
ttl := config.Registration.VerifyTimeout
ttl := time.Duration(config.Registration.VerifyTimeout)
if ttl != 0 {
setOptions = &buntdb.SetOptions{Expires: true, TTL: ttl}
}
@ -477,6 +513,58 @@ func (am *AccountManager) setPassword(account string, password string, hasPrivs
return err
}
func (am *AccountManager) saveChannels(account string, channels []string) {
channelsStr := strings.Join(channels, ",")
key := fmt.Sprintf(keyAccountJoinedChannels, account)
am.server.store.Update(func(tx *buntdb.Tx) error {
tx.Set(key, channelsStr, nil)
return nil
})
}
func (am *AccountManager) loadChannels(account string) (channels []string) {
key := fmt.Sprintf(keyAccountJoinedChannels, account)
var channelsStr string
am.server.store.View(func(tx *buntdb.Tx) error {
channelsStr, _ = tx.Get(key)
return nil
})
if channelsStr != "" {
return strings.Split(channelsStr, ",")
}
return
}
func (am *AccountManager) saveLastSignoff(account string, lastSignoff time.Time) {
key := fmt.Sprintf(keyAccountLastSignoff, account)
var val string
if !lastSignoff.IsZero() {
val = strconv.FormatInt(lastSignoff.UnixNano(), 10)
}
am.server.store.Update(func(tx *buntdb.Tx) error {
if val != "" {
tx.Set(key, val, nil)
} else {
tx.Delete(key)
}
return nil
})
}
func (am *AccountManager) loadLastSignoff(account string) (lastSignoff time.Time) {
key := fmt.Sprintf(keyAccountLastSignoff, account)
var lsText string
am.server.store.View(func(tx *buntdb.Tx) error {
lsText, _ = tx.Get(key)
return nil
})
lsNum, err := strconv.ParseInt(lsText, 10, 64)
if err != nil {
return time.Unix(0, lsNum).UTC()
}
return
}
func (am *AccountManager) addRemoveCertfp(account, certfp string, add bool, hasPrivs bool) (err error) {
certfp, err = utils.NormalizeCertfp(certfp)
if err != nil {
@ -685,7 +773,7 @@ func (am *AccountManager) Verify(client *Client, account string, code string) er
}
am.server.logger.Info("accounts", "client", nick, "registered account", casefoldedAccount)
raw.Verified = true
clientAccount, err := am.deserializeRawAccount(raw)
clientAccount, err := am.deserializeRawAccount(raw, casefoldedAccount)
if err != nil {
return err
}
@ -892,13 +980,13 @@ func (am *AccountManager) LoadAccount(accountName string) (result ClientAccount,
return
}
result, err = am.deserializeRawAccount(raw)
result.NameCasefolded = casefoldedAccount
result, err = am.deserializeRawAccount(raw, casefoldedAccount)
return
}
func (am *AccountManager) deserializeRawAccount(raw rawClientAccount) (result ClientAccount, err error) {
func (am *AccountManager) deserializeRawAccount(raw rawClientAccount, cfName string) (result ClientAccount, err error) {
result.Name = raw.Name
result.NameCasefolded = cfName
regTimeInt, _ := strconv.ParseInt(raw.RegisteredAt, 10, 64)
result.RegisteredAt = time.Unix(regTimeInt, 0).UTC()
e := json.Unmarshal([]byte(raw.Credentials), &result.Credentials)
@ -976,6 +1064,8 @@ func (am *AccountManager) Unregister(account string) error {
vhostKey := fmt.Sprintf(keyAccountVHost, casefoldedAccount)
vhostQueueKey := fmt.Sprintf(keyVHostQueueAcctToId, casefoldedAccount)
channelsKey := fmt.Sprintf(keyAccountChannels, casefoldedAccount)
joinedChannelsKey := fmt.Sprintf(keyAccountJoinedChannels, casefoldedAccount)
lastSignoffKey := fmt.Sprintf(keyAccountLastSignoff, casefoldedAccount)
var clients []*Client
@ -1011,6 +1101,8 @@ func (am *AccountManager) Unregister(account string) error {
tx.Delete(vhostKey)
channelsStr, _ = tx.Get(channelsKey)
tx.Delete(channelsKey)
tx.Delete(joinedChannelsKey)
tx.Delete(lastSignoffKey)
_, err := tx.Delete(vhostQueueKey)
am.decrementVHostQueueCount(casefoldedAccount, err)
@ -1087,13 +1179,13 @@ func (am *AccountManager) ChannelsForAccount(account string) (channels []string)
return unmarshalRegisteredChannels(channelStr)
}
func (am *AccountManager) AuthenticateByCertFP(client *Client, authzid string) error {
if client.certfp == "" {
func (am *AccountManager) AuthenticateByCertFP(client *Client, certfp, authzid string) error {
if certfp == "" {
return errAccountInvalidCredentials
}
var account string
certFPKey := fmt.Sprintf(keyCertToAccount, client.certfp)
certFPKey := fmt.Sprintf(keyCertToAccount, certfp)
err := am.server.store.View(func(tx *buntdb.Tx) error {
account, _ = tx.Get(certFPKey)
@ -1455,10 +1547,7 @@ func (am *AccountManager) applyVhostToClients(account string, result VHostInfo)
}
func (am *AccountManager) Login(client *Client, account ClientAccount) {
changed := client.SetAccountName(account.Name)
if !changed {
return
}
client.Login(account)
client.nickTimer.Touch(nil)
@ -1468,9 +1557,6 @@ func (am *AccountManager) Login(client *Client, account ClientAccount) {
am.Lock()
defer am.Unlock()
am.accountToClients[casefoldedAccount] = append(am.accountToClients[casefoldedAccount], client)
for _, client := range am.accountToClients[casefoldedAccount] {
client.SetAccountSettings(account.Settings)
}
}
func (am *AccountManager) Logout(client *Client) {
@ -1590,12 +1676,12 @@ func (ac *AccountCredentials) RemoveCertfp(certfp string) (err error) {
return nil
}
type BouncerAllowedSetting int
type MulticlientAllowedSetting int
const (
BouncerAllowedServerDefault BouncerAllowedSetting = iota
BouncerDisallowedByUser
BouncerAllowedByUser
MulticlientAllowedServerDefault MulticlientAllowedSetting = iota
MulticlientDisallowedByUser
MulticlientAllowedByUser
)
// controls whether/when clients without event-playback support see fake
@ -1622,11 +1708,16 @@ func replayJoinsSettingFromString(str string) (result ReplayJoinsSetting, err er
return
}
// XXX: AllowBouncer cannot be renamed AllowMulticlient because it is stored in
// persistent JSON blobs in the database
type AccountSettings struct {
AutoreplayLines *int
NickEnforcement NickEnforcementMethod
AllowBouncer BouncerAllowedSetting
ReplayJoins ReplayJoinsSetting
AutoreplayLines *int
NickEnforcement NickEnforcementMethod
AllowBouncer MulticlientAllowedSetting
ReplayJoins ReplayJoinsSetting
AlwaysOn PersistentStatus
AutoreplayMissed bool
DMHistory HistoryStatus
}
// ClientAccount represents a user account.
@ -1661,7 +1752,7 @@ func (am *AccountManager) logoutOfAccount(client *Client) {
return
}
client.SetAccountName("")
client.Logout()
go client.nickTimer.Touch(nil)
// dispatch account-notify

View File

@ -37,6 +37,10 @@ const (
// https://ircv3.net/specs/extensions/chghost-3.2.html
ChgHost Capability = iota
// Chathistory is the proposed IRCv3 capability named "draft/chathistory":
// https://github.com/ircv3/ircv3-specifications/pull/393
Chathistory Capability = iota
// EventPlayback is the proposed IRCv3 capability named "draft/event-playback":
// https://github.com/ircv3/ircv3-specifications/pull/362
EventPlayback Capability = iota
@ -85,10 +89,6 @@ const (
// https://ircv3.net/specs/extensions/multi-prefix-3.1.html
MultiPrefix Capability = iota
// Bouncer is the Oragono-specific capability named "oragono.io/bnc":
// https://oragono.io/bnc
Bouncer Capability = iota
// Nope is the Oragono vendor capability named "oragono.io/nope":
// https://oragono.io/nope
Nope Capability = iota
@ -127,6 +127,7 @@ var (
"batch",
"cap-notify",
"chghost",
"draft/chathistory",
"draft/event-playback",
"draft/languages",
"draft/multiline",
@ -139,7 +140,6 @@ var (
"labeled-response",
"message-tags",
"multi-prefix",
"oragono.io/bnc",
"oragono.io/nope",
"sasl",
"server-time",

View File

@ -24,6 +24,10 @@ const (
histServMask = "HistServ!HistServ@localhost"
)
type ChannelSettings struct {
History HistoryStatus
}
// Channel represents a channel that clients can join.
type Channel struct {
flags modes.ModeSet
@ -49,6 +53,7 @@ type Channel struct {
joinPartMutex sync.Mutex // tier 3
ensureLoaded utils.Once // manages loading stored registration info from the database
dirtyBits uint
settings ChannelSettings
}
// NewChannel creates a new channel from a `Server` and a `name`
@ -66,9 +71,10 @@ func NewChannel(s *Server, name, casefoldedName string, registered bool) *Channe
channel.initializeLists()
channel.writerSemaphore.Initialize(1)
channel.history.Initialize(config.History.ChannelLength, config.History.AutoresizeWindow)
channel.history.Initialize(0, 0)
if !registered {
channel.resizeHistory(config)
for _, mode := range config.Channels.defaultModes {
channel.flags.SetMode(mode, true)
}
@ -106,8 +112,19 @@ func (channel *Channel) IsLoaded() bool {
return channel.ensureLoaded.Done()
}
func (channel *Channel) resizeHistory(config *Config) {
_, ephemeral, _ := channel.historyStatus(config)
if ephemeral {
channel.history.Resize(config.History.ChannelLength, config.History.AutoresizeWindow)
} else {
channel.history.Resize(0, 0)
}
}
// read in channel state that was persisted in the DB
func (channel *Channel) applyRegInfo(chanReg RegisteredChannel) {
defer channel.resizeHistory(channel.server.Config())
channel.stateMutex.Lock()
defer channel.stateMutex.Unlock()
@ -120,6 +137,7 @@ func (channel *Channel) applyRegInfo(chanReg RegisteredChannel) {
channel.createdTime = chanReg.RegisteredAt
channel.key = chanReg.Key
channel.userLimit = chanReg.UserLimit
channel.settings = chanReg.Settings
for _, mode := range chanReg.Modes {
channel.flags.SetMode(mode, true)
@ -164,6 +182,10 @@ func (channel *Channel) ExportRegistration(includeFlags uint) (info RegisteredCh
}
}
if includeFlags&IncludeSettings != 0 {
info.Settings = channel.settings
}
return
}
@ -434,7 +456,7 @@ func (channel *Channel) Names(client *Client, rb *ResponseBuffer) {
if modeSet == nil {
continue
}
if !isJoined && target.flags.HasMode(modes.Invisible) && !isOper {
if !isJoined && target.HasMode(modes.Invisible) && !isOper {
continue
}
prefix := modeSet.Prefixes(isMultiPrefix)
@ -564,6 +586,48 @@ func (channel *Channel) IsEmpty() bool {
return len(channel.members) == 0
}
// figure out where history is being stored: persistent, ephemeral, or neither
// target is only needed if we're doing persistent history
func (channel *Channel) historyStatus(config *Config) (persistent, ephemeral bool, target string) {
if !config.History.Persistent.Enabled {
return false, config.History.Enabled, ""
}
channel.stateMutex.RLock()
target = channel.nameCasefolded
historyStatus := channel.settings.History
registered := channel.registeredFounder != ""
channel.stateMutex.RUnlock()
historyStatus = historyEnabled(config.History.Persistent.RegisteredChannels, historyStatus)
// ephemeral history: either the channel owner explicitly set the ephemeral preference,
// or persistent history is disabled for unregistered channels
if registered {
ephemeral = (historyStatus == HistoryEphemeral)
persistent = (historyStatus == HistoryPersistent)
} else {
ephemeral = config.History.Enabled && !config.History.Persistent.UnregisteredChannels
persistent = config.History.Persistent.UnregisteredChannels
}
return
}
func (channel *Channel) AddHistoryItem(item history.Item) (err error) {
if !item.IsStorable() {
return
}
persistent, ephemeral, target := channel.historyStatus(channel.server.Config())
if ephemeral {
channel.history.Add(item)
}
if persistent {
return channel.server.historyDB.AddChannelItem(target, item)
}
return nil
}
// Join joins the given client to this channel (if they can be joined).
func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *ResponseBuffer) {
details := client.Details()
@ -618,8 +682,6 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
client.server.logger.Debug("join", fmt.Sprintf("%s joined channel %s", details.nick, chname))
var message utils.SplitMessage
givenMode := func() (givenMode modes.Mode) {
channel.joinPartMutex.Lock()
defer channel.joinPartMutex.Unlock()
@ -643,6 +705,12 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
channel.regenerateMembersCache()
return
}()
var message utils.SplitMessage
// no history item for fake persistent joins
if rb != nil {
message = utils.MakeMessage("")
histItem := history.Item{
Type: history.Join,
@ -651,13 +719,15 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
Message: message,
}
histItem.Params[0] = details.realname
channel.history.Add(histItem)
return
}()
channel.AddHistoryItem(histItem)
}
client.addChannel(channel)
if rb == nil {
return
}
var modestr string
if givenMode != 0 {
modestr = fmt.Sprintf("+%v", givenMode)
@ -702,10 +772,22 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
func (channel *Channel) autoReplayHistory(client *Client, rb *ResponseBuffer, skipMsgid string) {
// autoreplay any messages as necessary
config := channel.server.Config()
var items []history.Item
var after, before time.Time
if rb.session.zncPlaybackTimes != nil && (rb.session.zncPlaybackTimes.targets == nil || rb.session.zncPlaybackTimes.targets.Has(channel.NameCasefolded())) {
items, _ = channel.history.Between(rb.session.zncPlaybackTimes.after, rb.session.zncPlaybackTimes.before, false, config.History.ChathistoryMax)
after, before = rb.session.zncPlaybackTimes.after, rb.session.zncPlaybackTimes.before
} else if !rb.session.lastSignoff.IsZero() {
// we already checked for history caps in `playReattachMessages`
after = rb.session.lastSignoff
}
if !after.IsZero() || !before.IsZero() {
_, seq, _ := channel.server.GetHistorySequence(channel, client, "")
if seq != nil {
zncMax := channel.server.Config().History.ZNCMax
items, _, _ = seq.Between(history.Selector{Time: after}, history.Selector{Time: before}, zncMax)
}
} else if !rb.session.HasHistoryCaps() {
var replayLimit int
customReplayLimit := client.AccountSettings().AutoreplayLines
@ -719,7 +801,10 @@ func (channel *Channel) autoReplayHistory(client *Client, rb *ResponseBuffer, sk
replayLimit = channel.server.Config().History.AutoreplayOnJoin
}
if 0 < replayLimit {
items = channel.history.Latest(replayLimit)
_, seq, _ := channel.server.GetHistorySequence(channel, client, "")
if seq != nil {
items, _, _ = seq.Between(history.Selector{}, history.Selector{}, replayLimit)
}
}
}
// remove the client's own JOIN line from the replay
@ -784,7 +869,7 @@ func (channel *Channel) Part(client *Client, message string, rb *ResponseBuffer)
}
}
channel.history.Add(history.Item{
channel.AddHistoryItem(history.Item{
Type: history.Part,
Nick: details.nickMask,
AccountName: details.accountName,
@ -799,10 +884,9 @@ func (channel *Channel) Part(client *Client, message string, rb *ResponseBuffer)
// 2. Send JOIN and MODE lines to channel participants (including the new client)
// 3. Replay missed message history to the client
func (channel *Channel) Resume(session *Session, timestamp time.Time) {
now := time.Now().UTC()
channel.resumeAndAnnounce(session)
if !timestamp.IsZero() {
channel.replayHistoryForResume(session, timestamp, now)
channel.replayHistoryForResume(session, timestamp, time.Time{})
}
}
@ -852,9 +936,17 @@ func (channel *Channel) resumeAndAnnounce(session *Session) {
}
func (channel *Channel) replayHistoryForResume(session *Session, after time.Time, before time.Time) {
items, complete := channel.history.Between(after, before, false, 0)
var items []history.Item
var complete bool
afterS, beforeS := history.Selector{Time: after}, history.Selector{Time: before}
_, seq, _ := channel.server.GetHistorySequence(channel, session.client, "")
if seq != nil {
items, complete, _ = seq.Between(afterS, beforeS, channel.server.Config().History.ZNCMax)
}
rb := NewResponseBuffer(session)
channel.replayHistoryItems(rb, items, false)
if len(items) != 0 {
channel.replayHistoryItems(rb, items, false)
}
if !complete && !session.resumeDetails.HistoryIncomplete {
// warn here if we didn't warn already
rb.Add(nil, histServMask, "NOTICE", channel.Name(), session.client.t("Some additional message history may have been lost"))
@ -871,10 +963,7 @@ func stripMaskFromNick(nickMask string) (nick string) {
}
func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.Item, autoreplay bool) {
if len(items) == 0 {
return
}
// send an empty batch if necessary, as per the CHATHISTORY spec
chname := channel.Name()
client := rb.target
eventPlayback := rb.session.capabilities.Has(caps.EventPlayback)
@ -908,9 +997,9 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I
case history.Join:
if eventPlayback {
if extendedJoin {
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "JOIN", chname, item.AccountName, item.Params[0])
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "JOIN", chname, item.AccountName, item.Params[0])
} else {
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "JOIN", chname)
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "JOIN", chname)
}
} else {
if !playJoinsAsPrivmsg {
@ -926,7 +1015,7 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I
}
case history.Part:
if eventPlayback {
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "PART", chname, item.Message.Message)
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "PART", chname, item.Message.Message)
} else {
if !playJoinsAsPrivmsg {
continue // #474
@ -936,14 +1025,14 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I
}
case history.Kick:
if eventPlayback {
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "KICK", chname, item.Params[0], item.Message.Message)
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "KICK", chname, item.Params[0], item.Message.Message)
} else {
message := fmt.Sprintf(client.t("%[1]s kicked %[2]s (%[3]s)"), nick, item.Params[0], item.Message.Message)
rb.AddFromClient(item.Message.Time, utils.MungeSecretToken(item.Message.Msgid), histServMask, "*", nil, "PRIVMSG", chname, message)
}
case history.Quit:
if eventPlayback {
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "QUIT", item.Message.Message)
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "QUIT", item.Message.Message)
} else {
if !playJoinsAsPrivmsg {
continue // #474
@ -953,7 +1042,7 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I
}
case history.Nick:
if eventPlayback {
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "NICK", item.Params[0])
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "NICK", item.Params[0])
} else {
message := fmt.Sprintf(client.t("%[1]s changed nick to %[2]s"), nick, item.Params[0])
rb.AddFromClient(item.Message.Time, utils.MungeSecretToken(item.Message.Msgid), histServMask, "*", nil, "PRIVMSG", chname, message)
@ -1124,11 +1213,12 @@ func (channel *Channel) SendSplitMessage(command string, minPrefixMode modes.Mod
// STATUSMSG
continue
}
if isCTCP && member.isTor {
continue // #753
}
for _, session := range member.Sessions() {
if isCTCP && session.isTor {
continue // #753
}
var tagsToUse map[string]string
if session.capabilities.Has(caps.MessageTags) {
tagsToUse = clientOnlyTags
@ -1144,7 +1234,7 @@ func (channel *Channel) SendSplitMessage(command string, minPrefixMode modes.Mod
}
}
channel.history.Add(history.Item{
channel.AddHistoryItem(history.Item{
Type: histType,
Message: message,
Nick: nickmask,
@ -1266,7 +1356,7 @@ func (channel *Channel) Kick(client *Client, target *Client, comment string, rb
Message: message,
}
histItem.Params[0] = targetNick
channel.history.Add(histItem)
channel.AddHistoryItem(histItem)
channel.Quit(target)
}

View File

@ -33,6 +33,7 @@ const (
keyChannelModes = "channel.modes %s"
keyChannelAccountToUMode = "channel.accounttoumode %s"
keyChannelUserLimit = "channel.userlimit %s"
keyChannelSettings = "channel.settings %s"
keyChannelPurged = "channel.purged %s"
)
@ -53,6 +54,7 @@ var (
keyChannelModes,
keyChannelAccountToUMode,
keyChannelUserLimit,
keyChannelSettings,
}
)
@ -63,6 +65,7 @@ const (
IncludeTopic
IncludeModes
IncludeLists
IncludeSettings
)
// this is an OR of all possible flags
@ -100,6 +103,8 @@ type RegisteredChannel struct {
Excepts map[string]MaskInfo
// Invites represents the invite exceptions set on the channel.
Invites map[string]MaskInfo
// Settings are the chanserv-modifiable settings
Settings ChannelSettings
}
type ChannelPurgeRecord struct {
@ -203,6 +208,7 @@ func (reg *ChannelRegistry) LoadChannel(nameCasefolded string) (info RegisteredC
exceptlistString, _ := tx.Get(fmt.Sprintf(keyChannelExceptlist, channelKey))
invitelistString, _ := tx.Get(fmt.Sprintf(keyChannelInvitelist, channelKey))
accountToUModeString, _ := tx.Get(fmt.Sprintf(keyChannelAccountToUMode, channelKey))
settingsString, _ := tx.Get(fmt.Sprintf(keyChannelSettings, channelKey))
modeSlice := make([]modes.Mode, len(modeString))
for i, mode := range modeString {
@ -220,6 +226,9 @@ func (reg *ChannelRegistry) LoadChannel(nameCasefolded string) (info RegisteredC
accountToUMode := make(map[string]modes.Mode)
_ = json.Unmarshal([]byte(accountToUModeString), &accountToUMode)
var settings ChannelSettings
_ = json.Unmarshal([]byte(settingsString), &settings)
info = RegisteredChannel{
Name: name,
RegisteredAt: time.Unix(regTimeInt, 0).UTC(),
@ -234,6 +243,7 @@ func (reg *ChannelRegistry) LoadChannel(nameCasefolded string) (info RegisteredC
Invites: invitelist,
AccountToUMode: accountToUMode,
UserLimit: int(userLimit),
Settings: settings,
}
return nil
})
@ -261,7 +271,7 @@ func (reg *ChannelRegistry) deleteChannel(tx *buntdb.Tx, key string, info Regist
if err == nil {
regTime, _ := tx.Get(fmt.Sprintf(keyChannelRegTime, key))
regTimeInt, _ := strconv.ParseInt(regTime, 10, 64)
registeredAt := time.Unix(regTimeInt, 0)
registeredAt := time.Unix(regTimeInt, 0).UTC()
founder, _ := tx.Get(fmt.Sprintf(keyChannelFounder, key))
// to see if we're deleting the right channel, confirm the founder and the registration time
@ -357,6 +367,11 @@ func (reg *ChannelRegistry) saveChannel(tx *buntdb.Tx, channelInfo RegisteredCha
accountToUModeString, _ := json.Marshal(channelInfo.AccountToUMode)
tx.Set(fmt.Sprintf(keyChannelAccountToUMode, channelKey), string(accountToUModeString), nil)
}
if includeFlags&IncludeSettings != 0 {
settingsString, _ := json.Marshal(channelInfo.Settings)
tx.Set(fmt.Sprintf(keyChannelSettings, channelKey), string(settingsString), nil)
}
}
// PurgeChannel records a channel purge.

View File

@ -135,6 +135,35 @@ INFO displays info about a registered channel.`,
enabled: chanregEnabled,
minParams: 1,
},
"get": {
handler: csGetHandler,
help: `Syntax: $bGET #channel <setting>$b
GET queries the current values of the channel settings. For more information
on the settings and their possible values, see HELP SET.`,
helpShort: `$bGET$b queries the current values of a channel's settings`,
enabled: chanregEnabled,
minParams: 2,
},
"set": {
handler: csSetHandler,
helpShort: `$bSET$b modifies a channel's settings`,
// these are broken out as separate strings so they can be translated separately
helpStrings: []string{
`Syntax $bSET #channel <setting> <value>$b
SET modifies a channel's settings. The following settings are available:`,
`$bHISTORY$b
'history' lets you control how channel history is stored. Your options are:
1. 'off' [no history]
2. 'ephemeral' [a limited amount of temporary history, not stored on disk]
3. 'on' [history stored in a permanent database, if available]
4. 'default' [use the server default]`,
},
enabled: chanregEnabled,
minParams: 3,
},
}
)
@ -318,6 +347,22 @@ func checkChanLimit(client *Client, rb *ResponseBuffer) (ok bool) {
return
}
func csPrivsCheck(channel RegisteredChannel, client *Client, rb *ResponseBuffer) (success bool) {
founder := channel.Founder
if founder == "" {
csNotice(rb, client.t("That channel is not registered"))
return false
}
if client.HasRoleCapabs("chanreg") {
return true
}
if founder != client.Account() {
csNotice(rb, client.t("Insufficient privileges"))
return false
}
return true
}
func csUnregisterHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
channelName := params[0]
var verificationCode string
@ -325,31 +370,18 @@ func csUnregisterHandler(server *Server, client *Client, command string, params
verificationCode = params[1]
}
channelKey, err := CasefoldChannel(channelName)
if channelKey == "" || err != nil {
csNotice(rb, client.t("Channel name is not valid"))
return
}
channel := server.channels.Get(channelKey)
channel := server.channels.Get(channelName)
if channel == nil {
csNotice(rb, client.t("No such channel"))
return
}
founder := channel.Founder()
if founder == "" {
csNotice(rb, client.t("That channel is not registered"))
return
}
hasPrivs := client.HasRoleCapabs("chanreg") || founder == client.Account()
if !hasPrivs {
csNotice(rb, client.t("Insufficient privileges"))
return
}
info := channel.ExportRegistration(0)
channelKey := info.NameCasefolded
if !csPrivsCheck(info, client, rb) {
return
}
expectedCode := utils.ConfirmationCode(info.Name, info.RegisteredAt)
if expectedCode != verificationCode {
csNotice(rb, ircfmt.Unescape(client.t("$bWarning: unregistering this channel will remove all stored channel attributes.$b")))
@ -357,7 +389,7 @@ func csUnregisterHandler(server *Server, client *Client, command string, params
return
}
server.channels.SetUnregistered(channelKey, founder)
server.channels.SetUnregistered(channelKey, info.Founder)
csNotice(rb, fmt.Sprintf(client.t("Channel %s is now unregistered"), channelKey))
}
@ -367,9 +399,7 @@ func csClearHandler(server *Server, client *Client, command string, params []str
csNotice(rb, client.t("Channel does not exist"))
return
}
account := client.Account()
if !(client.HasRoleCapabs("chanreg") || (account != "" && account == channel.Founder())) {
csNotice(rb, client.t("Insufficient privileges"))
if !csPrivsCheck(channel.ExportRegistration(0), client, rb) {
return
}
@ -563,3 +593,68 @@ func csInfoHandler(server *Server, client *Client, command string, params []stri
csNotice(rb, fmt.Sprintf(client.t("Founder: %s"), chinfo.Founder))
csNotice(rb, fmt.Sprintf(client.t("Registered at: %s"), chinfo.RegisteredAt.Format(time.RFC1123)))
}
func displayChannelSetting(settingName string, settings ChannelSettings, client *Client, rb *ResponseBuffer) {
config := client.server.Config()
switch strings.ToLower(settingName) {
case "history":
effectiveValue := historyEnabled(config.History.Persistent.RegisteredChannels, settings.History)
csNotice(rb, fmt.Sprintf(client.t("The stored channel history setting is: %s"), historyStatusToString(settings.History)))
csNotice(rb, fmt.Sprintf(client.t("Given current server settings, the channel history setting is: %s"), historyStatusToString(effectiveValue)))
default:
csNotice(rb, client.t("Invalid params"))
}
}
func csGetHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
chname, setting := params[0], params[1]
channel := server.channels.Get(chname)
if channel == nil {
csNotice(rb, client.t("No such channel"))
return
}
info := channel.ExportRegistration(IncludeSettings)
if !csPrivsCheck(info, client, rb) {
return
}
displayChannelSetting(setting, info.Settings, client, rb)
}
func csSetHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
chname, setting, value := params[0], params[1], params[2]
channel := server.channels.Get(chname)
if channel == nil {
csNotice(rb, client.t("No such channel"))
return
}
info := channel.ExportRegistration(IncludeSettings)
settings := info.Settings
if !csPrivsCheck(info, client, rb) {
return
}
var err error
switch strings.ToLower(setting) {
case "history":
settings.History, err = historyStatusFromString(value)
if err != nil {
err = errInvalidParams
break
}
channel.SetSettings(settings)
channel.resizeHistory(server.Config())
}
switch err {
case nil:
csNotice(rb, client.t("Successfully changed the channel settings"))
displayChannelSetting(setting, settings, client, rb)
case errInvalidParams:
csNotice(rb, client.t("Invalid parameters"))
default:
server.logger.Error("internal", "CS SET error:", err.Error())
csNotice(rb, client.t("An error occurred"))
}
}

View File

@ -29,7 +29,7 @@ import (
const (
// IdentTimeoutSeconds is how many seconds before our ident (username) check times out.
IdentTimeoutSeconds = 1.5
IRCv3TimestampFormat = "2006-01-02T15:04:05.000Z"
IRCv3TimestampFormat = utils.IRCv3TimestampFormat
)
// ResumeDetails is a place to stash data at various stages of
@ -45,22 +45,22 @@ type ResumeDetails struct {
type Client struct {
account string
accountName string // display name of the account: uncasefolded, '*' if not logged in
accountRegDate time.Time
accountSettings AccountSettings
atime time.Time
away bool
awayMessage string
brbTimer BrbTimer
certfp string
channels ChannelSet
ctime time.Time
destroyed bool
exitedSnomaskSent bool
flags modes.ModeSet
modes modes.ModeSet
hostname string
invitedTo map[string]bool
isSTSOnly bool
isTor bool
languages []string
lastSignoff time.Time // for always-on clients, the time their last session quit
loginThrottle connection_limits.GenericThrottle
nick string
nickCasefolded string
@ -76,17 +76,25 @@ type Client struct {
realIP net.IP
registered bool
resumeID string
saslInProgress bool
saslMechanism string
saslValue string
sentPassCommand bool
server *Server
skeleton string
sessions []*Session
stateMutex sync.RWMutex // tier 1
alwaysOn bool
username string
vhost string
history history.Buffer
dirtyBits uint
writerSemaphore utils.Semaphore // tier 1.5
}
type saslStatus struct {
mechanism string
value string
}
func (s *saslStatus) Clear() {
*s = saslStatus{}
}
// Session is an individual client connection to the server (TCP connection
@ -102,11 +110,16 @@ type Session struct {
realIP net.IP
proxiedIP net.IP
rawHostname string
isTor bool
idletimer IdleTimer
fakelag Fakelag
destroyed uint32
certfp string
sasl saslStatus
sentPassCommand bool
batchCounter uint32
quitMessage string
@ -120,6 +133,7 @@ type Session struct {
resumeID string
resumeDetails *ResumeDetails
zncPlaybackTimes *zncPlaybackTimes
lastSignoff time.Time
batch MultilineBatch
}
@ -147,6 +161,13 @@ func (sd *Session) SetQuitMessage(message string) (set bool) {
}
}
func (s *Session) IP() net.IP {
if s.proxiedIP != nil {
return s.proxiedIP
}
return s.realIP
}
// returns whether the session was actively destroyed (for example, by ping
// timeout or NS GHOST).
// avoids a race condition between asynchronous idle-timing-out of sessions,
@ -164,8 +185,7 @@ func (session *Session) SetDestroyed() {
// returns whether the client supports a smart history replay cap,
// and therefore autoreplay-on-join and similar should be suppressed
func (session *Session) HasHistoryCaps() bool {
// TODO the chathistory cap will go here as well
return session.capabilities.Has(caps.ZNCPlayback)
return session.capabilities.Has(caps.Chathistory) || session.capabilities.Has(caps.ZNCPlayback)
}
// generates a batch ID. the uniqueness requirements for this are fairly weak:
@ -231,7 +251,6 @@ func (server *Server) RunClient(conn clientConn, proxyLine string) {
channels: make(ChannelSet),
ctime: now,
isSTSOnly: conn.Config.STSOnly,
isTor: conn.Config.Tor,
languages: server.Languages().Default(),
loginThrottle: connection_limits.GenericThrottle{
Duration: config.Accounts.LoginThrottling.Duration,
@ -253,13 +272,14 @@ func (server *Server) RunClient(conn clientConn, proxyLine string) {
ctime: now,
atime: now,
realIP: realIP,
isTor: conn.Config.Tor,
}
client.sessions = []*Session{session}
if conn.Config.TLSConfig != nil {
client.SetMode(modes.TLS, true)
// error is not useful to us here anyways so we can ignore it
client.certfp, _ = socket.CertFP()
session.certfp, _ = socket.CertFP()
}
if conn.Config.Tor {
@ -272,7 +292,7 @@ func (server *Server) RunClient(conn clientConn, proxyLine string) {
client.rawHostname = session.rawHostname
} else {
remoteAddr := conn.Conn.RemoteAddr()
if utils.AddrIsLocal(remoteAddr) {
if realIP.IsLoopback() || utils.IPInNets(realIP, config.Server.secureNets) {
// treat local connections as secure (may be overridden later by WEBIRC)
client.SetMode(modes.TLS, true)
}
@ -286,10 +306,66 @@ func (server *Server) RunClient(conn clientConn, proxyLine string) {
client.run(session, proxyLine)
}
func (server *Server) AddAlwaysOnClient(account ClientAccount, chnames []string, lastSignoff time.Time) {
now := time.Now().UTC()
config := server.Config()
client := &Client{
atime: now,
channels: make(ChannelSet),
ctime: now,
languages: server.Languages().Default(),
server: server,
// TODO figure out how to set these on reattach?
username: "~user",
rawHostname: server.name,
realIP: utils.IPv4LoopbackAddress,
alwaysOn: true,
lastSignoff: lastSignoff,
}
client.SetMode(modes.TLS, true)
client.writerSemaphore.Initialize(1)
client.history.Initialize(0, 0)
client.brbTimer.Initialize(client)
server.accounts.Login(client, account)
client.resizeHistory(config)
_, err := server.clients.SetNick(client, nil, account.Name)
if err != nil {
server.logger.Error("internal", "could not establish always-on client", account.Name, err.Error())
return
} else {
server.logger.Debug("accounts", "established always-on client", account.Name)
}
// XXX set this last to avoid confusing SetNick:
client.registered = true
for _, chname := range chnames {
// XXX we're using isSajoin=true, to make these joins succeed even without channel key
// this is *probably* ok as long as the persisted memberships are accurate
server.channels.Join(client, chname, "", true, nil)
}
}
func (client *Client) resizeHistory(config *Config) {
_, ephemeral, _ := client.historyStatus(config)
if ephemeral {
client.history.Resize(config.History.ClientLength, config.History.AutoresizeWindow)
} else {
client.history.Resize(0, 0)
}
}
// resolve an IP to an IRC-ready hostname, using reverse DNS, forward-confirming if necessary,
// and sending appropriate notices to the client
func (client *Client) lookupHostname(session *Session, overwrite bool) {
if client.isTor {
if session.isTor {
return
} // else: even if cloaking is enabled, look up the real hostname to show to operators
@ -384,18 +460,18 @@ const (
authFailSaslRequired
)
func (client *Client) isAuthorized(config *Config) AuthOutcome {
func (client *Client) isAuthorized(config *Config, session *Session) AuthOutcome {
saslSent := client.account != ""
// PASS requirement
if (config.Server.passwordBytes != nil) && !client.sentPassCommand && !(config.Accounts.SkipServerPassword && saslSent) {
if (config.Server.passwordBytes != nil) && !session.sentPassCommand && !(config.Accounts.SkipServerPassword && saslSent) {
return authFailPass
}
// Tor connections may be required to authenticate with SASL
if client.isTor && config.Server.TorListeners.RequireSasl && !saslSent {
if session.isTor && config.Server.TorListeners.RequireSasl && !saslSent {
return authFailTorSaslRequired
}
// finally, enforce require-sasl
if config.Accounts.RequireSasl.Enabled && !saslSent && !utils.IPInNets(client.IP(), config.Accounts.RequireSasl.exemptedNets) {
if config.Accounts.RequireSasl.Enabled && !saslSent && !utils.IPInNets(session.IP(), config.Accounts.RequireSasl.exemptedNets) {
return authFailSaslRequired
}
return authSuccess
@ -572,9 +648,13 @@ func (client *Client) run(session *Session, proxyLine string) {
func (client *Client) playReattachMessages(session *Session) {
client.server.playRegistrationBurst(session)
hasHistoryCaps := session.HasHistoryCaps()
for _, channel := range session.client.Channels() {
channel.playJoinForSession(session)
// clients should receive autoreplay-on-join lines, if applicable;
// clients should receive autoreplay-on-join lines, if applicable:
if hasHistoryCaps {
continue
}
// if they negotiated znc.in/playback or chathistory, they will receive nothing,
// because those caps disable autoreplay-on-join and they haven't sent the relevant
// *playback PRIVMSG or CHATHISTORY command yet
@ -582,6 +662,12 @@ func (client *Client) playReattachMessages(session *Session) {
channel.autoReplayHistory(client, rb, "")
rb.Send(true)
}
if !session.lastSignoff.IsZero() && !hasHistoryCaps {
rb := NewResponseBuffer(session)
zncPlayPrivmsgs(client, rb, session.lastSignoff, time.Time{})
rb.Send(true)
}
session.lastSignoff = time.Time{}
}
//
@ -634,11 +720,6 @@ func (session *Session) tryResume() (success bool) {
return
}
if oldClient.isTor != client.isTor {
session.Send(nil, server.name, "FAIL", "RESUME", "INSECURE_SESSION", client.t("Cannot resume connection from Tor to non-Tor or vice versa"))
return
}
err := server.clients.Resume(oldClient, session)
if err != nil {
session.Send(nil, server.name, "FAIL", "RESUME", "CANNOT_RESUME", client.t("Cannot resume connection"))
@ -657,37 +738,45 @@ func (session *Session) tryResume() (success bool) {
func (session *Session) playResume() {
client := session.client
server := client.server
config := server.Config()
friends := make(ClientSet)
oldestLostMessage := time.Now().UTC()
var oldestLostMessage time.Time
// work out how much time, if any, is not covered by history buffers
// assume that a persistent buffer covers the whole resume period
for _, channel := range client.Channels() {
for _, member := range channel.Members() {
friends.Add(member)
}
_, ephemeral, _ := channel.historyStatus(config)
if ephemeral {
lastDiscarded := channel.history.LastDiscarded()
if lastDiscarded.Before(oldestLostMessage) {
if oldestLostMessage.Before(lastDiscarded) {
oldestLostMessage = lastDiscarded
}
}
}
privmsgMatcher := func(item history.Item) bool {
return item.Type == history.Privmsg || item.Type == history.Notice || item.Type == history.Tagmsg
_, cEphemeral, _ := client.historyStatus(config)
if cEphemeral {
lastDiscarded := client.history.LastDiscarded()
if oldestLostMessage.Before(lastDiscarded) {
oldestLostMessage = lastDiscarded
}
}
privmsgHistory := client.history.Match(privmsgMatcher, false, 0)
lastDiscarded := client.history.LastDiscarded()
if lastDiscarded.Before(oldestLostMessage) {
oldestLostMessage = lastDiscarded
}
for _, item := range privmsgHistory {
sender := server.clients.Get(stripMaskFromNick(item.Nick))
if sender != nil {
friends.Add(sender)
_, privmsgSeq, _ := server.GetHistorySequence(nil, client, "*")
if privmsgSeq != nil {
privmsgs, _, _ := privmsgSeq.Between(history.Selector{}, history.Selector{}, config.History.ClientLength)
for _, item := range privmsgs {
sender := server.clients.Get(stripMaskFromNick(item.Nick))
if sender != nil {
friends.Add(sender)
}
}
}
timestamp := session.resumeDetails.Timestamp
gap := lastDiscarded.Sub(timestamp)
gap := oldestLostMessage.Sub(timestamp)
session.resumeDetails.HistoryIncomplete = gap > 0 || timestamp.IsZero()
gapSeconds := int(gap.Seconds()) + 1 // round up to avoid confusion
@ -723,10 +812,12 @@ func (session *Session) playResume() {
}
}
if session.resumeDetails.HistoryIncomplete && !timestamp.IsZero() {
session.Send(nil, client.server.name, "WARN", "RESUME", "HISTORY_LOST", fmt.Sprintf(client.t("Resume may have lost up to %d seconds of history"), gapSeconds))
} else {
session.Send(nil, client.server.name, "WARN", "RESUME", "HISTORY_LOST", client.t("Resume may have lost some message history"))
if session.resumeDetails.HistoryIncomplete {
if !timestamp.IsZero() {
session.Send(nil, client.server.name, "WARN", "RESUME", "HISTORY_LOST", fmt.Sprintf(client.t("Resume may have lost up to %d seconds of history"), gapSeconds))
} else {
session.Send(nil, client.server.name, "WARN", "RESUME", "HISTORY_LOST", client.t("Resume may have lost some message history"))
}
}
session.Send(nil, client.server.name, "RESUME", "SUCCESS", details.nick)
@ -738,24 +829,27 @@ func (session *Session) playResume() {
}
// replay direct PRIVSMG history
if !timestamp.IsZero() {
now := time.Now().UTC()
items, complete := client.history.Between(timestamp, now, false, 0)
rb := NewResponseBuffer(client.Sessions()[0])
client.replayPrivmsgHistory(rb, items, complete)
rb.Send(true)
if !timestamp.IsZero() && privmsgSeq != nil {
after := history.Selector{Time: timestamp}
items, complete, _ := privmsgSeq.Between(after, history.Selector{}, config.History.ZNCMax)
if len(items) != 0 {
rb := NewResponseBuffer(session)
client.replayPrivmsgHistory(rb, items, "", complete)
rb.Send(true)
}
}
session.resumeDetails = nil
}
func (client *Client) replayPrivmsgHistory(rb *ResponseBuffer, items []history.Item, complete bool) {
func (client *Client) replayPrivmsgHistory(rb *ResponseBuffer, items []history.Item, target string, complete bool) {
var batchID string
details := client.Details()
nick := details.nick
if 0 < len(items) {
batchID = rb.StartNestedHistoryBatch(nick)
if target == "" {
target = nick
}
batchID = rb.StartNestedHistoryBatch(target)
allowTags := rb.session.capabilities.Has(caps.MessageTags)
for _, item := range items {
@ -778,12 +872,16 @@ func (client *Client) replayPrivmsgHistory(rb *ResponseBuffer, items []history.I
if allowTags {
tags = item.Tags
}
if item.Params[0] == "" {
// this message was sent *to* the client from another nick
// XXX: Params[0] is the message target. if the source of this message is an in-memory
// buffer, then it's "" for an incoming message and the recipient's nick for an outgoing
// message. if the source of the message is mysql, then mysql only sees one copy of the
// message, and it's the version with the recipient's nick filled in. so this is an
// incoming message if Params[0] (the recipient's nick) equals the client's nick:
if item.Params[0] == "" || item.Params[0] == nick {
rb.AddSplitMessageFromClient(item.Nick, item.AccountName, tags, command, nick, item.Message)
} else {
// this message was sent *from* the client to another nick; the target is item.Params[0]
// substitute the client's current nickmask in case they changed nick
// substitute client's current nickmask in case client changed nick
rb.AddSplitMessageFromClient(details.nickMask, item.AccountName, tags, command, item.Params[0], item.Message)
}
}
@ -875,7 +973,7 @@ func (client *Client) HasRoleCapabs(capabs ...string) bool {
// ModeString returns the mode string for this client.
func (client *Client) ModeString() (str string) {
return "+" + client.flags.String()
return "+" + client.modes.String()
}
// Friends refers to clients that share a channel with this client.
@ -1053,6 +1151,12 @@ func (client *Client) Quit(message string, session *Session) {
// has no more sessions.
func (client *Client) destroy(session *Session) {
var sessionsToDestroy []*Session
var lastSignoff time.Time
if session != nil {
lastSignoff = session.idletimer.LastTouch()
} else {
lastSignoff = time.Now().UTC()
}
client.stateMutex.Lock()
details := client.detailsNoMutex()
@ -1060,6 +1164,8 @@ func (client *Client) destroy(session *Session) {
brbAt := client.brbTimer.brbAt
wasReattach := session != nil && session.client != client
sessionRemoved := false
registered := client.registered
alwaysOn := client.alwaysOn
var remainingSessions int
if session == nil {
sessionsToDestroy = client.sessions
@ -1074,15 +1180,25 @@ func (client *Client) destroy(session *Session) {
// should we destroy the whole client this time?
// BRB is not respected if this is a destroy of the whole client (i.e., session == nil)
brbEligible := session != nil && (brbState == BrbEnabled || brbState == BrbSticky)
brbEligible := session != nil && (brbState == BrbEnabled || alwaysOn)
shouldDestroy := !client.destroyed && remainingSessions == 0 && !brbEligible
if shouldDestroy {
// if it's our job to destroy it, don't let anyone else try
client.destroyed = true
}
if alwaysOn && remainingSessions == 0 {
client.lastSignoff = lastSignoff
client.dirtyBits |= IncludeLastSignoff
} else {
lastSignoff = time.Time{}
}
exitedSnomaskSent := client.exitedSnomaskSent
client.stateMutex.Unlock()
if !lastSignoff.IsZero() {
client.wakeWriter()
}
// destroy all applicable sessions:
var quitMessage string
for _, session := range sessionsToDestroy {
@ -1099,7 +1215,7 @@ func (client *Client) destroy(session *Session) {
// remove from connection limits
var source string
if client.isTor {
if session.isTor {
client.server.torLimiter.RemoveClient()
source = "tor"
} else {
@ -1113,11 +1229,33 @@ func (client *Client) destroy(session *Session) {
client.server.logger.Info("localconnect-ip", fmt.Sprintf("disconnecting session of %s from %s", details.nick, source))
}
// decrement stats if we have no more sessions, even if the client will not be destroyed
if shouldDestroy || remainingSessions == 0 {
invisible := client.HasMode(modes.Invisible)
operator := client.HasMode(modes.LocalOperator) || client.HasMode(modes.Operator)
client.server.stats.Remove(registered, invisible, operator)
}
// do not destroy the client if it has either remaining sessions, or is BRB'ed
if !shouldDestroy {
return
}
splitQuitMessage := utils.MakeMessage(quitMessage)
quitItem := history.Item{
Type: history.Quit,
Nick: details.nickMask,
AccountName: details.accountName,
Message: splitQuitMessage,
}
var channels []*Channel
// use a defer here to avoid writing to mysql while holding the destroy semaphore:
defer func() {
for _, channel := range channels {
channel.AddHistoryItem(quitItem)
}
}()
// see #235: deduplicating the list of PART recipients uses (comparatively speaking)
// a lot of RAM, so limit concurrency to avoid thrashing
client.server.semaphores.ClientDestroy.Acquire()
@ -1127,7 +1265,6 @@ func (client *Client) destroy(session *Session) {
client.server.logger.Debug("quit", fmt.Sprintf("%s is no longer on the server", details.nick))
}
registered := client.Registered()
if registered {
client.server.whoWas.Append(client.WhoWas())
}
@ -1141,18 +1278,12 @@ func (client *Client) destroy(session *Session) {
// clean up monitor state
client.server.monitorManager.RemoveAll(client)
splitQuitMessage := utils.MakeMessage(quitMessage)
// clean up channels
// (note that if this is a reattach, client has no channels and therefore no friends)
friends := make(ClientSet)
for _, channel := range client.Channels() {
channels = client.Channels()
for _, channel := range channels {
channel.Quit(client)
channel.history.Add(history.Item{
Type: history.Quit,
Nick: details.nickMask,
AccountName: details.accountName,
Message: splitQuitMessage,
})
for _, member := range channel.Members() {
friends.Add(member)
}
@ -1168,9 +1299,6 @@ func (client *Client) destroy(session *Session) {
client.server.accounts.Logout(client)
client.server.stats.Remove(registered, client.HasMode(modes.Invisible),
client.HasMode(modes.Operator) || client.HasMode(modes.LocalOperator))
// this happens under failure to return from BRB
if quitMessage == "" {
if brbState == BrbDead && !brbAt.IsZero() {
@ -1196,11 +1324,10 @@ func (client *Client) destroy(session *Session) {
// SendSplitMsgFromClient sends an IRC PRIVMSG/NOTICE coming from a specific client.
// Adds account-tag to the line as well.
func (session *Session) sendSplitMsgFromClientInternal(blocking bool, nickmask, accountName string, tags map[string]string, command, target string, message utils.SplitMessage) {
// TODO no maxline support
if message.Is512() {
session.sendFromClientInternal(blocking, message.Time, message.Msgid, nickmask, accountName, tags, command, target, message.Message)
} else {
if message.IsMultiline() && session.capabilities.Has(caps.Multiline) {
if session.capabilities.Has(caps.Multiline) {
for _, msg := range session.composeMultilineBatch(nickmask, accountName, tags, command, target, message) {
session.SendRawMessage(msg, blocking)
}
@ -1366,13 +1493,23 @@ func (session *Session) Notice(text string) {
func (client *Client) addChannel(channel *Channel) {
client.stateMutex.Lock()
client.channels[channel] = true
alwaysOn := client.alwaysOn
client.stateMutex.Unlock()
if alwaysOn {
client.markDirty(IncludeChannels)
}
}
func (client *Client) removeChannel(channel *Channel) {
client.stateMutex.Lock()
delete(client.channels, channel)
alwaysOn := client.alwaysOn
client.stateMutex.Unlock()
if alwaysOn {
client.markDirty(IncludeChannels)
}
}
// Records that the client has been invited to join an invite-only channel
@ -1401,11 +1538,11 @@ func (client *Client) CheckInvited(casefoldedChannel string) (invited bool) {
// Implements auto-oper by certfp (scans for an auto-eligible operator block that matches
// the client's cert, then applies it).
func (client *Client) attemptAutoOper(session *Session) {
if client.certfp == "" || client.HasMode(modes.Operator) {
if session.certfp == "" || client.HasMode(modes.Operator) {
return
}
for _, oper := range client.server.Config().operators {
if oper.Auto && oper.Pass == nil && oper.Fingerprint != "" && oper.Fingerprint == client.certfp {
if oper.Auto && oper.Pass == nil && oper.Fingerprint != "" && oper.Fingerprint == session.certfp {
rb := NewResponseBuffer(session)
applyOper(client, oper, rb)
rb.Send(true)
@ -1413,3 +1550,96 @@ func (client *Client) attemptAutoOper(session *Session) {
}
}
}
func (client *Client) historyStatus(config *Config) (persistent, ephemeral bool, target string) {
if !config.History.Enabled {
return
} else if !config.History.Persistent.Enabled {
ephemeral = true
return
}
client.stateMutex.RLock()
alwaysOn := client.alwaysOn
historyStatus := client.accountSettings.DMHistory
target = client.nickCasefolded
client.stateMutex.RUnlock()
if !alwaysOn {
ephemeral = true
return
}
historyStatus = historyEnabled(config.History.Persistent.DirectMessages, historyStatus)
ephemeral = (historyStatus == HistoryEphemeral)
persistent = (historyStatus == HistoryPersistent)
return
}
// these are bit flags indicating what part of the client status is "dirty"
// and needs to be read from memory and written to the db
// TODO add a dirty flag for lastSignoff
const (
IncludeChannels uint = 1 << iota
IncludeLastSignoff
)
func (client *Client) markDirty(dirtyBits uint) {
client.stateMutex.Lock()
alwaysOn := client.alwaysOn
client.dirtyBits = client.dirtyBits | dirtyBits
client.stateMutex.Unlock()
if alwaysOn {
client.wakeWriter()
}
}
func (client *Client) wakeWriter() {
if client.writerSemaphore.TryAcquire() {
go client.writeLoop()
}
}
func (client *Client) writeLoop() {
for {
client.performWrite()
client.writerSemaphore.Release()
client.stateMutex.RLock()
isDirty := client.dirtyBits != 0
client.stateMutex.RUnlock()
if !isDirty || !client.writerSemaphore.TryAcquire() {
return
}
}
}
func (client *Client) performWrite() {
client.stateMutex.Lock()
dirtyBits := client.dirtyBits
client.dirtyBits = 0
account := client.account
client.stateMutex.Unlock()
if account == "" {
client.server.logger.Error("internal", "attempting to persist logged-out client", client.Nick())
return
}
if (dirtyBits & IncludeChannels) != 0 {
channels := client.Channels()
channelNames := make([]string, len(channels))
for i, channel := range channels {
channelNames[i] = channel.Name()
}
client.server.accounts.saveChannels(account, channelNames)
}
if (dirtyBits & IncludeLastSignoff) != 0 {
client.stateMutex.RLock()
lastSignoff := client.lastSignoff
client.stateMutex.RUnlock()
client.server.accounts.saveLastSignoff(account, lastSignoff)
}
}

View File

@ -105,7 +105,8 @@ func (clients *ClientManager) Resume(oldClient *Client, session *Session) (err e
return errNickMissing
}
if !oldClient.AddSession(session) {
success, _, _ := oldClient.AddSession(session)
if !success {
return errNickMissing
}
@ -113,35 +114,51 @@ func (clients *ClientManager) Resume(oldClient *Client, session *Session) (err e
}
// SetNick sets a client's nickname, validating it against nicknames in use
func (clients *ClientManager) SetNick(client *Client, session *Session, newNick string) error {
if len(newNick) > client.server.Config().Limits.NickLen {
return errNicknameInvalid
}
func (clients *ClientManager) SetNick(client *Client, session *Session, newNick string) (setNick string, err error) {
config := client.server.Config()
newcfnick, err := CasefoldName(newNick)
if err != nil {
return errNicknameInvalid
return "", errNicknameInvalid
}
if len(newNick) > config.Limits.NickLen || len(newcfnick) > config.Limits.NickLen {
return "", errNicknameInvalid
}
newSkeleton, err := Skeleton(newNick)
if err != nil {
return errNicknameInvalid
return "", errNicknameInvalid
}
if restrictedCasefoldedNicks[newcfnick] || restrictedSkeletons[newSkeleton] {
return errNicknameInvalid
return "", errNicknameInvalid
}
reservedAccount, method := client.server.accounts.EnforcementStatus(newcfnick, newSkeleton)
account := client.Account()
config := client.server.Config()
client.stateMutex.RLock()
account := client.account
accountName := client.accountName
settings := client.accountSettings
registered := client.registered
realname := client.realname
client.stateMutex.RUnlock()
// recompute this (client.alwaysOn is not set for unregistered clients):
alwaysOn := account != "" && persistenceEnabled(config.Accounts.Multiclient.AlwaysOn, settings.AlwaysOn)
if alwaysOn && registered {
return "", errCantChangeNick
}
var bouncerAllowed bool
if config.Accounts.Bouncer.Enabled {
if session != nil && session.capabilities.Has(caps.Bouncer) {
if config.Accounts.Multiclient.Enabled {
if alwaysOn {
// ignore the pre-reg nick, force a reattach
newNick = accountName
newcfnick = account
bouncerAllowed = true
} else {
settings := client.AccountSettings()
if config.Accounts.Bouncer.AllowedByDefault && settings.AllowBouncer != BouncerDisallowedByUser {
if config.Accounts.Multiclient.AllowedByDefault && settings.AllowBouncer != MulticlientDisallowedByUser {
bouncerAllowed = true
} else if settings.AllowBouncer == BouncerAllowedByUser {
} else if settings.AllowBouncer == MulticlientAllowedByUser {
bouncerAllowed = true
}
}
@ -154,28 +171,41 @@ func (clients *ClientManager) SetNick(client *Client, session *Session, newNick
// the client may just be changing case
if currentClient != nil && currentClient != client && session != nil {
// these conditions forbid reattaching to an existing session:
if client.Registered() || !bouncerAllowed || account == "" || account != currentClient.Account() || client.HasMode(modes.TLS) != currentClient.HasMode(modes.TLS) {
return errNicknameInUse
if registered || !bouncerAllowed || account == "" || account != currentClient.Account() || client.HasMode(modes.TLS) != currentClient.HasMode(modes.TLS) {
return "", errNicknameInUse
}
if !currentClient.AddSession(session) {
return errNicknameInUse
reattachSuccessful, numSessions, lastSignoff := currentClient.AddSession(session)
if !reattachSuccessful {
return "", errNicknameInUse
}
if numSessions == 1 {
invisible := client.HasMode(modes.Invisible)
operator := client.HasMode(modes.Operator) || client.HasMode(modes.LocalOperator)
client.server.stats.AddRegistered(invisible, operator)
}
session.lastSignoff = lastSignoff
// XXX SetNames only changes names if they are unset, so the realname change only
// takes effect on first attach to an always-on client (good), but the user/ident
// change is always a no-op (bad). we could make user/ident act the same way as
// realname, but then we'd have to send CHGHOST and i don't want to deal with that
// for performance reasons
currentClient.SetNames("user", realname, true)
// successful reattach!
return nil
return newNick, nil
}
// analogous checks for skeletons
skeletonHolder := clients.bySkeleton[newSkeleton]
if skeletonHolder != nil && skeletonHolder != client {
return errNicknameInUse
return "", errNicknameInUse
}
if method == NickEnforcementStrict && reservedAccount != "" && reservedAccount != account {
return errNicknameReserved
return "", errNicknameReserved
}
clients.removeInternal(client)
clients.byNick[newcfnick] = client
clients.bySkeleton[newSkeleton] = client
client.updateNick(newNick, newcfnick, newSkeleton)
return nil
return newNick, nil
}
func (clients *ClientManager) AllClients() (result []*Client) {

View File

@ -54,6 +54,12 @@ func (cmd *Command) Run(server *Server, client *Client, session *Session, msg ir
return cmd.handler(server, client, msg, rb)
}()
// most servers do this only for PING/PONG, but we'll do it for any command:
if client.registered {
// touch even if `exiting`, so we record the time of a QUIT accurately
session.idletimer.Touch()
}
if exiting {
return
}
@ -63,11 +69,6 @@ func (cmd *Command) Run(server *Server, client *Client, session *Session, msg ir
exiting = server.tryRegister(client, session)
}
// most servers do this only for PING/PONG, but we'll do it for any command:
if client.registered {
session.idletimer.Touch()
}
if client.registered && !cmd.leaveClientIdle {
client.Active(session)
}
@ -109,7 +110,7 @@ func init() {
},
"CHATHISTORY": {
handler: chathistoryHandler,
minParams: 3,
minParams: 4,
},
"DEBUG": {
handler: debugHandler,

View File

@ -28,6 +28,7 @@ import (
"github.com/oragono/oragono/irc/ldap"
"github.com/oragono/oragono/irc/logger"
"github.com/oragono/oragono/irc/modes"
"github.com/oragono/oragono/irc/mysql"
"github.com/oragono/oragono/irc/passwd"
"github.com/oragono/oragono/irc/utils"
"gopkg.in/yaml.v2"
@ -61,6 +62,157 @@ type listenerConfig struct {
ProxyBeforeTLS bool
}
type PersistentStatus uint
const (
PersistentUnspecified PersistentStatus = iota
PersistentDisabled
PersistentOptIn
PersistentOptOut
PersistentMandatory
)
func persistentStatusToString(status PersistentStatus) string {
switch status {
case PersistentUnspecified:
return "default"
case PersistentDisabled:
return "disabled"
case PersistentOptIn:
return "opt-in"
case PersistentOptOut:
return "opt-out"
case PersistentMandatory:
return "mandatory"
default:
return ""
}
}
func persistentStatusFromString(status string) (PersistentStatus, error) {
switch strings.ToLower(status) {
case "default":
return PersistentUnspecified, nil
case "":
return PersistentDisabled, nil
case "opt-in":
return PersistentOptIn, nil
case "opt-out":
return PersistentOptOut, nil
case "mandatory":
return PersistentMandatory, nil
default:
b, err := utils.StringToBool(status)
if b {
return PersistentMandatory, err
} else {
return PersistentDisabled, err
}
}
}
func (ps *PersistentStatus) UnmarshalYAML(unmarshal func(interface{}) error) error {
var orig string
var err error
if err = unmarshal(&orig); err != nil {
return err
}
result, err := persistentStatusFromString(orig)
if err == nil {
if result == PersistentUnspecified {
result = PersistentDisabled
}
*ps = result
}
return err
}
func persistenceEnabled(serverSetting, clientSetting PersistentStatus) (enabled bool) {
if serverSetting == PersistentDisabled {
return false
} else if serverSetting == PersistentMandatory {
return true
} else if clientSetting == PersistentDisabled {
return false
} else if clientSetting == PersistentMandatory {
return true
} else if serverSetting == PersistentOptOut {
return true
} else {
return false
}
}
type HistoryStatus uint
const (
HistoryDefault HistoryStatus = iota
HistoryDisabled
HistoryEphemeral
HistoryPersistent
)
func historyStatusFromString(str string) (status HistoryStatus, err error) {
switch strings.ToLower(str) {
case "default":
return HistoryDefault, nil
case "ephemeral":
return HistoryEphemeral, nil
case "persistent":
return HistoryPersistent, nil
default:
b, err := utils.StringToBool(str)
if b {
return HistoryPersistent, err
} else {
return HistoryDisabled, err
}
}
}
func historyStatusToString(status HistoryStatus) string {
switch status {
case HistoryDefault:
return "default"
case HistoryDisabled:
return "disabled"
case HistoryEphemeral:
return "ephemeral"
case HistoryPersistent:
return "persistent"
default:
return ""
}
}
func historyEnabled(serverSetting PersistentStatus, localSetting HistoryStatus) (result HistoryStatus) {
if serverSetting == PersistentDisabled {
return HistoryDisabled
} else if serverSetting == PersistentMandatory {
return HistoryPersistent
} else if serverSetting == PersistentOptOut {
if localSetting == HistoryDefault {
return HistoryPersistent
} else {
return localSetting
}
} else if serverSetting == PersistentOptIn {
if localSetting >= HistoryEphemeral {
return localSetting
} else {
return HistoryDisabled
}
} else {
return HistoryDisabled
}
}
type MulticlientConfig struct {
Enabled bool
AllowedByDefault bool `yaml:"allowed-by-default"`
AlwaysOn PersistentStatus `yaml:"always-on"`
}
type AccountConfig struct {
Registration AccountRegistrationConfig
AuthenticationEnabled bool `yaml:"authentication-enabled"`
@ -77,19 +229,17 @@ type AccountConfig struct {
} `yaml:"login-throttling"`
SkipServerPassword bool `yaml:"skip-server-password"`
NickReservation NickReservationConfig `yaml:"nick-reservation"`
Bouncer struct {
Enabled bool
AllowedByDefault bool `yaml:"allowed-by-default"`
}
VHosts VHostConfig
Multiclient MulticlientConfig
Bouncer *MulticlientConfig // # handle old name for 'multiclient'
VHosts VHostConfig
}
// AccountRegistrationConfig controls account registration.
type AccountRegistrationConfig struct {
Enabled bool
EnabledCallbacks []string `yaml:"enabled-callbacks"`
EnabledCredentialTypes []string `yaml:"-"`
VerifyTimeout time.Duration `yaml:"verify-timeout"`
EnabledCallbacks []string `yaml:"enabled-callbacks"`
EnabledCredentialTypes []string `yaml:"-"`
VerifyTimeout custime.Duration `yaml:"verify-timeout"`
Callbacks struct {
Mailto struct {
Server string
@ -117,7 +267,7 @@ type VHostConfig struct {
UserRequests struct {
Enabled bool
Channel string
Cooldown time.Duration
Cooldown custime.Duration
} `yaml:"user-requests"`
OfferList []string `yaml:"offer-list"`
}
@ -260,18 +410,17 @@ type Limits struct {
// STSConfig controls the STS configuration/
type STSConfig struct {
Enabled bool
Duration time.Duration `yaml:"duration-real"`
DurationString string `yaml:"duration"`
Port int
Preload bool
STSOnlyBanner string `yaml:"sts-only-banner"`
bannerLines []string
Enabled bool
Duration custime.Duration
Port int
Preload bool
STSOnlyBanner string `yaml:"sts-only-banner"`
bannerLines []string
}
// Value returns the STS value to advertise in CAP
func (sts *STSConfig) Value() string {
val := fmt.Sprintf("duration=%d", int(sts.Duration.Seconds()))
val := fmt.Sprintf("duration=%d", int(time.Duration(sts.Duration).Seconds()))
if sts.Enabled && sts.Port > 0 {
val += fmt.Sprintf(",port=%d", sts.Port)
}
@ -337,6 +486,8 @@ type Config struct {
isupport isupport.List
IPLimits connection_limits.LimiterConfig `yaml:"ip-limits"`
Cloaks cloaks.CloakConfig `yaml:"ip-cloaking"`
SecureNetDefs []string `yaml:"secure-nets"`
secureNets []net.IPNet
supportedCaps *caps.Set
capValues caps.Values
Casemapping Casemapping
@ -353,6 +504,7 @@ type Config struct {
Datastore struct {
Path string
AutoUpgrade bool
MySQL mysql.Config
}
Accounts AccountConfig
@ -392,6 +544,18 @@ type Config struct {
AutoresizeWindow time.Duration `yaml:"autoresize-window"`
AutoreplayOnJoin int `yaml:"autoreplay-on-join"`
ChathistoryMax int `yaml:"chathistory-maxmessages"`
ZNCMax int `yaml:"znc-maxmessages"`
Restrictions struct {
ExpireTime custime.Duration `yaml:"expire-time"`
EnforceRegistrationDate bool `yaml:"enforce-registration-date"`
GracePeriod custime.Duration `yaml:"grace-period"`
}
Persistent struct {
Enabled bool
UnregisteredChannels bool `yaml:"unregistered-channels"`
RegisteredChannels PersistentStatus `yaml:"registered-channels"`
DirectMessages PersistentStatus `yaml:"direct-messages"`
}
}
Filename string
@ -626,6 +790,11 @@ func LoadConfig(filename string) (config *Config, err error) {
if config.Limits.RegistrationMessages == 0 {
config.Limits.RegistrationMessages = 1024
}
if config.Datastore.MySQL.Enabled {
if config.Limits.NickLen > mysql.MaxTargetLength || config.Limits.ChannelLen > mysql.MaxTargetLength {
return nil, fmt.Errorf("to use MySQL, nick and channel length limits must be %d or lower", mysql.MaxTargetLength)
}
}
config.Server.supportedCaps = caps.NewCompleteSet()
config.Server.capValues = make(caps.Values)
@ -636,10 +805,6 @@ func LoadConfig(filename string) (config *Config, err error) {
}
if config.Server.STS.Enabled {
config.Server.STS.Duration, err = custime.ParseDuration(config.Server.STS.DurationString)
if err != nil {
return nil, fmt.Errorf("Could not parse STS duration: %s", err.Error())
}
if config.Server.STS.Port < 0 || config.Server.STS.Port > 65535 {
return nil, fmt.Errorf("STS port is incorrect, should be 0 if disabled: %d", config.Server.STS.Port)
}
@ -692,8 +857,15 @@ func LoadConfig(filename string) (config *Config, err error) {
config.Server.capValues[caps.Multiline] = multilineCapValue
}
if !config.Accounts.Bouncer.Enabled {
config.Server.supportedCaps.Disable(caps.Bouncer)
// handle legacy name 'bouncer' for 'multiclient' section:
if config.Accounts.Bouncer != nil {
config.Accounts.Multiclient = *config.Accounts.Bouncer
}
if !config.Accounts.Multiclient.Enabled {
config.Accounts.Multiclient.AlwaysOn = PersistentDisabled
} else if config.Accounts.Multiclient.AlwaysOn >= PersistentOptOut {
config.Accounts.Multiclient.AllowedByDefault = true
}
var newLogConfigs []logger.LoggingConfig
@ -762,6 +934,11 @@ func LoadConfig(filename string) (config *Config, err error) {
return nil, fmt.Errorf("Could not parse proxy-allowed-from nets: %v", err.Error())
}
config.Server.secureNets, err = utils.ParseNetList(config.Server.SecureNetDefs)
if err != nil {
return nil, fmt.Errorf("Could not parse secure-nets: %v\n", err.Error())
}
rawRegexp := config.Accounts.VHosts.ValidRegexpRaw
if rawRegexp != "" {
regexp, err := regexp.Compile(rawRegexp)
@ -858,6 +1035,18 @@ func LoadConfig(filename string) (config *Config, err error) {
config.History.ClientLength = 0
}
if !config.History.Enabled || !config.History.Persistent.Enabled {
config.History.Persistent.UnregisteredChannels = false
config.History.Persistent.RegisteredChannels = PersistentDisabled
config.History.Persistent.DirectMessages = PersistentDisabled
}
if config.History.ZNCMax == 0 {
config.History.ZNCMax = config.History.ChathistoryMax
}
config.Datastore.MySQL.ExpireTime = time.Duration(config.History.Restrictions.ExpireTime)
config.Server.Cloaks.Initialize()
if config.Server.Cloaks.Enabled {
if config.Server.Cloaks.Secret == "" || config.Server.Cloaks.Secret == "siaELnk6Kaeo65K3RCrwJjlWaZ-Bt3WuZ2L8MXLbNb4" {
@ -942,12 +1131,6 @@ func (config *Config) Diff(oldConfig *Config) (addedCaps, removedCaps *caps.Set)
removedCaps.Add(caps.SASL)
}
if !oldConfig.Accounts.Bouncer.Enabled && config.Accounts.Bouncer.Enabled {
addedCaps.Add(caps.Bouncer)
} else if oldConfig.Accounts.Bouncer.Enabled && !config.Accounts.Bouncer.Enabled {
removedCaps.Add(caps.Bouncer)
}
if oldConfig.Limits.Multiline.MaxBytes != 0 && config.Limits.Multiline.MaxBytes == 0 {
removedCaps.Add(caps.Multiline)
} else if oldConfig.Limits.Multiline.MaxBytes == 0 && config.Limits.Multiline.MaxBytes != 0 {

View File

@ -75,8 +75,9 @@ var unitMap = map[string]int64{
"m": int64(time.Minute),
"h": int64(time.Hour),
"d": int64(time.Hour * 24),
"w": int64(time.Hour * 24 * 7),
"mo": int64(time.Hour * 24 * 30),
"y": int64(time.Hour * 24 * 265),
"y": int64(time.Hour * 24 * 365),
}
// ParseDuration parses a duration string.
@ -181,3 +182,18 @@ func ParseDuration(s string) (time.Duration, error) {
}
return time.Duration(d), nil
}
type Duration time.Duration
func (d *Duration) UnmarshalYAML(unmarshal func(interface{}) error) error {
var orig string
var err error
if err = unmarshal(&orig); err != nil {
return err
}
result, err := ParseDuration(orig)
if err == nil {
*d = Duration(result)
}
return err
}

View File

@ -36,22 +36,6 @@ type SchemaChange struct {
// maps an initial version to a schema change capable of upgrading it
var schemaChanges map[string]SchemaChange
type incompatibleSchemaError struct {
currentVersion string
requiredVersion string
}
func IncompatibleSchemaError(currentVersion string) (result *incompatibleSchemaError) {
return &incompatibleSchemaError{
currentVersion: currentVersion,
requiredVersion: latestDbSchema,
}
}
func (err *incompatibleSchemaError) Error() string {
return fmt.Sprintf("Database requires update. Expected schema v%s, got v%s", err.requiredVersion, err.currentVersion)
}
// InitDB creates the database, implementing the `oragono initdb` command.
func InitDB(path string) {
_, err := os.Stat(path)
@ -129,7 +113,7 @@ func openDatabaseInternal(config *Config, allowAutoupgrade bool) (db *buntdb.DB,
// successful autoupgrade, let's try this again:
return openDatabaseInternal(config, false)
} else {
err = IncompatibleSchemaError(version)
err = &utils.IncompatibleSchemaError{CurrentVersion: version, RequiredVersion: latestDbSchema}
return
}
}
@ -179,7 +163,7 @@ func UpgradeDB(config *Config) (err error) {
break
}
// unable to upgrade to the desired version, roll back
return IncompatibleSchemaError(version)
return &utils.IncompatibleSchemaError{CurrentVersion: version, RequiredVersion: latestDbSchema}
}
log.Println("attempting to update schema from version " + version)
err := change.Changer(config, tx)
@ -509,14 +493,14 @@ func schemaChangeV6ToV7(config *Config, tx *buntdb.Tx) error {
type accountSettingsLegacyV7 struct {
AutoreplayLines *int
NickEnforcement NickEnforcementMethod
AllowBouncer BouncerAllowedSetting
AllowBouncer MulticlientAllowedSetting
AutoreplayJoins bool
}
type accountSettingsLegacyV8 struct {
AutoreplayLines *int
NickEnforcement NickEnforcementMethod
AllowBouncer BouncerAllowedSetting
AllowBouncer MulticlientAllowedSetting
ReplayJoins ReplayJoinsSetting
}

View File

@ -33,6 +33,7 @@ var (
errChannelNotOwnedByAccount = errors.New("Channel not owned by the specified account")
errChannelTransferNotOffered = errors.New(`You weren't offered ownership of that channel`)
errChannelAlreadyRegistered = errors.New("Channel is already registered")
errChannelNotRegistered = errors.New("Channel is not registered")
errChannelNameInUse = errors.New(`Channel name in use`)
errInvalidChannelName = errors.New(`Invalid channel name`)
errMonitorLimitExceeded = errors.New("Monitor limit exceeded")
@ -40,6 +41,7 @@ var (
errNicknameInvalid = errors.New("invalid nickname")
errNicknameInUse = errors.New("nickname in use")
errNicknameReserved = errors.New("nickname is reserved")
errCantChangeNick = errors.New(`Always-on clients can't change nicknames`)
errNoExistingBan = errors.New("Ban does not exist")
errNoSuchChannel = errors.New(`No such channel`)
errChannelPurged = errors.New(`This channel was purged by the server operators and cannot be used`)

View File

@ -61,7 +61,7 @@ func (wc *webircConfig) Populate() (err error) {
func (client *Client) ApplyProxiedIP(session *Session, proxiedIP string, tls bool) (err error, quitMsg string) {
// PROXY and WEBIRC are never accepted from a Tor listener, even if the address itself
// is whitelisted:
if client.isTor {
if session.isTor {
return errBadProxyLine, ""
}
@ -88,7 +88,7 @@ func (client *Client) ApplyProxiedIP(session *Session, proxiedIP string, tls boo
session.proxiedIP = parsedProxiedIP
// nickmask will be updated when the client completes registration
// set tls info
client.certfp = ""
session.certfp = ""
client.SetMode(modes.TLS, tls)
return nil, ""

View File

@ -65,6 +65,7 @@ type SessionData struct {
atime time.Time
ip net.IP
hostname string
certfp string
}
func (client *Client) AllSessionData(currentSession *Session) (data []SessionData, currentIndex int) {
@ -81,6 +82,7 @@ func (client *Client) AllSessionData(currentSession *Session) (data []SessionDat
atime: session.atime,
ctime: session.ctime,
hostname: session.rawHostname,
certfp: session.certfp,
}
if session.proxiedIP != nil {
data[i].ip = session.proxiedIP
@ -91,21 +93,34 @@ func (client *Client) AllSessionData(currentSession *Session) (data []SessionDat
return
}
func (client *Client) AddSession(session *Session) (success bool) {
func (client *Client) AddSession(session *Session) (success bool, numSessions int, lastSignoff time.Time) {
defer func() {
if !lastSignoff.IsZero() {
client.wakeWriter()
}
}()
client.stateMutex.Lock()
defer client.stateMutex.Unlock()
// client may be dying and ineligible to receive another session
if client.destroyed {
return false
return
}
// success, attach the new session to the client
session.client = client
newSessions := make([]*Session, len(client.sessions)+1)
copy(newSessions, client.sessions)
newSessions[len(newSessions)-1] = session
if len(client.sessions) == 0 && client.accountSettings.AutoreplayMissed {
// n.b. this is only possible if client is persistent and remained
// on the server with no sessions:
lastSignoff = client.lastSignoff
client.lastSignoff = time.Time{}
client.dirtyBits |= IncludeLastSignoff
}
client.sessions = newSessions
return true
return true, len(client.sessions), lastSignoff
}
func (client *Client) removeSession(session *Session) (success bool, length int) {
@ -189,6 +204,13 @@ func (client *Client) SetExitedSnomaskSent() {
client.stateMutex.Unlock()
}
func (client *Client) AlwaysOn() (alwaysOn bool) {
client.stateMutex.Lock()
alwaysOn = client.alwaysOn
client.stateMutex.Unlock()
return
}
// uniqueIdentifiers returns the strings for which the server enforces per-client
// uniqueness/ownership; no two clients can have colliding casefolded nicks or
// skeletons.
@ -264,23 +286,43 @@ func (client *Client) AccountName() string {
return client.accountName
}
func (client *Client) SetAccountName(account string) (changed bool) {
var casefoldedAccount string
var err error
if account != "" {
if casefoldedAccount, err = CasefoldName(account); err != nil {
return
}
}
func (client *Client) Login(account ClientAccount) {
alwaysOn := persistenceEnabled(client.server.Config().Accounts.Multiclient.AlwaysOn, account.Settings.AlwaysOn)
client.stateMutex.Lock()
defer client.stateMutex.Unlock()
changed = client.account != casefoldedAccount
client.account = casefoldedAccount
client.accountName = account
client.account = account.NameCasefolded
client.accountName = account.Name
client.accountSettings = account.Settings
// check `registered` to avoid incorrectly marking a temporary (pre-reattach),
// SASL'ing client as always-on
if client.registered {
client.alwaysOn = alwaysOn
}
client.accountRegDate = account.RegisteredAt
return
}
func (client *Client) historyCutoff() (cutoff time.Time) {
client.stateMutex.Lock()
if client.account != "" {
cutoff = client.accountRegDate
} else {
cutoff = client.ctime
}
client.stateMutex.Unlock()
return
}
func (client *Client) Logout() {
client.stateMutex.Lock()
client.account = ""
client.accountName = ""
client.alwaysOn = false
client.accountRegDate = time.Time{}
client.accountSettings = AccountSettings{}
client.stateMutex.Unlock()
}
func (client *Client) AccountSettings() (result AccountSettings) {
client.stateMutex.RLock()
result = client.accountSettings
@ -289,8 +331,12 @@ func (client *Client) AccountSettings() (result AccountSettings) {
}
func (client *Client) SetAccountSettings(settings AccountSettings) {
alwaysOn := persistenceEnabled(client.server.Config().Accounts.Multiclient.AlwaysOn, settings.AlwaysOn)
client.stateMutex.Lock()
client.accountSettings = settings
if client.registered {
client.alwaysOn = alwaysOn
}
client.stateMutex.Unlock()
}
@ -309,11 +355,17 @@ func (client *Client) SetLanguages(languages []string) {
func (client *Client) HasMode(mode modes.Mode) bool {
// client.flags has its own synch
return client.flags.HasMode(mode)
return client.modes.HasMode(mode)
}
func (client *Client) SetMode(mode modes.Mode, on bool) bool {
return client.flags.SetMode(mode, on)
return client.modes.SetMode(mode, on)
}
func (client *Client) SetRealname(realname string) {
client.stateMutex.Lock()
client.realname = realname
client.stateMutex.Unlock()
}
func (client *Client) Channels() (result []*Channel) {
@ -410,3 +462,17 @@ func (channel *Channel) HighestUserMode(client *Client) (result modes.Mode) {
channel.stateMutex.RUnlock()
return clientModes.HighestChannelUserMode()
}
func (channel *Channel) Settings() (result ChannelSettings) {
channel.stateMutex.RLock()
result = channel.settings
channel.stateMutex.RUnlock()
return result
}
func (channel *Channel) SetSettings(settings ChannelSettings) {
channel.stateMutex.Lock()
channel.settings = settings
channel.stateMutex.Unlock()
channel.MarkDirty(IncludeSettings)
}

View File

@ -112,6 +112,7 @@ func sendSuccessfulAccountAuth(client *Client, rb *ResponseBuffer, forNS, forSAS
// AUTHENTICATE [<mechanism>|<data>|*]
func authenticateHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
session := rb.session
config := server.Config()
details := client.Details()
@ -128,20 +129,17 @@ func authenticateHandler(server *Server, client *Client, msg ircmsg.IrcMessage,
// sasl abort
if !server.AccountConfig().AuthenticationEnabled || len(msg.Params) == 1 && msg.Params[0] == "*" {
rb.Add(nil, server.name, ERR_SASLABORTED, details.nick, client.t("SASL authentication aborted"))
client.saslInProgress = false
client.saslMechanism = ""
client.saslValue = ""
session.sasl.Clear()
return false
}
// start new sasl session
if !client.saslInProgress {
if session.sasl.mechanism == "" {
mechanism := strings.ToUpper(msg.Params[0])
_, mechanismIsEnabled := EnabledSaslMechanisms[mechanism]
if mechanismIsEnabled {
client.saslInProgress = true
client.saslMechanism = mechanism
session.sasl.mechanism = mechanism
if !config.Server.Compatibility.SendUnprefixedSasl {
// normal behavior
rb.Add(nil, server.name, "AUTHENTICATE", "+")
@ -162,58 +160,46 @@ func authenticateHandler(server *Server, client *Client, msg ircmsg.IrcMessage,
if len(rawData) > 400 {
rb.Add(nil, server.name, ERR_SASLTOOLONG, details.nick, client.t("SASL message too long"))
client.saslInProgress = false
client.saslMechanism = ""
client.saslValue = ""
session.sasl.Clear()
return false
} else if len(rawData) == 400 {
client.saslValue += rawData
// allow 4 'continuation' lines before rejecting for length
if len(client.saslValue) > 400*4 {
if len(session.sasl.value) >= 400*4 {
rb.Add(nil, server.name, ERR_SASLFAIL, details.nick, client.t("SASL authentication failed: Passphrase too long"))
client.saslInProgress = false
client.saslMechanism = ""
client.saslValue = ""
session.sasl.Clear()
return false
}
session.sasl.value += rawData
return false
}
if rawData != "+" {
client.saslValue += rawData
session.sasl.value += rawData
}
var data []byte
var err error
if client.saslValue != "+" {
data, err = base64.StdEncoding.DecodeString(client.saslValue)
if session.sasl.value != "+" {
data, err = base64.StdEncoding.DecodeString(session.sasl.value)
if err != nil {
rb.Add(nil, server.name, ERR_SASLFAIL, details.nick, client.t("SASL authentication failed: Invalid b64 encoding"))
client.saslInProgress = false
client.saslMechanism = ""
client.saslValue = ""
session.sasl.Clear()
return false
}
}
// call actual handler
handler, handlerExists := EnabledSaslMechanisms[client.saslMechanism]
handler, handlerExists := EnabledSaslMechanisms[session.sasl.mechanism]
// like 100% not required, but it's good to be safe I guess
if !handlerExists {
rb.Add(nil, server.name, ERR_SASLFAIL, details.nick, client.t("SASL authentication failed"))
client.saslInProgress = false
client.saslMechanism = ""
client.saslValue = ""
session.sasl.Clear()
return false
}
// let the SASL handler do its thing
exiting := handler(server, client, client.saslMechanism, data, rb)
// wait 'til SASL is done before emptying the sasl vars
client.saslInProgress = false
client.saslMechanism = ""
client.saslValue = ""
exiting := handler(server, client, session.sasl.mechanism, data, rb)
session.sasl.Clear()
return exiting
}
@ -270,7 +256,7 @@ func authErrorToMessage(server *Server, err error) (msg string) {
// AUTHENTICATE EXTERNAL
func authExternalHandler(server *Server, client *Client, mechanism string, value []byte, rb *ResponseBuffer) bool {
if client.certfp == "" {
if rb.session.certfp == "" {
rb.Add(nil, server.name, ERR_SASLFAIL, client.nick, client.t("SASL authentication failed, you are not connecting with a certificate"))
return false
}
@ -287,7 +273,7 @@ func authExternalHandler(server *Server, client *Client, mechanism string, value
}
if err == nil {
err = server.accounts.AuthenticateByCertFP(client, authzid)
err = server.accounts.AuthenticateByCertFP(client, rb.session.certfp, authzid)
}
if err != nil {
@ -531,64 +517,49 @@ func capHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Respo
// CHATHISTORY <target> BETWEEN <query> <query> <direction> [<limit>]
// e.g., CHATHISTORY #ircv3 BETWEEN timestamp=YYYY-MM-DDThh:mm:ss.sssZ timestamp=YYYY-MM-DDThh:mm:ss.sssZ + 100
func chathistoryHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) (exiting bool) {
config := server.Config()
var items []history.Item
success := false
var hist *history.Buffer
unknown_command := false
var target string
var channel *Channel
var sequence history.Sequence
var err error
defer func() {
// successful responses are sent as a chathistory or history batch
if success && 0 < len(items) {
if channel == nil {
client.replayPrivmsgHistory(rb, items, true)
} else {
if err == nil {
if channel != nil {
channel.replayHistoryItems(rb, items, false)
} else {
client.replayPrivmsgHistory(rb, items, target, true)
}
return
}
// errors are sent either without a batch, or in a draft/labeled-response batch as usual
// TODO: send `WARN CHATHISTORY MAX_MESSAGES_EXCEEDED` when appropriate
if hist == nil {
rb.Add(nil, server.name, "ERR", "CHATHISTORY", "NO_SUCH_CHANNEL")
} else if len(items) == 0 {
rb.Add(nil, server.name, "ERR", "CHATHISTORY", "NO_TEXT_TO_SEND")
} else if !success {
rb.Add(nil, server.name, "ERR", "CHATHISTORY", "NEED_MORE_PARAMS")
if unknown_command {
rb.Add(nil, server.name, "FAIL", "CHATHISTORY", "UNKNOWN_COMMAND", utils.SafeErrorParam(msg.Params[0]), client.t("Unknown command"))
} else if err == utils.ErrInvalidParams {
rb.Add(nil, server.name, "FAIL", "CHATHISTORY", "INVALID_PARAMETERS", msg.Params[0], client.t("Invalid parameters"))
} else if err != nil {
rb.Add(nil, server.name, "FAIL", "CHATHISTORY", "MESSAGE_ERROR", msg.Params[0], client.t("Messages could not be retrieved"))
} else if sequence == nil {
rb.Add(nil, server.name, "FAIL", "CHATHISTORY", "NO_SUCH_CHANNEL", utils.SafeErrorParam(msg.Params[1]), client.t("No such channel"))
}
}()
target := msg.Params[0]
channel = server.channels.Get(target)
if channel != nil && channel.hasClient(client) {
// "If [...] the user does not have permission to view the requested content, [...]
// NO_SUCH_CHANNEL SHOULD be returned"
hist = &channel.history
} else {
targetClient := server.clients.Get(target)
if targetClient != nil {
myAccount := client.Account()
targetAccount := targetClient.Account()
if myAccount != "" && targetAccount != "" && myAccount == targetAccount {
hist = &targetClient.history
}
}
}
if hist == nil {
config := server.Config()
maxChathistoryLimit := config.History.ChathistoryMax
if maxChathistoryLimit == 0 {
return
}
preposition := strings.ToLower(msg.Params[1])
parseQueryParam := func(param string) (msgid string, timestamp time.Time, err error) {
err = errInvalidParams
err = utils.ErrInvalidParams
pieces := strings.SplitN(param, "=", 2)
if len(pieces) < 2 {
return
}
identifier, value := strings.ToLower(pieces[0]), pieces[1]
if identifier == "id" {
if identifier == "msgid" {
msgid, err = value, nil
return
} else if identifier == "timestamp" {
@ -598,10 +569,6 @@ func chathistoryHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
return
}
maxChathistoryLimit := config.History.ChathistoryMax
if maxChathistoryLimit == 0 {
return
}
parseHistoryLimit := func(paramIndex int) (limit int) {
if len(msg.Params) < (paramIndex + 1) {
return maxChathistoryLimit
@ -613,140 +580,74 @@ func chathistoryHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
return
}
// TODO: as currently implemented, almost all of thes queries are worst-case O(n)
// in the number of stored history entries. Every one of them can be made O(1)
// if necessary, without too much difficulty. Some ideas:
// * Ensure that the ring buffer is sorted by time, enabling binary search for times
// * Maintain a map from msgid to position in the ring buffer
if preposition == "between" {
if len(msg.Params) >= 5 {
startMsgid, startTimestamp, startErr := parseQueryParam(msg.Params[2])
endMsgid, endTimestamp, endErr := parseQueryParam(msg.Params[3])
ascending := msg.Params[4] == "+"
limit := parseHistoryLimit(5)
if startErr != nil || endErr != nil {
success = false
} else if startMsgid != "" && endMsgid != "" {
inInterval := false
matches := func(item history.Item) (result bool) {
result = inInterval
if item.HasMsgid(startMsgid) {
if ascending {
inInterval = true
} else {
inInterval = false
return false // interval is exclusive
}
} else if item.HasMsgid(endMsgid) {
if ascending {
inInterval = false
return false
} else {
inInterval = true
}
}
return
}
items = hist.Match(matches, ascending, limit)
success = true
} else if !startTimestamp.IsZero() && !endTimestamp.IsZero() {
items, _ = hist.Between(startTimestamp, endTimestamp, ascending, limit)
if !ascending {
history.Reverse(items)
}
success = true
}
// else: mismatched params, success = false, fail
}
preposition := strings.ToLower(msg.Params[0])
target = msg.Params[1]
channel, sequence, err = server.GetHistorySequence(nil, client, target)
if err != nil || sequence == nil {
return
}
// before, after, latest, around
queryParam := msg.Params[2]
msgid, timestamp, err := parseQueryParam(queryParam)
limit := parseHistoryLimit(3)
before := false
switch preposition {
case "before":
before = true
fallthrough
case "after":
var matches history.Predicate
if err != nil {
break
} else if msgid != "" {
inInterval := false
matches = func(item history.Item) (result bool) {
result = inInterval
if item.HasMsgid(msgid) {
inInterval = true
}
return
}
} else {
matches = func(item history.Item) bool {
return before == item.Message.Time.Before(timestamp)
}
}
items = hist.Match(matches, !before, limit)
success = true
case "latest":
if queryParam == "*" {
items = hist.Latest(limit)
} else if err != nil {
break
} else {
var matches history.Predicate
if msgid != "" {
shouldStop := false
matches = func(item history.Item) bool {
if shouldStop {
return false
}
shouldStop = item.HasMsgid(msgid)
return !shouldStop
}
} else {
matches = func(item history.Item) bool {
return item.Message.Time.After(timestamp)
}
}
items = hist.Match(matches, false, limit)
}
success = true
case "around":
if err != nil {
break
}
var initialMatcher history.Predicate
if msgid != "" {
inInterval := false
initialMatcher = func(item history.Item) (result bool) {
if inInterval {
return true
} else {
inInterval = item.HasMsgid(msgid)
return inInterval
}
}
} else {
initialMatcher = func(item history.Item) (result bool) {
return item.Message.Time.Before(timestamp)
}
}
var halfLimit int
halfLimit = (limit + 1) / 2
firstPass := hist.Match(initialMatcher, false, halfLimit)
if len(firstPass) > 0 {
timeWindowStart := firstPass[0].Message.Time
items = hist.Match(func(item history.Item) bool {
return item.Message.Time.Equal(timeWindowStart) || item.Message.Time.After(timeWindowStart)
}, true, limit)
}
success = true
roundUp := func(endpoint time.Time) (result time.Time) {
return endpoint.Truncate(time.Millisecond).Add(time.Millisecond)
}
var start, end history.Selector
var limit int
switch preposition {
case "between":
start.Msgid, start.Time, err = parseQueryParam(msg.Params[2])
if err != nil {
return
}
end.Msgid, end.Time, err = parseQueryParam(msg.Params[3])
if err != nil {
return
}
// XXX preserve the ordering of the two parameters, since we might be going backwards,
// but round up the chronologically first one, whichever it is, to make it exclusive
if !start.Time.IsZero() && !end.Time.IsZero() {
if start.Time.Before(end.Time) {
start.Time = roundUp(start.Time)
} else {
end.Time = roundUp(end.Time)
}
}
limit = parseHistoryLimit(4)
case "before", "after", "around":
start.Msgid, start.Time, err = parseQueryParam(msg.Params[2])
if err != nil {
return
}
if preposition == "after" && !start.Time.IsZero() {
start.Time = roundUp(start.Time)
}
if preposition == "before" {
end = start
start = history.Selector{}
}
limit = parseHistoryLimit(3)
case "latest":
if msg.Params[2] != "*" {
end.Msgid, end.Time, err = parseQueryParam(msg.Params[2])
if err != nil {
return
}
if !end.Time.IsZero() {
end.Time = roundUp(end.Time)
}
start.Time = time.Now().UTC()
}
limit = parseHistoryLimit(3)
default:
unknown_command = true
return
}
if preposition == "around" {
items, err = sequence.Around(start, limit)
} else {
items, _, err = sequence.Between(start, end, limit)
}
return
}
@ -1026,6 +927,7 @@ Get an explanation of <argument>, or "index" for a list of help topics.`), rb)
// HISTORY <target> [<limit>]
// e.g., HISTORY #ubuntu 10
// HISTORY me 15
// HISTORY #darwin 1h
func historyHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
config := server.Config()
if !config.History.Enabled {
@ -1034,53 +936,55 @@ func historyHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R
}
target := msg.Params[0]
var hist *history.Buffer
channel := server.channels.Get(target)
if channel != nil && channel.hasClient(client) {
hist = &channel.history
} else {
if strings.ToLower(target) == "me" {
hist = &client.history
} else {
targetClient := server.clients.Get(target)
if targetClient != nil {
myAccount, targetAccount := client.Account(), targetClient.Account()
if myAccount != "" && targetAccount != "" && myAccount == targetAccount {
hist = &targetClient.history
}
if strings.ToLower(target) == "me" {
target = "*"
}
channel, sequence, err := server.GetHistorySequence(nil, client, target)
if sequence == nil || err != nil {
// whatever
rb.Add(nil, server.name, ERR_NOSUCHCHANNEL, client.Nick(), utils.SafeErrorParam(target), client.t("No such channel"))
return false
}
var duration time.Duration
maxChathistoryLimit := config.History.ChathistoryMax
limit := 100
if maxChathistoryLimit < limit {
limit = maxChathistoryLimit
}
if len(msg.Params) > 1 {
providedLimit, err := strconv.Atoi(msg.Params[1])
if err == nil && providedLimit != 0 {
limit = providedLimit
if maxChathistoryLimit < limit {
limit = maxChathistoryLimit
}
} else if err != nil {
duration, err = time.ParseDuration(msg.Params[1])
if err == nil {
limit = maxChathistoryLimit
}
}
}
if hist == nil {
if channel == nil {
rb.Add(nil, server.name, ERR_NOSUCHCHANNEL, client.Nick(), utils.SafeErrorParam(target), client.t("No such channel"))
} else {
rb.Add(nil, server.name, ERR_NOTONCHANNEL, client.Nick(), target, client.t("You're not on that channel"))
}
return false
}
limit := 10
maxChathistoryLimit := config.History.ChathistoryMax
if len(msg.Params) > 1 {
providedLimit, err := strconv.Atoi(msg.Params[1])
if providedLimit > maxChathistoryLimit {
providedLimit = maxChathistoryLimit
}
if err == nil && providedLimit != 0 {
limit = providedLimit
}
}
items := hist.Latest(limit)
if channel != nil {
channel.replayHistoryItems(rb, items, false)
var items []history.Item
if duration == 0 {
items, _, err = sequence.Between(history.Selector{}, history.Selector{}, limit)
} else {
client.replayPrivmsgHistory(rb, items, true)
now := time.Now().UTC()
start := history.Selector{Time: now}
end := history.Selector{Time: now.Add(-duration)}
items, _, err = sequence.Between(start, end, limit)
}
if err == nil && len(items) != 0 {
if channel != nil {
channel.replayHistoryItems(rb, items, false)
} else {
client.replayPrivmsgHistory(rb, items, "", true)
}
}
return false
}
@ -1964,7 +1868,7 @@ func messageHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R
return false
}
if client.isTor && utils.IsRestrictedCTCPMessage(message) {
if rb.session.isTor && utils.IsRestrictedCTCPMessage(message) {
// note that error replies are never sent for NOTICE
if histType != history.Notice {
rb.Notice(client.t("CTCP messages are disabled over Tor"))
@ -2021,46 +1925,34 @@ func dispatchMessageToTarget(client *Client, tags map[string]string, histType hi
}
return
}
tnick := user.Nick()
tDetails := user.Details()
tnick := tDetails.nick
nickMaskString := client.NickMaskString()
accountName := client.AccountName()
details := client.Details()
nickMaskString := details.nickMask
accountName := details.accountName
var deliverySessions []*Session
// restrict messages appropriately when +R is set
// intentionally make the sending user think the message went through fine
allowedPlusR := !user.HasMode(modes.RegisteredOnly) || client.LoggedIntoAccount()
allowedTor := !user.isTor || !message.IsRestrictedCTCPMessage()
if allowedPlusR && allowedTor {
for _, session := range user.Sessions() {
hasTagsCap := session.capabilities.Has(caps.MessageTags)
// don't send TAGMSG at all if they don't have the tags cap
if histType == history.Tagmsg && hasTagsCap {
session.sendFromClientInternal(false, message.Time, message.Msgid, nickMaskString, accountName, tags, command, tnick)
} else if histType != history.Tagmsg {
tagsToSend := tags
if !hasTagsCap {
tagsToSend = nil
}
session.sendSplitMsgFromClientInternal(false, nickMaskString, accountName, tagsToSend, command, tnick, message)
allowedPlusR := details.account != "" || !user.HasMode(modes.RegisteredOnly)
if allowedPlusR {
deliverySessions = append(deliverySessions, user.Sessions()...)
}
// all sessions of the sender, except the originating session, get a copy as well:
if client != user {
for _, session := range client.Sessions() {
if session != rb.session {
deliverySessions = append(deliverySessions, session)
}
}
}
// an echo-message may need to be included in the response:
if rb.session.capabilities.Has(caps.EchoMessage) {
if histType == history.Tagmsg && rb.session.capabilities.Has(caps.MessageTags) {
rb.AddFromClient(message.Time, message.Msgid, nickMaskString, accountName, tags, command, tnick)
} else {
rb.AddSplitMessageFromClient(nickMaskString, accountName, tags, command, tnick, message)
}
}
// an echo-message may need to go out to other client sessions:
for _, session := range client.Sessions() {
if session == rb.session {
continue
}
for _, session := range deliverySessions {
hasTagsCap := session.capabilities.Has(caps.MessageTags)
// don't send TAGMSG at all if they don't have the tags cap
if histType == history.Tagmsg && hasTagsCap {
session.sendFromClientInternal(false, message.Time, message.Msgid, nickMaskString, accountName, tags, command, tnick)
} else if histType != history.Tagmsg {
} else if histType != history.Tagmsg && !(session.isTor && message.IsRestrictedCTCPMessage()) {
tagsToSend := tags
if !hasTagsCap {
tagsToSend = nil
@ -2068,22 +1960,56 @@ func dispatchMessageToTarget(client *Client, tags map[string]string, histType hi
session.sendSplitMsgFromClientInternal(false, nickMaskString, accountName, tagsToSend, command, tnick, message)
}
}
// the originating session may get an echo message:
if rb.session.capabilities.Has(caps.EchoMessage) {
hasTagsCap := rb.session.capabilities.Has(caps.MessageTags)
if histType == history.Tagmsg && hasTagsCap {
rb.AddFromClient(message.Time, message.Msgid, nickMaskString, accountName, tags, command, tnick)
} else {
tagsToSend := tags
if !hasTagsCap {
tagsToSend = nil
}
rb.AddSplitMessageFromClient(nickMaskString, accountName, tagsToSend, command, tnick, message)
}
}
if histType != history.Notice && user.Away() {
//TODO(dan): possibly implement cooldown of away notifications to users
rb.Add(nil, server.name, RPL_AWAY, client.Nick(), tnick, user.AwayMessage())
}
config := server.Config()
if !config.History.Enabled {
return
}
item := history.Item{
Type: histType,
Message: message,
Nick: nickMaskString,
AccountName: accountName,
Tags: tags,
}
if !item.IsStorable() {
return
}
targetedItem := item
targetedItem.Params[0] = tnick
cPersistent, cEphemeral, _ := client.historyStatus(config)
tPersistent, tEphemeral, _ := user.historyStatus(config)
// add to ephemeral history
if cEphemeral {
targetedItem.CfCorrespondent = tDetails.nickCasefolded
client.history.Add(targetedItem)
}
if tEphemeral && client != user {
item.CfCorrespondent = details.nickCasefolded
user.history.Add(item)
}
if cPersistent || tPersistent {
item.CfCorrespondent = ""
server.historyDB.AddDirectMessage(details.nickCasefolded, user.NickCasefolded(), cPersistent, tPersistent, targetedItem)
}
// add to the target's history:
user.history.Add(item)
// add this to the client's history as well, recording the target:
item.Params[0] = tnick
client.history.Add(item)
}
}
@ -2136,7 +2062,7 @@ func operHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
oper := server.GetOperator(msg.Params[0])
if oper != nil {
if oper.Fingerprint != "" {
if oper.Fingerprint == client.certfp {
if oper.Fingerprint == rb.session.certfp {
checkPassed = true
} else {
checkFailed = true
@ -2239,7 +2165,7 @@ func passHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
// check the provided password
password := []byte(msg.Params[0])
client.sentPassCommand = bcrypt.CompareHashAndPassword(serverPassword, password) == nil
rb.session.sentPassCommand = bcrypt.CompareHashAndPassword(serverPassword, password) == nil
// if they failed the check, we'll bounce them later when they try to complete registration
return false
@ -2409,11 +2335,7 @@ func sceneHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res
// SETNAME <realname>
func setnameHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
realname := msg.Params[0]
client.stateMutex.Lock()
client.realname = realname
client.stateMutex.Unlock()
client.SetRealname(realname)
details := client.Details()
// alert friends
@ -2622,7 +2544,7 @@ func webircHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
if 0 < len(info.Password) && bcrypt.CompareHashAndPassword(info.Password, givenPassword) != nil {
continue
}
if info.Fingerprint != "" && info.Fingerprint != client.certfp {
if info.Fingerprint != "" && info.Fingerprint != rb.session.certfp {
continue
}

View File

@ -145,9 +145,9 @@ http://ircv3.net/specs/core/capability-negotiation-3.2.html`,
"chathistory": {
text: `CHATHISTORY [params]
CHATHISTORY is an experimental history replay command. See these documents:
https://github.com/MuffinMedic/ircv3-specifications/blob/chathistory/extensions/chathistory.md
https://gist.github.com/DanielOaks/c104ad6e8759c01eb5c826d627caf80d`,
CHATHISTORY is a history replay command associated with the IRCv3
specification draft/chathistory. See this document:
https://github.com/ircv3/ircv3-specifications/pull/393`,
},
"debug": {
oper: true,
@ -213,8 +213,9 @@ Get an explanation of <argument>, or "index" for a list of help topics.`,
Replay message history. <target> can be a channel name, "me" to replay direct
message history, or a nickname to replay another client's direct message
history (they must be logged into the same account as you). At most [limit]
messages will be replayed.`,
history (they must be logged into the same account as you). [limit] can be
either an integer (the maximum number of messages to replay), or a time
duration like 10m or 1h (the time window within which to replay messages).`,
},
"info": {
text: `INFO

View File

@ -6,7 +6,6 @@ package history
import (
"github.com/oragono/oragono/irc/utils"
"sync"
"sync/atomic"
"time"
)
@ -46,6 +45,10 @@ type Item struct {
Message utils.SplitMessage
Tags map[string]string
Params [1]string
// for a DM, this is the casefolded nickname of the other party (whether this is
// an incoming or outgoing message). this lets us emulate the "query buffer" functionality
// required by CHATHISTORY:
CfCorrespondent string
}
// HasMsgid tests whether a message has the message id `msgid`.
@ -53,20 +56,30 @@ func (item *Item) HasMsgid(msgid string) bool {
return item.Message.Msgid == msgid
}
func (item *Item) isStorable() bool {
if item.Type == Tagmsg {
func (item *Item) IsStorable() bool {
switch item.Type {
case Tagmsg:
for name := range item.Tags {
if !transientTags[name] {
return true
}
}
return false // all tags were blacklisted
} else {
case Privmsg, Notice:
// don't store CTCP other than ACTION
return !item.Message.IsRestrictedCTCPMessage()
default:
return true
}
}
type Predicate func(item Item) (matches bool)
type Predicate func(item *Item) (matches bool)
func Reverse(results []Item) {
for i, j := 0, len(results)-1; i < j; i, j = i+1, j-1 {
results[i], results[j] = results[j], results[i]
}
}
// Buffer is a ring buffer holding message/event history for a channel or user
type Buffer struct {
@ -81,8 +94,6 @@ type Buffer struct {
lastDiscarded time.Time
enabled uint32
nowFunc func() time.Time
}
@ -99,8 +110,6 @@ func (hist *Buffer) Initialize(size int, window time.Duration) {
hist.window = window
hist.maximumSize = size
hist.nowFunc = time.Now
hist.setEnabled(size)
}
// compute the initial size for the buffer, taking into account autoresize
@ -115,31 +124,8 @@ func (hist *Buffer) initialSize(size int, window time.Duration) (result int) {
return
}
func (hist *Buffer) setEnabled(size int) {
var enabled uint32
if size != 0 {
enabled = 1
}
atomic.StoreUint32(&hist.enabled, enabled)
}
// Enabled returns whether the buffer is currently storing messages
// (a disabled buffer blackholes everything it sees)
func (list *Buffer) Enabled() bool {
return atomic.LoadUint32(&list.enabled) != 0
}
// Add adds a history item to the buffer
func (list *Buffer) Add(item Item) {
// fast path without a lock acquisition for when we are not storing history
if !list.Enabled() {
return
}
if !item.isStorable() {
return
}
if item.Message.Time.IsZero() {
item.Message.Time = time.Now().UTC()
}
@ -147,6 +133,10 @@ func (list *Buffer) Add(item Item) {
list.Lock()
defer list.Unlock()
if len(list.buffer) == 0 {
return
}
list.maybeExpand()
var pos int
@ -170,55 +160,100 @@ func (list *Buffer) Add(item Item) {
list.buffer[pos] = item
}
// Reverse reverses an []Item, in-place.
func Reverse(results []Item) {
for i, j := 0, len(results)-1; i < j; i, j = i+1, j-1 {
results[i], results[j] = results[j], results[i]
func (list *Buffer) lookup(msgid string) (result Item, found bool) {
predicate := func(item *Item) bool {
return item.HasMsgid(msgid)
}
results := list.matchInternal(predicate, false, 1)
if len(results) != 0 {
return results[0], true
}
return
}
// Between returns all history items with a time `after` <= time <= `before`,
// with an indication of whether the results are complete or are missing items
// because some of that period was discarded. A zero value of `before` is considered
// higher than all other times.
func (list *Buffer) Between(after, before time.Time, ascending bool, limit int) (results []Item, complete bool) {
if !list.Enabled() {
return
}
func (list *Buffer) betweenHelper(start, end Selector, cutoff time.Time, pred Predicate, limit int) (results []Item, complete bool, err error) {
var ascending bool
defer func() {
if !ascending {
Reverse(results)
}
}()
list.RLock()
defer list.RUnlock()
if len(list.buffer) == 0 {
return
}
after := start.Time
if start.Msgid != "" {
item, found := list.lookup(start.Msgid)
if !found {
return
}
after = item.Message.Time
}
before := end.Time
if end.Msgid != "" {
item, found := list.lookup(end.Msgid)
if !found {
return
}
before = item.Message.Time
}
after, before, ascending = MinMaxAsc(after, before, cutoff)
complete = after.Equal(list.lastDiscarded) || after.After(list.lastDiscarded)
satisfies := func(item Item) bool {
return (after.IsZero() || item.Message.Time.After(after)) && (before.IsZero() || item.Message.Time.Before(before))
satisfies := func(item *Item) bool {
return (after.IsZero() || item.Message.Time.After(after)) &&
(before.IsZero() || item.Message.Time.Before(before)) &&
(pred == nil || pred(item))
}
return list.matchInternal(satisfies, ascending, limit), complete
return list.matchInternal(satisfies, ascending, limit), complete, nil
}
// Match returns all history items such that `predicate` returns true for them.
// Items are considered in reverse insertion order if `ascending` is false, or
// in insertion order if `ascending` is true, up to a total of `limit` matches
// if `limit` > 0 (unlimited otherwise).
// `predicate` MAY be a closure that maintains its own state across invocations;
// it MUST NOT acquire any locks or otherwise do anything weird.
// Results are always returned in insertion order.
func (list *Buffer) Match(predicate Predicate, ascending bool, limit int) (results []Item) {
if !list.Enabled() {
return
// implements history.Sequence, emulating a single history buffer (for a channel,
// a single user's DMs, or a DM conversation)
type bufferSequence struct {
list *Buffer
pred Predicate
cutoff time.Time
}
func (list *Buffer) MakeSequence(correspondent string, cutoff time.Time) Sequence {
var pred Predicate
if correspondent != "" {
pred = func(item *Item) bool {
return item.CfCorrespondent == correspondent
}
}
return &bufferSequence{
list: list,
pred: pred,
cutoff: cutoff,
}
}
list.RLock()
defer list.RUnlock()
func (seq *bufferSequence) Between(start, end Selector, limit int) (results []Item, complete bool, err error) {
return seq.list.betweenHelper(start, end, seq.cutoff, seq.pred, limit)
}
return list.matchInternal(predicate, ascending, limit)
func (seq *bufferSequence) Around(start Selector, limit int) (results []Item, err error) {
return GenericAround(seq, start, limit)
}
// you must be holding the read lock to call this
func (list *Buffer) matchInternal(predicate Predicate, ascending bool, limit int) (results []Item) {
if list.start == -1 {
if list.start == -1 || len(list.buffer) == 0 {
return
}
@ -232,7 +267,7 @@ func (list *Buffer) matchInternal(predicate Predicate, ascending bool, limit int
}
for {
if predicate(list.buffer[pos]) {
if predicate(&list.buffer[pos]) {
results = append(results, list.buffer[pos])
}
if pos == stop || (limit != 0 && len(results) == limit) {
@ -245,18 +280,14 @@ func (list *Buffer) matchInternal(predicate Predicate, ascending bool, limit int
}
}
// TODO sort by time instead?
if !ascending {
Reverse(results)
}
return
}
// Latest returns the items most recently added, up to `limit`. If `limit` is 0,
// latest returns the items most recently added, up to `limit`. If `limit` is 0,
// it returns all items.
func (list *Buffer) Latest(limit int) (results []Item) {
matchAll := func(item Item) bool { return true }
return list.Match(matchAll, false, limit)
func (list *Buffer) latest(limit int) (results []Item) {
results, _, _ = list.betweenHelper(Selector{}, Selector{}, time.Time{}, nil, limit)
return
}
// LastDiscarded returns the latest time of any entry that was evicted
@ -355,8 +386,6 @@ func (list *Buffer) Resize(maximumSize int, window time.Duration) {
func (list *Buffer) resize(size int) {
newbuffer := make([]Item, size)
list.setEnabled(size)
if list.start == -1 {
// indices are already correct and nothing needs to be copied
} else if size == 0 {

View File

@ -14,19 +14,21 @@ const (
timeFormat = "2006-01-02 15:04:05Z"
)
func betweenTimestamps(buf *Buffer, start, end time.Time, limit int) (result []Item, complete bool) {
result, complete, _ = buf.betweenHelper(Selector{Time: start}, Selector{Time: end}, time.Time{}, nil, limit)
return
}
func TestEmptyBuffer(t *testing.T) {
pastTime := easyParse(timeFormat)
buf := NewHistoryBuffer(0, 0)
if buf.Enabled() {
t.Error("the buffer of size 0 must be considered disabled")
}
buf.Add(Item{
Nick: "testnick",
})
since, complete := buf.Between(pastTime, time.Now(), false, 0)
since, complete := betweenTimestamps(buf, pastTime, time.Now(), 0)
if len(since) != 0 {
t.Error("shouldn't be able to add to disabled buf")
}
@ -35,16 +37,13 @@ func TestEmptyBuffer(t *testing.T) {
}
buf.Resize(1, 0)
if !buf.Enabled() {
t.Error("the buffer of size 1 must be considered enabled")
}
since, complete = buf.Between(pastTime, time.Now(), false, 0)
since, complete = betweenTimestamps(buf, pastTime, time.Now(), 0)
assertEqual(complete, true, t)
assertEqual(len(since), 0, t)
buf.Add(Item{
Nick: "testnick",
})
since, complete = buf.Between(pastTime, time.Now(), false, 0)
since, complete = betweenTimestamps(buf, pastTime, time.Now(), 0)
if len(since) != 1 {
t.Error("should be able to store items in a nonempty buffer")
}
@ -58,7 +57,7 @@ func TestEmptyBuffer(t *testing.T) {
buf.Add(Item{
Nick: "testnick2",
})
since, complete = buf.Between(pastTime, time.Now(), false, 0)
since, complete = betweenTimestamps(buf, pastTime, time.Now(), 0)
if len(since) != 1 {
t.Error("expect exactly 1 item")
}
@ -68,8 +67,7 @@ func TestEmptyBuffer(t *testing.T) {
if since[0].Nick != "testnick2" {
t.Error("retrieved junk data")
}
matchAll := func(item Item) bool { return true }
assertEqual(toNicks(buf.Match(matchAll, false, 0)), []string{"testnick2"}, t)
assertEqual(toNicks(buf.latest(0)), []string{"testnick2"}, t)
}
func toNicks(items []Item) (result []string) {
@ -110,27 +108,27 @@ func TestBuffer(t *testing.T) {
buf.Add(easyItem("testnick2", "2006-01-03 15:04:05Z"))
since, complete := buf.Between(start, time.Now(), false, 0)
since, complete := betweenTimestamps(buf, start, time.Now(), 0)
assertEqual(complete, true, t)
assertEqual(toNicks(since), []string{"testnick0", "testnick1", "testnick2"}, t)
// add another item, evicting the first
buf.Add(easyItem("testnick3", "2006-01-04 15:04:05Z"))
since, complete = buf.Between(start, time.Now(), false, 0)
since, complete = betweenTimestamps(buf, start, time.Now(), 0)
assertEqual(complete, false, t)
assertEqual(toNicks(since), []string{"testnick1", "testnick2", "testnick3"}, t)
// now exclude the time of the discarded entry; results should be complete again
since, complete = buf.Between(easyParse("2006-01-02 00:00:00Z"), time.Now(), false, 0)
since, complete = betweenTimestamps(buf, easyParse("2006-01-02 00:00:00Z"), time.Now(), 0)
assertEqual(complete, true, t)
assertEqual(toNicks(since), []string{"testnick1", "testnick2", "testnick3"}, t)
since, complete = buf.Between(easyParse("2006-01-02 00:00:00Z"), easyParse("2006-01-03 00:00:00Z"), false, 0)
since, complete = betweenTimestamps(buf, easyParse("2006-01-02 00:00:00Z"), easyParse("2006-01-03 00:00:00Z"), 0)
assertEqual(complete, true, t)
assertEqual(toNicks(since), []string{"testnick1"}, t)
// shrink the buffer, cutting off testnick1
buf.Resize(2, 0)
since, complete = buf.Between(easyParse("2006-01-02 00:00:00Z"), time.Now(), false, 0)
since, complete = betweenTimestamps(buf, easyParse("2006-01-02 00:00:00Z"), time.Now(), 0)
assertEqual(complete, false, t)
assertEqual(toNicks(since), []string{"testnick2", "testnick3"}, t)
@ -138,18 +136,19 @@ func TestBuffer(t *testing.T) {
buf.Add(easyItem("testnick4", "2006-01-05 15:04:05Z"))
buf.Add(easyItem("testnick5", "2006-01-06 15:04:05Z"))
buf.Add(easyItem("testnick6", "2006-01-07 15:04:05Z"))
since, complete = buf.Between(easyParse("2006-01-03 00:00:00Z"), time.Now(), false, 0)
since, complete = betweenTimestamps(buf, easyParse("2006-01-03 00:00:00Z"), time.Now(), 0)
assertEqual(complete, true, t)
assertEqual(toNicks(since), []string{"testnick2", "testnick3", "testnick4", "testnick5", "testnick6"}, t)
// test ascending order
since, _ = buf.Between(easyParse("2006-01-03 00:00:00Z"), time.Now(), true, 2)
since, _ = betweenTimestamps(buf, easyParse("2006-01-03 00:00:00Z"), time.Time{}, 2)
assertEqual(toNicks(since), []string{"testnick2", "testnick3"}, t)
}
func autoItem(id int, t time.Time) (result Item) {
result.Message.Time = t
result.Nick = strconv.Itoa(id)
result.Message.Msgid = result.Nick
return
}
@ -181,7 +180,7 @@ func TestAutoresize(t *testing.T) {
now = now.Add(time.Minute * 10)
id += 1
}
items := buf.Latest(0)
items := buf.latest(0)
assertEqual(len(items), initialAutoSize, t)
assertEqual(atoi(items[0].Nick), 40, t)
assertEqual(atoi(items[len(items)-1].Nick), 71, t)
@ -195,7 +194,7 @@ func TestAutoresize(t *testing.T) {
// ok, 5 items from the first batch are still in the 1-hour window;
// we should overwrite until only those 5 are left, then start expanding
// the buffer so that it retains those 5 and the 100 new items
items = buf.Latest(0)
items = buf.latest(0)
assertEqual(len(items), 105, t)
assertEqual(atoi(items[0].Nick), 67, t)
assertEqual(atoi(items[len(items)-1].Nick), 171, t)
@ -207,7 +206,7 @@ func TestAutoresize(t *testing.T) {
id += 1
}
// should fill up to the maximum size of 128 and start overwriting
items = buf.Latest(0)
items = buf.latest(0)
assertEqual(len(items), 128, t)
assertEqual(atoi(items[0].Nick), 144, t)
assertEqual(atoi(items[len(items)-1].Nick), 271, t)
@ -222,7 +221,7 @@ func TestEnabledByResize(t *testing.T) {
buf.Resize(128, time.Hour)
// add an item and test that it is stored and retrievable
buf.Add(autoItem(0, now))
items := buf.Latest(0)
items := buf.latest(0)
assertEqual(len(items), 1, t)
assertEqual(atoi(items[0].Nick), 0, t)
}
@ -232,13 +231,13 @@ func TestDisabledByResize(t *testing.T) {
// enabled autoresizing buffer
buf := NewHistoryBuffer(128, time.Hour)
buf.Add(autoItem(0, now))
items := buf.Latest(0)
items := buf.latest(0)
assertEqual(len(items), 1, t)
assertEqual(atoi(items[0].Nick), 0, t)
// disable as during a rehash, confirm that nothing can be retrieved
buf.Resize(0, time.Hour)
items = buf.Latest(0)
items = buf.latest(0)
assertEqual(len(items), 0, t)
}
@ -252,3 +251,25 @@ func TestRoundUp(t *testing.T) {
assertEqual(roundUpToPowerOfTwo(1025), 2048, t)
assertEqual(roundUpToPowerOfTwo(269435457), 536870912, t)
}
func BenchmarkInsert(b *testing.B) {
buf := NewHistoryBuffer(1024, 0)
b.ResetTimer()
for i := 0; i < b.N; i++ {
buf.Add(Item{})
}
}
func BenchmarkMatch(b *testing.B) {
buf := NewHistoryBuffer(1024, 0)
var now time.Time
for i := 0; i < 1024; i += 1 {
buf.Add(autoItem(i, now))
now = now.Add(time.Second)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
buf.lookup("512")
}
}

71
irc/history/queries.go Normal file
View File

@ -0,0 +1,71 @@
// Copyright (c) 2020 Shivaram Lingamneni <slingamn@cs.stanford.edu>
// released under the MIT license
package history
import (
"time"
)
// Selector represents a parameter to a CHATHISTORY command;
// at most one of Msgid or Time may be nonzero
type Selector struct {
Msgid string
Time time.Time
}
// Sequence is an abstract sequence of history entries that can be queried;
// it encapsulates restrictions such as registration time cutoffs, or
// only looking at a single "query buffer" (DMs with a particular correspondent)
type Sequence interface {
Between(start, end Selector, limit int) (results []Item, complete bool, err error)
Around(start Selector, limit int) (results []Item, err error)
}
// This is a bad, slow implementation of CHATHISTORY AROUND using the BETWEEN semantics
func GenericAround(seq Sequence, start Selector, limit int) (results []Item, err error) {
var halfLimit int
halfLimit = (limit + 1) / 2
initialResults, _, err := seq.Between(Selector{}, start, halfLimit)
if err != nil {
return
} else if len(initialResults) == 0 {
// TODO: this fails if we're doing an AROUND on the first message in the buffer
// would be nice to fix this but whatever
return
}
newStart := Selector{Time: initialResults[0].Message.Time}
results, _, err = seq.Between(newStart, Selector{}, limit)
return
}
// MinMaxAsc converts CHATHISTORY arguments into time intervals, handling the most
// general case (BETWEEN going forwards or backwards) natively and the other ordering
// queries (AFTER, BEFORE, LATEST) as special cases.
func MinMaxAsc(after, before, cutoff time.Time) (min, max time.Time, ascending bool) {
startIsZero, endIsZero := after.IsZero(), before.IsZero()
if !startIsZero && endIsZero {
// AFTER
ascending = true
} else if startIsZero && !endIsZero {
// BEFORE
ascending = false
} else if !startIsZero && !endIsZero {
if before.Before(after) {
// BETWEEN going backwards
before, after = after, before
ascending = false
} else {
// BETWEEN going forwards
ascending = true
}
} else if startIsZero && endIsZero {
// LATEST
ascending = false
}
if after.IsZero() || after.Before(cutoff) {
// this may result in an impossible query, which is fine
after = cutoff
}
return after, before, ascending
}

View File

@ -7,6 +7,7 @@ import (
"errors"
"fmt"
"regexp"
"time"
"github.com/oragono/oragono/irc/sno"
)
@ -214,7 +215,7 @@ func hsRequestHandler(server *Server, client *Client, command string, params []s
}
accountName := client.Account()
_, err := server.accounts.VHostRequest(accountName, vhost, server.Config().Accounts.VHosts.UserRequests.Cooldown)
_, err := server.accounts.VHostRequest(accountName, vhost, time.Duration(server.Config().Accounts.VHosts.UserRequests.Cooldown))
if err != nil {
if throttled, ok := err.(*vhostThrottleExceeded); ok {
hsNotice(rb, fmt.Sprintf(client.t("You must wait an additional %v before making another request"), throttled.timeRemaining))
@ -411,7 +412,7 @@ func hsTakeHandler(server *Server, client *Client, command string, params []stri
}
account := client.Account()
_, err := server.accounts.VHostTake(account, vhost, config.Accounts.VHosts.UserRequests.Cooldown)
_, err := server.accounts.VHostTake(account, vhost, time.Duration(config.Accounts.VHosts.UserRequests.Cooldown))
if err != nil {
if throttled, ok := err.(*vhostThrottleExceeded); ok {
hsNotice(rb, fmt.Sprintf(client.t("You must wait an additional %v before taking a vhost"), throttled.timeRemaining))

View File

@ -52,6 +52,7 @@ type IdleTimer struct {
quitTimeout time.Duration
state TimerState
timer *time.Timer
lastTouch time.Time
}
// Initialize sets up an IdleTimer and starts counting idle time;
@ -61,9 +62,11 @@ func (it *IdleTimer) Initialize(session *Session) {
it.registerTimeout = RegisterTimeout
it.idleTimeout, it.quitTimeout = it.recomputeDurations()
registered := session.client.Registered()
now := time.Now().UTC()
it.Lock()
defer it.Unlock()
it.lastTouch = now
if registered {
it.state = TimerActive
} else {
@ -82,7 +85,7 @@ func (it *IdleTimer) recomputeDurations() (idleTimeout, quitTimeout time.Duratio
}
idleTimeout = DefaultIdleTimeout
if it.session.client.isTor {
if it.session.isTor {
idleTimeout = TorIdleTimeout
}
@ -92,10 +95,12 @@ func (it *IdleTimer) recomputeDurations() (idleTimeout, quitTimeout time.Duratio
func (it *IdleTimer) Touch() {
idleTimeout, quitTimeout := it.recomputeDurations()
now := time.Now().UTC()
it.Lock()
defer it.Unlock()
it.idleTimeout, it.quitTimeout = idleTimeout, quitTimeout
it.lastTouch = now
// a touch transitions TimerUnregistered or TimerIdle into TimerActive
if it.state != TimerDead {
it.state = TimerActive
@ -103,6 +108,13 @@ func (it *IdleTimer) Touch() {
}
}
func (it *IdleTimer) LastTouch() (result time.Time) {
it.Lock()
result = it.lastTouch
it.Unlock()
return
}
func (it *IdleTimer) processTimeout() {
idleTimeout, quitTimeout := it.recomputeDurations()
@ -322,9 +334,6 @@ const (
// BrbDead is the state of a client after its timeout has expired; it will be removed
// and therefore new sessions cannot be attached to it
BrbDead
// BrbSticky allows a client to remain online without sessions, with no timeout.
// This is not used yet.
BrbSticky
)
type BrbTimer struct {
@ -345,16 +354,16 @@ func (bt *BrbTimer) Initialize(client *Client) {
// attempts to enable BRB for a client, returns whether it succeeded
func (bt *BrbTimer) Enable() (success bool, duration time.Duration) {
if !bt.client.Registered() || bt.client.ResumeID() == "" {
return
}
// TODO make this configurable
duration = ResumeableTotalTimeout
bt.client.stateMutex.Lock()
defer bt.client.stateMutex.Unlock()
if !bt.client.registered || bt.client.alwaysOn || bt.client.resumeID == "" {
return
}
switch bt.state {
case BrbDisabled, BrbEnabled:
bt.state = BrbEnabled
@ -366,8 +375,6 @@ func (bt *BrbTimer) Enable() (success bool, duration time.Duration) {
bt.brbAt = time.Now().UTC()
}
success = true
case BrbSticky:
success = true
default:
// BrbDead
success = false
@ -416,6 +423,10 @@ func (bt *BrbTimer) processTimeout() {
bt.client.stateMutex.Lock()
defer bt.client.stateMutex.Unlock()
if bt.client.alwaysOn {
return
}
switch bt.state {
case BrbDisabled, BrbEnabled:
if len(bt.client.sessions) == 0 {
@ -432,16 +443,3 @@ func (bt *BrbTimer) processTimeout() {
}
bt.resetTimeout()
}
// sets a client to be "sticky", i.e., indefinitely exempt from removal for
// lack of sessions
func (bt *BrbTimer) SetSticky() (success bool) {
bt.client.stateMutex.Lock()
defer bt.client.stateMutex.Unlock()
if bt.state != BrbDead {
success = true
bt.state = BrbSticky
}
bt.resetTimeout()
return
}

View File

@ -9,9 +9,9 @@ import (
)
func TestZncTimestampParser(t *testing.T) {
assertEqual(zncWireTimeToTime("1558338348.988"), time.Unix(1558338348, 988000000), t)
assertEqual(zncWireTimeToTime("1558338348.9"), time.Unix(1558338348, 900000000), t)
assertEqual(zncWireTimeToTime("1558338348"), time.Unix(1558338348, 0), t)
assertEqual(zncWireTimeToTime(".988"), time.Unix(0, 988000000), t)
assertEqual(zncWireTimeToTime("garbage"), time.Unix(0, 0), t)
assertEqual(zncWireTimeToTime("1558338348.988"), time.Unix(1558338348, 988000000).UTC(), t)
assertEqual(zncWireTimeToTime("1558338348.9"), time.Unix(1558338348, 900000000).UTC(), t)
assertEqual(zncWireTimeToTime("1558338348"), time.Unix(1558338348, 0).UTC(), t)
assertEqual(zncWireTimeToTime(".988"), time.Unix(0, 988000000).UTC(), t)
assertEqual(zncWireTimeToTime("garbage"), time.Unix(0, 0).UTC(), t)
}

22
irc/mysql/config.go Normal file
View File

@ -0,0 +1,22 @@
// Copyright (c) 2020 Shivaram Lingamneni
// released under the MIT license
package mysql
import (
"time"
)
type Config struct {
// these are intended to be written directly into the config file:
Enabled bool
Host string
Port int
User string
Password string
HistoryDatabase string `yaml:"history-database"`
Timeout time.Duration
// XXX these are copied from elsewhere in the config:
ExpireTime time.Duration
}

557
irc/mysql/history.go Normal file
View File

@ -0,0 +1,557 @@
// Copyright (c) 2020 Shivaram Lingamneni
// released under the MIT license
package mysql
import (
"bytes"
"context"
"database/sql"
"fmt"
"runtime/debug"
"sync"
"sync/atomic"
"time"
_ "github.com/go-sql-driver/mysql"
"github.com/oragono/oragono/irc/history"
"github.com/oragono/oragono/irc/logger"
"github.com/oragono/oragono/irc/utils"
)
const (
// maximum length in bytes of any message target (nickname or channel name) in its
// canonicalized (i.e., casefolded) state:
MaxTargetLength = 64
// latest schema of the db
latestDbSchema = "1"
keySchemaVersion = "db.version"
cleanupRowLimit = 50
cleanupPauseTime = 10 * time.Minute
)
type MySQL struct {
timeout int64
db *sql.DB
logger *logger.Manager
insertHistory *sql.Stmt
insertSequence *sql.Stmt
insertConversation *sql.Stmt
stateMutex sync.Mutex
config Config
}
func (mysql *MySQL) Initialize(logger *logger.Manager, config Config) {
mysql.logger = logger
mysql.SetConfig(config)
}
func (mysql *MySQL) SetConfig(config Config) {
atomic.StoreInt64(&mysql.timeout, int64(config.Timeout))
mysql.stateMutex.Lock()
mysql.config = config
mysql.stateMutex.Unlock()
}
func (mysql *MySQL) getExpireTime() (expireTime time.Duration) {
mysql.stateMutex.Lock()
expireTime = mysql.config.ExpireTime
mysql.stateMutex.Unlock()
return
}
func (m *MySQL) Open() (err error) {
var address string
if m.config.Port != 0 {
address = fmt.Sprintf("tcp(%s:%d)", m.config.Host, m.config.Port)
}
m.db, err = sql.Open("mysql", fmt.Sprintf("%s:%s@%s/%s", m.config.User, m.config.Password, address, m.config.HistoryDatabase))
if err != nil {
return err
}
err = m.fixSchemas()
if err != nil {
return err
}
err = m.prepareStatements()
if err != nil {
return err
}
go m.cleanupLoop()
return nil
}
func (mysql *MySQL) fixSchemas() (err error) {
_, err = mysql.db.Exec(`CREATE TABLE IF NOT EXISTS metadata (
key_name VARCHAR(32) primary key,
value VARCHAR(32) NOT NULL
) CHARSET=ascii COLLATE=ascii_bin;`)
if err != nil {
return err
}
var schema string
err = mysql.db.QueryRow(`select value from metadata where key_name = ?;`, keySchemaVersion).Scan(&schema)
if err == sql.ErrNoRows {
err = mysql.createTables()
if err != nil {
return
}
_, err = mysql.db.Exec(`insert into metadata (key_name, value) values (?, ?);`, keySchemaVersion, latestDbSchema)
if err != nil {
return
}
} else if err == nil && schema != latestDbSchema {
// TODO figure out what to do about schema changes
return &utils.IncompatibleSchemaError{CurrentVersion: schema, RequiredVersion: latestDbSchema}
} else {
return err
}
return nil
}
func (mysql *MySQL) createTables() (err error) {
_, err = mysql.db.Exec(`CREATE TABLE history (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
data BLOB NOT NULL,
msgid BINARY(16) NOT NULL,
KEY (msgid(4))
) CHARSET=ascii COLLATE=ascii_bin;`)
if err != nil {
return err
}
_, err = mysql.db.Exec(fmt.Sprintf(`CREATE TABLE sequence (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
target VARBINARY(%[1]d) NOT NULL,
nanotime BIGINT UNSIGNED NOT NULL,
history_id BIGINT NOT NULL,
KEY (target, nanotime),
KEY (history_id)
) CHARSET=ascii COLLATE=ascii_bin;`, MaxTargetLength))
if err != nil {
return err
}
_, err = mysql.db.Exec(fmt.Sprintf(`CREATE TABLE conversations (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
lower_target VARBINARY(%[1]d) NOT NULL,
upper_target VARBINARY(%[1]d) NOT NULL,
nanotime BIGINT UNSIGNED NOT NULL,
history_id BIGINT NOT NULL,
KEY (lower_target, upper_target, nanotime),
KEY (history_id)
) CHARSET=ascii COLLATE=ascii_bin;`, MaxTargetLength))
if err != nil {
return err
}
return nil
}
func (mysql *MySQL) cleanupLoop() {
defer func() {
if r := recover(); r != nil {
mysql.logger.Error("mysql",
fmt.Sprintf("Panic in cleanup routine: %v\n%s", r, debug.Stack()))
time.Sleep(cleanupPauseTime)
go mysql.cleanupLoop()
}
}()
for {
expireTime := mysql.getExpireTime()
if expireTime != 0 {
for {
startTime := time.Now()
rowsDeleted, err := mysql.doCleanup(expireTime)
elapsed := time.Now().Sub(startTime)
mysql.logError("error during row cleanup", err)
// keep going as long as we're accomplishing significant work
// (don't busy-wait on small numbers of rows expiring):
if rowsDeleted < (cleanupRowLimit / 10) {
break
}
// crude backpressure mechanism: if the database is slow,
// give it time to process other queries
time.Sleep(elapsed)
}
}
time.Sleep(cleanupPauseTime)
}
}
func (mysql *MySQL) doCleanup(age time.Duration) (count int, err error) {
ids, maxNanotime, err := mysql.selectCleanupIDs(age)
if len(ids) == 0 {
mysql.logger.Debug("mysql", "found no rows to clean up")
return
}
mysql.logger.Debug("mysql", fmt.Sprintf("deleting %d history rows, max age %s", len(ids), utils.NanoToTimestamp(maxNanotime)))
// can't use ? binding for a variable number of arguments, build the IN clause manually
var inBuf bytes.Buffer
inBuf.WriteByte('(')
for i, id := range ids {
if i != 0 {
inBuf.WriteRune(',')
}
fmt.Fprintf(&inBuf, "%d", id)
}
inBuf.WriteRune(')')
_, err = mysql.db.Exec(fmt.Sprintf(`DELETE FROM conversations WHERE history_id in %s;`, inBuf.Bytes()))
if err != nil {
return
}
_, err = mysql.db.Exec(fmt.Sprintf(`DELETE FROM sequence WHERE history_id in %s;`, inBuf.Bytes()))
if err != nil {
return
}
_, err = mysql.db.Exec(fmt.Sprintf(`DELETE FROM history WHERE id in %s;`, inBuf.Bytes()))
if err != nil {
return
}
count = len(ids)
return
}
func (mysql *MySQL) selectCleanupIDs(age time.Duration) (ids []uint64, maxNanotime int64, err error) {
rows, err := mysql.db.Query(`
SELECT history.id, sequence.nanotime
FROM history
LEFT JOIN sequence ON history.id = sequence.history_id
ORDER BY history.id LIMIT ?;`, cleanupRowLimit)
if err != nil {
return
}
defer rows.Close()
// a history ID may have 0-2 rows in sequence: 1 for a channel entry,
// 2 for a DM, 0 if the data is inconsistent. therefore, deduplicate
// and delete anything that doesn't have a sequence entry:
idset := make(map[uint64]struct{}, cleanupRowLimit)
threshold := time.Now().Add(-age).UnixNano()
for rows.Next() {
var id uint64
var nanotime sql.NullInt64
err = rows.Scan(&id, &nanotime)
if err != nil {
return
}
if !nanotime.Valid || nanotime.Int64 < threshold {
idset[id] = struct{}{}
if nanotime.Valid && nanotime.Int64 > maxNanotime {
maxNanotime = nanotime.Int64
}
}
}
ids = make([]uint64, len(idset))
i := 0
for id := range idset {
ids[i] = id
i++
}
return
}
func (mysql *MySQL) prepareStatements() (err error) {
mysql.insertHistory, err = mysql.db.Prepare(`INSERT INTO history
(data, msgid) VALUES (?, ?);`)
if err != nil {
return
}
mysql.insertSequence, err = mysql.db.Prepare(`INSERT INTO sequence
(target, nanotime, history_id) VALUES (?, ?, ?);`)
if err != nil {
return
}
mysql.insertConversation, err = mysql.db.Prepare(`INSERT INTO conversations
(lower_target, upper_target, nanotime, history_id) VALUES (?, ?, ?, ?);`)
if err != nil {
return
}
return
}
func (mysql *MySQL) getTimeout() time.Duration {
return time.Duration(atomic.LoadInt64(&mysql.timeout))
}
func (mysql *MySQL) logError(context string, err error) (quit bool) {
if err != nil {
mysql.logger.Error("mysql", context, err.Error())
return true
}
return false
}
func (mysql *MySQL) AddChannelItem(target string, item history.Item) (err error) {
if mysql.db == nil {
return
}
if target == "" {
return utils.ErrInvalidParams
}
ctx, cancel := context.WithTimeout(context.Background(), mysql.getTimeout())
defer cancel()
id, err := mysql.insertBase(ctx, item)
if err != nil {
return
}
err = mysql.insertSequenceEntry(ctx, target, item.Message.Time, id)
return
}
func (mysql *MySQL) insertSequenceEntry(ctx context.Context, target string, messageTime time.Time, id int64) (err error) {
_, err = mysql.insertSequence.ExecContext(ctx, target, messageTime.UnixNano(), id)
mysql.logError("could not insert sequence entry", err)
return
}
func (mysql *MySQL) insertConversationEntry(ctx context.Context, sender, recipient string, messageTime time.Time, id int64) (err error) {
lower, higher := stringMinMax(sender, recipient)
_, err = mysql.insertConversation.ExecContext(ctx, lower, higher, messageTime.UnixNano(), id)
mysql.logError("could not insert conversations entry", err)
return
}
func (mysql *MySQL) insertBase(ctx context.Context, item history.Item) (id int64, err error) {
value, err := marshalItem(&item)
if mysql.logError("could not marshal item", err) {
return
}
msgidBytes, err := decodeMsgid(item.Message.Msgid)
if mysql.logError("could not decode msgid", err) {
return
}
result, err := mysql.insertHistory.ExecContext(ctx, value, msgidBytes)
if mysql.logError("could not insert item", err) {
return
}
id, err = result.LastInsertId()
if mysql.logError("could not insert item", err) {
return
}
return
}
func stringMinMax(first, second string) (min, max string) {
if first < second {
return first, second
} else {
return second, first
}
}
func (mysql *MySQL) AddDirectMessage(sender, recipient string, senderPersistent, recipientPersistent bool, item history.Item) (err error) {
if mysql.db == nil {
return
}
if !(senderPersistent || recipientPersistent) {
return
}
if sender == "" || recipient == "" {
return utils.ErrInvalidParams
}
ctx, cancel := context.WithTimeout(context.Background(), mysql.getTimeout())
defer cancel()
id, err := mysql.insertBase(ctx, item)
if err != nil {
return
}
if senderPersistent {
mysql.insertSequenceEntry(ctx, sender, item.Message.Time, id)
if err != nil {
return
}
}
if recipientPersistent && sender != recipient {
err = mysql.insertSequenceEntry(ctx, recipient, item.Message.Time, id)
if err != nil {
return
}
}
err = mysql.insertConversationEntry(ctx, sender, recipient, item.Message.Time, id)
return
}
func (mysql *MySQL) msgidToTime(ctx context.Context, msgid string) (result time.Time, err error) {
// in theory, we could optimize out a roundtrip to the database by using a subquery instead:
// sequence.nanotime > (
// SELECT sequence.nanotime FROM sequence, history
// WHERE sequence.history_id = history.id AND history.msgid = ?
// LIMIT 1)
// however, this doesn't handle the BETWEEN case with one or two msgids, where we
// don't initially know whether the interval is going forwards or backwards. to simplify
// the logic, resolve msgids to timestamps "manually" in all cases, using a separate query.
decoded, err := decodeMsgid(msgid)
if err != nil {
return
}
row := mysql.db.QueryRowContext(ctx, `
SELECT sequence.nanotime FROM sequence
INNER JOIN history ON history.id = sequence.history_id
WHERE history.msgid = ? LIMIT 1;`, decoded)
var nanotime int64
err = row.Scan(&nanotime)
if mysql.logError("could not resolve msgid to time", err) {
return
}
result = time.Unix(0, nanotime).UTC()
return
}
func (mysql *MySQL) selectItems(ctx context.Context, query string, args ...interface{}) (results []history.Item, err error) {
rows, err := mysql.db.QueryContext(ctx, query, args...)
if mysql.logError("could not select history items", err) {
return
}
defer rows.Close()
for rows.Next() {
var blob []byte
var item history.Item
err = rows.Scan(&blob)
if mysql.logError("could not scan history item", err) {
return
}
err = unmarshalItem(blob, &item)
if mysql.logError("could not unmarshal history item", err) {
return
}
results = append(results, item)
}
return
}
func (mysql *MySQL) betweenTimestamps(ctx context.Context, sender, recipient string, after, before, cutoff time.Time, limit int) (results []history.Item, err error) {
useSequence := true
var lowerTarget, upperTarget string
if sender != "" {
lowerTarget, upperTarget = stringMinMax(sender, recipient)
useSequence = false
}
table := "sequence"
if !useSequence {
table = "conversations"
}
after, before, ascending := history.MinMaxAsc(after, before, cutoff)
direction := "ASC"
if !ascending {
direction = "DESC"
}
var queryBuf bytes.Buffer
args := make([]interface{}, 0, 6)
fmt.Fprintf(&queryBuf,
"SELECT history.data from history INNER JOIN %[1]s ON history.id = %[1]s.history_id WHERE", table)
if useSequence {
fmt.Fprintf(&queryBuf, " sequence.target = ?")
args = append(args, recipient)
} else {
fmt.Fprintf(&queryBuf, " conversations.lower_target = ? AND conversations.upper_target = ?")
args = append(args, lowerTarget)
args = append(args, upperTarget)
}
if !after.IsZero() {
fmt.Fprintf(&queryBuf, " AND %s.nanotime > ?", table)
args = append(args, after.UnixNano())
}
if !before.IsZero() {
fmt.Fprintf(&queryBuf, " AND %s.nanotime < ?", table)
args = append(args, before.UnixNano())
}
fmt.Fprintf(&queryBuf, " ORDER BY %[1]s.nanotime %[2]s LIMIT ?;", table, direction)
args = append(args, limit)
results, err = mysql.selectItems(ctx, queryBuf.String(), args...)
if err == nil && !ascending {
history.Reverse(results)
}
return
}
func (mysql *MySQL) Close() {
// closing the database will close our prepared statements as well
if mysql.db != nil {
mysql.db.Close()
}
mysql.db = nil
}
// implements history.Sequence, emulating a single history buffer (for a channel,
// a single user's DMs, or a DM conversation)
type mySQLHistorySequence struct {
mysql *MySQL
sender string
recipient string
cutoff time.Time
}
func (s *mySQLHistorySequence) Between(start, end history.Selector, limit int) (results []history.Item, complete bool, err error) {
ctx, cancel := context.WithTimeout(context.Background(), s.mysql.getTimeout())
defer cancel()
startTime := start.Time
if start.Msgid != "" {
startTime, err = s.mysql.msgidToTime(ctx, start.Msgid)
if err != nil {
return nil, false, err
}
}
endTime := end.Time
if end.Msgid != "" {
endTime, err = s.mysql.msgidToTime(ctx, end.Msgid)
if err != nil {
return nil, false, err
}
}
results, err = s.mysql.betweenTimestamps(ctx, s.sender, s.recipient, startTime, endTime, s.cutoff, limit)
return results, (err == nil), err
}
func (s *mySQLHistorySequence) Around(start history.Selector, limit int) (results []history.Item, err error) {
return history.GenericAround(s, start, limit)
}
func (mysql *MySQL) MakeSequence(sender, recipient string, cutoff time.Time) history.Sequence {
return &mySQLHistorySequence{
sender: sender,
recipient: recipient,
mysql: mysql,
cutoff: cutoff,
}
}

View File

@ -0,0 +1,23 @@
package mysql
import (
"encoding/json"
"github.com/oragono/oragono/irc/history"
"github.com/oragono/oragono/irc/utils"
)
// 123 / '{' is the magic number that means JSON;
// if we want to do a binary encoding later, we just have to add different magic version numbers
func marshalItem(item *history.Item) (result []byte, err error) {
return json.Marshal(item)
}
func unmarshalItem(data []byte, result *history.Item) (err error) {
return json.Unmarshal(data, result)
}
func decodeMsgid(msgid string) ([]byte, error) {
return utils.B32Encoder.DecodeString(msgid)
}

View File

@ -43,13 +43,15 @@ func performNickChange(server *Server, client *Client, target *Client, session *
hadNick := target.HasNick()
origNickMask := target.NickMaskString()
details := target.Details()
err := client.server.clients.SetNick(target, session, nickname)
assignedNickname, err := client.server.clients.SetNick(target, session, nickname)
if err == errNicknameInUse {
rb.Add(nil, server.name, ERR_NICKNAMEINUSE, currentNick, nickname, client.t("Nickname is already in use"))
} else if err == errNicknameReserved {
rb.Add(nil, server.name, ERR_NICKNAMEINUSE, currentNick, nickname, client.t("Nickname is reserved by a different account"))
} else if err == errNicknameInvalid {
rb.Add(nil, server.name, ERR_ERRONEUSNICKNAME, currentNick, utils.SafeErrorParam(nickname), client.t("Erroneous nickname"))
} else if err == errCantChangeNick {
rb.Add(nil, server.name, ERR_NICKNAMEINUSE, currentNick, utils.SafeErrorParam(nickname), client.t(err.Error()))
} else if err != nil {
rb.Add(nil, server.name, ERR_UNKNOWNERROR, currentNick, "NICK", fmt.Sprintf(client.t("Could not set or change nickname: %s"), err.Error()))
}
@ -64,26 +66,26 @@ func performNickChange(server *Server, client *Client, target *Client, session *
AccountName: details.accountName,
Message: message,
}
histItem.Params[0] = nickname
histItem.Params[0] = assignedNickname
client.server.logger.Debug("nick", fmt.Sprintf("%s changed nickname to %s [%s]", origNickMask, nickname, client.NickCasefolded()))
client.server.logger.Debug("nick", fmt.Sprintf("%s changed nickname to %s [%s]", origNickMask, assignedNickname, client.NickCasefolded()))
if hadNick {
if client == target {
target.server.snomasks.Send(sno.LocalNicks, fmt.Sprintf(ircfmt.Unescape("$%s$r changed nickname to %s"), details.nick, nickname))
target.server.snomasks.Send(sno.LocalNicks, fmt.Sprintf(ircfmt.Unescape("$%s$r changed nickname to %s"), details.nick, assignedNickname))
} else {
target.server.snomasks.Send(sno.LocalNicks, fmt.Sprintf(ircfmt.Unescape("Operator %s changed nickname of $%s$r to %s"), client.Nick(), details.nick, nickname))
target.server.snomasks.Send(sno.LocalNicks, fmt.Sprintf(ircfmt.Unescape("Operator %s changed nickname of $%s$r to %s"), client.Nick(), details.nick, assignedNickname))
}
target.server.whoWas.Append(details.WhoWas)
rb.AddFromClient(message.Time, message.Msgid, origNickMask, details.accountName, nil, "NICK", nickname)
rb.AddFromClient(message.Time, message.Msgid, origNickMask, details.accountName, nil, "NICK", assignedNickname)
for session := range target.Friends() {
if session != rb.session {
session.sendFromClientInternal(false, message.Time, message.Msgid, origNickMask, details.accountName, nil, "NICK", nickname)
session.sendFromClientInternal(false, message.Time, message.Msgid, origNickMask, details.accountName, nil, "NICK", assignedNickname)
}
}
}
for _, channel := range client.Channels() {
channel.history.Add(histItem)
channel.AddHistoryItem(histItem)
}
if target.Registered() {

View File

@ -31,7 +31,7 @@ func servCmdRequiresNickRes(config *Config) bool {
}
func servCmdRequiresBouncerEnabled(config *Config) bool {
return config.Accounts.Bouncer.Enabled
return config.Accounts.Multiclient.Enabled
}
const (
@ -147,7 +147,7 @@ an administrator can set use this command to set up user accounts.`,
help: `Syntax: $bSESSIONS [nickname]$b
SESSIONS lists information about the sessions currently attached, via
the server's bouncer functionality, to your nickname. An administrator
the server's multiclient functionality, to your nickname. An administrator
can use this command to list another user's sessions.`,
helpShort: `$bSESSIONS$b lists the sessions attached to a nickname.`,
enabled: servCmdRequiresBouncerEnabled,
@ -217,7 +217,7 @@ information on the settings and their possible values, see HELP SET.`,
helpStrings: []string{
`Syntax $bSET <setting> <value>$b
Set modifies your account settings. The following settings are available:`,
SET modifies your account settings. The following settings are available:`,
`$bENFORCE$b
'enforce' lets you specify a custom enforcement mechanism for your registered
@ -228,8 +228,8 @@ nicknames. Your options are:
3. 'strict' [you must already be authenticated to use the nick]
4. 'default' [use the server default]`,
`$bBOUNCER$b
If 'bouncer' is enabled and you are already logged in and using a nick, a
`$bMULTICLIENT$b
If 'multiclient' is enabled and you are already logged in and using a nick, a
second client of yours that authenticates with SASL and requests the same nick
is allowed to attach to the nick as well (this is comparable to the behavior
of IRC "bouncers" like ZNC). Your options are 'on' (allow this behavior),
@ -247,6 +247,22 @@ lines for join and part. This provides more information about the context of
messages, but may be spammy. Your options are 'always', 'never', and the default
of 'commands-only' (the messages will be replayed in /HISTORY output, but not
during autoreplay).`,
`$bALWAYS-ON$b
'always-on' controls whether your nickname/identity will remain active
even while you are disconnected from the server. Your options are 'true',
'false', and 'default' (use the server default value).`,
`$bAUTOREPLAY-MISSED$b
'autoreplay-missed' is only effective for always-on clients. If enabled,
if you have at most one active session, the server will remember the time
you disconnect and then replay missed messages to you when you reconnect.
Your options are 'on' and 'off'.`,
`$bDM-HISTORY$b
'dm-history' is only effective for always-on clients. It lets you control
how the history of your direct messages is stored. Your options are:
1. 'off' [no history]
2. 'ephemeral' [a limited amount of temporary history, not stored on disk]
3. 'on' [history stored in a permanent database, if available]
4. 'default' [use the server default]`,
},
authRequired: true,
enabled: servCmdRequiresAccreg,
@ -332,23 +348,48 @@ func displaySetting(settingName string, settings AccountSettings, client *Client
case ReplayJoinsNever:
nsNotice(rb, client.t("You will not see JOINs and PARTs in /HISTORY output or in autoreplay"))
}
case "bouncer":
if !config.Accounts.Bouncer.Enabled {
case "multiclient":
if !config.Accounts.Multiclient.Enabled {
nsNotice(rb, client.t("This feature has been disabled by the server administrators"))
} else {
switch settings.AllowBouncer {
case BouncerAllowedServerDefault:
if config.Accounts.Bouncer.AllowedByDefault {
nsNotice(rb, client.t("Bouncer functionality is currently enabled for your account, but you can opt out"))
case MulticlientAllowedServerDefault:
if config.Accounts.Multiclient.AllowedByDefault {
nsNotice(rb, client.t("Multiclient functionality is currently enabled for your account, but you can opt out"))
} else {
nsNotice(rb, client.t("Bouncer functionality is currently disabled for your account, but you can opt in"))
nsNotice(rb, client.t("Multiclient functionality is currently disabled for your account, but you can opt in"))
}
case BouncerDisallowedByUser:
nsNotice(rb, client.t("Bouncer functionality is currently disabled for your account"))
case BouncerAllowedByUser:
nsNotice(rb, client.t("Bouncer functionality is currently enabled for your account"))
case MulticlientDisallowedByUser:
nsNotice(rb, client.t("Multiclient functionality is currently disabled for your account"))
case MulticlientAllowedByUser:
nsNotice(rb, client.t("Multiclient functionality is currently enabled for your account"))
}
}
case "always-on":
stored := settings.AlwaysOn
actual := client.AlwaysOn()
nsNotice(rb, fmt.Sprintf(client.t("Your stored always-on setting is: %s"), persistentStatusToString(stored)))
if actual {
nsNotice(rb, client.t("Given current server settings, your client is always-on"))
} else {
nsNotice(rb, client.t("Given current server settings, your client is not always-on"))
}
case "autoreplay-missed":
stored := settings.AutoreplayMissed
if stored {
if client.AlwaysOn() {
nsNotice(rb, client.t("Autoreplay of missed messages is enabled"))
} else {
nsNotice(rb, client.t("You have enabled autoreplay of missed messages, but you can't receive them because your client isn't set to always-on"))
}
} else {
nsNotice(rb, client.t("Your account is not configured to receive autoreplayed missed messages"))
}
case "dm-history":
effectiveValue := historyEnabled(config.History.Persistent.DirectMessages, settings.DMHistory)
csNotice(rb, fmt.Sprintf(client.t("Your stored direct message history setting is: %s"), historyStatusToString(settings.DMHistory)))
csNotice(rb, fmt.Sprintf(client.t("Given current server settings, your direct message history setting is: %s"), historyStatusToString(effectiveValue)))
default:
nsNotice(rb, client.t("No such setting"))
}
@ -399,17 +440,17 @@ func nsSetHandler(server *Server, client *Client, command string, params []strin
out.AutoreplayLines = newValue
return
}
case "bouncer":
var newValue BouncerAllowedSetting
case "multiclient":
var newValue MulticlientAllowedSetting
if strings.ToLower(params[1]) == "default" {
newValue = BouncerAllowedServerDefault
newValue = MulticlientAllowedServerDefault
} else {
var enabled bool
enabled, err = utils.StringToBool(params[1])
if enabled {
newValue = BouncerAllowedByUser
newValue = MulticlientAllowedByUser
} else {
newValue = BouncerDisallowedByUser
newValue = MulticlientDisallowedByUser
}
}
if err == nil {
@ -429,6 +470,37 @@ func nsSetHandler(server *Server, client *Client, command string, params []strin
return
}
}
case "always-on":
var newValue PersistentStatus
newValue, err = persistentStatusFromString(params[1])
// "opt-in" and "opt-out" don't make sense as user preferences
if err == nil && newValue != PersistentOptIn && newValue != PersistentOptOut {
munger = func(in AccountSettings) (out AccountSettings, err error) {
out = in
out.AlwaysOn = newValue
return
}
}
case "autoreplay-missed":
var newValue bool
newValue, err = utils.StringToBool(params[1])
if err == nil {
munger = func(in AccountSettings) (out AccountSettings, err error) {
out = in
out.AutoreplayMissed = newValue
return
}
}
case "dm-history":
var newValue HistoryStatus
newValue, err = historyStatusFromString(params[1])
if err == nil {
munger = func(in AccountSettings) (out AccountSettings, err error) {
out = in
out.DMHistory = newValue
return
}
}
default:
err = errInvalidParams
}
@ -480,6 +552,9 @@ func nsGhostHandler(server *Server, client *Client, command string, params []str
} else if ghost == client {
nsNotice(rb, client.t("You can't GHOST yourself (try /QUIT instead)"))
return
} else if ghost.AlwaysOn() {
nsNotice(rb, client.t("You can't GHOST an always-on client"))
return
}
authorized := false
@ -530,7 +605,7 @@ func nsIdentifyHandler(server *Server, client *Client, command string, params []
var username, passphrase string
if len(params) == 1 {
if client.certfp != "" {
if rb.session.certfp != "" {
username = params[0]
} else {
// XXX undocumented compatibility mode with other nickservs, allowing
@ -553,8 +628,8 @@ func nsIdentifyHandler(server *Server, client *Client, command string, params []
}
// try certfp
if !loginSuccessful && client.certfp != "" {
err := server.accounts.AuthenticateByCertFP(client, "")
if !loginSuccessful && rb.session.certfp != "" {
err := server.accounts.AuthenticateByCertFP(client, rb.session.certfp, "")
loginSuccessful = (err == nil)
}
@ -618,7 +693,7 @@ func nsRegisterHandler(server *Server, client *Client, command string, params []
email = params[1]
}
certfp := client.certfp
certfp := rb.session.certfp
if passphrase == "*" {
if certfp == "" {
nsNotice(rb, client.t("You must be connected with TLS and a client certificate to do this"))
@ -658,7 +733,7 @@ func nsRegisterHandler(server *Server, client *Client, command string, params []
}
}
err := server.accounts.Register(client, account, callbackNamespace, callbackValue, passphrase, client.certfp)
err := server.accounts.Register(client, account, callbackNamespace, callbackValue, passphrase, rb.session.certfp)
if err == nil {
if callbackNamespace == "*" {
err = server.accounts.Verify(client, account, "")
@ -876,6 +951,9 @@ func nsSessionsHandler(server *Server, client *Client, command string, params []
nsNotice(rb, fmt.Sprintf(client.t("Hostname: %s"), session.hostname))
nsNotice(rb, fmt.Sprintf(client.t("Created at: %s"), session.ctime.Format(time.RFC1123)))
nsNotice(rb, fmt.Sprintf(client.t("Last active: %s"), session.atime.Format(time.RFC1123)))
if session.certfp != "" {
nsNotice(rb, fmt.Sprintf(client.t("Certfp: %s"), session.certfp))
}
}
}

View File

@ -121,7 +121,7 @@ func (rb *ResponseBuffer) AddSplitMessageFromClient(fromNickMask string, fromAcc
if message.Is512() {
rb.AddFromClient(message.Time, message.Msgid, fromNickMask, fromAccount, tags, command, target, message.Message)
} else {
if message.IsMultiline() && rb.session.capabilities.Has(caps.Multiline) {
if rb.session.capabilities.Has(caps.Multiline) {
batch := rb.session.composeMultilineBatch(fromNickMask, fromAccount, tags, command, target, message)
rb.setNestedBatchTag(&batch[0])
rb.setNestedBatchTag(&batch[len(batch)-1])
@ -292,5 +292,5 @@ func (rb *ResponseBuffer) flushInternal(final bool, blocking bool) error {
// Notice sends the client the given notice from the server.
func (rb *ResponseBuffer) Notice(text string) {
rb.Add(nil, rb.target.server.name, "NOTICE", rb.target.nick, text)
rb.Add(nil, rb.target.server.name, "NOTICE", rb.target.Nick(), text)
}

View File

@ -24,8 +24,10 @@ import (
"github.com/goshuirc/irc-go/ircfmt"
"github.com/oragono/oragono/irc/caps"
"github.com/oragono/oragono/irc/connection_limits"
"github.com/oragono/oragono/irc/history"
"github.com/oragono/oragono/irc/logger"
"github.com/oragono/oragono/irc/modes"
"github.com/oragono/oragono/irc/mysql"
"github.com/oragono/oragono/irc/sno"
"github.com/tidwall/buntdb"
)
@ -84,6 +86,7 @@ type Server struct {
signals chan os.Signal
snomasks SnoManager
store *buntdb.DB
historyDB mysql.MySQL
torLimiter connection_limits.TorLimiter
whoWas WhoWasList
stats Stats
@ -122,7 +125,7 @@ func NewServer(config *Config, logger *logger.Manager) (*Server, error) {
server.monitorManager.Initialize()
server.snomasks.Initialize()
if err := server.applyConfig(config, true); err != nil {
if err := server.applyConfig(config); err != nil {
return nil, err
}
@ -143,6 +146,8 @@ func (server *Server) Shutdown() {
if err := server.store.Close(); err != nil {
server.logger.Error("shutdown", fmt.Sprintln("Could not close datastore:", err))
}
server.historyDB.Close()
}
// Run starts the server.
@ -316,7 +321,7 @@ func (server *Server) tryRegister(c *Client, session *Session) (exiting bool) {
// client MUST send PASS if necessary, or authenticate with SASL if necessary,
// before completing the other registration commands
authOutcome := c.isAuthorized(server.Config())
authOutcome := c.isAuthorized(server.Config(), session)
var quitMessage string
switch authOutcome {
case authFailPass:
@ -376,7 +381,7 @@ func (server *Server) playRegistrationBurst(session *Session) {
// continue registration
d := c.Details()
server.logger.Info("localconnect", fmt.Sprintf("Client connected [%s] [u:%s] [r:%s]", d.nick, d.username, d.realname))
server.snomasks.Send(sno.LocalConnects, fmt.Sprintf("Client connected [%s] [u:%s] [h:%s] [ip:%s] [r:%s]", d.nick, d.username, c.RawHostname(), c.IPString(), d.realname))
server.snomasks.Send(sno.LocalConnects, fmt.Sprintf("Client connected [%s] [u:%s] [h:%s] [ip:%s] [r:%s]", d.nick, d.username, session.rawHostname, session.IP().String(), d.realname))
// send welcome text
//NOTE(dan): we specifically use the NICK here instead of the nickmask
@ -503,8 +508,12 @@ func (client *Client) getWhoisOf(target *Client, rb *ResponseBuffer) {
rb.Add(nil, client.server.name, RPL_WHOISBOT, cnick, tnick, ircfmt.Unescape(fmt.Sprintf(client.t("is a $bBot$b on %s"), client.server.Config().Network.Name)))
}
if target.certfp != "" && (client.HasMode(modes.Operator) || client == target) {
rb.Add(nil, client.server.name, RPL_WHOISCERTFP, cnick, tnick, fmt.Sprintf(client.t("has client certificate fingerprint %s"), target.certfp))
if client == target || client.HasMode(modes.Operator) {
for _, session := range target.Sessions() {
if session.certfp != "" {
rb.Add(nil, client.server.name, RPL_WHOISCERTFP, cnick, tnick, fmt.Sprintf(client.t("has client certificate fingerprint %s"), session.certfp))
}
}
}
rb.Add(nil, client.server.name, RPL_WHOISIDLE, cnick, tnick, strconv.FormatUint(target.IdleSeconds(), 10), strconv.FormatInt(target.SignonTime(), 10), client.t("seconds idle, signon time"))
}
@ -550,7 +559,7 @@ func (server *Server) rehash() error {
return fmt.Errorf("Error loading config file config: %s", err.Error())
}
err = server.applyConfig(config, false)
err = server.applyConfig(config)
if err != nil {
return fmt.Errorf("Error applying config changes: %s", err.Error())
}
@ -558,7 +567,10 @@ func (server *Server) rehash() error {
return nil
}
func (server *Server) applyConfig(config *Config, initial bool) (err error) {
func (server *Server) applyConfig(config *Config) (err error) {
oldConfig := server.Config()
initial := oldConfig == nil
if initial {
server.configFilename = config.Filename
server.name = config.Server.Name
@ -568,7 +580,7 @@ func (server *Server) applyConfig(config *Config, initial bool) (err error) {
// enforce configs that can't be changed after launch:
if server.name != config.Server.Name {
return fmt.Errorf("Server name cannot be changed after launching the server, rehash aborted")
} else if server.Config().Datastore.Path != config.Datastore.Path {
} else if oldConfig.Datastore.Path != config.Datastore.Path {
return fmt.Errorf("Datastore path cannot be changed after launching the server, rehash aborted")
} else if globalCasemappingSetting != config.Server.Casemapping {
return fmt.Errorf("Casemapping cannot be changed after launching the server, rehash aborted")
@ -576,7 +588,6 @@ func (server *Server) applyConfig(config *Config, initial bool) (err error) {
}
server.logger.Info("server", "Using config file", server.configFilename)
oldConfig := server.Config()
// first, reload config sections for functionality implemented in subpackages:
wasLoggingRawIO := !initial && server.logger.IsLoggingRawIO()
@ -609,14 +620,13 @@ func (server *Server) applyConfig(config *Config, initial bool) (err error) {
if !oldConfig.Channels.Registration.Enabled {
server.channels.loadRegisteredChannels(config)
}
// resize history buffers as needed
if oldConfig.History != config.History {
for _, channel := range server.channels.Channels() {
channel.history.Resize(config.History.ChannelLength, config.History.AutoresizeWindow)
channel.resizeHistory(config)
}
for _, client := range server.clients.AllClients() {
client.history.Resize(config.History.ClientLength, config.History.AutoresizeWindow)
client.resizeHistory(config)
}
}
}
@ -658,6 +668,10 @@ func (server *Server) applyConfig(config *Config, initial bool) (err error) {
if err := server.loadDatastore(config); err != nil {
return err
}
} else {
if config.Datastore.MySQL.Enabled && config.Datastore.MySQL != oldConfig.Datastore.MySQL {
server.historyDB.SetConfig(config.Datastore.MySQL)
}
}
server.setupPprofListener(config)
@ -778,6 +792,15 @@ func (server *Server) loadDatastore(config *Config) error {
server.channels.Initialize(server)
server.accounts.Initialize(server)
if config.Datastore.MySQL.Enabled {
server.historyDB.Initialize(server.logger, config.Datastore.MySQL)
err = server.historyDB.Open()
if err != nil {
server.logger.Error("internal", "could not connect to mysql", err.Error())
return err
}
}
return nil
}
@ -835,6 +858,75 @@ func (server *Server) setupListeners(config *Config) (err error) {
return
}
// Gets the abstract sequence from which we're going to query history;
// we may already know the channel we're querying, or we may have
// to look it up via a string target. This function is responsible for
// privilege checking.
func (server *Server) GetHistorySequence(providedChannel *Channel, client *Client, target string) (channel *Channel, sequence history.Sequence, err error) {
config := server.Config()
var sender, recipient string
var hist *history.Buffer
if target == "*" {
persistent, ephemeral, target := client.historyStatus(config)
if persistent {
recipient = target
} else if ephemeral {
hist = &client.history
} else {
return
}
} else {
channel = providedChannel
if channel == nil {
channel = server.channels.Get(target)
}
if channel != nil {
if !channel.hasClient(client) {
err = errInsufficientPrivs
return
}
persistent, ephemeral, cfTarget := channel.historyStatus(config)
if persistent {
recipient = cfTarget
} else if ephemeral {
hist = &channel.history
} else {
return
}
} else {
sender = client.NickCasefolded()
var cfTarget string
cfTarget, err = CasefoldName(target)
if err != nil {
return
}
recipient = cfTarget
if !client.AlwaysOn() {
hist = &client.history
}
}
}
var cutoff time.Time
if config.History.Restrictions.ExpireTime != 0 {
cutoff = time.Now().UTC().Add(-time.Duration(config.History.Restrictions.ExpireTime))
}
if config.History.Restrictions.EnforceRegistrationDate {
regCutoff := client.historyCutoff()
regCutoff.Add(-time.Duration(config.History.Restrictions.GracePeriod))
// take the earlier of the two cutoffs
if regCutoff.After(cutoff) {
cutoff = regCutoff
}
}
if hist != nil {
sequence = hist.MakeSequence(recipient, cutoff)
} else if recipient != "" {
sequence = server.historyDB.MakeSequence(sender, recipient, cutoff)
}
return
}
// elistMatcher takes and matches ELIST conditions
type elistMatcher struct {
MinClientsActive bool

View File

@ -26,15 +26,33 @@ func (s *Stats) Add() {
s.mutex.Unlock()
}
// Activates a registered client, e.g., for the initial attach to a persistent client
func (s *Stats) AddRegistered(invisible, operator bool) {
s.mutex.Lock()
if invisible {
s.Invisible += 1
}
if operator {
s.Operators += 1
}
s.Total += 1
s.setMax()
s.mutex.Unlock()
}
// Transition a client from unregistered to registered
func (s *Stats) Register() {
s.mutex.Lock()
s.Unknown -= 1
s.Total += 1
s.setMax()
s.mutex.Unlock()
}
func (s *Stats) setMax() {
if s.Max < s.Total {
s.Max = s.Total
}
s.mutex.Unlock()
}
// Modify the Invisible count

View File

@ -5,7 +5,13 @@ package utils
import (
"errors"
"fmt"
"strings"
"time"
)
const (
IRCv3TimestampFormat = "2006-01-02T15:04:05.000Z"
)
var (
@ -45,9 +51,9 @@ func ArgsToStrings(maxLength int, arguments []string, delim string) []string {
func StringToBool(str string) (result bool, err error) {
switch strings.ToLower(str) {
case "on", "true", "t", "yes", "y":
case "on", "true", "t", "yes", "y", "enabled":
result = true
case "off", "false", "f", "no", "n":
case "off", "false", "f", "no", "n", "disabled":
result = false
default:
err = ErrInvalidParams
@ -63,3 +69,16 @@ func SafeErrorParam(param string) string {
}
return param
}
type IncompatibleSchemaError struct {
CurrentVersion string
RequiredVersion string
}
func (err *IncompatibleSchemaError) Error() string {
return fmt.Sprintf("Database requires update. Expected schema v%s, got v%s", err.RequiredVersion, err.CurrentVersion)
}
func NanoToTimestamp(nanotime int64) string {
return time.Unix(0, nanotime).UTC().Format(IRCv3TimestampFormat)
}

View File

@ -18,14 +18,6 @@ var (
validHostnameLabelRegexp = regexp.MustCompile(`^[0-9A-Za-z.\-]+$`)
)
// AddrIsLocal returns whether the address is from a trusted local connection (loopback or unix).
func AddrIsLocal(addr net.Addr) bool {
if tcpaddr, ok := addr.(*net.TCPAddr); ok {
return tcpaddr.IP.IsLoopback()
}
return AddrIsUnix(addr)
}
// AddrToIP returns the IP address for a net.Addr; unix domain sockets are treated as IPv4 loopback
func AddrToIP(addr net.Addr) net.IP {
if tcpaddr, ok := addr.(*net.TCPAddr); ok {

View File

@ -23,9 +23,9 @@ type MessagePair struct {
// SplitMessage represents a message that's been split for sending.
// Two possibilities:
// (a) Standard message that can be relayed on a single 512-byte line
// (MessagePair contains the message, Wrapped == nil)
// (MessagePair contains the message, Split == nil)
// (b) multiline message that was split on the client side
// (Message == "", Wrapped contains the split lines)
// (Message == "", Split contains the split lines)
type SplitMessage struct {
Message string
Msgid string
@ -36,7 +36,7 @@ type SplitMessage struct {
func MakeMessage(original string) (result SplitMessage) {
result.Message = original
result.Msgid = GenerateSecretToken()
result.Time = time.Now().UTC()
result.SetTime()
return
}
@ -52,7 +52,8 @@ func (sm *SplitMessage) Append(message string, concat bool) {
}
func (sm *SplitMessage) SetTime() {
sm.Time = time.Now().UTC()
// strip the monotonic time, it's a potential source of problems:
sm.Time = time.Now().UTC().Round(0)
}
func (sm *SplitMessage) LenLines() int {
@ -88,10 +89,6 @@ func (sm *SplitMessage) IsRestrictedCTCPMessage() bool {
return false
}
func (sm *SplitMessage) IsMultiline() bool {
return sm.Message == "" && len(sm.Split) != 0
}
func (sm *SplitMessage) Is512() bool {
return sm.Message != ""
}

View File

@ -8,6 +8,8 @@ import (
"strconv"
"strings"
"time"
"github.com/oragono/oragono/irc/history"
)
type zncCommandHandler func(client *Client, command string, params []string, rb *ResponseBuffer)
@ -43,7 +45,7 @@ func zncWireTimeToTime(str string) (result time.Time) {
}
seconds, _ := strconv.ParseInt(secondsPortion, 10, 64)
fraction, _ := strconv.ParseFloat(fracPortion, 64)
return time.Unix(seconds, int64(fraction*1000000000))
return time.Unix(seconds, int64(fraction*1000000000)).UTC()
}
type zncPlaybackTimes struct {
@ -89,10 +91,8 @@ func zncPlaybackHandler(client *Client, command string, params []string, rb *Res
// 3.3 When the client sends a subsequent redundant JOIN line for those
// channels; redundant JOIN is a complete no-op so we won't replay twice
config := client.server.Config()
if params[1] == "*" {
items, _ := client.history.Between(after, before, false, config.History.ChathistoryMax)
client.replayPrivmsgHistory(rb, items, true)
zncPlayPrivmsgs(client, rb, after, before)
} else {
targets = make(StringSet)
// TODO actually handle nickname targets
@ -116,3 +116,15 @@ func zncPlaybackHandler(client *Client, command string, params []string, rb *Res
}
}
}
func zncPlayPrivmsgs(client *Client, rb *ResponseBuffer, after, before time.Time) {
_, sequence, _ := client.server.GetHistorySequence(nil, client, "*")
if sequence == nil {
return
}
zncMax := client.server.Config().History.ZNCMax
items, _, err := sequence.Between(history.Selector{Time: after}, history.Selector{Time: before}, zncMax)
if err == nil && len(items) != 0 {
client.replayPrivmsgHistory(rb, items, "", true)
}
}

View File

@ -245,6 +245,15 @@ server:
# all users will receive simply `netname` as their cloaked hostname.
num-bits: 80
# secure-nets identifies IPs and CIDRs which are secure at layer 3,
# for example, because they are on a trusted internal LAN or a VPN.
# plaintext connections from these IPs and CIDRs will be considered
# secure (clients will receive the +Z mode and be allowed to resume
# or reattach to secure connections). note that loopback IPs are always
# considered secure:
secure-nets:
# - "10.0.0.0/8"
# account options
accounts:
@ -337,9 +346,10 @@ accounts:
# rename-prefix - this is the prefix to use when renaming clients (e.g. Guest-AB54U31)
rename-prefix: Guest-
# bouncer controls whether oragono can act as a bouncer, i.e., allowing
# multiple connections to attach to the same client/nickname identity
bouncer:
# multiclient controls whether oragono allows multiple connections to
# attach to the same client/nickname identity; this is part of the
# functionality traditionally provided by a bouncer like ZNC
multiclient:
# when disabled, each connection must use a separate nickname (as is the
# typical behavior of IRC servers). when enabled, a new connection that
# has authenticated with SASL can associate itself with an existing
@ -351,6 +361,11 @@ accounts:
# via nickserv
allowed-by-default: true
# whether to allow clients that remain on the server even
# when they have no active connections. The possible values are:
# "disabled", "opt-in", "opt-out", or "mandatory".
always-on: "disabled"
# vhosts controls the assignment of vhosts (strings displayed in place of the user's
# hostname/IP) by the HostServ service
vhosts:
@ -585,6 +600,17 @@ datastore:
# up, and if the upgrade fails, the original database will be restored.
autoupgrade: true
# connection information for MySQL (currently only used for persistent history):
mysql:
enabled: false
host: "localhost"
# port is unnecessary for connections via unix domain socket:
#port: 3306
user: "oragono"
password: "KOHw8WSaRwaoo-avo0qVpQ"
history-database: "oragono_history"
timeout: 3s
# languages config
languages:
# whether to load languages
@ -657,7 +683,7 @@ fakelag:
# message history tracking, for the RESUME extension and possibly other uses in future
history:
# should we store messages for later playback?
# the current implementation stores messages in RAM only; they do not persist
# by default, messages are stored in RAM only; they do not persist
# across server restarts. however, you should not enable this unless you understand
# how it interacts with the GDPR and/or any data privacy laws that apply
# in your country and the countries of your users.
@ -683,3 +709,41 @@ history:
# maximum number of CHATHISTORY messages that can be
# requested at once (0 disables support for CHATHISTORY)
chathistory-maxmessages: 100
# maximum number of messages that can be replayed at once during znc emulation
# (znc.in/playback, or automatic replay on initial reattach to a persistent client):
znc-maxmessages: 2048
# options to delete old messages, or prevent them from being retrieved
restrictions:
# if this is set, messages older than this cannot be retrieved by anyone
# (and will eventually be deleted from persistent storage, if that's enabled)
#expire-time: 1w
# if this is set, logged-in users cannot retrieve messages older than their
# account registration date, and logged-out users cannot retrieve messages
# older than their sign-on time (modulo grace-period, see below):
enforce-registration-date: false
# but if this is set, you can retrieve messages that are up to `grace-period`
# older than the above cutoff time. this is recommended to allow logged-out
# users to do session resumption / query history after disconnections.
grace-period: 1h
# options to store history messages in a persistent database (currently only MySQL):
persistent:
enabled: false
# store unregistered channel messages in the persistent database?
unregistered-channels: false
# for a registered channel, the channel owner can potentially customize
# the history storage setting. as the server operator, your options are
# 'disabled' (no persistent storage, regardless of per-channel setting),
# 'opt-in', 'opt-out', and 'mandatory' (force persistent storage, ignoring
# per-channel setting):
registered-channels: "opt-out"
# direct messages are only stored in the database for persistent clients;
# you can control how they are stored here (same options as above)
direct-messages: "opt-out"

9
vendor/github.com/go-sql-driver/mysql/.gitignore generated vendored Normal file
View File

@ -0,0 +1,9 @@
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
Icon?
ehthumbs.db
Thumbs.db
.idea

129
vendor/github.com/go-sql-driver/mysql/.travis.yml generated vendored Normal file
View File

@ -0,0 +1,129 @@
sudo: false
language: go
go:
- 1.10.x
- 1.11.x
- 1.12.x
- 1.13.x
- master
before_install:
- go get golang.org/x/tools/cmd/cover
- go get github.com/mattn/goveralls
before_script:
- echo -e "[server]\ninnodb_log_file_size=256MB\ninnodb_buffer_pool_size=512MB\nmax_allowed_packet=16MB" | sudo tee -a /etc/mysql/my.cnf
- sudo service mysql restart
- .travis/wait_mysql.sh
- mysql -e 'create database gotest;'
matrix:
include:
- env: DB=MYSQL8
sudo: required
dist: trusty
go: 1.10.x
services:
- docker
before_install:
- go get golang.org/x/tools/cmd/cover
- go get github.com/mattn/goveralls
- docker pull mysql:8.0
- docker run -d -p 127.0.0.1:3307:3306 --name mysqld -e MYSQL_DATABASE=gotest -e MYSQL_USER=gotest -e MYSQL_PASSWORD=secret -e MYSQL_ROOT_PASSWORD=verysecret
mysql:8.0 --innodb_log_file_size=256MB --innodb_buffer_pool_size=512MB --max_allowed_packet=16MB --local-infile=1
- cp .travis/docker.cnf ~/.my.cnf
- .travis/wait_mysql.sh
before_script:
- export MYSQL_TEST_USER=gotest
- export MYSQL_TEST_PASS=secret
- export MYSQL_TEST_ADDR=127.0.0.1:3307
- export MYSQL_TEST_CONCURRENT=1
- env: DB=MYSQL57
sudo: required
dist: trusty
go: 1.10.x
services:
- docker
before_install:
- go get golang.org/x/tools/cmd/cover
- go get github.com/mattn/goveralls
- docker pull mysql:5.7
- docker run -d -p 127.0.0.1:3307:3306 --name mysqld -e MYSQL_DATABASE=gotest -e MYSQL_USER=gotest -e MYSQL_PASSWORD=secret -e MYSQL_ROOT_PASSWORD=verysecret
mysql:5.7 --innodb_log_file_size=256MB --innodb_buffer_pool_size=512MB --max_allowed_packet=16MB --local-infile=1
- cp .travis/docker.cnf ~/.my.cnf
- .travis/wait_mysql.sh
before_script:
- export MYSQL_TEST_USER=gotest
- export MYSQL_TEST_PASS=secret
- export MYSQL_TEST_ADDR=127.0.0.1:3307
- export MYSQL_TEST_CONCURRENT=1
- env: DB=MARIA55
sudo: required
dist: trusty
go: 1.10.x
services:
- docker
before_install:
- go get golang.org/x/tools/cmd/cover
- go get github.com/mattn/goveralls
- docker pull mariadb:5.5
- docker run -d -p 127.0.0.1:3307:3306 --name mysqld -e MYSQL_DATABASE=gotest -e MYSQL_USER=gotest -e MYSQL_PASSWORD=secret -e MYSQL_ROOT_PASSWORD=verysecret
mariadb:5.5 --innodb_log_file_size=256MB --innodb_buffer_pool_size=512MB --max_allowed_packet=16MB --local-infile=1
- cp .travis/docker.cnf ~/.my.cnf
- .travis/wait_mysql.sh
before_script:
- export MYSQL_TEST_USER=gotest
- export MYSQL_TEST_PASS=secret
- export MYSQL_TEST_ADDR=127.0.0.1:3307
- export MYSQL_TEST_CONCURRENT=1
- env: DB=MARIA10_1
sudo: required
dist: trusty
go: 1.10.x
services:
- docker
before_install:
- go get golang.org/x/tools/cmd/cover
- go get github.com/mattn/goveralls
- docker pull mariadb:10.1
- docker run -d -p 127.0.0.1:3307:3306 --name mysqld -e MYSQL_DATABASE=gotest -e MYSQL_USER=gotest -e MYSQL_PASSWORD=secret -e MYSQL_ROOT_PASSWORD=verysecret
mariadb:10.1 --innodb_log_file_size=256MB --innodb_buffer_pool_size=512MB --max_allowed_packet=16MB --local-infile=1
- cp .travis/docker.cnf ~/.my.cnf
- .travis/wait_mysql.sh
before_script:
- export MYSQL_TEST_USER=gotest
- export MYSQL_TEST_PASS=secret
- export MYSQL_TEST_ADDR=127.0.0.1:3307
- export MYSQL_TEST_CONCURRENT=1
- os: osx
osx_image: xcode10.1
addons:
homebrew:
packages:
- mysql
update: true
go: 1.12.x
before_install:
- go get golang.org/x/tools/cmd/cover
- go get github.com/mattn/goveralls
before_script:
- echo -e "[server]\ninnodb_log_file_size=256MB\ninnodb_buffer_pool_size=512MB\nmax_allowed_packet=16MB\nlocal_infile=1" >> /usr/local/etc/my.cnf
- mysql.server start
- mysql -uroot -e 'CREATE USER gotest IDENTIFIED BY "secret"'
- mysql -uroot -e 'GRANT ALL ON *.* TO gotest'
- mysql -uroot -e 'create database gotest;'
- export MYSQL_TEST_USER=gotest
- export MYSQL_TEST_PASS=secret
- export MYSQL_TEST_ADDR=127.0.0.1:3306
- export MYSQL_TEST_CONCURRENT=1
script:
- go test -v -covermode=count -coverprofile=coverage.out
- go vet ./...
- .travis/gofmt.sh
after_script:
- $HOME/gopath/bin/goveralls -coverprofile=coverage.out -service=travis-ci

105
vendor/github.com/go-sql-driver/mysql/AUTHORS generated vendored Normal file
View File

@ -0,0 +1,105 @@
# This is the official list of Go-MySQL-Driver authors for copyright purposes.
# If you are submitting a patch, please add your name or the name of the
# organization which holds the copyright to this list in alphabetical order.
# Names should be added to this file as
# Name <email address>
# The email address is not required for organizations.
# Please keep the list sorted.
# Individual Persons
Aaron Hopkins <go-sql-driver at die.net>
Achille Roussel <achille.roussel at gmail.com>
Alexey Palazhchenko <alexey.palazhchenko at gmail.com>
Andrew Reid <andrew.reid at tixtrack.com>
Arne Hormann <arnehormann at gmail.com>
Asta Xie <xiemengjun at gmail.com>
Bulat Gaifullin <gaifullinbf at gmail.com>
Carlos Nieto <jose.carlos at menteslibres.net>
Chris Moos <chris at tech9computers.com>
Craig Wilson <craiggwilson at gmail.com>
Daniel Montoya <dsmontoyam at gmail.com>
Daniel Nichter <nil at codenode.com>
Daniël van Eeden <git at myname.nl>
Dave Protasowski <dprotaso at gmail.com>
DisposaBoy <disposaboy at dby.me>
Egor Smolyakov <egorsmkv at gmail.com>
Erwan Martin <hello at erwan.io>
Evan Shaw <evan at vendhq.com>
Frederick Mayle <frederickmayle at gmail.com>
Gustavo Kristic <gkristic at gmail.com>
Hajime Nakagami <nakagami at gmail.com>
Hanno Braun <mail at hannobraun.com>
Henri Yandell <flamefew at gmail.com>
Hirotaka Yamamoto <ymmt2005 at gmail.com>
Huyiguang <hyg at webterren.com>
ICHINOSE Shogo <shogo82148 at gmail.com>
Ilia Cimpoes <ichimpoesh at gmail.com>
INADA Naoki <songofacandy at gmail.com>
Jacek Szwec <szwec.jacek at gmail.com>
James Harr <james.harr at gmail.com>
Jeff Hodges <jeff at somethingsimilar.com>
Jeffrey Charles <jeffreycharles at gmail.com>
Jerome Meyer <jxmeyer at gmail.com>
Jiajia Zhong <zhong2plus at gmail.com>
Jian Zhen <zhenjl at gmail.com>
Joshua Prunier <joshua.prunier at gmail.com>
Julien Lefevre <julien.lefevr at gmail.com>
Julien Schmidt <go-sql-driver at julienschmidt.com>
Justin Li <jli at j-li.net>
Justin Nuß <nuss.justin at gmail.com>
Kamil Dziedzic <kamil at klecza.pl>
Kevin Malachowski <kevin at chowski.com>
Kieron Woodhouse <kieron.woodhouse at infosum.com>
Lennart Rudolph <lrudolph at hmc.edu>
Leonardo YongUk Kim <dalinaum at gmail.com>
Linh Tran Tuan <linhduonggnu at gmail.com>
Lion Yang <lion at aosc.xyz>
Luca Looz <luca.looz92 at gmail.com>
Lucas Liu <extrafliu at gmail.com>
Luke Scott <luke at webconnex.com>
Maciej Zimnoch <maciej.zimnoch at codilime.com>
Michael Woolnough <michael.woolnough at gmail.com>
Nathanial Murphy <nathanial.murphy at gmail.com>
Nicola Peduzzi <thenikso at gmail.com>
Olivier Mengué <dolmen at cpan.org>
oscarzhao <oscarzhaosl at gmail.com>
Paul Bonser <misterpib at gmail.com>
Peter Schultz <peter.schultz at classmarkets.com>
Rebecca Chin <rchin at pivotal.io>
Reed Allman <rdallman10 at gmail.com>
Richard Wilkes <wilkes at me.com>
Robert Russell <robert at rrbrussell.com>
Runrioter Wung <runrioter at gmail.com>
Shuode Li <elemount at qq.com>
Simon J Mudd <sjmudd at pobox.com>
Soroush Pour <me at soroushjp.com>
Stan Putrya <root.vagner at gmail.com>
Stanley Gunawan <gunawan.stanley at gmail.com>
Steven Hartland <steven.hartland at multiplay.co.uk>
Thomas Wodarek <wodarekwebpage at gmail.com>
Tim Ruffles <timruffles at gmail.com>
Tom Jenkinson <tom at tjenkinson.me>
Vladimir Kovpak <cn007b at gmail.com>
Xiangyu Hu <xiangyu.hu at outlook.com>
Xiaobing Jiang <s7v7nislands at gmail.com>
Xiuming Chen <cc at cxm.cc>
Zhenye Xie <xiezhenye at gmail.com>
# Organizations
Barracuda Networks, Inc.
Counting Ltd.
DigitalOcean Inc.
Facebook Inc.
GitHub Inc.
Google Inc.
InfoSum Ltd.
Keybase Inc.
Multiplay Ltd.
Percona LLC
Pivotal Inc.
Stripe Inc.

206
vendor/github.com/go-sql-driver/mysql/CHANGELOG.md generated vendored Normal file
View File

@ -0,0 +1,206 @@
## Version 1.5 (2020-01-07)
Changes:
- Dropped support Go 1.9 and lower (#823, #829, #886, #1016, #1017)
- Improve buffer handling (#890)
- Document potentially insecure TLS configs (#901)
- Use a double-buffering scheme to prevent data races (#943)
- Pass uint64 values without converting them to string (#838, #955)
- Update collations and make utf8mb4 default (#877, #1054)
- Make NullTime compatible with sql.NullTime in Go 1.13+ (#995)
- Removed CloudSQL support (#993, #1007)
- Add Go Module support (#1003)
New Features:
- Implement support of optional TLS (#900)
- Check connection liveness (#934, #964, #997, #1048, #1051, #1052)
- Implement Connector Interface (#941, #958, #1020, #1035)
Bugfixes:
- Mark connections as bad on error during ping (#875)
- Mark connections as bad on error during dial (#867)
- Fix connection leak caused by rapid context cancellation (#1024)
- Mark connections as bad on error during Conn.Prepare (#1030)
## Version 1.4.1 (2018-11-14)
Bugfixes:
- Fix TIME format for binary columns (#818)
- Fix handling of empty auth plugin names (#835)
- Fix caching_sha2_password with empty password (#826)
- Fix canceled context broke mysqlConn (#862)
- Fix OldAuthSwitchRequest support (#870)
- Fix Auth Response packet for cleartext password (#887)
## Version 1.4 (2018-06-03)
Changes:
- Documentation fixes (#530, #535, #567)
- Refactoring (#575, #579, #580, #581, #603, #615, #704)
- Cache column names (#444)
- Sort the DSN parameters in DSNs generated from a config (#637)
- Allow native password authentication by default (#644)
- Use the default port if it is missing in the DSN (#668)
- Removed the `strict` mode (#676)
- Do not query `max_allowed_packet` by default (#680)
- Dropped support Go 1.6 and lower (#696)
- Updated `ConvertValue()` to match the database/sql/driver implementation (#760)
- Document the usage of `0000-00-00T00:00:00` as the time.Time zero value (#783)
- Improved the compatibility of the authentication system (#807)
New Features:
- Multi-Results support (#537)
- `rejectReadOnly` DSN option (#604)
- `context.Context` support (#608, #612, #627, #761)
- Transaction isolation level support (#619, #744)
- Read-Only transactions support (#618, #634)
- `NewConfig` function which initializes a config with default values (#679)
- Implemented the `ColumnType` interfaces (#667, #724)
- Support for custom string types in `ConvertValue` (#623)
- Implemented `NamedValueChecker`, improving support for uint64 with high bit set (#690, #709, #710)
- `caching_sha2_password` authentication plugin support (#794, #800, #801, #802)
- Implemented `driver.SessionResetter` (#779)
- `sha256_password` authentication plugin support (#808)
Bugfixes:
- Use the DSN hostname as TLS default ServerName if `tls=true` (#564, #718)
- Fixed LOAD LOCAL DATA INFILE for empty files (#590)
- Removed columns definition cache since it sometimes cached invalid data (#592)
- Don't mutate registered TLS configs (#600)
- Make RegisterTLSConfig concurrency-safe (#613)
- Handle missing auth data in the handshake packet correctly (#646)
- Do not retry queries when data was written to avoid data corruption (#302, #736)
- Cache the connection pointer for error handling before invalidating it (#678)
- Fixed imports for appengine/cloudsql (#700)
- Fix sending STMT_LONG_DATA for 0 byte data (#734)
- Set correct capacity for []bytes read from length-encoded strings (#766)
- Make RegisterDial concurrency-safe (#773)
## Version 1.3 (2016-12-01)
Changes:
- Go 1.1 is no longer supported
- Use decimals fields in MySQL to format time types (#249)
- Buffer optimizations (#269)
- TLS ServerName defaults to the host (#283)
- Refactoring (#400, #410, #437)
- Adjusted documentation for second generation CloudSQL (#485)
- Documented DSN system var quoting rules (#502)
- Made statement.Close() calls idempotent to avoid errors in Go 1.6+ (#512)
New Features:
- Enable microsecond resolution on TIME, DATETIME and TIMESTAMP (#249)
- Support for returning table alias on Columns() (#289, #359, #382)
- Placeholder interpolation, can be actived with the DSN parameter `interpolateParams=true` (#309, #318, #490)
- Support for uint64 parameters with high bit set (#332, #345)
- Cleartext authentication plugin support (#327)
- Exported ParseDSN function and the Config struct (#403, #419, #429)
- Read / Write timeouts (#401)
- Support for JSON field type (#414)
- Support for multi-statements and multi-results (#411, #431)
- DSN parameter to set the driver-side max_allowed_packet value manually (#489)
- Native password authentication plugin support (#494, #524)
Bugfixes:
- Fixed handling of queries without columns and rows (#255)
- Fixed a panic when SetKeepAlive() failed (#298)
- Handle ERR packets while reading rows (#321)
- Fixed reading NULL length-encoded integers in MySQL 5.6+ (#349)
- Fixed absolute paths support in LOAD LOCAL DATA INFILE (#356)
- Actually zero out bytes in handshake response (#378)
- Fixed race condition in registering LOAD DATA INFILE handler (#383)
- Fixed tests with MySQL 5.7.9+ (#380)
- QueryUnescape TLS config names (#397)
- Fixed "broken pipe" error by writing to closed socket (#390)
- Fixed LOAD LOCAL DATA INFILE buffering (#424)
- Fixed parsing of floats into float64 when placeholders are used (#434)
- Fixed DSN tests with Go 1.7+ (#459)
- Handle ERR packets while waiting for EOF (#473)
- Invalidate connection on error while discarding additional results (#513)
- Allow terminating packets of length 0 (#516)
## Version 1.2 (2014-06-03)
Changes:
- We switched back to a "rolling release". `go get` installs the current master branch again
- Version v1 of the driver will not be maintained anymore. Go 1.0 is no longer supported by this driver
- Exported errors to allow easy checking from application code
- Enabled TCP Keepalives on TCP connections
- Optimized INFILE handling (better buffer size calculation, lazy init, ...)
- The DSN parser also checks for a missing separating slash
- Faster binary date / datetime to string formatting
- Also exported the MySQLWarning type
- mysqlConn.Close returns the first error encountered instead of ignoring all errors
- writePacket() automatically writes the packet size to the header
- readPacket() uses an iterative approach instead of the recursive approach to merge splitted packets
New Features:
- `RegisterDial` allows the usage of a custom dial function to establish the network connection
- Setting the connection collation is possible with the `collation` DSN parameter. This parameter should be preferred over the `charset` parameter
- Logging of critical errors is configurable with `SetLogger`
- Google CloudSQL support
Bugfixes:
- Allow more than 32 parameters in prepared statements
- Various old_password fixes
- Fixed TestConcurrent test to pass Go's race detection
- Fixed appendLengthEncodedInteger for large numbers
- Renamed readLengthEnodedString to readLengthEncodedString and skipLengthEnodedString to skipLengthEncodedString (fixed typo)
## Version 1.1 (2013-11-02)
Changes:
- Go-MySQL-Driver now requires Go 1.1
- Connections now use the collation `utf8_general_ci` by default. Adding `&charset=UTF8` to the DSN should not be necessary anymore
- Made closing rows and connections error tolerant. This allows for example deferring rows.Close() without checking for errors
- `[]byte(nil)` is now treated as a NULL value. Before, it was treated like an empty string / `[]byte("")`
- DSN parameter values must now be url.QueryEscape'ed. This allows text values to contain special characters, such as '&'.
- Use the IO buffer also for writing. This results in zero allocations (by the driver) for most queries
- Optimized the buffer for reading
- stmt.Query now caches column metadata
- New Logo
- Changed the copyright header to include all contributors
- Improved the LOAD INFILE documentation
- The driver struct is now exported to make the driver directly accessible
- Refactored the driver tests
- Added more benchmarks and moved all to a separate file
- Other small refactoring
New Features:
- Added *old_passwords* support: Required in some cases, but must be enabled by adding `allowOldPasswords=true` to the DSN since it is insecure
- Added a `clientFoundRows` parameter: Return the number of matching rows instead of the number of rows changed on UPDATEs
- Added TLS/SSL support: Use a TLS/SSL encrypted connection to the server. Custom TLS configs can be registered and used
Bugfixes:
- Fixed MySQL 4.1 support: MySQL 4.1 sends packets with lengths which differ from the specification
- Convert to DB timezone when inserting `time.Time`
- Splitted packets (more than 16MB) are now merged correctly
- Fixed false positive `io.EOF` errors when the data was fully read
- Avoid panics on reuse of closed connections
- Fixed empty string producing false nil values
- Fixed sign byte for positive TIME fields
## Version 1.0 (2013-05-14)
Initial Release

373
vendor/github.com/go-sql-driver/mysql/LICENSE generated vendored Normal file
View File

@ -0,0 +1,373 @@
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.

501
vendor/github.com/go-sql-driver/mysql/README.md generated vendored Normal file
View File

@ -0,0 +1,501 @@
# Go-MySQL-Driver
A MySQL-Driver for Go's [database/sql](https://golang.org/pkg/database/sql/) package
![Go-MySQL-Driver logo](https://raw.github.com/wiki/go-sql-driver/mysql/gomysql_m.png "Golang Gopher holding the MySQL Dolphin")
---------------------------------------
* [Features](#features)
* [Requirements](#requirements)
* [Installation](#installation)
* [Usage](#usage)
* [DSN (Data Source Name)](#dsn-data-source-name)
* [Password](#password)
* [Protocol](#protocol)
* [Address](#address)
* [Parameters](#parameters)
* [Examples](#examples)
* [Connection pool and timeouts](#connection-pool-and-timeouts)
* [context.Context Support](#contextcontext-support)
* [ColumnType Support](#columntype-support)
* [LOAD DATA LOCAL INFILE support](#load-data-local-infile-support)
* [time.Time support](#timetime-support)
* [Unicode support](#unicode-support)
* [Testing / Development](#testing--development)
* [License](#license)
---------------------------------------
## Features
* Lightweight and [fast](https://github.com/go-sql-driver/sql-benchmark "golang MySQL-Driver performance")
* Native Go implementation. No C-bindings, just pure Go
* Connections over TCP/IPv4, TCP/IPv6, Unix domain sockets or [custom protocols](https://godoc.org/github.com/go-sql-driver/mysql#DialFunc)
* Automatic handling of broken connections
* Automatic Connection Pooling *(by database/sql package)*
* Supports queries larger than 16MB
* Full [`sql.RawBytes`](https://golang.org/pkg/database/sql/#RawBytes) support.
* Intelligent `LONG DATA` handling in prepared statements
* Secure `LOAD DATA LOCAL INFILE` support with file Whitelisting and `io.Reader` support
* Optional `time.Time` parsing
* Optional placeholder interpolation
## Requirements
* Go 1.10 or higher. We aim to support the 3 latest versions of Go.
* MySQL (4.1+), MariaDB, Percona Server, Google CloudSQL or Sphinx (2.2.3+)
---------------------------------------
## Installation
Simple install the package to your [$GOPATH](https://github.com/golang/go/wiki/GOPATH "GOPATH") with the [go tool](https://golang.org/cmd/go/ "go command") from shell:
```bash
$ go get -u github.com/go-sql-driver/mysql
```
Make sure [Git is installed](https://git-scm.com/downloads) on your machine and in your system's `PATH`.
## Usage
_Go MySQL Driver_ is an implementation of Go's `database/sql/driver` interface. You only need to import the driver and can use the full [`database/sql`](https://golang.org/pkg/database/sql/) API then.
Use `mysql` as `driverName` and a valid [DSN](#dsn-data-source-name) as `dataSourceName`:
```go
import "database/sql"
import _ "github.com/go-sql-driver/mysql"
db, err := sql.Open("mysql", "user:password@/dbname")
```
[Examples are available in our Wiki](https://github.com/go-sql-driver/mysql/wiki/Examples "Go-MySQL-Driver Examples").
### DSN (Data Source Name)
The Data Source Name has a common format, like e.g. [PEAR DB](http://pear.php.net/manual/en/package.database.db.intro-dsn.php) uses it, but without type-prefix (optional parts marked by squared brackets):
```
[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...&paramN=valueN]
```
A DSN in its fullest form:
```
username:password@protocol(address)/dbname?param=value
```
Except for the databasename, all values are optional. So the minimal DSN is:
```
/dbname
```
If you do not want to preselect a database, leave `dbname` empty:
```
/
```
This has the same effect as an empty DSN string:
```
```
Alternatively, [Config.FormatDSN](https://godoc.org/github.com/go-sql-driver/mysql#Config.FormatDSN) can be used to create a DSN string by filling a struct.
#### Password
Passwords can consist of any character. Escaping is **not** necessary.
#### Protocol
See [net.Dial](https://golang.org/pkg/net/#Dial) for more information which networks are available.
In general you should use an Unix domain socket if available and TCP otherwise for best performance.
#### Address
For TCP and UDP networks, addresses have the form `host[:port]`.
If `port` is omitted, the default port will be used.
If `host` is a literal IPv6 address, it must be enclosed in square brackets.
The functions [net.JoinHostPort](https://golang.org/pkg/net/#JoinHostPort) and [net.SplitHostPort](https://golang.org/pkg/net/#SplitHostPort) manipulate addresses in this form.
For Unix domain sockets the address is the absolute path to the MySQL-Server-socket, e.g. `/var/run/mysqld/mysqld.sock` or `/tmp/mysql.sock`.
#### Parameters
*Parameters are case-sensitive!*
Notice that any of `true`, `TRUE`, `True` or `1` is accepted to stand for a true boolean value. Not surprisingly, false can be specified as any of: `false`, `FALSE`, `False` or `0`.
##### `allowAllFiles`
```
Type: bool
Valid Values: true, false
Default: false
```
`allowAllFiles=true` disables the file Whitelist for `LOAD DATA LOCAL INFILE` and allows *all* files.
[*Might be insecure!*](http://dev.mysql.com/doc/refman/5.7/en/load-data-local.html)
##### `allowCleartextPasswords`
```
Type: bool
Valid Values: true, false
Default: false
```
`allowCleartextPasswords=true` allows using the [cleartext client side plugin](http://dev.mysql.com/doc/en/cleartext-authentication-plugin.html) if required by an account, such as one defined with the [PAM authentication plugin](http://dev.mysql.com/doc/en/pam-authentication-plugin.html). Sending passwords in clear text may be a security problem in some configurations. To avoid problems if there is any possibility that the password would be intercepted, clients should connect to MySQL Server using a method that protects the password. Possibilities include [TLS / SSL](#tls), IPsec, or a private network.
##### `allowNativePasswords`
```
Type: bool
Valid Values: true, false
Default: true
```
`allowNativePasswords=false` disallows the usage of MySQL native password method.
##### `allowOldPasswords`
```
Type: bool
Valid Values: true, false
Default: false
```
`allowOldPasswords=true` allows the usage of the insecure old password method. This should be avoided, but is necessary in some cases. See also [the old_passwords wiki page](https://github.com/go-sql-driver/mysql/wiki/old_passwords).
##### `charset`
```
Type: string
Valid Values: <name>
Default: none
```
Sets the charset used for client-server interaction (`"SET NAMES <value>"`). If multiple charsets are set (separated by a comma), the following charset is used if setting the charset failes. This enables for example support for `utf8mb4` ([introduced in MySQL 5.5.3](http://dev.mysql.com/doc/refman/5.5/en/charset-unicode-utf8mb4.html)) with fallback to `utf8` for older servers (`charset=utf8mb4,utf8`).
Usage of the `charset` parameter is discouraged because it issues additional queries to the server.
Unless you need the fallback behavior, please use `collation` instead.
##### `checkConnLiveness`
```
Type: bool
Valid Values: true, false
Default: true
```
On supported platforms connections retrieved from the connection pool are checked for liveness before using them. If the check fails, the respective connection is marked as bad and the query retried with another connection.
`checkConnLiveness=false` disables this liveness check of connections.
##### `collation`
```
Type: string
Valid Values: <name>
Default: utf8mb4_general_ci
```
Sets the collation used for client-server interaction on connection. In contrast to `charset`, `collation` does not issue additional queries. If the specified collation is unavailable on the target server, the connection will fail.
A list of valid charsets for a server is retrievable with `SHOW COLLATION`.
The default collation (`utf8mb4_general_ci`) is supported from MySQL 5.5. You should use an older collation (e.g. `utf8_general_ci`) for older MySQL.
Collations for charset "ucs2", "utf16", "utf16le", and "utf32" can not be used ([ref](https://dev.mysql.com/doc/refman/5.7/en/charset-connection.html#charset-connection-impermissible-client-charset)).
##### `clientFoundRows`
```
Type: bool
Valid Values: true, false
Default: false
```
`clientFoundRows=true` causes an UPDATE to return the number of matching rows instead of the number of rows changed.
##### `columnsWithAlias`
```
Type: bool
Valid Values: true, false
Default: false
```
When `columnsWithAlias` is true, calls to `sql.Rows.Columns()` will return the table alias and the column name separated by a dot. For example:
```
SELECT u.id FROM users as u
```
will return `u.id` instead of just `id` if `columnsWithAlias=true`.
##### `interpolateParams`
```
Type: bool
Valid Values: true, false
Default: false
```
If `interpolateParams` is true, placeholders (`?`) in calls to `db.Query()` and `db.Exec()` are interpolated into a single query string with given parameters. This reduces the number of roundtrips, since the driver has to prepare a statement, execute it with given parameters and close the statement again with `interpolateParams=false`.
*This can not be used together with the multibyte encodings BIG5, CP932, GB2312, GBK or SJIS. These are blacklisted as they may [introduce a SQL injection vulnerability](http://stackoverflow.com/a/12118602/3430118)!*
##### `loc`
```
Type: string
Valid Values: <escaped name>
Default: UTC
```
Sets the location for time.Time values (when using `parseTime=true`). *"Local"* sets the system's location. See [time.LoadLocation](https://golang.org/pkg/time/#LoadLocation) for details.
Note that this sets the location for time.Time values but does not change MySQL's [time_zone setting](https://dev.mysql.com/doc/refman/5.5/en/time-zone-support.html). For that see the [time_zone system variable](#system-variables), which can also be set as a DSN parameter.
Please keep in mind, that param values must be [url.QueryEscape](https://golang.org/pkg/net/url/#QueryEscape)'ed. Alternatively you can manually replace the `/` with `%2F`. For example `US/Pacific` would be `loc=US%2FPacific`.
##### `maxAllowedPacket`
```
Type: decimal number
Default: 4194304
```
Max packet size allowed in bytes. The default value is 4 MiB and should be adjusted to match the server settings. `maxAllowedPacket=0` can be used to automatically fetch the `max_allowed_packet` variable from server *on every connection*.
##### `multiStatements`
```
Type: bool
Valid Values: true, false
Default: false
```
Allow multiple statements in one query. While this allows batch queries, it also greatly increases the risk of SQL injections. Only the result of the first query is returned, all other results are silently discarded.
When `multiStatements` is used, `?` parameters must only be used in the first statement.
##### `parseTime`
```
Type: bool
Valid Values: true, false
Default: false
```
`parseTime=true` changes the output type of `DATE` and `DATETIME` values to `time.Time` instead of `[]byte` / `string`
The date or datetime like `0000-00-00 00:00:00` is converted into zero value of `time.Time`.
##### `readTimeout`
```
Type: duration
Default: 0
```
I/O read timeout. The value must be a decimal number with a unit suffix (*"ms"*, *"s"*, *"m"*, *"h"*), such as *"30s"*, *"0.5m"* or *"1m30s"*.
##### `rejectReadOnly`
```
Type: bool
Valid Values: true, false
Default: false
```
`rejectReadOnly=true` causes the driver to reject read-only connections. This
is for a possible race condition during an automatic failover, where the mysql
client gets connected to a read-only replica after the failover.
Note that this should be a fairly rare case, as an automatic failover normally
happens when the primary is down, and the race condition shouldn't happen
unless it comes back up online as soon as the failover is kicked off. On the
other hand, when this happens, a MySQL application can get stuck on a
read-only connection until restarted. It is however fairly easy to reproduce,
for example, using a manual failover on AWS Aurora's MySQL-compatible cluster.
If you are not relying on read-only transactions to reject writes that aren't
supposed to happen, setting this on some MySQL providers (such as AWS Aurora)
is safer for failovers.
Note that ERROR 1290 can be returned for a `read-only` server and this option will
cause a retry for that error. However the same error number is used for some
other cases. You should ensure your application will never cause an ERROR 1290
except for `read-only` mode when enabling this option.
##### `serverPubKey`
```
Type: string
Valid Values: <name>
Default: none
```
Server public keys can be registered with [`mysql.RegisterServerPubKey`](https://godoc.org/github.com/go-sql-driver/mysql#RegisterServerPubKey), which can then be used by the assigned name in the DSN.
Public keys are used to transmit encrypted data, e.g. for authentication.
If the server's public key is known, it should be set manually to avoid expensive and potentially insecure transmissions of the public key from the server to the client each time it is required.
##### `timeout`
```
Type: duration
Default: OS default
```
Timeout for establishing connections, aka dial timeout. The value must be a decimal number with a unit suffix (*"ms"*, *"s"*, *"m"*, *"h"*), such as *"30s"*, *"0.5m"* or *"1m30s"*.
##### `tls`
```
Type: bool / string
Valid Values: true, false, skip-verify, preferred, <name>
Default: false
```
`tls=true` enables TLS / SSL encrypted connection to the server. Use `skip-verify` if you want to use a self-signed or invalid certificate (server side) or use `preferred` to use TLS only when advertised by the server. This is similar to `skip-verify`, but additionally allows a fallback to a connection which is not encrypted. Neither `skip-verify` nor `preferred` add any reliable security. You can use a custom TLS config after registering it with [`mysql.RegisterTLSConfig`](https://godoc.org/github.com/go-sql-driver/mysql#RegisterTLSConfig).
##### `writeTimeout`
```
Type: duration
Default: 0
```
I/O write timeout. The value must be a decimal number with a unit suffix (*"ms"*, *"s"*, *"m"*, *"h"*), such as *"30s"*, *"0.5m"* or *"1m30s"*.
##### System Variables
Any other parameters are interpreted as system variables:
* `<boolean_var>=<value>`: `SET <boolean_var>=<value>`
* `<enum_var>=<value>`: `SET <enum_var>=<value>`
* `<string_var>=%27<value>%27`: `SET <string_var>='<value>'`
Rules:
* The values for string variables must be quoted with `'`.
* The values must also be [url.QueryEscape](http://golang.org/pkg/net/url/#QueryEscape)'ed!
(which implies values of string variables must be wrapped with `%27`).
Examples:
* `autocommit=1`: `SET autocommit=1`
* [`time_zone=%27Europe%2FParis%27`](https://dev.mysql.com/doc/refman/5.5/en/time-zone-support.html): `SET time_zone='Europe/Paris'`
* [`tx_isolation=%27REPEATABLE-READ%27`](https://dev.mysql.com/doc/refman/5.5/en/server-system-variables.html#sysvar_tx_isolation): `SET tx_isolation='REPEATABLE-READ'`
#### Examples
```
user@unix(/path/to/socket)/dbname
```
```
root:pw@unix(/tmp/mysql.sock)/myDatabase?loc=Local
```
```
user:password@tcp(localhost:5555)/dbname?tls=skip-verify&autocommit=true
```
Treat warnings as errors by setting the system variable [`sql_mode`](https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html):
```
user:password@/dbname?sql_mode=TRADITIONAL
```
TCP via IPv6:
```
user:password@tcp([de:ad:be:ef::ca:fe]:80)/dbname?timeout=90s&collation=utf8mb4_unicode_ci
```
TCP on a remote host, e.g. Amazon RDS:
```
id:password@tcp(your-amazonaws-uri.com:3306)/dbname
```
Google Cloud SQL on App Engine:
```
user:password@unix(/cloudsql/project-id:region-name:instance-name)/dbname
```
TCP using default port (3306) on localhost:
```
user:password@tcp/dbname?charset=utf8mb4,utf8&sys_var=esc%40ped
```
Use the default protocol (tcp) and host (localhost:3306):
```
user:password@/dbname
```
No Database preselected:
```
user:password@/
```
### Connection pool and timeouts
The connection pool is managed by Go's database/sql package. For details on how to configure the size of the pool and how long connections stay in the pool see `*DB.SetMaxOpenConns`, `*DB.SetMaxIdleConns`, and `*DB.SetConnMaxLifetime` in the [database/sql documentation](https://golang.org/pkg/database/sql/). The read, write, and dial timeouts for each individual connection are configured with the DSN parameters [`readTimeout`](#readtimeout), [`writeTimeout`](#writetimeout), and [`timeout`](#timeout), respectively.
## `ColumnType` Support
This driver supports the [`ColumnType` interface](https://golang.org/pkg/database/sql/#ColumnType) introduced in Go 1.8, with the exception of [`ColumnType.Length()`](https://golang.org/pkg/database/sql/#ColumnType.Length), which is currently not supported.
## `context.Context` Support
Go 1.8 added `database/sql` support for `context.Context`. This driver supports query timeouts and cancellation via contexts.
See [context support in the database/sql package](https://golang.org/doc/go1.8#database_sql) for more details.
### `LOAD DATA LOCAL INFILE` support
For this feature you need direct access to the package. Therefore you must change the import path (no `_`):
```go
import "github.com/go-sql-driver/mysql"
```
Files must be whitelisted by registering them with `mysql.RegisterLocalFile(filepath)` (recommended) or the Whitelist check must be deactivated by using the DSN parameter `allowAllFiles=true` ([*Might be insecure!*](http://dev.mysql.com/doc/refman/5.7/en/load-data-local.html)).
To use a `io.Reader` a handler function must be registered with `mysql.RegisterReaderHandler(name, handler)` which returns a `io.Reader` or `io.ReadCloser`. The Reader is available with the filepath `Reader::<name>` then. Choose different names for different handlers and `DeregisterReaderHandler` when you don't need it anymore.
See the [godoc of Go-MySQL-Driver](https://godoc.org/github.com/go-sql-driver/mysql "golang mysql driver documentation") for details.
### `time.Time` support
The default internal output type of MySQL `DATE` and `DATETIME` values is `[]byte` which allows you to scan the value into a `[]byte`, `string` or `sql.RawBytes` variable in your program.
However, many want to scan MySQL `DATE` and `DATETIME` values into `time.Time` variables, which is the logical equivalent in Go to `DATE` and `DATETIME` in MySQL. You can do that by changing the internal output type from `[]byte` to `time.Time` with the DSN parameter `parseTime=true`. You can set the default [`time.Time` location](https://golang.org/pkg/time/#Location) with the `loc` DSN parameter.
**Caution:** As of Go 1.1, this makes `time.Time` the only variable type you can scan `DATE` and `DATETIME` values into. This breaks for example [`sql.RawBytes` support](https://github.com/go-sql-driver/mysql/wiki/Examples#rawbytes).
Alternatively you can use the [`NullTime`](https://godoc.org/github.com/go-sql-driver/mysql#NullTime) type as the scan destination, which works with both `time.Time` and `string` / `[]byte`.
### Unicode support
Since version 1.5 Go-MySQL-Driver automatically uses the collation ` utf8mb4_general_ci` by default.
Other collations / charsets can be set using the [`collation`](#collation) DSN parameter.
Version 1.0 of the driver recommended adding `&charset=utf8` (alias for `SET NAMES utf8`) to the DSN to enable proper UTF-8 support. This is not necessary anymore. The [`collation`](#collation) parameter should be preferred to set another collation / charset than the default.
See http://dev.mysql.com/doc/refman/8.0/en/charset-unicode.html for more details on MySQL's Unicode support.
## Testing / Development
To run the driver tests you may need to adjust the configuration. See the [Testing Wiki-Page](https://github.com/go-sql-driver/mysql/wiki/Testing "Testing") for details.
Go-MySQL-Driver is not feature-complete yet. Your help is very appreciated.
If you want to contribute, you can work on an [open issue](https://github.com/go-sql-driver/mysql/issues?state=open) or review a [pull request](https://github.com/go-sql-driver/mysql/pulls).
See the [Contribution Guidelines](https://github.com/go-sql-driver/mysql/blob/master/CONTRIBUTING.md) for details.
---------------------------------------
## License
Go-MySQL-Driver is licensed under the [Mozilla Public License Version 2.0](https://raw.github.com/go-sql-driver/mysql/master/LICENSE)
Mozilla summarizes the license scope as follows:
> MPL: The copyleft applies to any files containing MPLed code.
That means:
* You can **use** the **unchanged** source code both in private and commercially.
* When distributing, you **must publish** the source code of any **changed files** licensed under the MPL 2.0 under a) the MPL 2.0 itself or b) a compatible license (e.g. GPL 3.0 or Apache License 2.0).
* You **needn't publish** the source code of your library as long as the files licensed under the MPL 2.0 are **unchanged**.
Please read the [MPL 2.0 FAQ](https://www.mozilla.org/en-US/MPL/2.0/FAQ/) if you have further questions regarding the license.
You can read the full terms here: [LICENSE](https://raw.github.com/go-sql-driver/mysql/master/LICENSE).
![Go Gopher and MySQL Dolphin](https://raw.github.com/wiki/go-sql-driver/mysql/go-mysql-driver_m.jpg "Golang Gopher transporting the MySQL Dolphin in a wheelbarrow")

422
vendor/github.com/go-sql-driver/mysql/auth.go generated vendored Normal file
View File

@ -0,0 +1,422 @@
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
//
// Copyright 2018 The Go-MySQL-Driver Authors. All rights reserved.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package mysql
import (
"crypto/rand"
"crypto/rsa"
"crypto/sha1"
"crypto/sha256"
"crypto/x509"
"encoding/pem"
"sync"
)
// server pub keys registry
var (
serverPubKeyLock sync.RWMutex
serverPubKeyRegistry map[string]*rsa.PublicKey
)
// RegisterServerPubKey registers a server RSA public key which can be used to
// send data in a secure manner to the server without receiving the public key
// in a potentially insecure way from the server first.
// Registered keys can afterwards be used adding serverPubKey=<name> to the DSN.
//
// Note: The provided rsa.PublicKey instance is exclusively owned by the driver
// after registering it and may not be modified.
//
// data, err := ioutil.ReadFile("mykey.pem")
// if err != nil {
// log.Fatal(err)
// }
//
// block, _ := pem.Decode(data)
// if block == nil || block.Type != "PUBLIC KEY" {
// log.Fatal("failed to decode PEM block containing public key")
// }
//
// pub, err := x509.ParsePKIXPublicKey(block.Bytes)
// if err != nil {
// log.Fatal(err)
// }
//
// if rsaPubKey, ok := pub.(*rsa.PublicKey); ok {
// mysql.RegisterServerPubKey("mykey", rsaPubKey)
// } else {
// log.Fatal("not a RSA public key")
// }
//
func RegisterServerPubKey(name string, pubKey *rsa.PublicKey) {
serverPubKeyLock.Lock()
if serverPubKeyRegistry == nil {
serverPubKeyRegistry = make(map[string]*rsa.PublicKey)
}
serverPubKeyRegistry[name] = pubKey
serverPubKeyLock.Unlock()
}
// DeregisterServerPubKey removes the public key registered with the given name.
func DeregisterServerPubKey(name string) {
serverPubKeyLock.Lock()
if serverPubKeyRegistry != nil {
delete(serverPubKeyRegistry, name)
}
serverPubKeyLock.Unlock()
}
func getServerPubKey(name string) (pubKey *rsa.PublicKey) {
serverPubKeyLock.RLock()
if v, ok := serverPubKeyRegistry[name]; ok {
pubKey = v
}
serverPubKeyLock.RUnlock()
return
}
// Hash password using pre 4.1 (old password) method
// https://github.com/atcurtis/mariadb/blob/master/mysys/my_rnd.c
type myRnd struct {
seed1, seed2 uint32
}
const myRndMaxVal = 0x3FFFFFFF
// Pseudo random number generator
func newMyRnd(seed1, seed2 uint32) *myRnd {
return &myRnd{
seed1: seed1 % myRndMaxVal,
seed2: seed2 % myRndMaxVal,
}
}
// Tested to be equivalent to MariaDB's floating point variant
// http://play.golang.org/p/QHvhd4qved
// http://play.golang.org/p/RG0q4ElWDx
func (r *myRnd) NextByte() byte {
r.seed1 = (r.seed1*3 + r.seed2) % myRndMaxVal
r.seed2 = (r.seed1 + r.seed2 + 33) % myRndMaxVal
return byte(uint64(r.seed1) * 31 / myRndMaxVal)
}
// Generate binary hash from byte string using insecure pre 4.1 method
func pwHash(password []byte) (result [2]uint32) {
var add uint32 = 7
var tmp uint32
result[0] = 1345345333
result[1] = 0x12345671
for _, c := range password {
// skip spaces and tabs in password
if c == ' ' || c == '\t' {
continue
}
tmp = uint32(c)
result[0] ^= (((result[0] & 63) + add) * tmp) + (result[0] << 8)
result[1] += (result[1] << 8) ^ result[0]
add += tmp
}
// Remove sign bit (1<<31)-1)
result[0] &= 0x7FFFFFFF
result[1] &= 0x7FFFFFFF
return
}
// Hash password using insecure pre 4.1 method
func scrambleOldPassword(scramble []byte, password string) []byte {
if len(password) == 0 {
return nil
}
scramble = scramble[:8]
hashPw := pwHash([]byte(password))
hashSc := pwHash(scramble)
r := newMyRnd(hashPw[0]^hashSc[0], hashPw[1]^hashSc[1])
var out [8]byte
for i := range out {
out[i] = r.NextByte() + 64
}
mask := r.NextByte()
for i := range out {
out[i] ^= mask
}
return out[:]
}
// Hash password using 4.1+ method (SHA1)
func scramblePassword(scramble []byte, password string) []byte {
if len(password) == 0 {
return nil
}
// stage1Hash = SHA1(password)
crypt := sha1.New()
crypt.Write([]byte(password))
stage1 := crypt.Sum(nil)
// scrambleHash = SHA1(scramble + SHA1(stage1Hash))
// inner Hash
crypt.Reset()
crypt.Write(stage1)
hash := crypt.Sum(nil)
// outer Hash
crypt.Reset()
crypt.Write(scramble)
crypt.Write(hash)
scramble = crypt.Sum(nil)
// token = scrambleHash XOR stage1Hash
for i := range scramble {
scramble[i] ^= stage1[i]
}
return scramble
}
// Hash password using MySQL 8+ method (SHA256)
func scrambleSHA256Password(scramble []byte, password string) []byte {
if len(password) == 0 {
return nil
}
// XOR(SHA256(password), SHA256(SHA256(SHA256(password)), scramble))
crypt := sha256.New()
crypt.Write([]byte(password))
message1 := crypt.Sum(nil)
crypt.Reset()
crypt.Write(message1)
message1Hash := crypt.Sum(nil)
crypt.Reset()
crypt.Write(message1Hash)
crypt.Write(scramble)
message2 := crypt.Sum(nil)
for i := range message1 {
message1[i] ^= message2[i]
}
return message1
}
func encryptPassword(password string, seed []byte, pub *rsa.PublicKey) ([]byte, error) {
plain := make([]byte, len(password)+1)
copy(plain, password)
for i := range plain {
j := i % len(seed)
plain[i] ^= seed[j]
}
sha1 := sha1.New()
return rsa.EncryptOAEP(sha1, rand.Reader, pub, plain, nil)
}
func (mc *mysqlConn) sendEncryptedPassword(seed []byte, pub *rsa.PublicKey) error {
enc, err := encryptPassword(mc.cfg.Passwd, seed, pub)
if err != nil {
return err
}
return mc.writeAuthSwitchPacket(enc)
}
func (mc *mysqlConn) auth(authData []byte, plugin string) ([]byte, error) {
switch plugin {
case "caching_sha2_password":
authResp := scrambleSHA256Password(authData, mc.cfg.Passwd)
return authResp, nil
case "mysql_old_password":
if !mc.cfg.AllowOldPasswords {
return nil, ErrOldPassword
}
// Note: there are edge cases where this should work but doesn't;
// this is currently "wontfix":
// https://github.com/go-sql-driver/mysql/issues/184
authResp := append(scrambleOldPassword(authData[:8], mc.cfg.Passwd), 0)
return authResp, nil
case "mysql_clear_password":
if !mc.cfg.AllowCleartextPasswords {
return nil, ErrCleartextPassword
}
// http://dev.mysql.com/doc/refman/5.7/en/cleartext-authentication-plugin.html
// http://dev.mysql.com/doc/refman/5.7/en/pam-authentication-plugin.html
return append([]byte(mc.cfg.Passwd), 0), nil
case "mysql_native_password":
if !mc.cfg.AllowNativePasswords {
return nil, ErrNativePassword
}
// https://dev.mysql.com/doc/internals/en/secure-password-authentication.html
// Native password authentication only need and will need 20-byte challenge.
authResp := scramblePassword(authData[:20], mc.cfg.Passwd)
return authResp, nil
case "sha256_password":
if len(mc.cfg.Passwd) == 0 {
return []byte{0}, nil
}
if mc.cfg.tls != nil || mc.cfg.Net == "unix" {
// write cleartext auth packet
return append([]byte(mc.cfg.Passwd), 0), nil
}
pubKey := mc.cfg.pubKey
if pubKey == nil {
// request public key from server
return []byte{1}, nil
}
// encrypted password
enc, err := encryptPassword(mc.cfg.Passwd, authData, pubKey)
return enc, err
default:
errLog.Print("unknown auth plugin:", plugin)
return nil, ErrUnknownPlugin
}
}
func (mc *mysqlConn) handleAuthResult(oldAuthData []byte, plugin string) error {
// Read Result Packet
authData, newPlugin, err := mc.readAuthResult()
if err != nil {
return err
}
// handle auth plugin switch, if requested
if newPlugin != "" {
// If CLIENT_PLUGIN_AUTH capability is not supported, no new cipher is
// sent and we have to keep using the cipher sent in the init packet.
if authData == nil {
authData = oldAuthData
} else {
// copy data from read buffer to owned slice
copy(oldAuthData, authData)
}
plugin = newPlugin
authResp, err := mc.auth(authData, plugin)
if err != nil {
return err
}
if err = mc.writeAuthSwitchPacket(authResp); err != nil {
return err
}
// Read Result Packet
authData, newPlugin, err = mc.readAuthResult()
if err != nil {
return err
}
// Do not allow to change the auth plugin more than once
if newPlugin != "" {
return ErrMalformPkt
}
}
switch plugin {
// https://insidemysql.com/preparing-your-community-connector-for-mysql-8-part-2-sha256/
case "caching_sha2_password":
switch len(authData) {
case 0:
return nil // auth successful
case 1:
switch authData[0] {
case cachingSha2PasswordFastAuthSuccess:
if err = mc.readResultOK(); err == nil {
return nil // auth successful
}
case cachingSha2PasswordPerformFullAuthentication:
if mc.cfg.tls != nil || mc.cfg.Net == "unix" {
// write cleartext auth packet
err = mc.writeAuthSwitchPacket(append([]byte(mc.cfg.Passwd), 0))
if err != nil {
return err
}
} else {
pubKey := mc.cfg.pubKey
if pubKey == nil {
// request public key from server
data, err := mc.buf.takeSmallBuffer(4 + 1)
if err != nil {
return err
}
data[4] = cachingSha2PasswordRequestPublicKey
mc.writePacket(data)
// parse public key
if data, err = mc.readPacket(); err != nil {
return err
}
block, _ := pem.Decode(data[1:])
pkix, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return err
}
pubKey = pkix.(*rsa.PublicKey)
}
// send encrypted password
err = mc.sendEncryptedPassword(oldAuthData, pubKey)
if err != nil {
return err
}
}
return mc.readResultOK()
default:
return ErrMalformPkt
}
default:
return ErrMalformPkt
}
case "sha256_password":
switch len(authData) {
case 0:
return nil // auth successful
default:
block, _ := pem.Decode(authData)
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return err
}
// send encrypted password
err = mc.sendEncryptedPassword(oldAuthData, pub.(*rsa.PublicKey))
if err != nil {
return err
}
return mc.readResultOK()
}
default:
return nil // auth successful
}
return err
}

182
vendor/github.com/go-sql-driver/mysql/buffer.go generated vendored Normal file
View File

@ -0,0 +1,182 @@
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
//
// Copyright 2013 The Go-MySQL-Driver Authors. All rights reserved.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package mysql
import (
"io"
"net"
"time"
)
const defaultBufSize = 4096
const maxCachedBufSize = 256 * 1024
// A buffer which is used for both reading and writing.
// This is possible since communication on each connection is synchronous.
// In other words, we can't write and read simultaneously on the same connection.
// The buffer is similar to bufio.Reader / Writer but zero-copy-ish
// Also highly optimized for this particular use case.
// This buffer is backed by two byte slices in a double-buffering scheme
type buffer struct {
buf []byte // buf is a byte buffer who's length and capacity are equal.
nc net.Conn
idx int
length int
timeout time.Duration
dbuf [2][]byte // dbuf is an array with the two byte slices that back this buffer
flipcnt uint // flipccnt is the current buffer counter for double-buffering
}
// newBuffer allocates and returns a new buffer.
func newBuffer(nc net.Conn) buffer {
fg := make([]byte, defaultBufSize)
return buffer{
buf: fg,
nc: nc,
dbuf: [2][]byte{fg, nil},
}
}
// flip replaces the active buffer with the background buffer
// this is a delayed flip that simply increases the buffer counter;
// the actual flip will be performed the next time we call `buffer.fill`
func (b *buffer) flip() {
b.flipcnt += 1
}
// fill reads into the buffer until at least _need_ bytes are in it
func (b *buffer) fill(need int) error {
n := b.length
// fill data into its double-buffering target: if we've called
// flip on this buffer, we'll be copying to the background buffer,
// and then filling it with network data; otherwise we'll just move
// the contents of the current buffer to the front before filling it
dest := b.dbuf[b.flipcnt&1]
// grow buffer if necessary to fit the whole packet.
if need > len(dest) {
// Round up to the next multiple of the default size
dest = make([]byte, ((need/defaultBufSize)+1)*defaultBufSize)
// if the allocated buffer is not too large, move it to backing storage
// to prevent extra allocations on applications that perform large reads
if len(dest) <= maxCachedBufSize {
b.dbuf[b.flipcnt&1] = dest
}
}
// if we're filling the fg buffer, move the existing data to the start of it.
// if we're filling the bg buffer, copy over the data
if n > 0 {
copy(dest[:n], b.buf[b.idx:])
}
b.buf = dest
b.idx = 0
for {
if b.timeout > 0 {
if err := b.nc.SetReadDeadline(time.Now().Add(b.timeout)); err != nil {
return err
}
}
nn, err := b.nc.Read(b.buf[n:])
n += nn
switch err {
case nil:
if n < need {
continue
}
b.length = n
return nil
case io.EOF:
if n >= need {
b.length = n
return nil
}
return io.ErrUnexpectedEOF
default:
return err
}
}
}
// returns next N bytes from buffer.
// The returned slice is only guaranteed to be valid until the next read
func (b *buffer) readNext(need int) ([]byte, error) {
if b.length < need {
// refill
if err := b.fill(need); err != nil {
return nil, err
}
}
offset := b.idx
b.idx += need
b.length -= need
return b.buf[offset:b.idx], nil
}
// takeBuffer returns a buffer with the requested size.
// If possible, a slice from the existing buffer is returned.
// Otherwise a bigger buffer is made.
// Only one buffer (total) can be used at a time.
func (b *buffer) takeBuffer(length int) ([]byte, error) {
if b.length > 0 {
return nil, ErrBusyBuffer
}
// test (cheap) general case first
if length <= cap(b.buf) {
return b.buf[:length], nil
}
if length < maxPacketSize {
b.buf = make([]byte, length)
return b.buf, nil
}
// buffer is larger than we want to store.
return make([]byte, length), nil
}
// takeSmallBuffer is shortcut which can be used if length is
// known to be smaller than defaultBufSize.
// Only one buffer (total) can be used at a time.
func (b *buffer) takeSmallBuffer(length int) ([]byte, error) {
if b.length > 0 {
return nil, ErrBusyBuffer
}
return b.buf[:length], nil
}
// takeCompleteBuffer returns the complete existing buffer.
// This can be used if the necessary buffer size is unknown.
// cap and len of the returned buffer will be equal.
// Only one buffer (total) can be used at a time.
func (b *buffer) takeCompleteBuffer() ([]byte, error) {
if b.length > 0 {
return nil, ErrBusyBuffer
}
return b.buf, nil
}
// store stores buf, an updated buffer, if its suitable to do so.
func (b *buffer) store(buf []byte) error {
if b.length > 0 {
return ErrBusyBuffer
} else if cap(buf) <= maxPacketSize && cap(buf) > cap(b.buf) {
b.buf = buf[:cap(buf)]
}
return nil
}

265
vendor/github.com/go-sql-driver/mysql/collations.go generated vendored Normal file
View File

@ -0,0 +1,265 @@
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
//
// Copyright 2014 The Go-MySQL-Driver Authors. All rights reserved.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package mysql
const defaultCollation = "utf8mb4_general_ci"
const binaryCollation = "binary"
// A list of available collations mapped to the internal ID.
// To update this map use the following MySQL query:
// SELECT COLLATION_NAME, ID FROM information_schema.COLLATIONS WHERE ID<256 ORDER BY ID
//
// Handshake packet have only 1 byte for collation_id. So we can't use collations with ID > 255.
//
// ucs2, utf16, and utf32 can't be used for connection charset.
// https://dev.mysql.com/doc/refman/5.7/en/charset-connection.html#charset-connection-impermissible-client-charset
// They are commented out to reduce this map.
var collations = map[string]byte{
"big5_chinese_ci": 1,
"latin2_czech_cs": 2,
"dec8_swedish_ci": 3,
"cp850_general_ci": 4,
"latin1_german1_ci": 5,
"hp8_english_ci": 6,
"koi8r_general_ci": 7,
"latin1_swedish_ci": 8,
"latin2_general_ci": 9,
"swe7_swedish_ci": 10,
"ascii_general_ci": 11,
"ujis_japanese_ci": 12,
"sjis_japanese_ci": 13,
"cp1251_bulgarian_ci": 14,
"latin1_danish_ci": 15,
"hebrew_general_ci": 16,
"tis620_thai_ci": 18,
"euckr_korean_ci": 19,
"latin7_estonian_cs": 20,
"latin2_hungarian_ci": 21,
"koi8u_general_ci": 22,
"cp1251_ukrainian_ci": 23,
"gb2312_chinese_ci": 24,
"greek_general_ci": 25,
"cp1250_general_ci": 26,
"latin2_croatian_ci": 27,
"gbk_chinese_ci": 28,
"cp1257_lithuanian_ci": 29,
"latin5_turkish_ci": 30,
"latin1_german2_ci": 31,
"armscii8_general_ci": 32,
"utf8_general_ci": 33,
"cp1250_czech_cs": 34,
//"ucs2_general_ci": 35,
"cp866_general_ci": 36,
"keybcs2_general_ci": 37,
"macce_general_ci": 38,
"macroman_general_ci": 39,
"cp852_general_ci": 40,
"latin7_general_ci": 41,
"latin7_general_cs": 42,
"macce_bin": 43,
"cp1250_croatian_ci": 44,
"utf8mb4_general_ci": 45,
"utf8mb4_bin": 46,
"latin1_bin": 47,
"latin1_general_ci": 48,
"latin1_general_cs": 49,
"cp1251_bin": 50,
"cp1251_general_ci": 51,
"cp1251_general_cs": 52,
"macroman_bin": 53,
//"utf16_general_ci": 54,
//"utf16_bin": 55,
//"utf16le_general_ci": 56,
"cp1256_general_ci": 57,
"cp1257_bin": 58,
"cp1257_general_ci": 59,
//"utf32_general_ci": 60,
//"utf32_bin": 61,
//"utf16le_bin": 62,
"binary": 63,
"armscii8_bin": 64,
"ascii_bin": 65,
"cp1250_bin": 66,
"cp1256_bin": 67,
"cp866_bin": 68,
"dec8_bin": 69,
"greek_bin": 70,
"hebrew_bin": 71,
"hp8_bin": 72,
"keybcs2_bin": 73,
"koi8r_bin": 74,
"koi8u_bin": 75,
"utf8_tolower_ci": 76,
"latin2_bin": 77,
"latin5_bin": 78,
"latin7_bin": 79,
"cp850_bin": 80,
"cp852_bin": 81,
"swe7_bin": 82,
"utf8_bin": 83,
"big5_bin": 84,
"euckr_bin": 85,
"gb2312_bin": 86,
"gbk_bin": 87,
"sjis_bin": 88,
"tis620_bin": 89,
//"ucs2_bin": 90,
"ujis_bin": 91,
"geostd8_general_ci": 92,
"geostd8_bin": 93,
"latin1_spanish_ci": 94,
"cp932_japanese_ci": 95,
"cp932_bin": 96,
"eucjpms_japanese_ci": 97,
"eucjpms_bin": 98,
"cp1250_polish_ci": 99,
//"utf16_unicode_ci": 101,
//"utf16_icelandic_ci": 102,
//"utf16_latvian_ci": 103,
//"utf16_romanian_ci": 104,
//"utf16_slovenian_ci": 105,
//"utf16_polish_ci": 106,
//"utf16_estonian_ci": 107,
//"utf16_spanish_ci": 108,
//"utf16_swedish_ci": 109,
//"utf16_turkish_ci": 110,
//"utf16_czech_ci": 111,
//"utf16_danish_ci": 112,
//"utf16_lithuanian_ci": 113,
//"utf16_slovak_ci": 114,
//"utf16_spanish2_ci": 115,
//"utf16_roman_ci": 116,
//"utf16_persian_ci": 117,
//"utf16_esperanto_ci": 118,
//"utf16_hungarian_ci": 119,
//"utf16_sinhala_ci": 120,
//"utf16_german2_ci": 121,
//"utf16_croatian_ci": 122,
//"utf16_unicode_520_ci": 123,
//"utf16_vietnamese_ci": 124,
//"ucs2_unicode_ci": 128,
//"ucs2_icelandic_ci": 129,
//"ucs2_latvian_ci": 130,
//"ucs2_romanian_ci": 131,
//"ucs2_slovenian_ci": 132,
//"ucs2_polish_ci": 133,
//"ucs2_estonian_ci": 134,
//"ucs2_spanish_ci": 135,
//"ucs2_swedish_ci": 136,
//"ucs2_turkish_ci": 137,
//"ucs2_czech_ci": 138,
//"ucs2_danish_ci": 139,
//"ucs2_lithuanian_ci": 140,
//"ucs2_slovak_ci": 141,
//"ucs2_spanish2_ci": 142,
//"ucs2_roman_ci": 143,
//"ucs2_persian_ci": 144,
//"ucs2_esperanto_ci": 145,
//"ucs2_hungarian_ci": 146,
//"ucs2_sinhala_ci": 147,
//"ucs2_german2_ci": 148,
//"ucs2_croatian_ci": 149,
//"ucs2_unicode_520_ci": 150,
//"ucs2_vietnamese_ci": 151,
//"ucs2_general_mysql500_ci": 159,
//"utf32_unicode_ci": 160,
//"utf32_icelandic_ci": 161,
//"utf32_latvian_ci": 162,
//"utf32_romanian_ci": 163,
//"utf32_slovenian_ci": 164,
//"utf32_polish_ci": 165,
//"utf32_estonian_ci": 166,
//"utf32_spanish_ci": 167,
//"utf32_swedish_ci": 168,
//"utf32_turkish_ci": 169,
//"utf32_czech_ci": 170,
//"utf32_danish_ci": 171,
//"utf32_lithuanian_ci": 172,
//"utf32_slovak_ci": 173,
//"utf32_spanish2_ci": 174,
//"utf32_roman_ci": 175,
//"utf32_persian_ci": 176,
//"utf32_esperanto_ci": 177,
//"utf32_hungarian_ci": 178,
//"utf32_sinhala_ci": 179,
//"utf32_german2_ci": 180,
//"utf32_croatian_ci": 181,
//"utf32_unicode_520_ci": 182,
//"utf32_vietnamese_ci": 183,
"utf8_unicode_ci": 192,
"utf8_icelandic_ci": 193,
"utf8_latvian_ci": 194,
"utf8_romanian_ci": 195,
"utf8_slovenian_ci": 196,
"utf8_polish_ci": 197,
"utf8_estonian_ci": 198,
"utf8_spanish_ci": 199,
"utf8_swedish_ci": 200,
"utf8_turkish_ci": 201,
"utf8_czech_ci": 202,
"utf8_danish_ci": 203,
"utf8_lithuanian_ci": 204,
"utf8_slovak_ci": 205,
"utf8_spanish2_ci": 206,
"utf8_roman_ci": 207,
"utf8_persian_ci": 208,
"utf8_esperanto_ci": 209,
"utf8_hungarian_ci": 210,
"utf8_sinhala_ci": 211,
"utf8_german2_ci": 212,
"utf8_croatian_ci": 213,
"utf8_unicode_520_ci": 214,
"utf8_vietnamese_ci": 215,
"utf8_general_mysql500_ci": 223,
"utf8mb4_unicode_ci": 224,
"utf8mb4_icelandic_ci": 225,
"utf8mb4_latvian_ci": 226,
"utf8mb4_romanian_ci": 227,
"utf8mb4_slovenian_ci": 228,
"utf8mb4_polish_ci": 229,
"utf8mb4_estonian_ci": 230,
"utf8mb4_spanish_ci": 231,
"utf8mb4_swedish_ci": 232,
"utf8mb4_turkish_ci": 233,
"utf8mb4_czech_ci": 234,
"utf8mb4_danish_ci": 235,
"utf8mb4_lithuanian_ci": 236,
"utf8mb4_slovak_ci": 237,
"utf8mb4_spanish2_ci": 238,
"utf8mb4_roman_ci": 239,
"utf8mb4_persian_ci": 240,
"utf8mb4_esperanto_ci": 241,
"utf8mb4_hungarian_ci": 242,
"utf8mb4_sinhala_ci": 243,
"utf8mb4_german2_ci": 244,
"utf8mb4_croatian_ci": 245,
"utf8mb4_unicode_520_ci": 246,
"utf8mb4_vietnamese_ci": 247,
"gb18030_chinese_ci": 248,
"gb18030_bin": 249,
"gb18030_unicode_520_ci": 250,
"utf8mb4_0900_ai_ci": 255,
}
// A blacklist of collations which is unsafe to interpolate parameters.
// These multibyte encodings may contains 0x5c (`\`) in their trailing bytes.
var unsafeCollations = map[string]bool{
"big5_chinese_ci": true,
"sjis_japanese_ci": true,
"gbk_chinese_ci": true,
"big5_bin": true,
"gb2312_bin": true,
"gbk_bin": true,
"sjis_bin": true,
"cp932_japanese_ci": true,
"cp932_bin": true,
"gb18030_chinese_ci": true,
"gb18030_bin": true,
"gb18030_unicode_520_ci": true,
}

54
vendor/github.com/go-sql-driver/mysql/conncheck.go generated vendored Normal file
View File

@ -0,0 +1,54 @@
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
//
// Copyright 2019 The Go-MySQL-Driver Authors. All rights reserved.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
// +build linux darwin dragonfly freebsd netbsd openbsd solaris illumos
package mysql
import (
"errors"
"io"
"net"
"syscall"
)
var errUnexpectedRead = errors.New("unexpected read from socket")
func connCheck(conn net.Conn) error {
var sysErr error
sysConn, ok := conn.(syscall.Conn)
if !ok {
return nil
}
rawConn, err := sysConn.SyscallConn()
if err != nil {
return err
}
err = rawConn.Read(func(fd uintptr) bool {
var buf [1]byte
n, err := syscall.Read(int(fd), buf[:])
switch {
case n == 0 && err == nil:
sysErr = io.EOF
case n > 0:
sysErr = errUnexpectedRead
case err == syscall.EAGAIN || err == syscall.EWOULDBLOCK:
sysErr = nil
default:
sysErr = err
}
return true
})
if err != nil {
return err
}
return sysErr
}

View File

@ -0,0 +1,17 @@
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
//
// Copyright 2019 The Go-MySQL-Driver Authors. All rights reserved.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
// +build !linux,!darwin,!dragonfly,!freebsd,!netbsd,!openbsd,!solaris,!illumos
package mysql
import "net"
func connCheck(conn net.Conn) error {
return nil
}

651
vendor/github.com/go-sql-driver/mysql/connection.go generated vendored Normal file
View File

@ -0,0 +1,651 @@
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
//
// Copyright 2012 The Go-MySQL-Driver Authors. All rights reserved.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package mysql
import (
"context"
"database/sql"
"database/sql/driver"
"io"
"net"
"strconv"
"strings"
"time"
)
type mysqlConn struct {
buf buffer
netConn net.Conn
rawConn net.Conn // underlying connection when netConn is TLS connection.
affectedRows uint64
insertId uint64
cfg *Config
maxAllowedPacket int
maxWriteSize int
writeTimeout time.Duration
flags clientFlag
status statusFlag
sequence uint8
parseTime bool
reset bool // set when the Go SQL package calls ResetSession
// for context support (Go 1.8+)
watching bool
watcher chan<- context.Context
closech chan struct{}
finished chan<- struct{}
canceled atomicError // set non-nil if conn is canceled
closed atomicBool // set when conn is closed, before closech is closed
}
// Handles parameters set in DSN after the connection is established
func (mc *mysqlConn) handleParams() (err error) {
for param, val := range mc.cfg.Params {
switch param {
// Charset
case "charset":
charsets := strings.Split(val, ",")
for i := range charsets {
// ignore errors here - a charset may not exist
err = mc.exec("SET NAMES " + charsets[i])
if err == nil {
break
}
}
if err != nil {
return
}
// System Vars
default:
err = mc.exec("SET " + param + "=" + val + "")
if err != nil {
return
}
}
}
return
}
func (mc *mysqlConn) markBadConn(err error) error {
if mc == nil {
return err
}
if err != errBadConnNoWrite {
return err
}
return driver.ErrBadConn
}
func (mc *mysqlConn) Begin() (driver.Tx, error) {
return mc.begin(false)
}
func (mc *mysqlConn) begin(readOnly bool) (driver.Tx, error) {
if mc.closed.IsSet() {
errLog.Print(ErrInvalidConn)
return nil, driver.ErrBadConn
}
var q string
if readOnly {
q = "START TRANSACTION READ ONLY"
} else {
q = "START TRANSACTION"
}
err := mc.exec(q)
if err == nil {
return &mysqlTx{mc}, err
}
return nil, mc.markBadConn(err)
}
func (mc *mysqlConn) Close() (err error) {
// Makes Close idempotent
if !mc.closed.IsSet() {
err = mc.writeCommandPacket(comQuit)
}
mc.cleanup()
return
}
// Closes the network connection and unsets internal variables. Do not call this
// function after successfully authentication, call Close instead. This function
// is called before auth or on auth failure because MySQL will have already
// closed the network connection.
func (mc *mysqlConn) cleanup() {
if !mc.closed.TrySet(true) {
return
}
// Makes cleanup idempotent
close(mc.closech)
if mc.netConn == nil {
return
}
if err := mc.netConn.Close(); err != nil {
errLog.Print(err)
}
}
func (mc *mysqlConn) error() error {
if mc.closed.IsSet() {
if err := mc.canceled.Value(); err != nil {
return err
}
return ErrInvalidConn
}
return nil
}
func (mc *mysqlConn) Prepare(query string) (driver.Stmt, error) {
if mc.closed.IsSet() {
errLog.Print(ErrInvalidConn)
return nil, driver.ErrBadConn
}
// Send command
err := mc.writeCommandPacketStr(comStmtPrepare, query)
if err != nil {
// STMT_PREPARE is safe to retry. So we can return ErrBadConn here.
errLog.Print(err)
return nil, driver.ErrBadConn
}
stmt := &mysqlStmt{
mc: mc,
}
// Read Result
columnCount, err := stmt.readPrepareResultPacket()
if err == nil {
if stmt.paramCount > 0 {
if err = mc.readUntilEOF(); err != nil {
return nil, err
}
}
if columnCount > 0 {
err = mc.readUntilEOF()
}
}
return stmt, err
}
func (mc *mysqlConn) interpolateParams(query string, args []driver.Value) (string, error) {
// Number of ? should be same to len(args)
if strings.Count(query, "?") != len(args) {
return "", driver.ErrSkip
}
buf, err := mc.buf.takeCompleteBuffer()
if err != nil {
// can not take the buffer. Something must be wrong with the connection
errLog.Print(err)
return "", ErrInvalidConn
}
buf = buf[:0]
argPos := 0
for i := 0; i < len(query); i++ {
q := strings.IndexByte(query[i:], '?')
if q == -1 {
buf = append(buf, query[i:]...)
break
}
buf = append(buf, query[i:i+q]...)
i += q
arg := args[argPos]
argPos++
if arg == nil {
buf = append(buf, "NULL"...)
continue
}
switch v := arg.(type) {
case int64:
buf = strconv.AppendInt(buf, v, 10)
case uint64:
// Handle uint64 explicitly because our custom ConvertValue emits unsigned values
buf = strconv.AppendUint(buf, v, 10)
case float64:
buf = strconv.AppendFloat(buf, v, 'g', -1, 64)
case bool:
if v {
buf = append(buf, '1')
} else {
buf = append(buf, '0')
}
case time.Time:
if v.IsZero() {
buf = append(buf, "'0000-00-00'"...)
} else {
v := v.In(mc.cfg.Loc)
v = v.Add(time.Nanosecond * 500) // To round under microsecond
year := v.Year()
year100 := year / 100
year1 := year % 100
month := v.Month()
day := v.Day()
hour := v.Hour()
minute := v.Minute()
second := v.Second()
micro := v.Nanosecond() / 1000
buf = append(buf, []byte{
'\'',
digits10[year100], digits01[year100],
digits10[year1], digits01[year1],
'-',
digits10[month], digits01[month],
'-',
digits10[day], digits01[day],
' ',
digits10[hour], digits01[hour],
':',
digits10[minute], digits01[minute],
':',
digits10[second], digits01[second],
}...)
if micro != 0 {
micro10000 := micro / 10000
micro100 := micro / 100 % 100
micro1 := micro % 100
buf = append(buf, []byte{
'.',
digits10[micro10000], digits01[micro10000],
digits10[micro100], digits01[micro100],
digits10[micro1], digits01[micro1],
}...)
}
buf = append(buf, '\'')
}
case []byte:
if v == nil {
buf = append(buf, "NULL"...)
} else {
buf = append(buf, "_binary'"...)
if mc.status&statusNoBackslashEscapes == 0 {
buf = escapeBytesBackslash(buf, v)
} else {
buf = escapeBytesQuotes(buf, v)
}
buf = append(buf, '\'')
}
case string:
buf = append(buf, '\'')
if mc.status&statusNoBackslashEscapes == 0 {
buf = escapeStringBackslash(buf, v)
} else {
buf = escapeStringQuotes(buf, v)
}
buf = append(buf, '\'')
default:
return "", driver.ErrSkip
}
if len(buf)+4 > mc.maxAllowedPacket {
return "", driver.ErrSkip
}
}
if argPos != len(args) {
return "", driver.ErrSkip
}
return string(buf), nil
}
func (mc *mysqlConn) Exec(query string, args []driver.Value) (driver.Result, error) {
if mc.closed.IsSet() {
errLog.Print(ErrInvalidConn)
return nil, driver.ErrBadConn
}
if len(args) != 0 {
if !mc.cfg.InterpolateParams {
return nil, driver.ErrSkip
}
// try to interpolate the parameters to save extra roundtrips for preparing and closing a statement
prepared, err := mc.interpolateParams(query, args)
if err != nil {
return nil, err
}
query = prepared
}
mc.affectedRows = 0
mc.insertId = 0
err := mc.exec(query)
if err == nil {
return &mysqlResult{
affectedRows: int64(mc.affectedRows),
insertId: int64(mc.insertId),
}, err
}
return nil, mc.markBadConn(err)
}
// Internal function to execute commands
func (mc *mysqlConn) exec(query string) error {
// Send command
if err := mc.writeCommandPacketStr(comQuery, query); err != nil {
return mc.markBadConn(err)
}
// Read Result
resLen, err := mc.readResultSetHeaderPacket()
if err != nil {
return err
}
if resLen > 0 {
// columns
if err := mc.readUntilEOF(); err != nil {
return err
}
// rows
if err := mc.readUntilEOF(); err != nil {
return err
}
}
return mc.discardResults()
}
func (mc *mysqlConn) Query(query string, args []driver.Value) (driver.Rows, error) {
return mc.query(query, args)
}
func (mc *mysqlConn) query(query string, args []driver.Value) (*textRows, error) {
if mc.closed.IsSet() {
errLog.Print(ErrInvalidConn)
return nil, driver.ErrBadConn
}
if len(args) != 0 {
if !mc.cfg.InterpolateParams {
return nil, driver.ErrSkip
}
// try client-side prepare to reduce roundtrip
prepared, err := mc.interpolateParams(query, args)
if err != nil {
return nil, err
}
query = prepared
}
// Send command
err := mc.writeCommandPacketStr(comQuery, query)
if err == nil {
// Read Result
var resLen int
resLen, err = mc.readResultSetHeaderPacket()
if err == nil {
rows := new(textRows)
rows.mc = mc
if resLen == 0 {
rows.rs.done = true
switch err := rows.NextResultSet(); err {
case nil, io.EOF:
return rows, nil
default:
return nil, err
}
}
// Columns
rows.rs.columns, err = mc.readColumns(resLen)
return rows, err
}
}
return nil, mc.markBadConn(err)
}
// Gets the value of the given MySQL System Variable
// The returned byte slice is only valid until the next read
func (mc *mysqlConn) getSystemVar(name string) ([]byte, error) {
// Send command
if err := mc.writeCommandPacketStr(comQuery, "SELECT @@"+name); err != nil {
return nil, err
}
// Read Result
resLen, err := mc.readResultSetHeaderPacket()
if err == nil {
rows := new(textRows)
rows.mc = mc
rows.rs.columns = []mysqlField{{fieldType: fieldTypeVarChar}}
if resLen > 0 {
// Columns
if err := mc.readUntilEOF(); err != nil {
return nil, err
}
}
dest := make([]driver.Value, resLen)
if err = rows.readRow(dest); err == nil {
return dest[0].([]byte), mc.readUntilEOF()
}
}
return nil, err
}
// finish is called when the query has canceled.
func (mc *mysqlConn) cancel(err error) {
mc.canceled.Set(err)
mc.cleanup()
}
// finish is called when the query has succeeded.
func (mc *mysqlConn) finish() {
if !mc.watching || mc.finished == nil {
return
}
select {
case mc.finished <- struct{}{}:
mc.watching = false
case <-mc.closech:
}
}
// Ping implements driver.Pinger interface
func (mc *mysqlConn) Ping(ctx context.Context) (err error) {
if mc.closed.IsSet() {
errLog.Print(ErrInvalidConn)
return driver.ErrBadConn
}
if err = mc.watchCancel(ctx); err != nil {
return
}
defer mc.finish()
if err = mc.writeCommandPacket(comPing); err != nil {
return mc.markBadConn(err)
}
return mc.readResultOK()
}
// BeginTx implements driver.ConnBeginTx interface
func (mc *mysqlConn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) {
if err := mc.watchCancel(ctx); err != nil {
return nil, err
}
defer mc.finish()
if sql.IsolationLevel(opts.Isolation) != sql.LevelDefault {
level, err := mapIsolationLevel(opts.Isolation)
if err != nil {
return nil, err
}
err = mc.exec("SET TRANSACTION ISOLATION LEVEL " + level)
if err != nil {
return nil, err
}
}
return mc.begin(opts.ReadOnly)
}
func (mc *mysqlConn) QueryContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Rows, error) {
dargs, err := namedValueToValue(args)
if err != nil {
return nil, err
}
if err := mc.watchCancel(ctx); err != nil {
return nil, err
}
rows, err := mc.query(query, dargs)
if err != nil {
mc.finish()
return nil, err
}
rows.finish = mc.finish
return rows, err
}
func (mc *mysqlConn) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) {
dargs, err := namedValueToValue(args)
if err != nil {
return nil, err
}
if err := mc.watchCancel(ctx); err != nil {
return nil, err
}
defer mc.finish()
return mc.Exec(query, dargs)
}
func (mc *mysqlConn) PrepareContext(ctx context.Context, query string) (driver.Stmt, error) {
if err := mc.watchCancel(ctx); err != nil {
return nil, err
}
stmt, err := mc.Prepare(query)
mc.finish()
if err != nil {
return nil, err
}
select {
default:
case <-ctx.Done():
stmt.Close()
return nil, ctx.Err()
}
return stmt, nil
}
func (stmt *mysqlStmt) QueryContext(ctx context.Context, args []driver.NamedValue) (driver.Rows, error) {
dargs, err := namedValueToValue(args)
if err != nil {
return nil, err
}
if err := stmt.mc.watchCancel(ctx); err != nil {
return nil, err
}
rows, err := stmt.query(dargs)
if err != nil {
stmt.mc.finish()
return nil, err
}
rows.finish = stmt.mc.finish
return rows, err
}
func (stmt *mysqlStmt) ExecContext(ctx context.Context, args []driver.NamedValue) (driver.Result, error) {
dargs, err := namedValueToValue(args)
if err != nil {
return nil, err
}
if err := stmt.mc.watchCancel(ctx); err != nil {
return nil, err
}
defer stmt.mc.finish()
return stmt.Exec(dargs)
}
func (mc *mysqlConn) watchCancel(ctx context.Context) error {
if mc.watching {
// Reach here if canceled,
// so the connection is already invalid
mc.cleanup()
return nil
}
// When ctx is already cancelled, don't watch it.
if err := ctx.Err(); err != nil {
return err
}
// When ctx is not cancellable, don't watch it.
if ctx.Done() == nil {
return nil
}
// When watcher is not alive, can't watch it.
if mc.watcher == nil {
return nil
}
mc.watching = true
mc.watcher <- ctx
return nil
}
func (mc *mysqlConn) startWatcher() {
watcher := make(chan context.Context, 1)
mc.watcher = watcher
finished := make(chan struct{})
mc.finished = finished
go func() {
for {
var ctx context.Context
select {
case ctx = <-watcher:
case <-mc.closech:
return
}
select {
case <-ctx.Done():
mc.cancel(ctx.Err())
case <-finished:
case <-mc.closech:
return
}
}
}()
}
func (mc *mysqlConn) CheckNamedValue(nv *driver.NamedValue) (err error) {
nv.Value, err = converter{}.ConvertValue(nv.Value)
return
}
// ResetSession implements driver.SessionResetter.
// (From Go 1.10)
func (mc *mysqlConn) ResetSession(ctx context.Context) error {
if mc.closed.IsSet() {
return driver.ErrBadConn
}
mc.reset = true
return nil
}

146
vendor/github.com/go-sql-driver/mysql/connector.go generated vendored Normal file
View File

@ -0,0 +1,146 @@
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
//
// Copyright 2018 The Go-MySQL-Driver Authors. All rights reserved.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package mysql
import (
"context"
"database/sql/driver"
"net"
)
type connector struct {
cfg *Config // immutable private copy.
}
// Connect implements driver.Connector interface.
// Connect returns a connection to the database.
func (c *connector) Connect(ctx context.Context) (driver.Conn, error) {
var err error
// New mysqlConn
mc := &mysqlConn{
maxAllowedPacket: maxPacketSize,
maxWriteSize: maxPacketSize - 1,
closech: make(chan struct{}),
cfg: c.cfg,
}
mc.parseTime = mc.cfg.ParseTime
// Connect to Server
dialsLock.RLock()
dial, ok := dials[mc.cfg.Net]
dialsLock.RUnlock()
if ok {
dctx := ctx
if mc.cfg.Timeout > 0 {
var cancel context.CancelFunc
dctx, cancel = context.WithTimeout(ctx, c.cfg.Timeout)
defer cancel()
}
mc.netConn, err = dial(dctx, mc.cfg.Addr)
} else {
nd := net.Dialer{Timeout: mc.cfg.Timeout}
mc.netConn, err = nd.DialContext(ctx, mc.cfg.Net, mc.cfg.Addr)
}
if err != nil {
return nil, err
}
// Enable TCP Keepalives on TCP connections
if tc, ok := mc.netConn.(*net.TCPConn); ok {
if err := tc.SetKeepAlive(true); err != nil {
// Don't send COM_QUIT before handshake.
mc.netConn.Close()
mc.netConn = nil
return nil, err
}
}
// Call startWatcher for context support (From Go 1.8)
mc.startWatcher()
if err := mc.watchCancel(ctx); err != nil {
mc.cleanup()
return nil, err
}
defer mc.finish()
mc.buf = newBuffer(mc.netConn)
// Set I/O timeouts
mc.buf.timeout = mc.cfg.ReadTimeout
mc.writeTimeout = mc.cfg.WriteTimeout
// Reading Handshake Initialization Packet
authData, plugin, err := mc.readHandshakePacket()
if err != nil {
mc.cleanup()
return nil, err
}
if plugin == "" {
plugin = defaultAuthPlugin
}
// Send Client Authentication Packet
authResp, err := mc.auth(authData, plugin)
if err != nil {
// try the default auth plugin, if using the requested plugin failed
errLog.Print("could not use requested auth plugin '"+plugin+"': ", err.Error())
plugin = defaultAuthPlugin
authResp, err = mc.auth(authData, plugin)
if err != nil {
mc.cleanup()
return nil, err
}
}
if err = mc.writeHandshakeResponsePacket(authResp, plugin); err != nil {
mc.cleanup()
return nil, err
}
// Handle response to auth packet, switch methods if possible
if err = mc.handleAuthResult(authData, plugin); err != nil {
// Authentication failed and MySQL has already closed the connection
// (https://dev.mysql.com/doc/internals/en/authentication-fails.html).
// Do not send COM_QUIT, just cleanup and return the error.
mc.cleanup()
return nil, err
}
if mc.cfg.MaxAllowedPacket > 0 {
mc.maxAllowedPacket = mc.cfg.MaxAllowedPacket
} else {
// Get max allowed packet size
maxap, err := mc.getSystemVar("max_allowed_packet")
if err != nil {
mc.Close()
return nil, err
}
mc.maxAllowedPacket = stringToInt(maxap) - 1
}
if mc.maxAllowedPacket < maxPacketSize {
mc.maxWriteSize = mc.maxAllowedPacket
}
// Handle DSN Params
err = mc.handleParams()
if err != nil {
mc.Close()
return nil, err
}
return mc, nil
}
// Driver implements driver.Connector interface.
// Driver returns &MySQLDriver{}.
func (c *connector) Driver() driver.Driver {
return &MySQLDriver{}
}

174
vendor/github.com/go-sql-driver/mysql/const.go generated vendored Normal file
View File

@ -0,0 +1,174 @@
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
//
// Copyright 2012 The Go-MySQL-Driver Authors. All rights reserved.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package mysql
const (
defaultAuthPlugin = "mysql_native_password"
defaultMaxAllowedPacket = 4 << 20 // 4 MiB
minProtocolVersion = 10
maxPacketSize = 1<<24 - 1
timeFormat = "2006-01-02 15:04:05.999999"
)
// MySQL constants documentation:
// http://dev.mysql.com/doc/internals/en/client-server-protocol.html
const (
iOK byte = 0x00
iAuthMoreData byte = 0x01
iLocalInFile byte = 0xfb
iEOF byte = 0xfe
iERR byte = 0xff
)
// https://dev.mysql.com/doc/internals/en/capability-flags.html#packet-Protocol::CapabilityFlags
type clientFlag uint32
const (
clientLongPassword clientFlag = 1 << iota
clientFoundRows
clientLongFlag
clientConnectWithDB
clientNoSchema
clientCompress
clientODBC
clientLocalFiles
clientIgnoreSpace
clientProtocol41
clientInteractive
clientSSL
clientIgnoreSIGPIPE
clientTransactions
clientReserved
clientSecureConn
clientMultiStatements
clientMultiResults
clientPSMultiResults
clientPluginAuth
clientConnectAttrs
clientPluginAuthLenEncClientData
clientCanHandleExpiredPasswords
clientSessionTrack
clientDeprecateEOF
)
const (
comQuit byte = iota + 1
comInitDB
comQuery
comFieldList
comCreateDB
comDropDB
comRefresh
comShutdown
comStatistics
comProcessInfo
comConnect
comProcessKill
comDebug
comPing
comTime
comDelayedInsert
comChangeUser
comBinlogDump
comTableDump
comConnectOut
comRegisterSlave
comStmtPrepare
comStmtExecute
comStmtSendLongData
comStmtClose
comStmtReset
comSetOption
comStmtFetch
)
// https://dev.mysql.com/doc/internals/en/com-query-response.html#packet-Protocol::ColumnType
type fieldType byte
const (
fieldTypeDecimal fieldType = iota
fieldTypeTiny
fieldTypeShort
fieldTypeLong
fieldTypeFloat
fieldTypeDouble
fieldTypeNULL
fieldTypeTimestamp
fieldTypeLongLong
fieldTypeInt24
fieldTypeDate
fieldTypeTime
fieldTypeDateTime
fieldTypeYear
fieldTypeNewDate
fieldTypeVarChar
fieldTypeBit
)
const (
fieldTypeJSON fieldType = iota + 0xf5
fieldTypeNewDecimal
fieldTypeEnum
fieldTypeSet
fieldTypeTinyBLOB
fieldTypeMediumBLOB
fieldTypeLongBLOB
fieldTypeBLOB
fieldTypeVarString
fieldTypeString
fieldTypeGeometry
)
type fieldFlag uint16
const (
flagNotNULL fieldFlag = 1 << iota
flagPriKey
flagUniqueKey
flagMultipleKey
flagBLOB
flagUnsigned
flagZeroFill
flagBinary
flagEnum
flagAutoIncrement
flagTimestamp
flagSet
flagUnknown1
flagUnknown2
flagUnknown3
flagUnknown4
)
// http://dev.mysql.com/doc/internals/en/status-flags.html
type statusFlag uint16
const (
statusInTrans statusFlag = 1 << iota
statusInAutocommit
statusReserved // Not in documentation
statusMoreResultsExists
statusNoGoodIndexUsed
statusNoIndexUsed
statusCursorExists
statusLastRowSent
statusDbDropped
statusNoBackslashEscapes
statusMetadataChanged
statusQueryWasSlow
statusPsOutParams
statusInTransReadonly
statusSessionStateChanged
)
const (
cachingSha2PasswordRequestPublicKey = 2
cachingSha2PasswordFastAuthSuccess = 3
cachingSha2PasswordPerformFullAuthentication = 4
)

107
vendor/github.com/go-sql-driver/mysql/driver.go generated vendored Normal file
View File

@ -0,0 +1,107 @@
// Copyright 2012 The Go-MySQL-Driver Authors. All rights reserved.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
// Package mysql provides a MySQL driver for Go's database/sql package.
//
// The driver should be used via the database/sql package:
//
// import "database/sql"
// import _ "github.com/go-sql-driver/mysql"
//
// db, err := sql.Open("mysql", "user:password@/dbname")
//
// See https://github.com/go-sql-driver/mysql#usage for details
package mysql
import (
"context"
"database/sql"
"database/sql/driver"
"net"
"sync"
)
// MySQLDriver is exported to make the driver directly accessible.
// In general the driver is used via the database/sql package.
type MySQLDriver struct{}
// DialFunc is a function which can be used to establish the network connection.
// Custom dial functions must be registered with RegisterDial
//
// Deprecated: users should register a DialContextFunc instead
type DialFunc func(addr string) (net.Conn, error)
// DialContextFunc is a function which can be used to establish the network connection.
// Custom dial functions must be registered with RegisterDialContext
type DialContextFunc func(ctx context.Context, addr string) (net.Conn, error)
var (
dialsLock sync.RWMutex
dials map[string]DialContextFunc
)
// RegisterDialContext registers a custom dial function. It can then be used by the
// network address mynet(addr), where mynet is the registered new network.
// The current context for the connection and its address is passed to the dial function.
func RegisterDialContext(net string, dial DialContextFunc) {
dialsLock.Lock()
defer dialsLock.Unlock()
if dials == nil {
dials = make(map[string]DialContextFunc)
}
dials[net] = dial
}
// RegisterDial registers a custom dial function. It can then be used by the
// network address mynet(addr), where mynet is the registered new network.
// addr is passed as a parameter to the dial function.
//
// Deprecated: users should call RegisterDialContext instead
func RegisterDial(network string, dial DialFunc) {
RegisterDialContext(network, func(_ context.Context, addr string) (net.Conn, error) {
return dial(addr)
})
}
// Open new Connection.
// See https://github.com/go-sql-driver/mysql#dsn-data-source-name for how
// the DSN string is formatted
func (d MySQLDriver) Open(dsn string) (driver.Conn, error) {
cfg, err := ParseDSN(dsn)
if err != nil {
return nil, err
}
c := &connector{
cfg: cfg,
}
return c.Connect(context.Background())
}
func init() {
sql.Register("mysql", &MySQLDriver{})
}
// NewConnector returns new driver.Connector.
func NewConnector(cfg *Config) (driver.Connector, error) {
cfg = cfg.Clone()
// normalize the contents of cfg so calls to NewConnector have the same
// behavior as MySQLDriver.OpenConnector
if err := cfg.normalize(); err != nil {
return nil, err
}
return &connector{cfg: cfg}, nil
}
// OpenConnector implements driver.DriverContext.
func (d MySQLDriver) OpenConnector(dsn string) (driver.Connector, error) {
cfg, err := ParseDSN(dsn)
if err != nil {
return nil, err
}
return &connector{
cfg: cfg,
}, nil
}

560
vendor/github.com/go-sql-driver/mysql/dsn.go generated vendored Normal file
View File

@ -0,0 +1,560 @@
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
//
// Copyright 2016 The Go-MySQL-Driver Authors. All rights reserved.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package mysql
import (
"bytes"
"crypto/rsa"
"crypto/tls"
"errors"
"fmt"
"math/big"
"net"
"net/url"
"sort"
"strconv"
"strings"
"time"
)
var (
errInvalidDSNUnescaped = errors.New("invalid DSN: did you forget to escape a param value?")
errInvalidDSNAddr = errors.New("invalid DSN: network address not terminated (missing closing brace)")
errInvalidDSNNoSlash = errors.New("invalid DSN: missing the slash separating the database name")
errInvalidDSNUnsafeCollation = errors.New("invalid DSN: interpolateParams can not be used with unsafe collations")
)
// Config is a configuration parsed from a DSN string.
// If a new Config is created instead of being parsed from a DSN string,
// the NewConfig function should be used, which sets default values.
type Config struct {
User string // Username
Passwd string // Password (requires User)
Net string // Network type
Addr string // Network address (requires Net)
DBName string // Database name
Params map[string]string // Connection parameters
Collation string // Connection collation
Loc *time.Location // Location for time.Time values
MaxAllowedPacket int // Max packet size allowed
ServerPubKey string // Server public key name
pubKey *rsa.PublicKey // Server public key
TLSConfig string // TLS configuration name
tls *tls.Config // TLS configuration
Timeout time.Duration // Dial timeout
ReadTimeout time.Duration // I/O read timeout
WriteTimeout time.Duration // I/O write timeout
AllowAllFiles bool // Allow all files to be used with LOAD DATA LOCAL INFILE
AllowCleartextPasswords bool // Allows the cleartext client side plugin
AllowNativePasswords bool // Allows the native password authentication method
AllowOldPasswords bool // Allows the old insecure password method
CheckConnLiveness bool // Check connections for liveness before using them
ClientFoundRows bool // Return number of matching rows instead of rows changed
ColumnsWithAlias bool // Prepend table alias to column names
InterpolateParams bool // Interpolate placeholders into query string
MultiStatements bool // Allow multiple statements in one query
ParseTime bool // Parse time values to time.Time
RejectReadOnly bool // Reject read-only connections
}
// NewConfig creates a new Config and sets default values.
func NewConfig() *Config {
return &Config{
Collation: defaultCollation,
Loc: time.UTC,
MaxAllowedPacket: defaultMaxAllowedPacket,
AllowNativePasswords: true,
CheckConnLiveness: true,
}
}
func (cfg *Config) Clone() *Config {
cp := *cfg
if cp.tls != nil {
cp.tls = cfg.tls.Clone()
}
if len(cp.Params) > 0 {
cp.Params = make(map[string]string, len(cfg.Params))
for k, v := range cfg.Params {
cp.Params[k] = v
}
}
if cfg.pubKey != nil {
cp.pubKey = &rsa.PublicKey{
N: new(big.Int).Set(cfg.pubKey.N),
E: cfg.pubKey.E,
}
}
return &cp
}
func (cfg *Config) normalize() error {
if cfg.InterpolateParams && unsafeCollations[cfg.Collation] {
return errInvalidDSNUnsafeCollation
}
// Set default network if empty
if cfg.Net == "" {
cfg.Net = "tcp"
}
// Set default address if empty
if cfg.Addr == "" {
switch cfg.Net {
case "tcp":
cfg.Addr = "127.0.0.1:3306"
case "unix":
cfg.Addr = "/tmp/mysql.sock"
default:
return errors.New("default addr for network '" + cfg.Net + "' unknown")
}
} else if cfg.Net == "tcp" {
cfg.Addr = ensureHavePort(cfg.Addr)
}
switch cfg.TLSConfig {
case "false", "":
// don't set anything
case "true":
cfg.tls = &tls.Config{}
case "skip-verify", "preferred":
cfg.tls = &tls.Config{InsecureSkipVerify: true}
default:
cfg.tls = getTLSConfigClone(cfg.TLSConfig)
if cfg.tls == nil {
return errors.New("invalid value / unknown config name: " + cfg.TLSConfig)
}
}
if cfg.tls != nil && cfg.tls.ServerName == "" && !cfg.tls.InsecureSkipVerify {
host, _, err := net.SplitHostPort(cfg.Addr)
if err == nil {
cfg.tls.ServerName = host
}
}
if cfg.ServerPubKey != "" {
cfg.pubKey = getServerPubKey(cfg.ServerPubKey)
if cfg.pubKey == nil {
return errors.New("invalid value / unknown server pub key name: " + cfg.ServerPubKey)
}
}
return nil
}
func writeDSNParam(buf *bytes.Buffer, hasParam *bool, name, value string) {
buf.Grow(1 + len(name) + 1 + len(value))
if !*hasParam {
*hasParam = true
buf.WriteByte('?')
} else {
buf.WriteByte('&')
}
buf.WriteString(name)
buf.WriteByte('=')
buf.WriteString(value)
}
// FormatDSN formats the given Config into a DSN string which can be passed to
// the driver.
func (cfg *Config) FormatDSN() string {
var buf bytes.Buffer
// [username[:password]@]
if len(cfg.User) > 0 {
buf.WriteString(cfg.User)
if len(cfg.Passwd) > 0 {
buf.WriteByte(':')
buf.WriteString(cfg.Passwd)
}
buf.WriteByte('@')
}
// [protocol[(address)]]
if len(cfg.Net) > 0 {
buf.WriteString(cfg.Net)
if len(cfg.Addr) > 0 {
buf.WriteByte('(')
buf.WriteString(cfg.Addr)
buf.WriteByte(')')
}
}
// /dbname
buf.WriteByte('/')
buf.WriteString(cfg.DBName)
// [?param1=value1&...&paramN=valueN]
hasParam := false
if cfg.AllowAllFiles {
hasParam = true
buf.WriteString("?allowAllFiles=true")
}
if cfg.AllowCleartextPasswords {
writeDSNParam(&buf, &hasParam, "allowCleartextPasswords", "true")
}
if !cfg.AllowNativePasswords {
writeDSNParam(&buf, &hasParam, "allowNativePasswords", "false")
}
if cfg.AllowOldPasswords {
writeDSNParam(&buf, &hasParam, "allowOldPasswords", "true")
}
if !cfg.CheckConnLiveness {
writeDSNParam(&buf, &hasParam, "checkConnLiveness", "false")
}
if cfg.ClientFoundRows {
writeDSNParam(&buf, &hasParam, "clientFoundRows", "true")
}
if col := cfg.Collation; col != defaultCollation && len(col) > 0 {
writeDSNParam(&buf, &hasParam, "collation", col)
}
if cfg.ColumnsWithAlias {
writeDSNParam(&buf, &hasParam, "columnsWithAlias", "true")
}
if cfg.InterpolateParams {
writeDSNParam(&buf, &hasParam, "interpolateParams", "true")
}
if cfg.Loc != time.UTC && cfg.Loc != nil {
writeDSNParam(&buf, &hasParam, "loc", url.QueryEscape(cfg.Loc.String()))
}
if cfg.MultiStatements {
writeDSNParam(&buf, &hasParam, "multiStatements", "true")
}
if cfg.ParseTime {
writeDSNParam(&buf, &hasParam, "parseTime", "true")
}
if cfg.ReadTimeout > 0 {
writeDSNParam(&buf, &hasParam, "readTimeout", cfg.ReadTimeout.String())
}
if cfg.RejectReadOnly {
writeDSNParam(&buf, &hasParam, "rejectReadOnly", "true")
}
if len(cfg.ServerPubKey) > 0 {
writeDSNParam(&buf, &hasParam, "serverPubKey", url.QueryEscape(cfg.ServerPubKey))
}
if cfg.Timeout > 0 {
writeDSNParam(&buf, &hasParam, "timeout", cfg.Timeout.String())
}
if len(cfg.TLSConfig) > 0 {
writeDSNParam(&buf, &hasParam, "tls", url.QueryEscape(cfg.TLSConfig))
}
if cfg.WriteTimeout > 0 {
writeDSNParam(&buf, &hasParam, "writeTimeout", cfg.WriteTimeout.String())
}
if cfg.MaxAllowedPacket != defaultMaxAllowedPacket {
writeDSNParam(&buf, &hasParam, "maxAllowedPacket", strconv.Itoa(cfg.MaxAllowedPacket))
}
// other params
if cfg.Params != nil {
var params []string
for param := range cfg.Params {
params = append(params, param)
}
sort.Strings(params)
for _, param := range params {
writeDSNParam(&buf, &hasParam, param, url.QueryEscape(cfg.Params[param]))
}
}
return buf.String()
}
// ParseDSN parses the DSN string to a Config
func ParseDSN(dsn string) (cfg *Config, err error) {
// New config with some default values
cfg = NewConfig()
// [user[:password]@][net[(addr)]]/dbname[?param1=value1&paramN=valueN]
// Find the last '/' (since the password or the net addr might contain a '/')
foundSlash := false
for i := len(dsn) - 1; i >= 0; i-- {
if dsn[i] == '/' {
foundSlash = true
var j, k int
// left part is empty if i <= 0
if i > 0 {
// [username[:password]@][protocol[(address)]]
// Find the last '@' in dsn[:i]
for j = i; j >= 0; j-- {
if dsn[j] == '@' {
// username[:password]
// Find the first ':' in dsn[:j]
for k = 0; k < j; k++ {
if dsn[k] == ':' {
cfg.Passwd = dsn[k+1 : j]
break
}
}
cfg.User = dsn[:k]
break
}
}
// [protocol[(address)]]
// Find the first '(' in dsn[j+1:i]
for k = j + 1; k < i; k++ {
if dsn[k] == '(' {
// dsn[i-1] must be == ')' if an address is specified
if dsn[i-1] != ')' {
if strings.ContainsRune(dsn[k+1:i], ')') {
return nil, errInvalidDSNUnescaped
}
return nil, errInvalidDSNAddr
}
cfg.Addr = dsn[k+1 : i-1]
break
}
}
cfg.Net = dsn[j+1 : k]
}
// dbname[?param1=value1&...&paramN=valueN]
// Find the first '?' in dsn[i+1:]
for j = i + 1; j < len(dsn); j++ {
if dsn[j] == '?' {
if err = parseDSNParams(cfg, dsn[j+1:]); err != nil {
return
}
break
}
}
cfg.DBName = dsn[i+1 : j]
break
}
}
if !foundSlash && len(dsn) > 0 {
return nil, errInvalidDSNNoSlash
}
if err = cfg.normalize(); err != nil {
return nil, err
}
return
}
// parseDSNParams parses the DSN "query string"
// Values must be url.QueryEscape'ed
func parseDSNParams(cfg *Config, params string) (err error) {
for _, v := range strings.Split(params, "&") {
param := strings.SplitN(v, "=", 2)
if len(param) != 2 {
continue
}
// cfg params
switch value := param[1]; param[0] {
// Disable INFILE whitelist / enable all files
case "allowAllFiles":
var isBool bool
cfg.AllowAllFiles, isBool = readBool(value)
if !isBool {
return errors.New("invalid bool value: " + value)
}
// Use cleartext authentication mode (MySQL 5.5.10+)
case "allowCleartextPasswords":
var isBool bool
cfg.AllowCleartextPasswords, isBool = readBool(value)
if !isBool {
return errors.New("invalid bool value: " + value)
}
// Use native password authentication
case "allowNativePasswords":
var isBool bool
cfg.AllowNativePasswords, isBool = readBool(value)
if !isBool {
return errors.New("invalid bool value: " + value)
}
// Use old authentication mode (pre MySQL 4.1)
case "allowOldPasswords":
var isBool bool
cfg.AllowOldPasswords, isBool = readBool(value)
if !isBool {
return errors.New("invalid bool value: " + value)
}
// Check connections for Liveness before using them
case "checkConnLiveness":
var isBool bool
cfg.CheckConnLiveness, isBool = readBool(value)
if !isBool {
return errors.New("invalid bool value: " + value)
}
// Switch "rowsAffected" mode
case "clientFoundRows":
var isBool bool
cfg.ClientFoundRows, isBool = readBool(value)
if !isBool {
return errors.New("invalid bool value: " + value)
}
// Collation
case "collation":
cfg.Collation = value
break
case "columnsWithAlias":
var isBool bool
cfg.ColumnsWithAlias, isBool = readBool(value)
if !isBool {
return errors.New("invalid bool value: " + value)
}
// Compression
case "compress":
return errors.New("compression not implemented yet")
// Enable client side placeholder substitution
case "interpolateParams":
var isBool bool
cfg.InterpolateParams, isBool = readBool(value)
if !isBool {
return errors.New("invalid bool value: " + value)
}
// Time Location
case "loc":
if value, err = url.QueryUnescape(value); err != nil {
return
}
cfg.Loc, err = time.LoadLocation(value)
if err != nil {
return
}
// multiple statements in one query
case "multiStatements":
var isBool bool
cfg.MultiStatements, isBool = readBool(value)
if !isBool {
return errors.New("invalid bool value: " + value)
}
// time.Time parsing
case "parseTime":
var isBool bool
cfg.ParseTime, isBool = readBool(value)
if !isBool {
return errors.New("invalid bool value: " + value)
}
// I/O read Timeout
case "readTimeout":
cfg.ReadTimeout, err = time.ParseDuration(value)
if err != nil {
return
}
// Reject read-only connections
case "rejectReadOnly":
var isBool bool
cfg.RejectReadOnly, isBool = readBool(value)
if !isBool {
return errors.New("invalid bool value: " + value)
}
// Server public key
case "serverPubKey":
name, err := url.QueryUnescape(value)
if err != nil {
return fmt.Errorf("invalid value for server pub key name: %v", err)
}
cfg.ServerPubKey = name
// Strict mode
case "strict":
panic("strict mode has been removed. See https://github.com/go-sql-driver/mysql/wiki/strict-mode")
// Dial Timeout
case "timeout":
cfg.Timeout, err = time.ParseDuration(value)
if err != nil {
return
}
// TLS-Encryption
case "tls":
boolValue, isBool := readBool(value)
if isBool {
if boolValue {
cfg.TLSConfig = "true"
} else {
cfg.TLSConfig = "false"
}
} else if vl := strings.ToLower(value); vl == "skip-verify" || vl == "preferred" {
cfg.TLSConfig = vl
} else {
name, err := url.QueryUnescape(value)
if err != nil {
return fmt.Errorf("invalid value for TLS config name: %v", err)
}
cfg.TLSConfig = name
}
// I/O write Timeout
case "writeTimeout":
cfg.WriteTimeout, err = time.ParseDuration(value)
if err != nil {
return
}
case "maxAllowedPacket":
cfg.MaxAllowedPacket, err = strconv.Atoi(value)
if err != nil {
return
}
default:
// lazy init
if cfg.Params == nil {
cfg.Params = make(map[string]string)
}
if cfg.Params[param[0]], err = url.QueryUnescape(value); err != nil {
return
}
}
}
return
}
func ensureHavePort(addr string) string {
if _, _, err := net.SplitHostPort(addr); err != nil {
return net.JoinHostPort(addr, "3306")
}
return addr
}

65
vendor/github.com/go-sql-driver/mysql/errors.go generated vendored Normal file
View File

@ -0,0 +1,65 @@
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
//
// Copyright 2013 The Go-MySQL-Driver Authors. All rights reserved.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package mysql
import (
"errors"
"fmt"
"log"
"os"
)
// Various errors the driver might return. Can change between driver versions.
var (
ErrInvalidConn = errors.New("invalid connection")
ErrMalformPkt = errors.New("malformed packet")
ErrNoTLS = errors.New("TLS requested but server does not support TLS")
ErrCleartextPassword = errors.New("this user requires clear text authentication. If you still want to use it, please add 'allowCleartextPasswords=1' to your DSN")
ErrNativePassword = errors.New("this user requires mysql native password authentication.")
ErrOldPassword = errors.New("this user requires old password authentication. If you still want to use it, please add 'allowOldPasswords=1' to your DSN. See also https://github.com/go-sql-driver/mysql/wiki/old_passwords")
ErrUnknownPlugin = errors.New("this authentication plugin is not supported")
ErrOldProtocol = errors.New("MySQL server does not support required protocol 41+")
ErrPktSync = errors.New("commands out of sync. You can't run this command now")
ErrPktSyncMul = errors.New("commands out of sync. Did you run multiple statements at once?")
ErrPktTooLarge = errors.New("packet for query is too large. Try adjusting the 'max_allowed_packet' variable on the server")
ErrBusyBuffer = errors.New("busy buffer")
// errBadConnNoWrite is used for connection errors where nothing was sent to the database yet.
// If this happens first in a function starting a database interaction, it should be replaced by driver.ErrBadConn
// to trigger a resend.
// See https://github.com/go-sql-driver/mysql/pull/302
errBadConnNoWrite = errors.New("bad connection")
)
var errLog = Logger(log.New(os.Stderr, "[mysql] ", log.Ldate|log.Ltime|log.Lshortfile))
// Logger is used to log critical error messages.
type Logger interface {
Print(v ...interface{})
}
// SetLogger is used to set the logger for critical errors.
// The initial logger is os.Stderr.
func SetLogger(logger Logger) error {
if logger == nil {
return errors.New("logger is nil")
}
errLog = logger
return nil
}
// MySQLError is an error type which represents a single MySQL error
type MySQLError struct {
Number uint16
Message string
}
func (me *MySQLError) Error() string {
return fmt.Sprintf("Error %d: %s", me.Number, me.Message)
}

194
vendor/github.com/go-sql-driver/mysql/fields.go generated vendored Normal file
View File

@ -0,0 +1,194 @@
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
//
// Copyright 2017 The Go-MySQL-Driver Authors. All rights reserved.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package mysql
import (
"database/sql"
"reflect"
)
func (mf *mysqlField) typeDatabaseName() string {
switch mf.fieldType {
case fieldTypeBit:
return "BIT"
case fieldTypeBLOB:
if mf.charSet != collations[binaryCollation] {
return "TEXT"
}
return "BLOB"
case fieldTypeDate:
return "DATE"
case fieldTypeDateTime:
return "DATETIME"
case fieldTypeDecimal:
return "DECIMAL"
case fieldTypeDouble:
return "DOUBLE"
case fieldTypeEnum:
return "ENUM"
case fieldTypeFloat:
return "FLOAT"
case fieldTypeGeometry:
return "GEOMETRY"
case fieldTypeInt24:
return "MEDIUMINT"
case fieldTypeJSON:
return "JSON"
case fieldTypeLong:
return "INT"
case fieldTypeLongBLOB:
if mf.charSet != collations[binaryCollation] {
return "LONGTEXT"
}
return "LONGBLOB"
case fieldTypeLongLong:
return "BIGINT"
case fieldTypeMediumBLOB:
if mf.charSet != collations[binaryCollation] {
return "MEDIUMTEXT"
}
return "MEDIUMBLOB"
case fieldTypeNewDate:
return "DATE"
case fieldTypeNewDecimal:
return "DECIMAL"
case fieldTypeNULL:
return "NULL"
case fieldTypeSet:
return "SET"
case fieldTypeShort:
return "SMALLINT"
case fieldTypeString:
if mf.charSet == collations[binaryCollation] {
return "BINARY"
}
return "CHAR"
case fieldTypeTime:
return "TIME"
case fieldTypeTimestamp:
return "TIMESTAMP"
case fieldTypeTiny:
return "TINYINT"
case fieldTypeTinyBLOB:
if mf.charSet != collations[binaryCollation] {
return "TINYTEXT"
}
return "TINYBLOB"
case fieldTypeVarChar:
if mf.charSet == collations[binaryCollation] {
return "VARBINARY"
}
return "VARCHAR"
case fieldTypeVarString:
if mf.charSet == collations[binaryCollation] {
return "VARBINARY"
}
return "VARCHAR"
case fieldTypeYear:
return "YEAR"
default:
return ""
}
}
var (
scanTypeFloat32 = reflect.TypeOf(float32(0))
scanTypeFloat64 = reflect.TypeOf(float64(0))
scanTypeInt8 = reflect.TypeOf(int8(0))
scanTypeInt16 = reflect.TypeOf(int16(0))
scanTypeInt32 = reflect.TypeOf(int32(0))
scanTypeInt64 = reflect.TypeOf(int64(0))
scanTypeNullFloat = reflect.TypeOf(sql.NullFloat64{})
scanTypeNullInt = reflect.TypeOf(sql.NullInt64{})
scanTypeNullTime = reflect.TypeOf(NullTime{})
scanTypeUint8 = reflect.TypeOf(uint8(0))
scanTypeUint16 = reflect.TypeOf(uint16(0))
scanTypeUint32 = reflect.TypeOf(uint32(0))
scanTypeUint64 = reflect.TypeOf(uint64(0))
scanTypeRawBytes = reflect.TypeOf(sql.RawBytes{})
scanTypeUnknown = reflect.TypeOf(new(interface{}))
)
type mysqlField struct {
tableName string
name string
length uint32
flags fieldFlag
fieldType fieldType
decimals byte
charSet uint8
}
func (mf *mysqlField) scanType() reflect.Type {
switch mf.fieldType {
case fieldTypeTiny:
if mf.flags&flagNotNULL != 0 {
if mf.flags&flagUnsigned != 0 {
return scanTypeUint8
}
return scanTypeInt8
}
return scanTypeNullInt
case fieldTypeShort, fieldTypeYear:
if mf.flags&flagNotNULL != 0 {
if mf.flags&flagUnsigned != 0 {
return scanTypeUint16
}
return scanTypeInt16
}
return scanTypeNullInt
case fieldTypeInt24, fieldTypeLong:
if mf.flags&flagNotNULL != 0 {
if mf.flags&flagUnsigned != 0 {
return scanTypeUint32
}
return scanTypeInt32
}
return scanTypeNullInt
case fieldTypeLongLong:
if mf.flags&flagNotNULL != 0 {
if mf.flags&flagUnsigned != 0 {
return scanTypeUint64
}
return scanTypeInt64
}
return scanTypeNullInt
case fieldTypeFloat:
if mf.flags&flagNotNULL != 0 {
return scanTypeFloat32
}
return scanTypeNullFloat
case fieldTypeDouble:
if mf.flags&flagNotNULL != 0 {
return scanTypeFloat64
}
return scanTypeNullFloat
case fieldTypeDecimal, fieldTypeNewDecimal, fieldTypeVarChar,
fieldTypeBit, fieldTypeEnum, fieldTypeSet, fieldTypeTinyBLOB,
fieldTypeMediumBLOB, fieldTypeLongBLOB, fieldTypeBLOB,
fieldTypeVarString, fieldTypeString, fieldTypeGeometry, fieldTypeJSON,
fieldTypeTime:
return scanTypeRawBytes
case fieldTypeDate, fieldTypeNewDate,
fieldTypeTimestamp, fieldTypeDateTime:
// NullTime is always returned for more consistent behavior as it can
// handle both cases of parseTime regardless if the field is nullable.
return scanTypeNullTime
default:
return scanTypeUnknown
}
}

3
vendor/github.com/go-sql-driver/mysql/go.mod generated vendored Normal file
View File

@ -0,0 +1,3 @@
module github.com/go-sql-driver/mysql
go 1.10

182
vendor/github.com/go-sql-driver/mysql/infile.go generated vendored Normal file
View File

@ -0,0 +1,182 @@
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
//
// Copyright 2013 The Go-MySQL-Driver Authors. All rights reserved.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package mysql
import (
"fmt"
"io"
"os"
"strings"
"sync"
)
var (
fileRegister map[string]bool
fileRegisterLock sync.RWMutex
readerRegister map[string]func() io.Reader
readerRegisterLock sync.RWMutex
)
// RegisterLocalFile adds the given file to the file whitelist,
// so that it can be used by "LOAD DATA LOCAL INFILE <filepath>".
// Alternatively you can allow the use of all local files with
// the DSN parameter 'allowAllFiles=true'
//
// filePath := "/home/gopher/data.csv"
// mysql.RegisterLocalFile(filePath)
// err := db.Exec("LOAD DATA LOCAL INFILE '" + filePath + "' INTO TABLE foo")
// if err != nil {
// ...
//
func RegisterLocalFile(filePath string) {
fileRegisterLock.Lock()
// lazy map init
if fileRegister == nil {
fileRegister = make(map[string]bool)
}
fileRegister[strings.Trim(filePath, `"`)] = true
fileRegisterLock.Unlock()
}
// DeregisterLocalFile removes the given filepath from the whitelist.
func DeregisterLocalFile(filePath string) {
fileRegisterLock.Lock()
delete(fileRegister, strings.Trim(filePath, `"`))
fileRegisterLock.Unlock()
}
// RegisterReaderHandler registers a handler function which is used
// to receive a io.Reader.
// The Reader can be used by "LOAD DATA LOCAL INFILE Reader::<name>".
// If the handler returns a io.ReadCloser Close() is called when the
// request is finished.
//
// mysql.RegisterReaderHandler("data", func() io.Reader {
// var csvReader io.Reader // Some Reader that returns CSV data
// ... // Open Reader here
// return csvReader
// })
// err := db.Exec("LOAD DATA LOCAL INFILE 'Reader::data' INTO TABLE foo")
// if err != nil {
// ...
//
func RegisterReaderHandler(name string, handler func() io.Reader) {
readerRegisterLock.Lock()
// lazy map init
if readerRegister == nil {
readerRegister = make(map[string]func() io.Reader)
}
readerRegister[name] = handler
readerRegisterLock.Unlock()
}
// DeregisterReaderHandler removes the ReaderHandler function with
// the given name from the registry.
func DeregisterReaderHandler(name string) {
readerRegisterLock.Lock()
delete(readerRegister, name)
readerRegisterLock.Unlock()
}
func deferredClose(err *error, closer io.Closer) {
closeErr := closer.Close()
if *err == nil {
*err = closeErr
}
}
func (mc *mysqlConn) handleInFileRequest(name string) (err error) {
var rdr io.Reader
var data []byte
packetSize := 16 * 1024 // 16KB is small enough for disk readahead and large enough for TCP
if mc.maxWriteSize < packetSize {
packetSize = mc.maxWriteSize
}
if idx := strings.Index(name, "Reader::"); idx == 0 || (idx > 0 && name[idx-1] == '/') { // io.Reader
// The server might return an an absolute path. See issue #355.
name = name[idx+8:]
readerRegisterLock.RLock()
handler, inMap := readerRegister[name]
readerRegisterLock.RUnlock()
if inMap {
rdr = handler()
if rdr != nil {
if cl, ok := rdr.(io.Closer); ok {
defer deferredClose(&err, cl)
}
} else {
err = fmt.Errorf("Reader '%s' is <nil>", name)
}
} else {
err = fmt.Errorf("Reader '%s' is not registered", name)
}
} else { // File
name = strings.Trim(name, `"`)
fileRegisterLock.RLock()
fr := fileRegister[name]
fileRegisterLock.RUnlock()
if mc.cfg.AllowAllFiles || fr {
var file *os.File
var fi os.FileInfo
if file, err = os.Open(name); err == nil {
defer deferredClose(&err, file)
// get file size
if fi, err = file.Stat(); err == nil {
rdr = file
if fileSize := int(fi.Size()); fileSize < packetSize {
packetSize = fileSize
}
}
}
} else {
err = fmt.Errorf("local file '%s' is not registered", name)
}
}
// send content packets
// if packetSize == 0, the Reader contains no data
if err == nil && packetSize > 0 {
data := make([]byte, 4+packetSize)
var n int
for err == nil {
n, err = rdr.Read(data[4:])
if n > 0 {
if ioErr := mc.writePacket(data[:4+n]); ioErr != nil {
return ioErr
}
}
}
if err == io.EOF {
err = nil
}
}
// send empty packet (termination)
if data == nil {
data = make([]byte, 4)
}
if ioErr := mc.writePacket(data[:4]); ioErr != nil {
return ioErr
}
// read OK packet
if err == nil {
return mc.readResultOK()
}
mc.readPacket()
return err
}

50
vendor/github.com/go-sql-driver/mysql/nulltime.go generated vendored Normal file
View File

@ -0,0 +1,50 @@
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
//
// Copyright 2013 The Go-MySQL-Driver Authors. All rights reserved.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package mysql
import (
"database/sql/driver"
"fmt"
"time"
)
// Scan implements the Scanner interface.
// The value type must be time.Time or string / []byte (formatted time-string),
// otherwise Scan fails.
func (nt *NullTime) Scan(value interface{}) (err error) {
if value == nil {
nt.Time, nt.Valid = time.Time{}, false
return
}
switch v := value.(type) {
case time.Time:
nt.Time, nt.Valid = v, true
return
case []byte:
nt.Time, err = parseDateTime(string(v), time.UTC)
nt.Valid = (err == nil)
return
case string:
nt.Time, err = parseDateTime(v, time.UTC)
nt.Valid = (err == nil)
return
}
nt.Valid = false
return fmt.Errorf("Can't convert %T to time.Time", value)
}
// Value implements the driver Valuer interface.
func (nt NullTime) Value() (driver.Value, error) {
if !nt.Valid {
return nil, nil
}
return nt.Time, nil
}

View File

@ -0,0 +1,31 @@
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
//
// Copyright 2013 The Go-MySQL-Driver Authors. All rights reserved.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
// +build go1.13
package mysql
import (
"database/sql"
)
// NullTime represents a time.Time that may be NULL.
// NullTime implements the Scanner interface so
// it can be used as a scan destination:
//
// var nt NullTime
// err := db.QueryRow("SELECT time FROM foo WHERE id=?", id).Scan(&nt)
// ...
// if nt.Valid {
// // use nt.Time
// } else {
// // NULL value
// }
//
// This NullTime implementation is not driver-specific
type NullTime sql.NullTime

View File

@ -0,0 +1,34 @@
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
//
// Copyright 2013 The Go-MySQL-Driver Authors. All rights reserved.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
// +build !go1.13
package mysql
import (
"time"
)
// NullTime represents a time.Time that may be NULL.
// NullTime implements the Scanner interface so
// it can be used as a scan destination:
//
// var nt NullTime
// err := db.QueryRow("SELECT time FROM foo WHERE id=?", id).Scan(&nt)
// ...
// if nt.Valid {
// // use nt.Time
// } else {
// // NULL value
// }
//
// This NullTime implementation is not driver-specific
type NullTime struct {
Time time.Time
Valid bool // Valid is true if Time is not NULL
}

1342
vendor/github.com/go-sql-driver/mysql/packets.go generated vendored Normal file

File diff suppressed because it is too large Load Diff

22
vendor/github.com/go-sql-driver/mysql/result.go generated vendored Normal file
View File

@ -0,0 +1,22 @@
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
//
// Copyright 2012 The Go-MySQL-Driver Authors. All rights reserved.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package mysql
type mysqlResult struct {
affectedRows int64
insertId int64
}
func (res *mysqlResult) LastInsertId() (int64, error) {
return res.insertId, nil
}
func (res *mysqlResult) RowsAffected() (int64, error) {
return res.affectedRows, nil
}

223
vendor/github.com/go-sql-driver/mysql/rows.go generated vendored Normal file
View File

@ -0,0 +1,223 @@
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
//
// Copyright 2012 The Go-MySQL-Driver Authors. All rights reserved.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package mysql
import (
"database/sql/driver"
"io"
"math"
"reflect"
)
type resultSet struct {
columns []mysqlField
columnNames []string
done bool
}
type mysqlRows struct {
mc *mysqlConn
rs resultSet
finish func()
}
type binaryRows struct {
mysqlRows
}
type textRows struct {
mysqlRows
}
func (rows *mysqlRows) Columns() []string {
if rows.rs.columnNames != nil {
return rows.rs.columnNames
}
columns := make([]string, len(rows.rs.columns))
if rows.mc != nil && rows.mc.cfg.ColumnsWithAlias {
for i := range columns {
if tableName := rows.rs.columns[i].tableName; len(tableName) > 0 {
columns[i] = tableName + "." + rows.rs.columns[i].name
} else {
columns[i] = rows.rs.columns[i].name
}
}
} else {
for i := range columns {
columns[i] = rows.rs.columns[i].name
}
}
rows.rs.columnNames = columns
return columns
}
func (rows *mysqlRows) ColumnTypeDatabaseTypeName(i int) string {
return rows.rs.columns[i].typeDatabaseName()
}
// func (rows *mysqlRows) ColumnTypeLength(i int) (length int64, ok bool) {
// return int64(rows.rs.columns[i].length), true
// }
func (rows *mysqlRows) ColumnTypeNullable(i int) (nullable, ok bool) {
return rows.rs.columns[i].flags&flagNotNULL == 0, true
}
func (rows *mysqlRows) ColumnTypePrecisionScale(i int) (int64, int64, bool) {
column := rows.rs.columns[i]
decimals := int64(column.decimals)
switch column.fieldType {
case fieldTypeDecimal, fieldTypeNewDecimal:
if decimals > 0 {
return int64(column.length) - 2, decimals, true
}
return int64(column.length) - 1, decimals, true
case fieldTypeTimestamp, fieldTypeDateTime, fieldTypeTime:
return decimals, decimals, true
case fieldTypeFloat, fieldTypeDouble:
if decimals == 0x1f {
return math.MaxInt64, math.MaxInt64, true
}
return math.MaxInt64, decimals, true
}
return 0, 0, false
}
func (rows *mysqlRows) ColumnTypeScanType(i int) reflect.Type {
return rows.rs.columns[i].scanType()
}
func (rows *mysqlRows) Close() (err error) {
if f := rows.finish; f != nil {
f()
rows.finish = nil
}
mc := rows.mc
if mc == nil {
return nil
}
if err := mc.error(); err != nil {
return err
}
// flip the buffer for this connection if we need to drain it.
// note that for a successful query (i.e. one where rows.next()
// has been called until it returns false), `rows.mc` will be nil
// by the time the user calls `(*Rows).Close`, so we won't reach this
// see: https://github.com/golang/go/commit/651ddbdb5056ded455f47f9c494c67b389622a47
mc.buf.flip()
// Remove unread packets from stream
if !rows.rs.done {
err = mc.readUntilEOF()
}
if err == nil {
if err = mc.discardResults(); err != nil {
return err
}
}
rows.mc = nil
return err
}
func (rows *mysqlRows) HasNextResultSet() (b bool) {
if rows.mc == nil {
return false
}
return rows.mc.status&statusMoreResultsExists != 0
}
func (rows *mysqlRows) nextResultSet() (int, error) {
if rows.mc == nil {
return 0, io.EOF
}
if err := rows.mc.error(); err != nil {
return 0, err
}
// Remove unread packets from stream
if !rows.rs.done {
if err := rows.mc.readUntilEOF(); err != nil {
return 0, err
}
rows.rs.done = true
}
if !rows.HasNextResultSet() {
rows.mc = nil
return 0, io.EOF
}
rows.rs = resultSet{}
return rows.mc.readResultSetHeaderPacket()
}
func (rows *mysqlRows) nextNotEmptyResultSet() (int, error) {
for {
resLen, err := rows.nextResultSet()
if err != nil {
return 0, err
}
if resLen > 0 {
return resLen, nil
}
rows.rs.done = true
}
}
func (rows *binaryRows) NextResultSet() error {
resLen, err := rows.nextNotEmptyResultSet()
if err != nil {
return err
}
rows.rs.columns, err = rows.mc.readColumns(resLen)
return err
}
func (rows *binaryRows) Next(dest []driver.Value) error {
if mc := rows.mc; mc != nil {
if err := mc.error(); err != nil {
return err
}
// Fetch next row from stream
return rows.readRow(dest)
}
return io.EOF
}
func (rows *textRows) NextResultSet() (err error) {
resLen, err := rows.nextNotEmptyResultSet()
if err != nil {
return err
}
rows.rs.columns, err = rows.mc.readColumns(resLen)
return err
}
func (rows *textRows) Next(dest []driver.Value) error {
if mc := rows.mc; mc != nil {
if err := mc.error(); err != nil {
return err
}
// Fetch next row from stream
return rows.readRow(dest)
}
return io.EOF
}

204
vendor/github.com/go-sql-driver/mysql/statement.go generated vendored Normal file
View File

@ -0,0 +1,204 @@
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
//
// Copyright 2012 The Go-MySQL-Driver Authors. All rights reserved.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package mysql
import (
"database/sql/driver"
"fmt"
"io"
"reflect"
)
type mysqlStmt struct {
mc *mysqlConn
id uint32
paramCount int
}
func (stmt *mysqlStmt) Close() error {
if stmt.mc == nil || stmt.mc.closed.IsSet() {
// driver.Stmt.Close can be called more than once, thus this function
// has to be idempotent.
// See also Issue #450 and golang/go#16019.
//errLog.Print(ErrInvalidConn)
return driver.ErrBadConn
}
err := stmt.mc.writeCommandPacketUint32(comStmtClose, stmt.id)
stmt.mc = nil
return err
}
func (stmt *mysqlStmt) NumInput() int {
return stmt.paramCount
}
func (stmt *mysqlStmt) ColumnConverter(idx int) driver.ValueConverter {
return converter{}
}
func (stmt *mysqlStmt) Exec(args []driver.Value) (driver.Result, error) {
if stmt.mc.closed.IsSet() {
errLog.Print(ErrInvalidConn)
return nil, driver.ErrBadConn
}
// Send command
err := stmt.writeExecutePacket(args)
if err != nil {
return nil, stmt.mc.markBadConn(err)
}
mc := stmt.mc
mc.affectedRows = 0
mc.insertId = 0
// Read Result
resLen, err := mc.readResultSetHeaderPacket()
if err != nil {
return nil, err
}
if resLen > 0 {
// Columns
if err = mc.readUntilEOF(); err != nil {
return nil, err
}
// Rows
if err := mc.readUntilEOF(); err != nil {
return nil, err
}
}
if err := mc.discardResults(); err != nil {
return nil, err
}
return &mysqlResult{
affectedRows: int64(mc.affectedRows),
insertId: int64(mc.insertId),
}, nil
}
func (stmt *mysqlStmt) Query(args []driver.Value) (driver.Rows, error) {
return stmt.query(args)
}
func (stmt *mysqlStmt) query(args []driver.Value) (*binaryRows, error) {
if stmt.mc.closed.IsSet() {
errLog.Print(ErrInvalidConn)
return nil, driver.ErrBadConn
}
// Send command
err := stmt.writeExecutePacket(args)
if err != nil {
return nil, stmt.mc.markBadConn(err)
}
mc := stmt.mc
// Read Result
resLen, err := mc.readResultSetHeaderPacket()
if err != nil {
return nil, err
}
rows := new(binaryRows)
if resLen > 0 {
rows.mc = mc
rows.rs.columns, err = mc.readColumns(resLen)
} else {
rows.rs.done = true
switch err := rows.NextResultSet(); err {
case nil, io.EOF:
return rows, nil
default:
return nil, err
}
}
return rows, err
}
type converter struct{}
// ConvertValue mirrors the reference/default converter in database/sql/driver
// with _one_ exception. We support uint64 with their high bit and the default
// implementation does not. This function should be kept in sync with
// database/sql/driver defaultConverter.ConvertValue() except for that
// deliberate difference.
func (c converter) ConvertValue(v interface{}) (driver.Value, error) {
if driver.IsValue(v) {
return v, nil
}
if vr, ok := v.(driver.Valuer); ok {
sv, err := callValuerValue(vr)
if err != nil {
return nil, err
}
if !driver.IsValue(sv) {
return nil, fmt.Errorf("non-Value type %T returned from Value", sv)
}
return sv, nil
}
rv := reflect.ValueOf(v)
switch rv.Kind() {
case reflect.Ptr:
// indirect pointers
if rv.IsNil() {
return nil, nil
} else {
return c.ConvertValue(rv.Elem().Interface())
}
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return rv.Int(), nil
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return rv.Uint(), nil
case reflect.Float32, reflect.Float64:
return rv.Float(), nil
case reflect.Bool:
return rv.Bool(), nil
case reflect.Slice:
ek := rv.Type().Elem().Kind()
if ek == reflect.Uint8 {
return rv.Bytes(), nil
}
return nil, fmt.Errorf("unsupported type %T, a slice of %s", v, ek)
case reflect.String:
return rv.String(), nil
}
return nil, fmt.Errorf("unsupported type %T, a %s", v, rv.Kind())
}
var valuerReflectType = reflect.TypeOf((*driver.Valuer)(nil)).Elem()
// callValuerValue returns vr.Value(), with one exception:
// If vr.Value is an auto-generated method on a pointer type and the
// pointer is nil, it would panic at runtime in the panicwrap
// method. Treat it like nil instead.
//
// This is so people can implement driver.Value on value types and
// still use nil pointers to those types to mean nil/NULL, just like
// string/*string.
//
// This is an exact copy of the same-named unexported function from the
// database/sql package.
func callValuerValue(vr driver.Valuer) (v driver.Value, err error) {
if rv := reflect.ValueOf(vr); rv.Kind() == reflect.Ptr &&
rv.IsNil() &&
rv.Type().Elem().Implements(valuerReflectType) {
return nil, nil
}
return vr.Value()
}

31
vendor/github.com/go-sql-driver/mysql/transaction.go generated vendored Normal file
View File

@ -0,0 +1,31 @@
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
//
// Copyright 2012 The Go-MySQL-Driver Authors. All rights reserved.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package mysql
type mysqlTx struct {
mc *mysqlConn
}
func (tx *mysqlTx) Commit() (err error) {
if tx.mc == nil || tx.mc.closed.IsSet() {
return ErrInvalidConn
}
err = tx.mc.exec("COMMIT")
tx.mc = nil
return
}
func (tx *mysqlTx) Rollback() (err error) {
if tx.mc == nil || tx.mc.closed.IsSet() {
return ErrInvalidConn
}
err = tx.mc.exec("ROLLBACK")
tx.mc = nil
return
}

701
vendor/github.com/go-sql-driver/mysql/utils.go generated vendored Normal file
View File

@ -0,0 +1,701 @@
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
//
// Copyright 2012 The Go-MySQL-Driver Authors. All rights reserved.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package mysql
import (
"crypto/tls"
"database/sql"
"database/sql/driver"
"encoding/binary"
"errors"
"fmt"
"io"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
)
// Registry for custom tls.Configs
var (
tlsConfigLock sync.RWMutex
tlsConfigRegistry map[string]*tls.Config
)
// RegisterTLSConfig registers a custom tls.Config to be used with sql.Open.
// Use the key as a value in the DSN where tls=value.
//
// Note: The provided tls.Config is exclusively owned by the driver after
// registering it.
//
// rootCertPool := x509.NewCertPool()
// pem, err := ioutil.ReadFile("/path/ca-cert.pem")
// if err != nil {
// log.Fatal(err)
// }
// if ok := rootCertPool.AppendCertsFromPEM(pem); !ok {
// log.Fatal("Failed to append PEM.")
// }
// clientCert := make([]tls.Certificate, 0, 1)
// certs, err := tls.LoadX509KeyPair("/path/client-cert.pem", "/path/client-key.pem")
// if err != nil {
// log.Fatal(err)
// }
// clientCert = append(clientCert, certs)
// mysql.RegisterTLSConfig("custom", &tls.Config{
// RootCAs: rootCertPool,
// Certificates: clientCert,
// })
// db, err := sql.Open("mysql", "user@tcp(localhost:3306)/test?tls=custom")
//
func RegisterTLSConfig(key string, config *tls.Config) error {
if _, isBool := readBool(key); isBool || strings.ToLower(key) == "skip-verify" || strings.ToLower(key) == "preferred" {
return fmt.Errorf("key '%s' is reserved", key)
}
tlsConfigLock.Lock()
if tlsConfigRegistry == nil {
tlsConfigRegistry = make(map[string]*tls.Config)
}
tlsConfigRegistry[key] = config
tlsConfigLock.Unlock()
return nil
}
// DeregisterTLSConfig removes the tls.Config associated with key.
func DeregisterTLSConfig(key string) {
tlsConfigLock.Lock()
if tlsConfigRegistry != nil {
delete(tlsConfigRegistry, key)
}
tlsConfigLock.Unlock()
}
func getTLSConfigClone(key string) (config *tls.Config) {
tlsConfigLock.RLock()
if v, ok := tlsConfigRegistry[key]; ok {
config = v.Clone()
}
tlsConfigLock.RUnlock()
return
}
// Returns the bool value of the input.
// The 2nd return value indicates if the input was a valid bool value
func readBool(input string) (value bool, valid bool) {
switch input {
case "1", "true", "TRUE", "True":
return true, true
case "0", "false", "FALSE", "False":
return false, true
}
// Not a valid bool value
return
}
/******************************************************************************
* Time related utils *
******************************************************************************/
func parseDateTime(str string, loc *time.Location) (t time.Time, err error) {
base := "0000-00-00 00:00:00.0000000"
switch len(str) {
case 10, 19, 21, 22, 23, 24, 25, 26: // up to "YYYY-MM-DD HH:MM:SS.MMMMMM"
if str == base[:len(str)] {
return
}
t, err = time.Parse(timeFormat[:len(str)], str)
default:
err = fmt.Errorf("invalid time string: %s", str)
return
}
// Adjust location
if err == nil && loc != time.UTC {
y, mo, d := t.Date()
h, mi, s := t.Clock()
t, err = time.Date(y, mo, d, h, mi, s, t.Nanosecond(), loc), nil
}
return
}
func parseBinaryDateTime(num uint64, data []byte, loc *time.Location) (driver.Value, error) {
switch num {
case 0:
return time.Time{}, nil
case 4:
return time.Date(
int(binary.LittleEndian.Uint16(data[:2])), // year
time.Month(data[2]), // month
int(data[3]), // day
0, 0, 0, 0,
loc,
), nil
case 7:
return time.Date(
int(binary.LittleEndian.Uint16(data[:2])), // year
time.Month(data[2]), // month
int(data[3]), // day
int(data[4]), // hour
int(data[5]), // minutes
int(data[6]), // seconds
0,
loc,
), nil
case 11:
return time.Date(
int(binary.LittleEndian.Uint16(data[:2])), // year
time.Month(data[2]), // month
int(data[3]), // day
int(data[4]), // hour
int(data[5]), // minutes
int(data[6]), // seconds
int(binary.LittleEndian.Uint32(data[7:11]))*1000, // nanoseconds
loc,
), nil
}
return nil, fmt.Errorf("invalid DATETIME packet length %d", num)
}
// zeroDateTime is used in formatBinaryDateTime to avoid an allocation
// if the DATE or DATETIME has the zero value.
// It must never be changed.
// The current behavior depends on database/sql copying the result.
var zeroDateTime = []byte("0000-00-00 00:00:00.000000")
const digits01 = "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"
const digits10 = "0000000000111111111122222222223333333333444444444455555555556666666666777777777788888888889999999999"
func appendMicrosecs(dst, src []byte, decimals int) []byte {
if decimals <= 0 {
return dst
}
if len(src) == 0 {
return append(dst, ".000000"[:decimals+1]...)
}
microsecs := binary.LittleEndian.Uint32(src[:4])
p1 := byte(microsecs / 10000)
microsecs -= 10000 * uint32(p1)
p2 := byte(microsecs / 100)
microsecs -= 100 * uint32(p2)
p3 := byte(microsecs)
switch decimals {
default:
return append(dst, '.',
digits10[p1], digits01[p1],
digits10[p2], digits01[p2],
digits10[p3], digits01[p3],
)
case 1:
return append(dst, '.',
digits10[p1],
)
case 2:
return append(dst, '.',
digits10[p1], digits01[p1],
)
case 3:
return append(dst, '.',
digits10[p1], digits01[p1],
digits10[p2],
)
case 4:
return append(dst, '.',
digits10[p1], digits01[p1],
digits10[p2], digits01[p2],
)
case 5:
return append(dst, '.',
digits10[p1], digits01[p1],
digits10[p2], digits01[p2],
digits10[p3],
)
}
}
func formatBinaryDateTime(src []byte, length uint8) (driver.Value, error) {
// length expects the deterministic length of the zero value,
// negative time and 100+ hours are automatically added if needed
if len(src) == 0 {
return zeroDateTime[:length], nil
}
var dst []byte // return value
var p1, p2, p3 byte // current digit pair
switch length {
case 10, 19, 21, 22, 23, 24, 25, 26:
default:
t := "DATE"
if length > 10 {
t += "TIME"
}
return nil, fmt.Errorf("illegal %s length %d", t, length)
}
switch len(src) {
case 4, 7, 11:
default:
t := "DATE"
if length > 10 {
t += "TIME"
}
return nil, fmt.Errorf("illegal %s packet length %d", t, len(src))
}
dst = make([]byte, 0, length)
// start with the date
year := binary.LittleEndian.Uint16(src[:2])
pt := year / 100
p1 = byte(year - 100*uint16(pt))
p2, p3 = src[2], src[3]
dst = append(dst,
digits10[pt], digits01[pt],
digits10[p1], digits01[p1], '-',
digits10[p2], digits01[p2], '-',
digits10[p3], digits01[p3],
)
if length == 10 {
return dst, nil
}
if len(src) == 4 {
return append(dst, zeroDateTime[10:length]...), nil
}
dst = append(dst, ' ')
p1 = src[4] // hour
src = src[5:]
// p1 is 2-digit hour, src is after hour
p2, p3 = src[0], src[1]
dst = append(dst,
digits10[p1], digits01[p1], ':',
digits10[p2], digits01[p2], ':',
digits10[p3], digits01[p3],
)
return appendMicrosecs(dst, src[2:], int(length)-20), nil
}
func formatBinaryTime(src []byte, length uint8) (driver.Value, error) {
// length expects the deterministic length of the zero value,
// negative time and 100+ hours are automatically added if needed
if len(src) == 0 {
return zeroDateTime[11 : 11+length], nil
}
var dst []byte // return value
switch length {
case
8, // time (can be up to 10 when negative and 100+ hours)
10, 11, 12, 13, 14, 15: // time with fractional seconds
default:
return nil, fmt.Errorf("illegal TIME length %d", length)
}
switch len(src) {
case 8, 12:
default:
return nil, fmt.Errorf("invalid TIME packet length %d", len(src))
}
// +2 to enable negative time and 100+ hours
dst = make([]byte, 0, length+2)
if src[0] == 1 {
dst = append(dst, '-')
}
days := binary.LittleEndian.Uint32(src[1:5])
hours := int64(days)*24 + int64(src[5])
if hours >= 100 {
dst = strconv.AppendInt(dst, hours, 10)
} else {
dst = append(dst, digits10[hours], digits01[hours])
}
min, sec := src[6], src[7]
dst = append(dst, ':',
digits10[min], digits01[min], ':',
digits10[sec], digits01[sec],
)
return appendMicrosecs(dst, src[8:], int(length)-9), nil
}
/******************************************************************************
* Convert from and to bytes *
******************************************************************************/
func uint64ToBytes(n uint64) []byte {
return []byte{
byte(n),
byte(n >> 8),
byte(n >> 16),
byte(n >> 24),
byte(n >> 32),
byte(n >> 40),
byte(n >> 48),
byte(n >> 56),
}
}
func uint64ToString(n uint64) []byte {
var a [20]byte
i := 20
// U+0030 = 0
// ...
// U+0039 = 9
var q uint64
for n >= 10 {
i--
q = n / 10
a[i] = uint8(n-q*10) + 0x30
n = q
}
i--
a[i] = uint8(n) + 0x30
return a[i:]
}
// treats string value as unsigned integer representation
func stringToInt(b []byte) int {
val := 0
for i := range b {
val *= 10
val += int(b[i] - 0x30)
}
return val
}
// returns the string read as a bytes slice, wheter the value is NULL,
// the number of bytes read and an error, in case the string is longer than
// the input slice
func readLengthEncodedString(b []byte) ([]byte, bool, int, error) {
// Get length
num, isNull, n := readLengthEncodedInteger(b)
if num < 1 {
return b[n:n], isNull, n, nil
}
n += int(num)
// Check data length
if len(b) >= n {
return b[n-int(num) : n : n], false, n, nil
}
return nil, false, n, io.EOF
}
// returns the number of bytes skipped and an error, in case the string is
// longer than the input slice
func skipLengthEncodedString(b []byte) (int, error) {
// Get length
num, _, n := readLengthEncodedInteger(b)
if num < 1 {
return n, nil
}
n += int(num)
// Check data length
if len(b) >= n {
return n, nil
}
return n, io.EOF
}
// returns the number read, whether the value is NULL and the number of bytes read
func readLengthEncodedInteger(b []byte) (uint64, bool, int) {
// See issue #349
if len(b) == 0 {
return 0, true, 1
}
switch b[0] {
// 251: NULL
case 0xfb:
return 0, true, 1
// 252: value of following 2
case 0xfc:
return uint64(b[1]) | uint64(b[2])<<8, false, 3
// 253: value of following 3
case 0xfd:
return uint64(b[1]) | uint64(b[2])<<8 | uint64(b[3])<<16, false, 4
// 254: value of following 8
case 0xfe:
return uint64(b[1]) | uint64(b[2])<<8 | uint64(b[3])<<16 |
uint64(b[4])<<24 | uint64(b[5])<<32 | uint64(b[6])<<40 |
uint64(b[7])<<48 | uint64(b[8])<<56,
false, 9
}
// 0-250: value of first byte
return uint64(b[0]), false, 1
}
// encodes a uint64 value and appends it to the given bytes slice
func appendLengthEncodedInteger(b []byte, n uint64) []byte {
switch {
case n <= 250:
return append(b, byte(n))
case n <= 0xffff:
return append(b, 0xfc, byte(n), byte(n>>8))
case n <= 0xffffff:
return append(b, 0xfd, byte(n), byte(n>>8), byte(n>>16))
}
return append(b, 0xfe, byte(n), byte(n>>8), byte(n>>16), byte(n>>24),
byte(n>>32), byte(n>>40), byte(n>>48), byte(n>>56))
}
// reserveBuffer checks cap(buf) and expand buffer to len(buf) + appendSize.
// If cap(buf) is not enough, reallocate new buffer.
func reserveBuffer(buf []byte, appendSize int) []byte {
newSize := len(buf) + appendSize
if cap(buf) < newSize {
// Grow buffer exponentially
newBuf := make([]byte, len(buf)*2+appendSize)
copy(newBuf, buf)
buf = newBuf
}
return buf[:newSize]
}
// escapeBytesBackslash escapes []byte with backslashes (\)
// This escapes the contents of a string (provided as []byte) by adding backslashes before special
// characters, and turning others into specific escape sequences, such as
// turning newlines into \n and null bytes into \0.
// https://github.com/mysql/mysql-server/blob/mysql-5.7.5/mysys/charset.c#L823-L932
func escapeBytesBackslash(buf, v []byte) []byte {
pos := len(buf)
buf = reserveBuffer(buf, len(v)*2)
for _, c := range v {
switch c {
case '\x00':
buf[pos] = '\\'
buf[pos+1] = '0'
pos += 2
case '\n':
buf[pos] = '\\'
buf[pos+1] = 'n'
pos += 2
case '\r':
buf[pos] = '\\'
buf[pos+1] = 'r'
pos += 2
case '\x1a':
buf[pos] = '\\'
buf[pos+1] = 'Z'
pos += 2
case '\'':
buf[pos] = '\\'
buf[pos+1] = '\''
pos += 2
case '"':
buf[pos] = '\\'
buf[pos+1] = '"'
pos += 2
case '\\':
buf[pos] = '\\'
buf[pos+1] = '\\'
pos += 2
default:
buf[pos] = c
pos++
}
}
return buf[:pos]
}
// escapeStringBackslash is similar to escapeBytesBackslash but for string.
func escapeStringBackslash(buf []byte, v string) []byte {
pos := len(buf)
buf = reserveBuffer(buf, len(v)*2)
for i := 0; i < len(v); i++ {
c := v[i]
switch c {
case '\x00':
buf[pos] = '\\'
buf[pos+1] = '0'
pos += 2
case '\n':
buf[pos] = '\\'
buf[pos+1] = 'n'
pos += 2
case '\r':
buf[pos] = '\\'
buf[pos+1] = 'r'
pos += 2
case '\x1a':
buf[pos] = '\\'
buf[pos+1] = 'Z'
pos += 2
case '\'':
buf[pos] = '\\'
buf[pos+1] = '\''
pos += 2
case '"':
buf[pos] = '\\'
buf[pos+1] = '"'
pos += 2
case '\\':
buf[pos] = '\\'
buf[pos+1] = '\\'
pos += 2
default:
buf[pos] = c
pos++
}
}
return buf[:pos]
}
// escapeBytesQuotes escapes apostrophes in []byte by doubling them up.
// This escapes the contents of a string by doubling up any apostrophes that
// it contains. This is used when the NO_BACKSLASH_ESCAPES SQL_MODE is in
// effect on the server.
// https://github.com/mysql/mysql-server/blob/mysql-5.7.5/mysys/charset.c#L963-L1038
func escapeBytesQuotes(buf, v []byte) []byte {
pos := len(buf)
buf = reserveBuffer(buf, len(v)*2)
for _, c := range v {
if c == '\'' {
buf[pos] = '\''
buf[pos+1] = '\''
pos += 2
} else {
buf[pos] = c
pos++
}
}
return buf[:pos]
}
// escapeStringQuotes is similar to escapeBytesQuotes but for string.
func escapeStringQuotes(buf []byte, v string) []byte {
pos := len(buf)
buf = reserveBuffer(buf, len(v)*2)
for i := 0; i < len(v); i++ {
c := v[i]
if c == '\'' {
buf[pos] = '\''
buf[pos+1] = '\''
pos += 2
} else {
buf[pos] = c
pos++
}
}
return buf[:pos]
}
/******************************************************************************
* Sync utils *
******************************************************************************/
// noCopy may be embedded into structs which must not be copied
// after the first use.
//
// See https://github.com/golang/go/issues/8005#issuecomment-190753527
// for details.
type noCopy struct{}
// Lock is a no-op used by -copylocks checker from `go vet`.
func (*noCopy) Lock() {}
// atomicBool is a wrapper around uint32 for usage as a boolean value with
// atomic access.
type atomicBool struct {
_noCopy noCopy
value uint32
}
// IsSet returns whether the current boolean value is true
func (ab *atomicBool) IsSet() bool {
return atomic.LoadUint32(&ab.value) > 0
}
// Set sets the value of the bool regardless of the previous value
func (ab *atomicBool) Set(value bool) {
if value {
atomic.StoreUint32(&ab.value, 1)
} else {
atomic.StoreUint32(&ab.value, 0)
}
}
// TrySet sets the value of the bool and returns whether the value changed
func (ab *atomicBool) TrySet(value bool) bool {
if value {
return atomic.SwapUint32(&ab.value, 1) == 0
}
return atomic.SwapUint32(&ab.value, 0) > 0
}
// atomicError is a wrapper for atomically accessed error values
type atomicError struct {
_noCopy noCopy
value atomic.Value
}
// Set sets the error value regardless of the previous value.
// The value must not be nil
func (ae *atomicError) Set(value error) {
ae.value.Store(value)
}
// Value returns the current error value
func (ae *atomicError) Value() error {
if v := ae.value.Load(); v != nil {
// this will panic if the value doesn't implement the error interface
return v.(error)
}
return nil
}
func namedValueToValue(named []driver.NamedValue) ([]driver.Value, error) {
dargs := make([]driver.Value, len(named))
for n, param := range named {
if len(param.Name) > 0 {
// TODO: support the use of Named Parameters #561
return nil, errors.New("mysql: driver does not support the use of Named Parameters")
}
dargs[n] = param.Value
}
return dargs, nil
}
func mapIsolationLevel(level driver.IsolationLevel) (string, error) {
switch sql.IsolationLevel(level) {
case sql.LevelRepeatableRead:
return "REPEATABLE READ", nil
case sql.LevelReadCommitted:
return "READ COMMITTED", nil
case sql.LevelReadUncommitted:
return "READ UNCOMMITTED", nil
case sql.LevelSerializable:
return "SERIALIZABLE", nil
default:
return "", fmt.Errorf("mysql: unsupported isolation level: %v", level)
}
}

View File

@ -44,6 +44,7 @@ includes_AIX='
#include <sys/stropts.h>
#include <sys/mman.h>
#include <sys/poll.h>
#include <sys/select.h>
#include <sys/termio.h>
#include <termios.h>
#include <fcntl.h>

View File

@ -459,6 +459,15 @@ const (
MAP_SHARED = 0x1
MAP_TYPE = 0xf0
MAP_VARIABLE = 0x0
MCAST_BLOCK_SOURCE = 0x40
MCAST_EXCLUDE = 0x2
MCAST_INCLUDE = 0x1
MCAST_JOIN_GROUP = 0x3e
MCAST_JOIN_SOURCE_GROUP = 0x42
MCAST_LEAVE_GROUP = 0x3f
MCAST_LEAVE_SOURCE_GROUP = 0x43
MCAST_SOURCE_FILTER = 0x49
MCAST_UNBLOCK_SOURCE = 0x41
MCL_CURRENT = 0x100
MCL_FUTURE = 0x200
MSG_ANY = 0x4
@ -483,6 +492,7 @@ const (
MS_INVALIDATE = 0x40
MS_PER_SEC = 0x3e8
MS_SYNC = 0x20
NFDBITS = 0x20
NL0 = 0x0
NL1 = 0x4000
NL2 = 0x8000
@ -688,7 +698,7 @@ const (
SIOCGHIWAT = 0x40047301
SIOCGIFADDR = -0x3fd796df
SIOCGIFADDRS = 0x2000698c
SIOCGIFBAUDRATE = -0x3fd79693
SIOCGIFBAUDRATE = -0x3fdf9669
SIOCGIFBRDADDR = -0x3fd796dd
SIOCGIFCONF = -0x3ff796bb
SIOCGIFCONFGLOB = -0x3ff79670

View File

@ -459,6 +459,15 @@ const (
MAP_SHARED = 0x1
MAP_TYPE = 0xf0
MAP_VARIABLE = 0x0
MCAST_BLOCK_SOURCE = 0x40
MCAST_EXCLUDE = 0x2
MCAST_INCLUDE = 0x1
MCAST_JOIN_GROUP = 0x3e
MCAST_JOIN_SOURCE_GROUP = 0x42
MCAST_LEAVE_GROUP = 0x3f
MCAST_LEAVE_SOURCE_GROUP = 0x43
MCAST_SOURCE_FILTER = 0x49
MCAST_UNBLOCK_SOURCE = 0x41
MCL_CURRENT = 0x100
MCL_FUTURE = 0x200
MSG_ANY = 0x4
@ -483,6 +492,7 @@ const (
MS_INVALIDATE = 0x40
MS_PER_SEC = 0x3e8
MS_SYNC = 0x20
NFDBITS = 0x40
NL0 = 0x0
NL1 = 0x4000
NL2 = 0x8000
@ -688,7 +698,7 @@ const (
SIOCGHIWAT = 0x40047301
SIOCGIFADDR = -0x3fd796df
SIOCGIFADDRS = 0x2000698c
SIOCGIFBAUDRATE = -0x3fd79693
SIOCGIFBAUDRATE = -0x3fdf9669
SIOCGIFBRDADDR = -0x3fd796dd
SIOCGIFCONF = -0x3fef96bb
SIOCGIFCONFGLOB = -0x3fef9670

View File

@ -229,15 +229,13 @@ func LookupSID(system, account string) (sid *SID, domain string, accType uint32,
// String converts SID to a string format suitable for display, storage, or transmission.
func (sid *SID) String() string {
// From https://docs.microsoft.com/en-us/windows/win32/secbiomet/general-constants
const SecurityMaxSidSize = 68
var s *uint16
e := ConvertSidToStringSid(sid, &s)
if e != nil {
return ""
}
defer LocalFree((Handle)(unsafe.Pointer(s)))
return UTF16ToString((*[SecurityMaxSidSize]uint16)(unsafe.Pointer(s))[:])
return UTF16ToString((*[256]uint16)(unsafe.Pointer(s))[:])
}
// Len returns the length, in bytes, of a valid security identifier SID.

View File

@ -313,6 +313,10 @@ func NewCallbackCDecl(fn interface{}) uintptr {
//sys CoTaskMemFree(address unsafe.Pointer) = ole32.CoTaskMemFree
//sys rtlGetVersion(info *OsVersionInfoEx) (ret error) = ntdll.RtlGetVersion
//sys rtlGetNtVersionNumbers(majorVersion *uint32, minorVersion *uint32, buildNumber *uint32) = ntdll.RtlGetNtVersionNumbers
//sys getProcessPreferredUILanguages(flags uint32, numLanguages *uint32, buf *uint16, bufSize *uint32) (err error) = kernel32.GetProcessPreferredUILanguages
//sys getThreadPreferredUILanguages(flags uint32, numLanguages *uint32, buf *uint16, bufSize *uint32) (err error) = kernel32.GetThreadPreferredUILanguages
//sys getUserPreferredUILanguages(flags uint32, numLanguages *uint32, buf *uint16, bufSize *uint32) (err error) = kernel32.GetUserPreferredUILanguages
//sys getSystemPreferredUILanguages(flags uint32, numLanguages *uint32, buf *uint16, bufSize *uint32) (err error) = kernel32.GetSystemPreferredUILanguages
// Process Status API (PSAPI)
//sys EnumProcesses(processIds []uint32, bytesReturned *uint32) (err error) = psapi.EnumProcesses
@ -1378,3 +1382,54 @@ func RtlGetNtVersionNumbers() (majorVersion, minorVersion, buildNumber uint32) {
buildNumber &= 0xffff
return
}
// GetProcessPreferredUILanguages retrieves the process preferred UI languages.
func GetProcessPreferredUILanguages(flags uint32) ([]string, error) {
return getUILanguages(flags, getProcessPreferredUILanguages)
}
// GetThreadPreferredUILanguages retrieves the thread preferred UI languages for the current thread.
func GetThreadPreferredUILanguages(flags uint32) ([]string, error) {
return getUILanguages(flags, getThreadPreferredUILanguages)
}
// GetUserPreferredUILanguages retrieves information about the user preferred UI languages.
func GetUserPreferredUILanguages(flags uint32) ([]string, error) {
return getUILanguages(flags, getUserPreferredUILanguages)
}
// GetSystemPreferredUILanguages retrieves the system preferred UI languages.
func GetSystemPreferredUILanguages(flags uint32) ([]string, error) {
return getUILanguages(flags, getSystemPreferredUILanguages)
}
func getUILanguages(flags uint32, f func(flags uint32, numLanguages *uint32, buf *uint16, bufSize *uint32) error) ([]string, error) {
size := uint32(128)
for {
var numLanguages uint32
buf := make([]uint16, size)
err := f(flags, &numLanguages, &buf[0], &size)
if err == ERROR_INSUFFICIENT_BUFFER {
continue
}
if err != nil {
return nil, err
}
buf = buf[:size]
if numLanguages == 0 || len(buf) == 0 { // GetProcessPreferredUILanguages may return numLanguages==0 with "\0\0"
return []string{}, nil
}
if buf[len(buf)-1] == 0 {
buf = buf[:len(buf)-1] // remove terminating null
}
languages := make([]string, 0, numLanguages)
from := 0
for i, c := range buf {
if c == 0 {
languages = append(languages, string(utf16.Decode(buf[from:i])))
from = i + 1
}
}
return languages, nil
}
}

View File

@ -1742,3 +1742,36 @@ const (
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT = 2
GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS = 4
)
// MUI function flag values
const (
MUI_LANGUAGE_ID = 0x4
MUI_LANGUAGE_NAME = 0x8
MUI_MERGE_SYSTEM_FALLBACK = 0x10
MUI_MERGE_USER_FALLBACK = 0x20
MUI_UI_FALLBACK = MUI_MERGE_SYSTEM_FALLBACK | MUI_MERGE_USER_FALLBACK
MUI_THREAD_LANGUAGES = 0x40
MUI_CONSOLE_FILTER = 0x100
MUI_COMPLEX_SCRIPT_FILTER = 0x200
MUI_RESET_FILTERS = 0x001
MUI_USER_PREFERRED_UI_LANGUAGES = 0x10
MUI_USE_INSTALLED_LANGUAGES = 0x20
MUI_USE_SEARCH_ALL_LANGUAGES = 0x40
MUI_LANG_NEUTRAL_PE_FILE = 0x100
MUI_NON_LANG_NEUTRAL_FILE = 0x200
MUI_MACHINE_LANGUAGE_SETTINGS = 0x400
MUI_FILETYPE_NOT_LANGUAGE_NEUTRAL = 0x001
MUI_FILETYPE_LANGUAGE_NEUTRAL_MAIN = 0x002
MUI_FILETYPE_LANGUAGE_NEUTRAL_MUI = 0x004
MUI_QUERY_TYPE = 0x001
MUI_QUERY_CHECKSUM = 0x002
MUI_QUERY_LANGUAGE_NAME = 0x004
MUI_QUERY_RESOURCE_TYPES = 0x008
MUI_FILEINFO_VERSION = 0x001
MUI_FULL_LANGUAGE = 0x01
MUI_PARTIAL_LANGUAGE = 0x02
MUI_LIP_LANGUAGE = 0x04
MUI_LANGUAGE_INSTALLED = 0x20
MUI_LANGUAGE_LICENSED = 0x40
)

View File

@ -248,6 +248,10 @@ var (
procCoTaskMemFree = modole32.NewProc("CoTaskMemFree")
procRtlGetVersion = modntdll.NewProc("RtlGetVersion")
procRtlGetNtVersionNumbers = modntdll.NewProc("RtlGetNtVersionNumbers")
procGetProcessPreferredUILanguages = modkernel32.NewProc("GetProcessPreferredUILanguages")
procGetThreadPreferredUILanguages = modkernel32.NewProc("GetThreadPreferredUILanguages")
procGetUserPreferredUILanguages = modkernel32.NewProc("GetUserPreferredUILanguages")
procGetSystemPreferredUILanguages = modkernel32.NewProc("GetSystemPreferredUILanguages")
procEnumProcesses = modpsapi.NewProc("EnumProcesses")
procWSAStartup = modws2_32.NewProc("WSAStartup")
procWSACleanup = modws2_32.NewProc("WSACleanup")
@ -2760,6 +2764,54 @@ func rtlGetNtVersionNumbers(majorVersion *uint32, minorVersion *uint32, buildNum
return
}
func getProcessPreferredUILanguages(flags uint32, numLanguages *uint32, buf *uint16, bufSize *uint32) (err error) {
r1, _, e1 := syscall.Syscall6(procGetProcessPreferredUILanguages.Addr(), 4, uintptr(flags), uintptr(unsafe.Pointer(numLanguages)), uintptr(unsafe.Pointer(buf)), uintptr(unsafe.Pointer(bufSize)), 0, 0)
if r1 == 0 {
if e1 != 0 {
err = errnoErr(e1)
} else {
err = syscall.EINVAL
}
}
return
}
func getThreadPreferredUILanguages(flags uint32, numLanguages *uint32, buf *uint16, bufSize *uint32) (err error) {
r1, _, e1 := syscall.Syscall6(procGetThreadPreferredUILanguages.Addr(), 4, uintptr(flags), uintptr(unsafe.Pointer(numLanguages)), uintptr(unsafe.Pointer(buf)), uintptr(unsafe.Pointer(bufSize)), 0, 0)
if r1 == 0 {
if e1 != 0 {
err = errnoErr(e1)
} else {
err = syscall.EINVAL
}
}
return
}
func getUserPreferredUILanguages(flags uint32, numLanguages *uint32, buf *uint16, bufSize *uint32) (err error) {
r1, _, e1 := syscall.Syscall6(procGetUserPreferredUILanguages.Addr(), 4, uintptr(flags), uintptr(unsafe.Pointer(numLanguages)), uintptr(unsafe.Pointer(buf)), uintptr(unsafe.Pointer(bufSize)), 0, 0)
if r1 == 0 {
if e1 != 0 {
err = errnoErr(e1)
} else {
err = syscall.EINVAL
}
}
return
}
func getSystemPreferredUILanguages(flags uint32, numLanguages *uint32, buf *uint16, bufSize *uint32) (err error) {
r1, _, e1 := syscall.Syscall6(procGetSystemPreferredUILanguages.Addr(), 4, uintptr(flags), uintptr(unsafe.Pointer(numLanguages)), uintptr(unsafe.Pointer(buf)), uintptr(unsafe.Pointer(bufSize)), 0, 0)
if r1 == 0 {
if e1 != 0 {
err = errnoErr(e1)
} else {
err = syscall.EINVAL
}
}
return
}
func EnumProcesses(processIds []uint32, bytesReturned *uint32) (err error) {
var _p0 *uint32
if len(processIds) > 0 {

19
vendor/modules.txt vendored
View File

@ -9,6 +9,9 @@ github.com/go-asn1-ber/asn1-ber
# github.com/go-ldap/ldap/v3 v3.1.6
## explicit
github.com/go-ldap/ldap/v3
# github.com/go-sql-driver/mysql v1.5.0
## explicit
github.com/go-sql-driver/mysql
# github.com/goshuirc/e-nfa v0.0.0-20160917075329-7071788e3940
## explicit
github.com/goshuirc/e-nfa
@ -26,36 +29,35 @@ github.com/mattn/go-isatty
# github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b
## explicit
github.com/mgutz/ansi
# github.com/onsi/ginkgo v1.12.0
## explicit
# github.com/onsi/gomega v1.9.0
## explicit
# github.com/oragono/confusables v0.0.0-20190624102032-fe1cf31a24b0
## explicit
github.com/oragono/confusables
# github.com/oragono/go-ident v0.0.0-20170110123031-337fed0fd21a
## explicit
github.com/oragono/go-ident
# github.com/tidwall/btree v0.0.0-20191029221954-400434d76274
# github.com/stretchr/testify v1.4.0
## explicit
# github.com/tidwall/btree v0.0.0-20191029221954-400434d76274
github.com/tidwall/btree
# github.com/tidwall/buntdb v1.1.2
## explicit
github.com/tidwall/buntdb
# github.com/tidwall/gjson v1.3.4
## explicit
github.com/tidwall/gjson
# github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb
## explicit
github.com/tidwall/grect
# github.com/tidwall/match v1.0.1
## explicit
github.com/tidwall/match
# github.com/tidwall/pretty v1.0.0
## explicit
github.com/tidwall/pretty
# github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e
## explicit
github.com/tidwall/rtree
github.com/tidwall/rtree/base
# github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563
## explicit
github.com/tidwall/tinyqueue
# golang.org/x/crypto v0.0.0-20191112222119-e1110fd1c708
## explicit
@ -63,8 +65,7 @@ golang.org/x/crypto/bcrypt
golang.org/x/crypto/blowfish
golang.org/x/crypto/sha3
golang.org/x/crypto/ssh/terminal
# golang.org/x/sys v0.0.0-20191115151921-52ab43148777
## explicit
# golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e
golang.org/x/sys/cpu
golang.org/x/sys/unix
golang.org/x/sys/windows