mirror of
https://github.com/ergochat/ergo.git
synced 2024-11-29 07:29:31 +01:00
Merge branch 'persistent.14'
This commit is contained in:
commit
fb8b73e29a
1
Makefile
1
Makefile
@ -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
|
||||
|
@ -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
12
go.mod
@ -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
91
go.sum
Normal 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=
|
141
irc/accounts.go
141
irc/accounts.go
@ -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
|
||||
|
@ -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",
|
||||
|
152
irc/channel.go
152
irc/channel.go
@ -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)
|
||||
}
|
||||
|
@ -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.
|
||||
|
141
irc/chanserv.go
141
irc/chanserv.go
@ -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"))
|
||||
}
|
||||
}
|
||||
|
376
irc/client.go
376
irc/client.go
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
|
241
irc/config.go
241
irc/config.go
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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`)
|
||||
|
@ -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, ""
|
||||
|
100
irc/getters.go
100
irc/getters.go
@ -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)
|
||||
}
|
||||
|
494
irc/handlers.go
494
irc/handlers.go
@ -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
|
||||
}
|
||||
|
||||
|
11
irc/help.go
11
irc/help.go
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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
71
irc/history/queries.go
Normal 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
|
||||
}
|
@ -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))
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
22
irc/mysql/config.go
Normal 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
557
irc/mysql/history.go
Normal 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,
|
||||
}
|
||||
}
|
23
irc/mysql/serialization.go
Normal file
23
irc/mysql/serialization.go
Normal 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)
|
||||
}
|
@ -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() {
|
||||
|
128
irc/nickserv.go
128
irc/nickserv.go
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
116
irc/server.go
116
irc/server.go
@ -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
|
||||
|
20
irc/stats.go
20
irc/stats.go
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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 != ""
|
||||
}
|
||||
|
20
irc/znc.go
20
irc/znc.go
@ -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)
|
||||
}
|
||||
}
|
||||
|
72
oragono.yaml
72
oragono.yaml
@ -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
9
vendor/github.com/go-sql-driver/mysql/.gitignore
generated
vendored
Normal 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
129
vendor/github.com/go-sql-driver/mysql/.travis.yml
generated
vendored
Normal 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
105
vendor/github.com/go-sql-driver/mysql/AUTHORS
generated
vendored
Normal 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
206
vendor/github.com/go-sql-driver/mysql/CHANGELOG.md
generated
vendored
Normal 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
373
vendor/github.com/go-sql-driver/mysql/LICENSE
generated
vendored
Normal 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
501
vendor/github.com/go-sql-driver/mysql/README.md
generated
vendored
Normal 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&...¶mN=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
422
vendor/github.com/go-sql-driver/mysql/auth.go
generated
vendored
Normal 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
182
vendor/github.com/go-sql-driver/mysql/buffer.go
generated
vendored
Normal 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
265
vendor/github.com/go-sql-driver/mysql/collations.go
generated
vendored
Normal 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
54
vendor/github.com/go-sql-driver/mysql/conncheck.go
generated
vendored
Normal 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
|
||||
}
|
17
vendor/github.com/go-sql-driver/mysql/conncheck_dummy.go
generated
vendored
Normal file
17
vendor/github.com/go-sql-driver/mysql/conncheck_dummy.go
generated
vendored
Normal 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
651
vendor/github.com/go-sql-driver/mysql/connection.go
generated
vendored
Normal 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
146
vendor/github.com/go-sql-driver/mysql/connector.go
generated
vendored
Normal 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
174
vendor/github.com/go-sql-driver/mysql/const.go
generated
vendored
Normal 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
107
vendor/github.com/go-sql-driver/mysql/driver.go
generated
vendored
Normal 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
560
vendor/github.com/go-sql-driver/mysql/dsn.go
generated
vendored
Normal 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&...¶mN=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¶mN=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&...¶mN=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
65
vendor/github.com/go-sql-driver/mysql/errors.go
generated
vendored
Normal 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
194
vendor/github.com/go-sql-driver/mysql/fields.go
generated
vendored
Normal 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
3
vendor/github.com/go-sql-driver/mysql/go.mod
generated
vendored
Normal 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
182
vendor/github.com/go-sql-driver/mysql/infile.go
generated
vendored
Normal 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
50
vendor/github.com/go-sql-driver/mysql/nulltime.go
generated
vendored
Normal 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
|
||||
}
|
31
vendor/github.com/go-sql-driver/mysql/nulltime_go113.go
generated
vendored
Normal file
31
vendor/github.com/go-sql-driver/mysql/nulltime_go113.go
generated
vendored
Normal 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
|
34
vendor/github.com/go-sql-driver/mysql/nulltime_legacy.go
generated
vendored
Normal file
34
vendor/github.com/go-sql-driver/mysql/nulltime_legacy.go
generated
vendored
Normal 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
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
22
vendor/github.com/go-sql-driver/mysql/result.go
generated
vendored
Normal 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
223
vendor/github.com/go-sql-driver/mysql/rows.go
generated
vendored
Normal 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
204
vendor/github.com/go-sql-driver/mysql/statement.go
generated
vendored
Normal 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
31
vendor/github.com/go-sql-driver/mysql/transaction.go
generated
vendored
Normal 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
701
vendor/github.com/go-sql-driver/mysql/utils.go
generated
vendored
Normal 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)
|
||||
}
|
||||
}
|
1
vendor/golang.org/x/sys/unix/mkerrors.sh
generated
vendored
1
vendor/golang.org/x/sys/unix/mkerrors.sh
generated
vendored
@ -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>
|
||||
|
12
vendor/golang.org/x/sys/unix/zerrors_aix_ppc.go
generated
vendored
12
vendor/golang.org/x/sys/unix/zerrors_aix_ppc.go
generated
vendored
@ -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
|
||||
|
12
vendor/golang.org/x/sys/unix/zerrors_aix_ppc64.go
generated
vendored
12
vendor/golang.org/x/sys/unix/zerrors_aix_ppc64.go
generated
vendored
@ -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
|
||||
|
4
vendor/golang.org/x/sys/windows/security_windows.go
generated
vendored
4
vendor/golang.org/x/sys/windows/security_windows.go
generated
vendored
@ -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.
|
||||
|
55
vendor/golang.org/x/sys/windows/syscall_windows.go
generated
vendored
55
vendor/golang.org/x/sys/windows/syscall_windows.go
generated
vendored
@ -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
|
||||
}
|
||||
}
|
||||
|
33
vendor/golang.org/x/sys/windows/types_windows.go
generated
vendored
33
vendor/golang.org/x/sys/windows/types_windows.go
generated
vendored
@ -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
|
||||
)
|
||||
|
52
vendor/golang.org/x/sys/windows/zsyscall_windows.go
generated
vendored
52
vendor/golang.org/x/sys/windows/zsyscall_windows.go
generated
vendored
@ -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
19
vendor/modules.txt
vendored
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user