Browse Source

Merge branch 'persistent.14'

tags/v2.0.0-rc1
Shivaram Lingamneni 4 years ago
parent
commit
fb8b73e29a
76 changed files with 9702 additions and 749 deletions
  1. 1
    0
      Makefile
  2. 6
    6
      gencapdefs.py
  3. 4
    8
      go.mod
  4. 91
    0
      go.sum
  5. 116
    25
      irc/accounts.go
  6. 5
    5
      irc/caps/defs.go
  7. 121
    31
      irc/channel.go
  8. 16
    1
      irc/channelreg.go
  9. 116
    21
      irc/chanserv.go
  10. 303
    73
      irc/client.go
  11. 53
    23
      irc/client_lookup_set.go
  12. 7
    6
      irc/commands.go
  13. 212
    29
      irc/config.go
  14. 17
    1
      irc/custime/parseduration.go
  15. 4
    20
      irc/database.go
  16. 2
    0
      irc/errors.go
  17. 2
    2
      irc/gateways.go
  18. 82
    16
      irc/getters.go
  19. 188
    266
      irc/handlers.go
  20. 6
    5
      irc/help.go
  21. 97
    68
      irc/history/history.go
  22. 46
    25
      irc/history/history_test.go
  23. 71
    0
      irc/history/queries.go
  24. 3
    2
      irc/hostserv.go
  25. 21
    23
      irc/idletimer.go
  26. 5
    5
      irc/misc_test.go
  27. 22
    0
      irc/mysql/config.go
  28. 557
    0
      irc/mysql/history.go
  29. 23
    0
      irc/mysql/serialization.go
  30. 10
    8
      irc/nickname.go
  31. 103
    25
      irc/nickserv.go
  32. 2
    2
      irc/responsebuffer.go
  33. 104
    12
      irc/server.go
  34. 19
    1
      irc/stats.go
  35. 21
    2
      irc/utils/args.go
  36. 0
    8
      irc/utils/net.go
  37. 5
    8
      irc/utils/text.go
  38. 16
    4
      irc/znc.go
  39. 68
    4
      oragono.yaml
  40. 9
    0
      vendor/github.com/go-sql-driver/mysql/.gitignore
  41. 129
    0
      vendor/github.com/go-sql-driver/mysql/.travis.yml
  42. 105
    0
      vendor/github.com/go-sql-driver/mysql/AUTHORS
  43. 206
    0
      vendor/github.com/go-sql-driver/mysql/CHANGELOG.md
  44. 373
    0
      vendor/github.com/go-sql-driver/mysql/LICENSE
  45. 501
    0
      vendor/github.com/go-sql-driver/mysql/README.md
  46. 422
    0
      vendor/github.com/go-sql-driver/mysql/auth.go
  47. 182
    0
      vendor/github.com/go-sql-driver/mysql/buffer.go
  48. 265
    0
      vendor/github.com/go-sql-driver/mysql/collations.go
  49. 54
    0
      vendor/github.com/go-sql-driver/mysql/conncheck.go
  50. 17
    0
      vendor/github.com/go-sql-driver/mysql/conncheck_dummy.go
  51. 651
    0
      vendor/github.com/go-sql-driver/mysql/connection.go
  52. 146
    0
      vendor/github.com/go-sql-driver/mysql/connector.go
  53. 174
    0
      vendor/github.com/go-sql-driver/mysql/const.go
  54. 107
    0
      vendor/github.com/go-sql-driver/mysql/driver.go
  55. 560
    0
      vendor/github.com/go-sql-driver/mysql/dsn.go
  56. 65
    0
      vendor/github.com/go-sql-driver/mysql/errors.go
  57. 194
    0
      vendor/github.com/go-sql-driver/mysql/fields.go
  58. 3
    0
      vendor/github.com/go-sql-driver/mysql/go.mod
  59. 182
    0
      vendor/github.com/go-sql-driver/mysql/infile.go
  60. 50
    0
      vendor/github.com/go-sql-driver/mysql/nulltime.go
  61. 31
    0
      vendor/github.com/go-sql-driver/mysql/nulltime_go113.go
  62. 34
    0
      vendor/github.com/go-sql-driver/mysql/nulltime_legacy.go
  63. 1342
    0
      vendor/github.com/go-sql-driver/mysql/packets.go
  64. 22
    0
      vendor/github.com/go-sql-driver/mysql/result.go
  65. 223
    0
      vendor/github.com/go-sql-driver/mysql/rows.go
  66. 204
    0
      vendor/github.com/go-sql-driver/mysql/statement.go
  67. 31
    0
      vendor/github.com/go-sql-driver/mysql/transaction.go
  68. 701
    0
      vendor/github.com/go-sql-driver/mysql/utils.go
  69. 1
    0
      vendor/golang.org/x/sys/unix/mkerrors.sh
  70. 11
    1
      vendor/golang.org/x/sys/unix/zerrors_aix_ppc.go
  71. 11
    1
      vendor/golang.org/x/sys/unix/zerrors_aix_ppc64.go
  72. 1
    3
      vendor/golang.org/x/sys/windows/security_windows.go
  73. 55
    0
      vendor/golang.org/x/sys/windows/syscall_windows.go
  74. 33
    0
      vendor/golang.org/x/sys/windows/types_windows.go
  75. 52
    0
      vendor/golang.org/x/sys/windows/zsyscall_windows.go
  76. 10
    9
      vendor/modules.txt

+ 1
- 0
Makefile View File

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

+ 6
- 6
gencapdefs.py View File

@@ -135,12 +135,6 @@ CAPDEFS = [
135 135
         url="https://ircv3.net/specs/extensions/userhost-in-names-3.2.html",
136 136
         standard="IRCv3",
137 137
     ),
138
-    CapDef(
139
-        identifier="Bouncer",
140
-        name="oragono.io/bnc",
141
-        url="https://oragono.io/bnc",
142
-        standard="Oragono-specific",
143
-    ),
144 138
     CapDef(
145 139
         identifier="ZNCSelfMessage",
146 140
         name="znc.in/self-message",
@@ -171,6 +165,12 @@ CAPDEFS = [
171 165
         url="https://github.com/ircv3/ircv3-specifications/pull/398",
172 166
         standard="proposed IRCv3",
173 167
     ),
168
+    CapDef(
169
+        identifier="Chathistory",
170
+        name="draft/chathistory",
171
+        url="https://github.com/ircv3/ircv3-specifications/pull/393",
172
+        standard="proposed IRCv3",
173
+    ),
174 174
 ]
175 175
 
176 176
 def validate_defs():

+ 4
- 8
go.mod View File

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

+ 91
- 0
go.sum View File

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

+ 116
- 25
irc/accounts.go View File

@@ -33,7 +33,9 @@ const (
33 33
 	keyAccountSettings         = "account.settings %s"
34 34
 	keyAccountVHost            = "account.vhost %s"
35 35
 	keyCertToAccount           = "account.creds.certfp %s"
36
-	keyAccountChannels         = "account.channels %s"
36
+	keyAccountChannels         = "account.channels %s" // channels registered to the account
37
+	keyAccountJoinedChannels   = "account.joinedto %s" // channels a persistent client has joined
38
+	keyAccountLastSignoff      = "account.lastsignoff %s"
37 39
 
38 40
 	keyVHostQueueAcctToId = "vhostQueue %s"
39 41
 	vhostRequestIdx       = "vhostQueue"
@@ -71,6 +73,40 @@ func (am *AccountManager) Initialize(server *Server) {
71 73
 	config := server.Config()
72 74
 	am.buildNickToAccountIndex(config)
73 75
 	am.initVHostRequestQueue(config)
76
+	am.createAlwaysOnClients(config)
77
+}
78
+
79
+func (am *AccountManager) createAlwaysOnClients(config *Config) {
80
+	if config.Accounts.Multiclient.AlwaysOn == PersistentDisabled {
81
+		return
82
+	}
83
+
84
+	verifiedPrefix := fmt.Sprintf(keyAccountVerified, "")
85
+
86
+	am.serialCacheUpdateMutex.Lock()
87
+	defer am.serialCacheUpdateMutex.Unlock()
88
+
89
+	var accounts []string
90
+
91
+	am.server.store.View(func(tx *buntdb.Tx) error {
92
+		err := tx.AscendGreaterOrEqual("", verifiedPrefix, func(key, value string) bool {
93
+			if !strings.HasPrefix(key, verifiedPrefix) {
94
+				return false
95
+			}
96
+			account := strings.TrimPrefix(key, verifiedPrefix)
97
+			accounts = append(accounts, account)
98
+			return true
99
+		})
100
+		return err
101
+	})
102
+
103
+	for _, accountName := range accounts {
104
+		account, err := am.LoadAccount(accountName)
105
+		if err == nil && account.Verified &&
106
+			persistenceEnabled(config.Accounts.Multiclient.AlwaysOn, account.Settings.AlwaysOn) {
107
+			am.server.AddAlwaysOnClient(account, am.loadChannels(accountName), am.loadLastSignoff(accountName))
108
+		}
109
+	}
74 110
 }
75 111
 
76 112
 func (am *AccountManager) buildNickToAccountIndex(config *Config) {
@@ -346,7 +382,7 @@ func (am *AccountManager) Register(client *Client, account string, callbackNames
346 382
 	callbackSpec := fmt.Sprintf("%s:%s", callbackNamespace, callbackValue)
347 383
 
348 384
 	var setOptions *buntdb.SetOptions
349
-	ttl := config.Registration.VerifyTimeout
385
+	ttl := time.Duration(config.Registration.VerifyTimeout)
350 386
 	if ttl != 0 {
351 387
 		setOptions = &buntdb.SetOptions{Expires: true, TTL: ttl}
352 388
 	}
@@ -477,6 +513,58 @@ func (am *AccountManager) setPassword(account string, password string, hasPrivs
477 513
 	return err
478 514
 }
479 515
 
516
+func (am *AccountManager) saveChannels(account string, channels []string) {
517
+	channelsStr := strings.Join(channels, ",")
518
+	key := fmt.Sprintf(keyAccountJoinedChannels, account)
519
+	am.server.store.Update(func(tx *buntdb.Tx) error {
520
+		tx.Set(key, channelsStr, nil)
521
+		return nil
522
+	})
523
+}
524
+
525
+func (am *AccountManager) loadChannels(account string) (channels []string) {
526
+	key := fmt.Sprintf(keyAccountJoinedChannels, account)
527
+	var channelsStr string
528
+	am.server.store.View(func(tx *buntdb.Tx) error {
529
+		channelsStr, _ = tx.Get(key)
530
+		return nil
531
+	})
532
+	if channelsStr != "" {
533
+		return strings.Split(channelsStr, ",")
534
+	}
535
+	return
536
+}
537
+
538
+func (am *AccountManager) saveLastSignoff(account string, lastSignoff time.Time) {
539
+	key := fmt.Sprintf(keyAccountLastSignoff, account)
540
+	var val string
541
+	if !lastSignoff.IsZero() {
542
+		val = strconv.FormatInt(lastSignoff.UnixNano(), 10)
543
+	}
544
+	am.server.store.Update(func(tx *buntdb.Tx) error {
545
+		if val != "" {
546
+			tx.Set(key, val, nil)
547
+		} else {
548
+			tx.Delete(key)
549
+		}
550
+		return nil
551
+	})
552
+}
553
+
554
+func (am *AccountManager) loadLastSignoff(account string) (lastSignoff time.Time) {
555
+	key := fmt.Sprintf(keyAccountLastSignoff, account)
556
+	var lsText string
557
+	am.server.store.View(func(tx *buntdb.Tx) error {
558
+		lsText, _ = tx.Get(key)
559
+		return nil
560
+	})
561
+	lsNum, err := strconv.ParseInt(lsText, 10, 64)
562
+	if err != nil {
563
+		return time.Unix(0, lsNum).UTC()
564
+	}
565
+	return
566
+}
567
+
480 568
 func (am *AccountManager) addRemoveCertfp(account, certfp string, add bool, hasPrivs bool) (err error) {
481 569
 	certfp, err = utils.NormalizeCertfp(certfp)
482 570
 	if err != nil {
@@ -685,7 +773,7 @@ func (am *AccountManager) Verify(client *Client, account string, code string) er
685 773
 	}
686 774
 	am.server.logger.Info("accounts", "client", nick, "registered account", casefoldedAccount)
687 775
 	raw.Verified = true
688
-	clientAccount, err := am.deserializeRawAccount(raw)
776
+	clientAccount, err := am.deserializeRawAccount(raw, casefoldedAccount)
689 777
 	if err != nil {
690 778
 		return err
691 779
 	}
@@ -892,13 +980,13 @@ func (am *AccountManager) LoadAccount(accountName string) (result ClientAccount,
892 980
 		return
893 981
 	}
894 982
 
895
-	result, err = am.deserializeRawAccount(raw)
896
-	result.NameCasefolded = casefoldedAccount
983
+	result, err = am.deserializeRawAccount(raw, casefoldedAccount)
897 984
 	return
898 985
 }
899 986
 
900
-func (am *AccountManager) deserializeRawAccount(raw rawClientAccount) (result ClientAccount, err error) {
987
+func (am *AccountManager) deserializeRawAccount(raw rawClientAccount, cfName string) (result ClientAccount, err error) {
901 988
 	result.Name = raw.Name
989
+	result.NameCasefolded = cfName
902 990
 	regTimeInt, _ := strconv.ParseInt(raw.RegisteredAt, 10, 64)
903 991
 	result.RegisteredAt = time.Unix(regTimeInt, 0).UTC()
904 992
 	e := json.Unmarshal([]byte(raw.Credentials), &result.Credentials)
@@ -976,6 +1064,8 @@ func (am *AccountManager) Unregister(account string) error {
976 1064
 	vhostKey := fmt.Sprintf(keyAccountVHost, casefoldedAccount)
977 1065
 	vhostQueueKey := fmt.Sprintf(keyVHostQueueAcctToId, casefoldedAccount)
978 1066
 	channelsKey := fmt.Sprintf(keyAccountChannels, casefoldedAccount)
1067
+	joinedChannelsKey := fmt.Sprintf(keyAccountJoinedChannels, casefoldedAccount)
1068
+	lastSignoffKey := fmt.Sprintf(keyAccountLastSignoff, casefoldedAccount)
979 1069
 
980 1070
 	var clients []*Client
981 1071
 
@@ -1011,6 +1101,8 @@ func (am *AccountManager) Unregister(account string) error {
1011 1101
 		tx.Delete(vhostKey)
1012 1102
 		channelsStr, _ = tx.Get(channelsKey)
1013 1103
 		tx.Delete(channelsKey)
1104
+		tx.Delete(joinedChannelsKey)
1105
+		tx.Delete(lastSignoffKey)
1014 1106
 
1015 1107
 		_, err := tx.Delete(vhostQueueKey)
1016 1108
 		am.decrementVHostQueueCount(casefoldedAccount, err)
@@ -1087,13 +1179,13 @@ func (am *AccountManager) ChannelsForAccount(account string) (channels []string)
1087 1179
 	return unmarshalRegisteredChannels(channelStr)
1088 1180
 }
1089 1181
 
1090
-func (am *AccountManager) AuthenticateByCertFP(client *Client, authzid string) error {
1091
-	if client.certfp == "" {
1182
+func (am *AccountManager) AuthenticateByCertFP(client *Client, certfp, authzid string) error {
1183
+	if certfp == "" {
1092 1184
 		return errAccountInvalidCredentials
1093 1185
 	}
1094 1186
 
1095 1187
 	var account string
1096
-	certFPKey := fmt.Sprintf(keyCertToAccount, client.certfp)
1188
+	certFPKey := fmt.Sprintf(keyCertToAccount, certfp)
1097 1189
 
1098 1190
 	err := am.server.store.View(func(tx *buntdb.Tx) error {
1099 1191
 		account, _ = tx.Get(certFPKey)
@@ -1455,10 +1547,7 @@ func (am *AccountManager) applyVhostToClients(account string, result VHostInfo)
1455 1547
 }
1456 1548
 
1457 1549
 func (am *AccountManager) Login(client *Client, account ClientAccount) {
1458
-	changed := client.SetAccountName(account.Name)
1459
-	if !changed {
1460
-		return
1461
-	}
1550
+	client.Login(account)
1462 1551
 
1463 1552
 	client.nickTimer.Touch(nil)
1464 1553
 
@@ -1468,9 +1557,6 @@ func (am *AccountManager) Login(client *Client, account ClientAccount) {
1468 1557
 	am.Lock()
1469 1558
 	defer am.Unlock()
1470 1559
 	am.accountToClients[casefoldedAccount] = append(am.accountToClients[casefoldedAccount], client)
1471
-	for _, client := range am.accountToClients[casefoldedAccount] {
1472
-		client.SetAccountSettings(account.Settings)
1473
-	}
1474 1560
 }
1475 1561
 
1476 1562
 func (am *AccountManager) Logout(client *Client) {
@@ -1590,12 +1676,12 @@ func (ac *AccountCredentials) RemoveCertfp(certfp string) (err error) {
1590 1676
 	return nil
1591 1677
 }
1592 1678
 
1593
-type BouncerAllowedSetting int
1679
+type MulticlientAllowedSetting int
1594 1680
 
1595 1681
 const (
1596
-	BouncerAllowedServerDefault BouncerAllowedSetting = iota
1597
-	BouncerDisallowedByUser
1598
-	BouncerAllowedByUser
1682
+	MulticlientAllowedServerDefault MulticlientAllowedSetting = iota
1683
+	MulticlientDisallowedByUser
1684
+	MulticlientAllowedByUser
1599 1685
 )
1600 1686
 
1601 1687
 // controls whether/when clients without event-playback support see fake
@@ -1622,11 +1708,16 @@ func replayJoinsSettingFromString(str string) (result ReplayJoinsSetting, err er
1622 1708
 	return
1623 1709
 }
1624 1710
 
1711
+// XXX: AllowBouncer cannot be renamed AllowMulticlient because it is stored in
1712
+// persistent JSON blobs in the database
1625 1713
 type AccountSettings struct {
1626
-	AutoreplayLines *int
1627
-	NickEnforcement NickEnforcementMethod
1628
-	AllowBouncer    BouncerAllowedSetting
1629
-	ReplayJoins     ReplayJoinsSetting
1714
+	AutoreplayLines  *int
1715
+	NickEnforcement  NickEnforcementMethod
1716
+	AllowBouncer     MulticlientAllowedSetting
1717
+	ReplayJoins      ReplayJoinsSetting
1718
+	AlwaysOn         PersistentStatus
1719
+	AutoreplayMissed bool
1720
+	DMHistory        HistoryStatus
1630 1721
 }
1631 1722
 
1632 1723
 // ClientAccount represents a user account.
@@ -1661,7 +1752,7 @@ func (am *AccountManager) logoutOfAccount(client *Client) {
1661 1752
 		return
1662 1753
 	}
1663 1754
 
1664
-	client.SetAccountName("")
1755
+	client.Logout()
1665 1756
 	go client.nickTimer.Touch(nil)
1666 1757
 
1667 1758
 	// dispatch account-notify

+ 5
- 5
irc/caps/defs.go View File

@@ -37,6 +37,10 @@ const (
37 37
 	// https://ircv3.net/specs/extensions/chghost-3.2.html
38 38
 	ChgHost Capability = iota
39 39
 
40
+	// Chathistory is the proposed IRCv3 capability named "draft/chathistory":
41
+	// https://github.com/ircv3/ircv3-specifications/pull/393
42
+	Chathistory Capability = iota
43
+
40 44
 	// EventPlayback is the proposed IRCv3 capability named "draft/event-playback":
41 45
 	// https://github.com/ircv3/ircv3-specifications/pull/362
42 46
 	EventPlayback Capability = iota
@@ -85,10 +89,6 @@ const (
85 89
 	// https://ircv3.net/specs/extensions/multi-prefix-3.1.html
86 90
 	MultiPrefix Capability = iota
87 91
 
88
-	// Bouncer is the Oragono-specific capability named "oragono.io/bnc":
89
-	// https://oragono.io/bnc
90
-	Bouncer Capability = iota
91
-
92 92
 	// Nope is the Oragono vendor capability named "oragono.io/nope":
93 93
 	// https://oragono.io/nope
94 94
 	Nope Capability = iota
@@ -127,6 +127,7 @@ var (
127 127
 		"batch",
128 128
 		"cap-notify",
129 129
 		"chghost",
130
+		"draft/chathistory",
130 131
 		"draft/event-playback",
131 132
 		"draft/languages",
132 133
 		"draft/multiline",
@@ -139,7 +140,6 @@ var (
139 140
 		"labeled-response",
140 141
 		"message-tags",
141 142
 		"multi-prefix",
142
-		"oragono.io/bnc",
143 143
 		"oragono.io/nope",
144 144
 		"sasl",
145 145
 		"server-time",

+ 121
- 31
irc/channel.go View File

@@ -24,6 +24,10 @@ const (
24 24
 	histServMask = "HistServ!HistServ@localhost"
25 25
 )
26 26
 
27
+type ChannelSettings struct {
28
+	History HistoryStatus
29
+}
30
+
27 31
 // Channel represents a channel that clients can join.
28 32
 type Channel struct {
29 33
 	flags             modes.ModeSet
@@ -49,6 +53,7 @@ type Channel struct {
49 53
 	joinPartMutex     sync.Mutex      // tier 3
50 54
 	ensureLoaded      utils.Once      // manages loading stored registration info from the database
51 55
 	dirtyBits         uint
56
+	settings          ChannelSettings
52 57
 }
53 58
 
54 59
 // 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
66 71
 
67 72
 	channel.initializeLists()
68 73
 	channel.writerSemaphore.Initialize(1)
69
-	channel.history.Initialize(config.History.ChannelLength, config.History.AutoresizeWindow)
74
+	channel.history.Initialize(0, 0)
70 75
 
71 76
 	if !registered {
77
+		channel.resizeHistory(config)
72 78
 		for _, mode := range config.Channels.defaultModes {
73 79
 			channel.flags.SetMode(mode, true)
74 80
 		}
@@ -106,8 +112,19 @@ func (channel *Channel) IsLoaded() bool {
106 112
 	return channel.ensureLoaded.Done()
107 113
 }
108 114
 
115
+func (channel *Channel) resizeHistory(config *Config) {
116
+	_, ephemeral, _ := channel.historyStatus(config)
117
+	if ephemeral {
118
+		channel.history.Resize(config.History.ChannelLength, config.History.AutoresizeWindow)
119
+	} else {
120
+		channel.history.Resize(0, 0)
121
+	}
122
+}
123
+
109 124
 // read in channel state that was persisted in the DB
110 125
 func (channel *Channel) applyRegInfo(chanReg RegisteredChannel) {
126
+	defer channel.resizeHistory(channel.server.Config())
127
+
111 128
 	channel.stateMutex.Lock()
112 129
 	defer channel.stateMutex.Unlock()
113 130
 
@@ -120,6 +137,7 @@ func (channel *Channel) applyRegInfo(chanReg RegisteredChannel) {
120 137
 	channel.createdTime = chanReg.RegisteredAt
121 138
 	channel.key = chanReg.Key
122 139
 	channel.userLimit = chanReg.UserLimit
140
+	channel.settings = chanReg.Settings
123 141
 
124 142
 	for _, mode := range chanReg.Modes {
125 143
 		channel.flags.SetMode(mode, true)
@@ -164,6 +182,10 @@ func (channel *Channel) ExportRegistration(includeFlags uint) (info RegisteredCh
164 182
 		}
165 183
 	}
166 184
 
185
+	if includeFlags&IncludeSettings != 0 {
186
+		info.Settings = channel.settings
187
+	}
188
+
167 189
 	return
168 190
 }
169 191
 
@@ -434,7 +456,7 @@ func (channel *Channel) Names(client *Client, rb *ResponseBuffer) {
434 456
 			if modeSet == nil {
435 457
 				continue
436 458
 			}
437
-			if !isJoined && target.flags.HasMode(modes.Invisible) && !isOper {
459
+			if !isJoined && target.HasMode(modes.Invisible) && !isOper {
438 460
 				continue
439 461
 			}
440 462
 			prefix := modeSet.Prefixes(isMultiPrefix)
@@ -564,6 +586,48 @@ func (channel *Channel) IsEmpty() bool {
564 586
 	return len(channel.members) == 0
565 587
 }
566 588
 
589
+// figure out where history is being stored: persistent, ephemeral, or neither
590
+// target is only needed if we're doing persistent history
591
+func (channel *Channel) historyStatus(config *Config) (persistent, ephemeral bool, target string) {
592
+	if !config.History.Persistent.Enabled {
593
+		return false, config.History.Enabled, ""
594
+	}
595
+
596
+	channel.stateMutex.RLock()
597
+	target = channel.nameCasefolded
598
+	historyStatus := channel.settings.History
599
+	registered := channel.registeredFounder != ""
600
+	channel.stateMutex.RUnlock()
601
+
602
+	historyStatus = historyEnabled(config.History.Persistent.RegisteredChannels, historyStatus)
603
+
604
+	// ephemeral history: either the channel owner explicitly set the ephemeral preference,
605
+	// or persistent history is disabled for unregistered channels
606
+	if registered {
607
+		ephemeral = (historyStatus == HistoryEphemeral)
608
+		persistent = (historyStatus == HistoryPersistent)
609
+	} else {
610
+		ephemeral = config.History.Enabled && !config.History.Persistent.UnregisteredChannels
611
+		persistent = config.History.Persistent.UnregisteredChannels
612
+	}
613
+	return
614
+}
615
+
616
+func (channel *Channel) AddHistoryItem(item history.Item) (err error) {
617
+	if !item.IsStorable() {
618
+		return
619
+	}
620
+
621
+	persistent, ephemeral, target := channel.historyStatus(channel.server.Config())
622
+	if ephemeral {
623
+		channel.history.Add(item)
624
+	}
625
+	if persistent {
626
+		return channel.server.historyDB.AddChannelItem(target, item)
627
+	}
628
+	return nil
629
+}
630
+
567 631
 // Join joins the given client to this channel (if they can be joined).
568 632
 func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *ResponseBuffer) {
569 633
 	details := client.Details()
@@ -618,8 +682,6 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
618 682
 
619 683
 	client.server.logger.Debug("join", fmt.Sprintf("%s joined channel %s", details.nick, chname))
620 684
 
621
-	var message utils.SplitMessage
622
-
623 685
 	givenMode := func() (givenMode modes.Mode) {
624 686
 		channel.joinPartMutex.Lock()
625 687
 		defer channel.joinPartMutex.Unlock()
@@ -643,6 +705,12 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
643 705
 
644 706
 		channel.regenerateMembersCache()
645 707
 
708
+		return
709
+	}()
710
+
711
+	var message utils.SplitMessage
712
+	// no history item for fake persistent joins
713
+	if rb != nil {
646 714
 		message = utils.MakeMessage("")
647 715
 		histItem := history.Item{
648 716
 			Type:        history.Join,
@@ -651,13 +719,15 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
651 719
 			Message:     message,
652 720
 		}
653 721
 		histItem.Params[0] = details.realname
654
-		channel.history.Add(histItem)
655
-
656
-		return
657
-	}()
722
+		channel.AddHistoryItem(histItem)
723
+	}
658 724
 
659 725
 	client.addChannel(channel)
660 726
 
727
+	if rb == nil {
728
+		return
729
+	}
730
+
661 731
 	var modestr string
662 732
 	if givenMode != 0 {
663 733
 		modestr = fmt.Sprintf("+%v", givenMode)
@@ -702,10 +772,22 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
702 772
 
703 773
 func (channel *Channel) autoReplayHistory(client *Client, rb *ResponseBuffer, skipMsgid string) {
704 774
 	// autoreplay any messages as necessary
705
-	config := channel.server.Config()
706 775
 	var items []history.Item
776
+
777
+	var after, before time.Time
707 778
 	if rb.session.zncPlaybackTimes != nil && (rb.session.zncPlaybackTimes.targets == nil || rb.session.zncPlaybackTimes.targets.Has(channel.NameCasefolded())) {
708
-		items, _ = channel.history.Between(rb.session.zncPlaybackTimes.after, rb.session.zncPlaybackTimes.before, false, config.History.ChathistoryMax)
779
+		after, before = rb.session.zncPlaybackTimes.after, rb.session.zncPlaybackTimes.before
780
+	} else if !rb.session.lastSignoff.IsZero() {
781
+		// we already checked for history caps in `playReattachMessages`
782
+		after = rb.session.lastSignoff
783
+	}
784
+
785
+	if !after.IsZero() || !before.IsZero() {
786
+		_, seq, _ := channel.server.GetHistorySequence(channel, client, "")
787
+		if seq != nil {
788
+			zncMax := channel.server.Config().History.ZNCMax
789
+			items, _, _ = seq.Between(history.Selector{Time: after}, history.Selector{Time: before}, zncMax)
790
+		}
709 791
 	} else if !rb.session.HasHistoryCaps() {
710 792
 		var replayLimit int
711 793
 		customReplayLimit := client.AccountSettings().AutoreplayLines
@@ -719,7 +801,10 @@ func (channel *Channel) autoReplayHistory(client *Client, rb *ResponseBuffer, sk
719 801
 			replayLimit = channel.server.Config().History.AutoreplayOnJoin
720 802
 		}
721 803
 		if 0 < replayLimit {
722
-			items = channel.history.Latest(replayLimit)
804
+			_, seq, _ := channel.server.GetHistorySequence(channel, client, "")
805
+			if seq != nil {
806
+				items, _, _ = seq.Between(history.Selector{}, history.Selector{}, replayLimit)
807
+			}
723 808
 		}
724 809
 	}
725 810
 	// remove the client's own JOIN line from the replay
@@ -784,7 +869,7 @@ func (channel *Channel) Part(client *Client, message string, rb *ResponseBuffer)
784 869
 		}
785 870
 	}
786 871
 
787
-	channel.history.Add(history.Item{
872
+	channel.AddHistoryItem(history.Item{
788 873
 		Type:        history.Part,
789 874
 		Nick:        details.nickMask,
790 875
 		AccountName: details.accountName,
@@ -799,10 +884,9 @@ func (channel *Channel) Part(client *Client, message string, rb *ResponseBuffer)
799 884
 // 2. Send JOIN and MODE lines to channel participants (including the new client)
800 885
 // 3. Replay missed message history to the client
801 886
 func (channel *Channel) Resume(session *Session, timestamp time.Time) {
802
-	now := time.Now().UTC()
803 887
 	channel.resumeAndAnnounce(session)
804 888
 	if !timestamp.IsZero() {
805
-		channel.replayHistoryForResume(session, timestamp, now)
889
+		channel.replayHistoryForResume(session, timestamp, time.Time{})
806 890
 	}
807 891
 }
808 892
 
@@ -852,9 +936,17 @@ func (channel *Channel) resumeAndAnnounce(session *Session) {
852 936
 }
853 937
 
854 938
 func (channel *Channel) replayHistoryForResume(session *Session, after time.Time, before time.Time) {
855
-	items, complete := channel.history.Between(after, before, false, 0)
939
+	var items []history.Item
940
+	var complete bool
941
+	afterS, beforeS := history.Selector{Time: after}, history.Selector{Time: before}
942
+	_, seq, _ := channel.server.GetHistorySequence(channel, session.client, "")
943
+	if seq != nil {
944
+		items, complete, _ = seq.Between(afterS, beforeS, channel.server.Config().History.ZNCMax)
945
+	}
856 946
 	rb := NewResponseBuffer(session)
857
-	channel.replayHistoryItems(rb, items, false)
947
+	if len(items) != 0 {
948
+		channel.replayHistoryItems(rb, items, false)
949
+	}
858 950
 	if !complete && !session.resumeDetails.HistoryIncomplete {
859 951
 		// warn here if we didn't warn already
860 952
 		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) {
871 963
 }
872 964
 
873 965
 func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.Item, autoreplay bool) {
874
-	if len(items) == 0 {
875
-		return
876
-	}
877
-
966
+	// send an empty batch if necessary, as per the CHATHISTORY spec
878 967
 	chname := channel.Name()
879 968
 	client := rb.target
880 969
 	eventPlayback := rb.session.capabilities.Has(caps.EventPlayback)
@@ -908,9 +997,9 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I
908 997
 		case history.Join:
909 998
 			if eventPlayback {
910 999
 				if extendedJoin {
911
-					rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "JOIN", chname, item.AccountName, item.Params[0])
1000
+					rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "JOIN", chname, item.AccountName, item.Params[0])
912 1001
 				} else {
913
-					rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "JOIN", chname)
1002
+					rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "JOIN", chname)
914 1003
 				}
915 1004
 			} else {
916 1005
 				if !playJoinsAsPrivmsg {
@@ -926,7 +1015,7 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I
926 1015
 			}
927 1016
 		case history.Part:
928 1017
 			if eventPlayback {
929
-				rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "PART", chname, item.Message.Message)
1018
+				rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "PART", chname, item.Message.Message)
930 1019
 			} else {
931 1020
 				if !playJoinsAsPrivmsg {
932 1021
 					continue // #474
@@ -936,14 +1025,14 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I
936 1025
 			}
937 1026
 		case history.Kick:
938 1027
 			if eventPlayback {
939
-				rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "KICK", chname, item.Params[0], item.Message.Message)
1028
+				rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "KICK", chname, item.Params[0], item.Message.Message)
940 1029
 			} else {
941 1030
 				message := fmt.Sprintf(client.t("%[1]s kicked %[2]s (%[3]s)"), nick, item.Params[0], item.Message.Message)
942 1031
 				rb.AddFromClient(item.Message.Time, utils.MungeSecretToken(item.Message.Msgid), histServMask, "*", nil, "PRIVMSG", chname, message)
943 1032
 			}
944 1033
 		case history.Quit:
945 1034
 			if eventPlayback {
946
-				rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "QUIT", item.Message.Message)
1035
+				rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "QUIT", item.Message.Message)
947 1036
 			} else {
948 1037
 				if !playJoinsAsPrivmsg {
949 1038
 					continue // #474
@@ -953,7 +1042,7 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I
953 1042
 			}
954 1043
 		case history.Nick:
955 1044
 			if eventPlayback {
956
-				rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "NICK", item.Params[0])
1045
+				rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "NICK", item.Params[0])
957 1046
 			} else {
958 1047
 				message := fmt.Sprintf(client.t("%[1]s changed nick to %[2]s"), nick, item.Params[0])
959 1048
 				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
1124 1213
 			// STATUSMSG
1125 1214
 			continue
1126 1215
 		}
1127
-		if isCTCP && member.isTor {
1128
-			continue // #753
1129
-		}
1130 1216
 
1131 1217
 		for _, session := range member.Sessions() {
1218
+			if isCTCP && session.isTor {
1219
+				continue // #753
1220
+			}
1221
+
1132 1222
 			var tagsToUse map[string]string
1133 1223
 			if session.capabilities.Has(caps.MessageTags) {
1134 1224
 				tagsToUse = clientOnlyTags
@@ -1144,7 +1234,7 @@ func (channel *Channel) SendSplitMessage(command string, minPrefixMode modes.Mod
1144 1234
 		}
1145 1235
 	}
1146 1236
 
1147
-	channel.history.Add(history.Item{
1237
+	channel.AddHistoryItem(history.Item{
1148 1238
 		Type:        histType,
1149 1239
 		Message:     message,
1150 1240
 		Nick:        nickmask,
@@ -1266,7 +1356,7 @@ func (channel *Channel) Kick(client *Client, target *Client, comment string, rb
1266 1356
 		Message:     message,
1267 1357
 	}
1268 1358
 	histItem.Params[0] = targetNick
1269
-	channel.history.Add(histItem)
1359
+	channel.AddHistoryItem(histItem)
1270 1360
 
1271 1361
 	channel.Quit(target)
1272 1362
 }

+ 16
- 1
irc/channelreg.go View File

@@ -33,6 +33,7 @@ const (
33 33
 	keyChannelModes          = "channel.modes %s"
34 34
 	keyChannelAccountToUMode = "channel.accounttoumode %s"
35 35
 	keyChannelUserLimit      = "channel.userlimit %s"
36
+	keyChannelSettings       = "channel.settings %s"
36 37
 
37 38
 	keyChannelPurged = "channel.purged %s"
38 39
 )
@@ -53,6 +54,7 @@ var (
53 54
 		keyChannelModes,
54 55
 		keyChannelAccountToUMode,
55 56
 		keyChannelUserLimit,
57
+		keyChannelSettings,
56 58
 	}
57 59
 )
58 60
 
@@ -63,6 +65,7 @@ const (
63 65
 	IncludeTopic
64 66
 	IncludeModes
65 67
 	IncludeLists
68
+	IncludeSettings
66 69
 )
67 70
 
68 71
 // this is an OR of all possible flags
@@ -100,6 +103,8 @@ type RegisteredChannel struct {
100 103
 	Excepts map[string]MaskInfo
101 104
 	// Invites represents the invite exceptions set on the channel.
102 105
 	Invites map[string]MaskInfo
106
+	// Settings are the chanserv-modifiable settings
107
+	Settings ChannelSettings
103 108
 }
104 109
 
105 110
 type ChannelPurgeRecord struct {
@@ -203,6 +208,7 @@ func (reg *ChannelRegistry) LoadChannel(nameCasefolded string) (info RegisteredC
203 208
 		exceptlistString, _ := tx.Get(fmt.Sprintf(keyChannelExceptlist, channelKey))
204 209
 		invitelistString, _ := tx.Get(fmt.Sprintf(keyChannelInvitelist, channelKey))
205 210
 		accountToUModeString, _ := tx.Get(fmt.Sprintf(keyChannelAccountToUMode, channelKey))
211
+		settingsString, _ := tx.Get(fmt.Sprintf(keyChannelSettings, channelKey))
206 212
 
207 213
 		modeSlice := make([]modes.Mode, len(modeString))
208 214
 		for i, mode := range modeString {
@@ -220,6 +226,9 @@ func (reg *ChannelRegistry) LoadChannel(nameCasefolded string) (info RegisteredC
220 226
 		accountToUMode := make(map[string]modes.Mode)
221 227
 		_ = json.Unmarshal([]byte(accountToUModeString), &accountToUMode)
222 228
 
229
+		var settings ChannelSettings
230
+		_ = json.Unmarshal([]byte(settingsString), &settings)
231
+
223 232
 		info = RegisteredChannel{
224 233
 			Name:           name,
225 234
 			RegisteredAt:   time.Unix(regTimeInt, 0).UTC(),
@@ -234,6 +243,7 @@ func (reg *ChannelRegistry) LoadChannel(nameCasefolded string) (info RegisteredC
234 243
 			Invites:        invitelist,
235 244
 			AccountToUMode: accountToUMode,
236 245
 			UserLimit:      int(userLimit),
246
+			Settings:       settings,
237 247
 		}
238 248
 		return nil
239 249
 	})
@@ -261,7 +271,7 @@ func (reg *ChannelRegistry) deleteChannel(tx *buntdb.Tx, key string, info Regist
261 271
 	if err == nil {
262 272
 		regTime, _ := tx.Get(fmt.Sprintf(keyChannelRegTime, key))
263 273
 		regTimeInt, _ := strconv.ParseInt(regTime, 10, 64)
264
-		registeredAt := time.Unix(regTimeInt, 0)
274
+		registeredAt := time.Unix(regTimeInt, 0).UTC()
265 275
 		founder, _ := tx.Get(fmt.Sprintf(keyChannelFounder, key))
266 276
 
267 277
 		// 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
357 367
 		accountToUModeString, _ := json.Marshal(channelInfo.AccountToUMode)
358 368
 		tx.Set(fmt.Sprintf(keyChannelAccountToUMode, channelKey), string(accountToUModeString), nil)
359 369
 	}
370
+
371
+	if includeFlags&IncludeSettings != 0 {
372
+		settingsString, _ := json.Marshal(channelInfo.Settings)
373
+		tx.Set(fmt.Sprintf(keyChannelSettings, channelKey), string(settingsString), nil)
374
+	}
360 375
 }
361 376
 
362 377
 // PurgeChannel records a channel purge.

+ 116
- 21
irc/chanserv.go View File

@@ -135,6 +135,35 @@ INFO displays info about a registered channel.`,
135 135
 			enabled:   chanregEnabled,
136 136
 			minParams: 1,
137 137
 		},
138
+		"get": {
139
+			handler: csGetHandler,
140
+			help: `Syntax: $bGET #channel <setting>$b
141
+
142
+GET queries the current values of the channel settings. For more information
143
+on the settings and their possible values, see HELP SET.`,
144
+			helpShort: `$bGET$b queries the current values of a channel's settings`,
145
+			enabled:   chanregEnabled,
146
+			minParams: 2,
147
+		},
148
+		"set": {
149
+			handler:   csSetHandler,
150
+			helpShort: `$bSET$b modifies a channel's settings`,
151
+			// these are broken out as separate strings so they can be translated separately
152
+			helpStrings: []string{
153
+				`Syntax $bSET #channel <setting> <value>$b
154
+
155
+SET modifies a channel's settings. The following settings are available:`,
156
+
157
+				`$bHISTORY$b
158
+'history' lets you control how channel history is stored. Your options are:
159
+1. 'off'        [no history]
160
+2. 'ephemeral'  [a limited amount of temporary history, not stored on disk]
161
+3. 'on'         [history stored in a permanent database, if available]
162
+4. 'default'    [use the server default]`,
163
+			},
164
+			enabled:   chanregEnabled,
165
+			minParams: 3,
166
+		},
138 167
 	}
139 168
 )
140 169
 
@@ -318,6 +347,22 @@ func checkChanLimit(client *Client, rb *ResponseBuffer) (ok bool) {
318 347
 	return
319 348
 }
320 349
 
350
+func csPrivsCheck(channel RegisteredChannel, client *Client, rb *ResponseBuffer) (success bool) {
351
+	founder := channel.Founder
352
+	if founder == "" {
353
+		csNotice(rb, client.t("That channel is not registered"))
354
+		return false
355
+	}
356
+	if client.HasRoleCapabs("chanreg") {
357
+		return true
358
+	}
359
+	if founder != client.Account() {
360
+		csNotice(rb, client.t("Insufficient privileges"))
361
+		return false
362
+	}
363
+	return true
364
+}
365
+
321 366
 func csUnregisterHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
322 367
 	channelName := params[0]
323 368
 	var verificationCode string
@@ -325,31 +370,18 @@ func csUnregisterHandler(server *Server, client *Client, command string, params
325 370
 		verificationCode = params[1]
326 371
 	}
327 372
 
328
-	channelKey, err := CasefoldChannel(channelName)
329
-	if channelKey == "" || err != nil {
330
-		csNotice(rb, client.t("Channel name is not valid"))
331
-		return
332
-	}
333
-
334
-	channel := server.channels.Get(channelKey)
373
+	channel := server.channels.Get(channelName)
335 374
 	if channel == nil {
336 375
 		csNotice(rb, client.t("No such channel"))
337 376
 		return
338 377
 	}
339 378
 
340
-	founder := channel.Founder()
341
-	if founder == "" {
342
-		csNotice(rb, client.t("That channel is not registered"))
343
-		return
344
-	}
345
-
346
-	hasPrivs := client.HasRoleCapabs("chanreg") || founder == client.Account()
347
-	if !hasPrivs {
348
-		csNotice(rb, client.t("Insufficient privileges"))
379
+	info := channel.ExportRegistration(0)
380
+	channelKey := info.NameCasefolded
381
+	if !csPrivsCheck(info, client, rb) {
349 382
 		return
350 383
 	}
351 384
 
352
-	info := channel.ExportRegistration(0)
353 385
 	expectedCode := utils.ConfirmationCode(info.Name, info.RegisteredAt)
354 386
 	if expectedCode != verificationCode {
355 387
 		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
357 389
 		return
358 390
 	}
359 391
 
360
-	server.channels.SetUnregistered(channelKey, founder)
392
+	server.channels.SetUnregistered(channelKey, info.Founder)
361 393
 	csNotice(rb, fmt.Sprintf(client.t("Channel %s is now unregistered"), channelKey))
362 394
 }
363 395
 
@@ -367,9 +399,7 @@ func csClearHandler(server *Server, client *Client, command string, params []str
367 399
 		csNotice(rb, client.t("Channel does not exist"))
368 400
 		return
369 401
 	}
370
-	account := client.Account()
371
-	if !(client.HasRoleCapabs("chanreg") || (account != "" && account == channel.Founder())) {
372
-		csNotice(rb, client.t("Insufficient privileges"))
402
+	if !csPrivsCheck(channel.ExportRegistration(0), client, rb) {
373 403
 		return
374 404
 	}
375 405
 
@@ -563,3 +593,68 @@ func csInfoHandler(server *Server, client *Client, command string, params []stri
563 593
 	csNotice(rb, fmt.Sprintf(client.t("Founder: %s"), chinfo.Founder))
564 594
 	csNotice(rb, fmt.Sprintf(client.t("Registered at: %s"), chinfo.RegisteredAt.Format(time.RFC1123)))
565 595
 }
596
+
597
+func displayChannelSetting(settingName string, settings ChannelSettings, client *Client, rb *ResponseBuffer) {
598
+	config := client.server.Config()
599
+
600
+	switch strings.ToLower(settingName) {
601
+	case "history":
602
+		effectiveValue := historyEnabled(config.History.Persistent.RegisteredChannels, settings.History)
603
+		csNotice(rb, fmt.Sprintf(client.t("The stored channel history setting is: %s"), historyStatusToString(settings.History)))
604
+		csNotice(rb, fmt.Sprintf(client.t("Given current server settings, the channel history setting is: %s"), historyStatusToString(effectiveValue)))
605
+	default:
606
+		csNotice(rb, client.t("Invalid params"))
607
+	}
608
+}
609
+
610
+func csGetHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
611
+	chname, setting := params[0], params[1]
612
+	channel := server.channels.Get(chname)
613
+	if channel == nil {
614
+		csNotice(rb, client.t("No such channel"))
615
+		return
616
+	}
617
+	info := channel.ExportRegistration(IncludeSettings)
618
+	if !csPrivsCheck(info, client, rb) {
619
+		return
620
+	}
621
+
622
+	displayChannelSetting(setting, info.Settings, client, rb)
623
+}
624
+
625
+func csSetHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
626
+	chname, setting, value := params[0], params[1], params[2]
627
+	channel := server.channels.Get(chname)
628
+	if channel == nil {
629
+		csNotice(rb, client.t("No such channel"))
630
+		return
631
+	}
632
+	info := channel.ExportRegistration(IncludeSettings)
633
+	settings := info.Settings
634
+	if !csPrivsCheck(info, client, rb) {
635
+		return
636
+	}
637
+
638
+	var err error
639
+	switch strings.ToLower(setting) {
640
+	case "history":
641
+		settings.History, err = historyStatusFromString(value)
642
+		if err != nil {
643
+			err = errInvalidParams
644
+			break
645
+		}
646
+		channel.SetSettings(settings)
647
+		channel.resizeHistory(server.Config())
648
+	}
649
+
650
+	switch err {
651
+	case nil:
652
+		csNotice(rb, client.t("Successfully changed the channel settings"))
653
+		displayChannelSetting(setting, settings, client, rb)
654
+	case errInvalidParams:
655
+		csNotice(rb, client.t("Invalid parameters"))
656
+	default:
657
+		server.logger.Error("internal", "CS SET error:", err.Error())
658
+		csNotice(rb, client.t("An error occurred"))
659
+	}
660
+}

+ 303
- 73
irc/client.go View File

@@ -29,7 +29,7 @@ import (
29 29
 const (
30 30
 	// IdentTimeoutSeconds is how many seconds before our ident (username) check times out.
31 31
 	IdentTimeoutSeconds  = 1.5
32
-	IRCv3TimestampFormat = "2006-01-02T15:04:05.000Z"
32
+	IRCv3TimestampFormat = utils.IRCv3TimestampFormat
33 33
 )
34 34
 
35 35
 // ResumeDetails is a place to stash data at various stages of
@@ -45,22 +45,22 @@ type ResumeDetails struct {
45 45
 type Client struct {
46 46
 	account            string
47 47
 	accountName        string // display name of the account: uncasefolded, '*' if not logged in
48
+	accountRegDate     time.Time
48 49
 	accountSettings    AccountSettings
49 50
 	atime              time.Time
50 51
 	away               bool
51 52
 	awayMessage        string
52 53
 	brbTimer           BrbTimer
53
-	certfp             string
54 54
 	channels           ChannelSet
55 55
 	ctime              time.Time
56 56
 	destroyed          bool
57 57
 	exitedSnomaskSent  bool
58
-	flags              modes.ModeSet
58
+	modes              modes.ModeSet
59 59
 	hostname           string
60 60
 	invitedTo          map[string]bool
61 61
 	isSTSOnly          bool
62
-	isTor              bool
63 62
 	languages          []string
63
+	lastSignoff        time.Time // for always-on clients, the time their last session quit
64 64
 	loginThrottle      connection_limits.GenericThrottle
65 65
 	nick               string
66 66
 	nickCasefolded     string
@@ -76,17 +76,25 @@ type Client struct {
76 76
 	realIP             net.IP
77 77
 	registered         bool
78 78
 	resumeID           string
79
-	saslInProgress     bool
80
-	saslMechanism      string
81
-	saslValue          string
82
-	sentPassCommand    bool
83 79
 	server             *Server
84 80
 	skeleton           string
85 81
 	sessions           []*Session
86 82
 	stateMutex         sync.RWMutex // tier 1
83
+	alwaysOn           bool
87 84
 	username           string
88 85
 	vhost              string
89 86
 	history            history.Buffer
87
+	dirtyBits          uint
88
+	writerSemaphore    utils.Semaphore // tier 1.5
89
+}
90
+
91
+type saslStatus struct {
92
+	mechanism string
93
+	value     string
94
+}
95
+
96
+func (s *saslStatus) Clear() {
97
+	*s = saslStatus{}
90 98
 }
91 99
 
92 100
 // Session is an individual client connection to the server (TCP connection
@@ -102,11 +110,16 @@ type Session struct {
102 110
 	realIP      net.IP
103 111
 	proxiedIP   net.IP
104 112
 	rawHostname string
113
+	isTor       bool
105 114
 
106 115
 	idletimer IdleTimer
107 116
 	fakelag   Fakelag
108 117
 	destroyed uint32
109 118
 
119
+	certfp          string
120
+	sasl            saslStatus
121
+	sentPassCommand bool
122
+
110 123
 	batchCounter uint32
111 124
 
112 125
 	quitMessage string
@@ -120,6 +133,7 @@ type Session struct {
120 133
 	resumeID         string
121 134
 	resumeDetails    *ResumeDetails
122 135
 	zncPlaybackTimes *zncPlaybackTimes
136
+	lastSignoff      time.Time
123 137
 
124 138
 	batch MultilineBatch
125 139
 }
@@ -147,6 +161,13 @@ func (sd *Session) SetQuitMessage(message string) (set bool) {
147 161
 	}
148 162
 }
149 163
 
164
+func (s *Session) IP() net.IP {
165
+	if s.proxiedIP != nil {
166
+		return s.proxiedIP
167
+	}
168
+	return s.realIP
169
+}
170
+
150 171
 // returns whether the session was actively destroyed (for example, by ping
151 172
 // timeout or NS GHOST).
152 173
 // avoids a race condition between asynchronous idle-timing-out of sessions,
@@ -164,8 +185,7 @@ func (session *Session) SetDestroyed() {
164 185
 // returns whether the client supports a smart history replay cap,
165 186
 // and therefore autoreplay-on-join and similar should be suppressed
166 187
 func (session *Session) HasHistoryCaps() bool {
167
-	// TODO the chathistory cap will go here as well
168
-	return session.capabilities.Has(caps.ZNCPlayback)
188
+	return session.capabilities.Has(caps.Chathistory) || session.capabilities.Has(caps.ZNCPlayback)
169 189
 }
170 190
 
171 191
 // generates a batch ID. the uniqueness requirements for this are fairly weak:
@@ -231,7 +251,6 @@ func (server *Server) RunClient(conn clientConn, proxyLine string) {
231 251
 		channels:  make(ChannelSet),
232 252
 		ctime:     now,
233 253
 		isSTSOnly: conn.Config.STSOnly,
234
-		isTor:     conn.Config.Tor,
235 254
 		languages: server.Languages().Default(),
236 255
 		loginThrottle: connection_limits.GenericThrottle{
237 256
 			Duration: config.Accounts.LoginThrottling.Duration,
@@ -253,13 +272,14 @@ func (server *Server) RunClient(conn clientConn, proxyLine string) {
253 272
 		ctime:      now,
254 273
 		atime:      now,
255 274
 		realIP:     realIP,
275
+		isTor:      conn.Config.Tor,
256 276
 	}
257 277
 	client.sessions = []*Session{session}
258 278
 
259 279
 	if conn.Config.TLSConfig != nil {
260 280
 		client.SetMode(modes.TLS, true)
261 281
 		// error is not useful to us here anyways so we can ignore it
262
-		client.certfp, _ = socket.CertFP()
282
+		session.certfp, _ = socket.CertFP()
263 283
 	}
264 284
 
265 285
 	if conn.Config.Tor {
@@ -272,7 +292,7 @@ func (server *Server) RunClient(conn clientConn, proxyLine string) {
272 292
 		client.rawHostname = session.rawHostname
273 293
 	} else {
274 294
 		remoteAddr := conn.Conn.RemoteAddr()
275
-		if utils.AddrIsLocal(remoteAddr) {
295
+		if realIP.IsLoopback() || utils.IPInNets(realIP, config.Server.secureNets) {
276 296
 			// treat local connections as secure (may be overridden later by WEBIRC)
277 297
 			client.SetMode(modes.TLS, true)
278 298
 		}
@@ -286,10 +306,66 @@ func (server *Server) RunClient(conn clientConn, proxyLine string) {
286 306
 	client.run(session, proxyLine)
287 307
 }
288 308
 
309
+func (server *Server) AddAlwaysOnClient(account ClientAccount, chnames []string, lastSignoff time.Time) {
310
+	now := time.Now().UTC()
311
+	config := server.Config()
312
+
313
+	client := &Client{
314
+		atime:     now,
315
+		channels:  make(ChannelSet),
316
+		ctime:     now,
317
+		languages: server.Languages().Default(),
318
+		server:    server,
319
+
320
+		// TODO figure out how to set these on reattach?
321
+		username:    "~user",
322
+		rawHostname: server.name,
323
+		realIP:      utils.IPv4LoopbackAddress,
324
+
325
+		alwaysOn:    true,
326
+		lastSignoff: lastSignoff,
327
+	}
328
+
329
+	client.SetMode(modes.TLS, true)
330
+	client.writerSemaphore.Initialize(1)
331
+	client.history.Initialize(0, 0)
332
+	client.brbTimer.Initialize(client)
333
+
334
+	server.accounts.Login(client, account)
335
+
336
+	client.resizeHistory(config)
337
+
338
+	_, err := server.clients.SetNick(client, nil, account.Name)
339
+	if err != nil {
340
+		server.logger.Error("internal", "could not establish always-on client", account.Name, err.Error())
341
+		return
342
+	} else {
343
+		server.logger.Debug("accounts", "established always-on client", account.Name)
344
+	}
345
+
346
+	// XXX set this last to avoid confusing SetNick:
347
+	client.registered = true
348
+
349
+	for _, chname := range chnames {
350
+		// XXX we're using isSajoin=true, to make these joins succeed even without channel key
351
+		// this is *probably* ok as long as the persisted memberships are accurate
352
+		server.channels.Join(client, chname, "", true, nil)
353
+	}
354
+}
355
+
356
+func (client *Client) resizeHistory(config *Config) {
357
+	_, ephemeral, _ := client.historyStatus(config)
358
+	if ephemeral {
359
+		client.history.Resize(config.History.ClientLength, config.History.AutoresizeWindow)
360
+	} else {
361
+		client.history.Resize(0, 0)
362
+	}
363
+}
364
+
289 365
 // resolve an IP to an IRC-ready hostname, using reverse DNS, forward-confirming if necessary,
290 366
 // and sending appropriate notices to the client
291 367
 func (client *Client) lookupHostname(session *Session, overwrite bool) {
292
-	if client.isTor {
368
+	if session.isTor {
293 369
 		return
294 370
 	} // else: even if cloaking is enabled, look up the real hostname to show to operators
295 371
 
@@ -384,18 +460,18 @@ const (
384 460
 	authFailSaslRequired
385 461
 )
386 462
 
387
-func (client *Client) isAuthorized(config *Config) AuthOutcome {
463
+func (client *Client) isAuthorized(config *Config, session *Session) AuthOutcome {
388 464
 	saslSent := client.account != ""
389 465
 	// PASS requirement
390
-	if (config.Server.passwordBytes != nil) && !client.sentPassCommand && !(config.Accounts.SkipServerPassword && saslSent) {
466
+	if (config.Server.passwordBytes != nil) && !session.sentPassCommand && !(config.Accounts.SkipServerPassword && saslSent) {
391 467
 		return authFailPass
392 468
 	}
393 469
 	// Tor connections may be required to authenticate with SASL
394
-	if client.isTor && config.Server.TorListeners.RequireSasl && !saslSent {
470
+	if session.isTor && config.Server.TorListeners.RequireSasl && !saslSent {
395 471
 		return authFailTorSaslRequired
396 472
 	}
397 473
 	// finally, enforce require-sasl
398
-	if config.Accounts.RequireSasl.Enabled && !saslSent && !utils.IPInNets(client.IP(), config.Accounts.RequireSasl.exemptedNets) {
474
+	if config.Accounts.RequireSasl.Enabled && !saslSent && !utils.IPInNets(session.IP(), config.Accounts.RequireSasl.exemptedNets) {
399 475
 		return authFailSaslRequired
400 476
 	}
401 477
 	return authSuccess
@@ -572,9 +648,13 @@ func (client *Client) run(session *Session, proxyLine string) {
572 648
 
573 649
 func (client *Client) playReattachMessages(session *Session) {
574 650
 	client.server.playRegistrationBurst(session)
651
+	hasHistoryCaps := session.HasHistoryCaps()
575 652
 	for _, channel := range session.client.Channels() {
576 653
 		channel.playJoinForSession(session)
577
-		// clients should receive autoreplay-on-join lines, if applicable;
654
+		// clients should receive autoreplay-on-join lines, if applicable:
655
+		if hasHistoryCaps {
656
+			continue
657
+		}
578 658
 		// if they negotiated znc.in/playback or chathistory, they will receive nothing,
579 659
 		// because those caps disable autoreplay-on-join and they haven't sent the relevant
580 660
 		// *playback PRIVMSG or CHATHISTORY command yet
@@ -582,6 +662,12 @@ func (client *Client) playReattachMessages(session *Session) {
582 662
 		channel.autoReplayHistory(client, rb, "")
583 663
 		rb.Send(true)
584 664
 	}
665
+	if !session.lastSignoff.IsZero() && !hasHistoryCaps {
666
+		rb := NewResponseBuffer(session)
667
+		zncPlayPrivmsgs(client, rb, session.lastSignoff, time.Time{})
668
+		rb.Send(true)
669
+	}
670
+	session.lastSignoff = time.Time{}
585 671
 }
586 672
 
587 673
 //
@@ -634,11 +720,6 @@ func (session *Session) tryResume() (success bool) {
634 720
 		return
635 721
 	}
636 722
 
637
-	if oldClient.isTor != client.isTor {
638
-		session.Send(nil, server.name, "FAIL", "RESUME", "INSECURE_SESSION", client.t("Cannot resume connection from Tor to non-Tor or vice versa"))
639
-		return
640
-	}
641
-
642 723
 	err := server.clients.Resume(oldClient, session)
643 724
 	if err != nil {
644 725
 		session.Send(nil, server.name, "FAIL", "RESUME", "CANNOT_RESUME", client.t("Cannot resume connection"))
@@ -657,37 +738,45 @@ func (session *Session) tryResume() (success bool) {
657 738
 func (session *Session) playResume() {
658 739
 	client := session.client
659 740
 	server := client.server
741
+	config := server.Config()
660 742
 
661 743
 	friends := make(ClientSet)
662
-	oldestLostMessage := time.Now().UTC()
744
+	var oldestLostMessage time.Time
663 745
 
664 746
 	// work out how much time, if any, is not covered by history buffers
747
+	// assume that a persistent buffer covers the whole resume period
665 748
 	for _, channel := range client.Channels() {
666 749
 		for _, member := range channel.Members() {
667 750
 			friends.Add(member)
751
+		}
752
+		_, ephemeral, _ := channel.historyStatus(config)
753
+		if ephemeral {
668 754
 			lastDiscarded := channel.history.LastDiscarded()
669
-			if lastDiscarded.Before(oldestLostMessage) {
755
+			if oldestLostMessage.Before(lastDiscarded) {
670 756
 				oldestLostMessage = lastDiscarded
671 757
 			}
672 758
 		}
673 759
 	}
674
-	privmsgMatcher := func(item history.Item) bool {
675
-		return item.Type == history.Privmsg || item.Type == history.Notice || item.Type == history.Tagmsg
676
-	}
677
-	privmsgHistory := client.history.Match(privmsgMatcher, false, 0)
678
-	lastDiscarded := client.history.LastDiscarded()
679
-	if lastDiscarded.Before(oldestLostMessage) {
680
-		oldestLostMessage = lastDiscarded
760
+	_, cEphemeral, _ := client.historyStatus(config)
761
+	if cEphemeral {
762
+		lastDiscarded := client.history.LastDiscarded()
763
+		if oldestLostMessage.Before(lastDiscarded) {
764
+			oldestLostMessage = lastDiscarded
765
+		}
681 766
 	}
682
-	for _, item := range privmsgHistory {
683
-		sender := server.clients.Get(stripMaskFromNick(item.Nick))
684
-		if sender != nil {
685
-			friends.Add(sender)
767
+	_, privmsgSeq, _ := server.GetHistorySequence(nil, client, "*")
768
+	if privmsgSeq != nil {
769
+		privmsgs, _, _ := privmsgSeq.Between(history.Selector{}, history.Selector{}, config.History.ClientLength)
770
+		for _, item := range privmsgs {
771
+			sender := server.clients.Get(stripMaskFromNick(item.Nick))
772
+			if sender != nil {
773
+				friends.Add(sender)
774
+			}
686 775
 		}
687 776
 	}
688 777
 
689 778
 	timestamp := session.resumeDetails.Timestamp
690
-	gap := lastDiscarded.Sub(timestamp)
779
+	gap := oldestLostMessage.Sub(timestamp)
691 780
 	session.resumeDetails.HistoryIncomplete = gap > 0 || timestamp.IsZero()
692 781
 	gapSeconds := int(gap.Seconds()) + 1 // round up to avoid confusion
693 782
 
@@ -723,10 +812,12 @@ func (session *Session) playResume() {
723 812
 		}
724 813
 	}
725 814
 
726
-	if session.resumeDetails.HistoryIncomplete && !timestamp.IsZero() {
727
-		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))
728
-	} else {
729
-		session.Send(nil, client.server.name, "WARN", "RESUME", "HISTORY_LOST", client.t("Resume may have lost some message history"))
815
+	if session.resumeDetails.HistoryIncomplete {
816
+		if !timestamp.IsZero() {
817
+			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))
818
+		} else {
819
+			session.Send(nil, client.server.name, "WARN", "RESUME", "HISTORY_LOST", client.t("Resume may have lost some message history"))
820
+		}
730 821
 	}
731 822
 
732 823
 	session.Send(nil, client.server.name, "RESUME", "SUCCESS", details.nick)
@@ -738,24 +829,27 @@ func (session *Session) playResume() {
738 829
 	}
739 830
 
740 831
 	// replay direct PRIVSMG history
741
-	if !timestamp.IsZero() {
742
-		now := time.Now().UTC()
743
-		items, complete := client.history.Between(timestamp, now, false, 0)
744
-		rb := NewResponseBuffer(client.Sessions()[0])
745
-		client.replayPrivmsgHistory(rb, items, complete)
746
-		rb.Send(true)
832
+	if !timestamp.IsZero() && privmsgSeq != nil {
833
+		after := history.Selector{Time: timestamp}
834
+		items, complete, _ := privmsgSeq.Between(after, history.Selector{}, config.History.ZNCMax)
835
+		if len(items) != 0 {
836
+			rb := NewResponseBuffer(session)
837
+			client.replayPrivmsgHistory(rb, items, "", complete)
838
+			rb.Send(true)
839
+		}
747 840
 	}
748 841
 
749 842
 	session.resumeDetails = nil
750 843
 }
751 844
 
752
-func (client *Client) replayPrivmsgHistory(rb *ResponseBuffer, items []history.Item, complete bool) {
845
+func (client *Client) replayPrivmsgHistory(rb *ResponseBuffer, items []history.Item, target string, complete bool) {
753 846
 	var batchID string
754 847
 	details := client.Details()
755 848
 	nick := details.nick
756
-	if 0 < len(items) {
757
-		batchID = rb.StartNestedHistoryBatch(nick)
849
+	if target == "" {
850
+		target = nick
758 851
 	}
852
+	batchID = rb.StartNestedHistoryBatch(target)
759 853
 
760 854
 	allowTags := rb.session.capabilities.Has(caps.MessageTags)
761 855
 	for _, item := range items {
@@ -778,12 +872,16 @@ func (client *Client) replayPrivmsgHistory(rb *ResponseBuffer, items []history.I
778 872
 		if allowTags {
779 873
 			tags = item.Tags
780 874
 		}
781
-		if item.Params[0] == "" {
782
-			// this message was sent *to* the client from another nick
875
+		// XXX: Params[0] is the message target. if the source of this message is an in-memory
876
+		// buffer, then it's "" for an incoming message and the recipient's nick for an outgoing
877
+		// message. if the source of the message is mysql, then mysql only sees one copy of the
878
+		// message, and it's the version with the recipient's nick filled in. so this is an
879
+		// incoming message if Params[0] (the recipient's nick) equals the client's nick:
880
+		if item.Params[0] == "" || item.Params[0] == nick {
783 881
 			rb.AddSplitMessageFromClient(item.Nick, item.AccountName, tags, command, nick, item.Message)
784 882
 		} else {
785 883
 			// this message was sent *from* the client to another nick; the target is item.Params[0]
786
-			// substitute the client's current nickmask in case they changed nick
884
+			// substitute client's current nickmask in case client changed nick
787 885
 			rb.AddSplitMessageFromClient(details.nickMask, item.AccountName, tags, command, item.Params[0], item.Message)
788 886
 		}
789 887
 	}
@@ -875,7 +973,7 @@ func (client *Client) HasRoleCapabs(capabs ...string) bool {
875 973
 
876 974
 // ModeString returns the mode string for this client.
877 975
 func (client *Client) ModeString() (str string) {
878
-	return "+" + client.flags.String()
976
+	return "+" + client.modes.String()
879 977
 }
880 978
 
881 979
 // Friends refers to clients that share a channel with this client.
@@ -1053,6 +1151,12 @@ func (client *Client) Quit(message string, session *Session) {
1053 1151
 // has no more sessions.
1054 1152
 func (client *Client) destroy(session *Session) {
1055 1153
 	var sessionsToDestroy []*Session
1154
+	var lastSignoff time.Time
1155
+	if session != nil {
1156
+		lastSignoff = session.idletimer.LastTouch()
1157
+	} else {
1158
+		lastSignoff = time.Now().UTC()
1159
+	}
1056 1160
 
1057 1161
 	client.stateMutex.Lock()
1058 1162
 	details := client.detailsNoMutex()
@@ -1060,6 +1164,8 @@ func (client *Client) destroy(session *Session) {
1060 1164
 	brbAt := client.brbTimer.brbAt
1061 1165
 	wasReattach := session != nil && session.client != client
1062 1166
 	sessionRemoved := false
1167
+	registered := client.registered
1168
+	alwaysOn := client.alwaysOn
1063 1169
 	var remainingSessions int
1064 1170
 	if session == nil {
1065 1171
 		sessionsToDestroy = client.sessions
@@ -1074,15 +1180,25 @@ func (client *Client) destroy(session *Session) {
1074 1180
 
1075 1181
 	// should we destroy the whole client this time?
1076 1182
 	// BRB is not respected if this is a destroy of the whole client (i.e., session == nil)
1077
-	brbEligible := session != nil && (brbState == BrbEnabled || brbState == BrbSticky)
1183
+	brbEligible := session != nil && (brbState == BrbEnabled || alwaysOn)
1078 1184
 	shouldDestroy := !client.destroyed && remainingSessions == 0 && !brbEligible
1079 1185
 	if shouldDestroy {
1080 1186
 		// if it's our job to destroy it, don't let anyone else try
1081 1187
 		client.destroyed = true
1082 1188
 	}
1189
+	if alwaysOn && remainingSessions == 0 {
1190
+		client.lastSignoff = lastSignoff
1191
+		client.dirtyBits |= IncludeLastSignoff
1192
+	} else {
1193
+		lastSignoff = time.Time{}
1194
+	}
1083 1195
 	exitedSnomaskSent := client.exitedSnomaskSent
1084 1196
 	client.stateMutex.Unlock()
1085 1197
 
1198
+	if !lastSignoff.IsZero() {
1199
+		client.wakeWriter()
1200
+	}
1201
+
1086 1202
 	// destroy all applicable sessions:
1087 1203
 	var quitMessage string
1088 1204
 	for _, session := range sessionsToDestroy {
@@ -1099,7 +1215,7 @@ func (client *Client) destroy(session *Session) {
1099 1215
 
1100 1216
 		// remove from connection limits
1101 1217
 		var source string
1102
-		if client.isTor {
1218
+		if session.isTor {
1103 1219
 			client.server.torLimiter.RemoveClient()
1104 1220
 			source = "tor"
1105 1221
 		} else {
@@ -1113,11 +1229,33 @@ func (client *Client) destroy(session *Session) {
1113 1229
 		client.server.logger.Info("localconnect-ip", fmt.Sprintf("disconnecting session of %s from %s", details.nick, source))
1114 1230
 	}
1115 1231
 
1232
+	// decrement stats if we have no more sessions, even if the client will not be destroyed
1233
+	if shouldDestroy || remainingSessions == 0 {
1234
+		invisible := client.HasMode(modes.Invisible)
1235
+		operator := client.HasMode(modes.LocalOperator) || client.HasMode(modes.Operator)
1236
+		client.server.stats.Remove(registered, invisible, operator)
1237
+	}
1238
+
1116 1239
 	// do not destroy the client if it has either remaining sessions, or is BRB'ed
1117 1240
 	if !shouldDestroy {
1118 1241
 		return
1119 1242
 	}
1120 1243
 
1244
+	splitQuitMessage := utils.MakeMessage(quitMessage)
1245
+	quitItem := history.Item{
1246
+		Type:        history.Quit,
1247
+		Nick:        details.nickMask,
1248
+		AccountName: details.accountName,
1249
+		Message:     splitQuitMessage,
1250
+	}
1251
+	var channels []*Channel
1252
+	// use a defer here to avoid writing to mysql while holding the destroy semaphore:
1253
+	defer func() {
1254
+		for _, channel := range channels {
1255
+			channel.AddHistoryItem(quitItem)
1256
+		}
1257
+	}()
1258
+
1121 1259
 	// see #235: deduplicating the list of PART recipients uses (comparatively speaking)
1122 1260
 	// a lot of RAM, so limit concurrency to avoid thrashing
1123 1261
 	client.server.semaphores.ClientDestroy.Acquire()
@@ -1127,7 +1265,6 @@ func (client *Client) destroy(session *Session) {
1127 1265
 		client.server.logger.Debug("quit", fmt.Sprintf("%s is no longer on the server", details.nick))
1128 1266
 	}
1129 1267
 
1130
-	registered := client.Registered()
1131 1268
 	if registered {
1132 1269
 		client.server.whoWas.Append(client.WhoWas())
1133 1270
 	}
@@ -1141,18 +1278,12 @@ func (client *Client) destroy(session *Session) {
1141 1278
 	// clean up monitor state
1142 1279
 	client.server.monitorManager.RemoveAll(client)
1143 1280
 
1144
-	splitQuitMessage := utils.MakeMessage(quitMessage)
1145 1281
 	// clean up channels
1146 1282
 	// (note that if this is a reattach, client has no channels and therefore no friends)
1147 1283
 	friends := make(ClientSet)
1148
-	for _, channel := range client.Channels() {
1284
+	channels = client.Channels()
1285
+	for _, channel := range channels {
1149 1286
 		channel.Quit(client)
1150
-		channel.history.Add(history.Item{
1151
-			Type:        history.Quit,
1152
-			Nick:        details.nickMask,
1153
-			AccountName: details.accountName,
1154
-			Message:     splitQuitMessage,
1155
-		})
1156 1287
 		for _, member := range channel.Members() {
1157 1288
 			friends.Add(member)
1158 1289
 		}
@@ -1168,9 +1299,6 @@ func (client *Client) destroy(session *Session) {
1168 1299
 
1169 1300
 	client.server.accounts.Logout(client)
1170 1301
 
1171
-	client.server.stats.Remove(registered, client.HasMode(modes.Invisible),
1172
-		client.HasMode(modes.Operator) || client.HasMode(modes.LocalOperator))
1173
-
1174 1302
 	// this happens under failure to return from BRB
1175 1303
 	if quitMessage == "" {
1176 1304
 		if brbState == BrbDead && !brbAt.IsZero() {
@@ -1196,11 +1324,10 @@ func (client *Client) destroy(session *Session) {
1196 1324
 // SendSplitMsgFromClient sends an IRC PRIVMSG/NOTICE coming from a specific client.
1197 1325
 // Adds account-tag to the line as well.
1198 1326
 func (session *Session) sendSplitMsgFromClientInternal(blocking bool, nickmask, accountName string, tags map[string]string, command, target string, message utils.SplitMessage) {
1199
-	// TODO no maxline support
1200 1327
 	if message.Is512() {
1201 1328
 		session.sendFromClientInternal(blocking, message.Time, message.Msgid, nickmask, accountName, tags, command, target, message.Message)
1202 1329
 	} else {
1203
-		if message.IsMultiline() && session.capabilities.Has(caps.Multiline) {
1330
+		if session.capabilities.Has(caps.Multiline) {
1204 1331
 			for _, msg := range session.composeMultilineBatch(nickmask, accountName, tags, command, target, message) {
1205 1332
 				session.SendRawMessage(msg, blocking)
1206 1333
 			}
@@ -1366,13 +1493,23 @@ func (session *Session) Notice(text string) {
1366 1493
 func (client *Client) addChannel(channel *Channel) {
1367 1494
 	client.stateMutex.Lock()
1368 1495
 	client.channels[channel] = true
1496
+	alwaysOn := client.alwaysOn
1369 1497
 	client.stateMutex.Unlock()
1498
+
1499
+	if alwaysOn {
1500
+		client.markDirty(IncludeChannels)
1501
+	}
1370 1502
 }
1371 1503
 
1372 1504
 func (client *Client) removeChannel(channel *Channel) {
1373 1505
 	client.stateMutex.Lock()
1374 1506
 	delete(client.channels, channel)
1507
+	alwaysOn := client.alwaysOn
1375 1508
 	client.stateMutex.Unlock()
1509
+
1510
+	if alwaysOn {
1511
+		client.markDirty(IncludeChannels)
1512
+	}
1376 1513
 }
1377 1514
 
1378 1515
 // 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) {
1401 1538
 // Implements auto-oper by certfp (scans for an auto-eligible operator block that matches
1402 1539
 // the client's cert, then applies it).
1403 1540
 func (client *Client) attemptAutoOper(session *Session) {
1404
-	if client.certfp == "" || client.HasMode(modes.Operator) {
1541
+	if session.certfp == "" || client.HasMode(modes.Operator) {
1405 1542
 		return
1406 1543
 	}
1407 1544
 	for _, oper := range client.server.Config().operators {
1408
-		if oper.Auto && oper.Pass == nil && oper.Fingerprint != "" && oper.Fingerprint == client.certfp {
1545
+		if oper.Auto && oper.Pass == nil && oper.Fingerprint != "" && oper.Fingerprint == session.certfp {
1409 1546
 			rb := NewResponseBuffer(session)
1410 1547
 			applyOper(client, oper, rb)
1411 1548
 			rb.Send(true)
@@ -1413,3 +1550,96 @@ func (client *Client) attemptAutoOper(session *Session) {
1413 1550
 		}
1414 1551
 	}
1415 1552
 }
1553
+
1554
+func (client *Client) historyStatus(config *Config) (persistent, ephemeral bool, target string) {
1555
+	if !config.History.Enabled {
1556
+		return
1557
+	} else if !config.History.Persistent.Enabled {
1558
+		ephemeral = true
1559
+		return
1560
+	}
1561
+
1562
+	client.stateMutex.RLock()
1563
+	alwaysOn := client.alwaysOn
1564
+	historyStatus := client.accountSettings.DMHistory
1565
+	target = client.nickCasefolded
1566
+	client.stateMutex.RUnlock()
1567
+
1568
+	if !alwaysOn {
1569
+		ephemeral = true
1570
+		return
1571
+	}
1572
+
1573
+	historyStatus = historyEnabled(config.History.Persistent.DirectMessages, historyStatus)
1574
+	ephemeral = (historyStatus == HistoryEphemeral)
1575
+	persistent = (historyStatus == HistoryPersistent)
1576
+	return
1577
+}
1578
+
1579
+// these are bit flags indicating what part of the client status is "dirty"
1580
+// and needs to be read from memory and written to the db
1581
+// TODO add a dirty flag for lastSignoff
1582
+const (
1583
+	IncludeChannels uint = 1 << iota
1584
+	IncludeLastSignoff
1585
+)
1586
+
1587
+func (client *Client) markDirty(dirtyBits uint) {
1588
+	client.stateMutex.Lock()
1589
+	alwaysOn := client.alwaysOn
1590
+	client.dirtyBits = client.dirtyBits | dirtyBits
1591
+	client.stateMutex.Unlock()
1592
+
1593
+	if alwaysOn {
1594
+		client.wakeWriter()
1595
+	}
1596
+}
1597
+
1598
+func (client *Client) wakeWriter() {
1599
+	if client.writerSemaphore.TryAcquire() {
1600
+		go client.writeLoop()
1601
+	}
1602
+}
1603
+
1604
+func (client *Client) writeLoop() {
1605
+	for {
1606
+		client.performWrite()
1607
+		client.writerSemaphore.Release()
1608
+
1609
+		client.stateMutex.RLock()
1610
+		isDirty := client.dirtyBits != 0
1611
+		client.stateMutex.RUnlock()
1612
+
1613
+		if !isDirty || !client.writerSemaphore.TryAcquire() {
1614
+			return
1615
+		}
1616
+	}
1617
+}
1618
+
1619
+func (client *Client) performWrite() {
1620
+	client.stateMutex.Lock()
1621
+	dirtyBits := client.dirtyBits
1622
+	client.dirtyBits = 0
1623
+	account := client.account
1624
+	client.stateMutex.Unlock()
1625
+
1626
+	if account == "" {
1627
+		client.server.logger.Error("internal", "attempting to persist logged-out client", client.Nick())
1628
+		return
1629
+	}
1630
+
1631
+	if (dirtyBits & IncludeChannels) != 0 {
1632
+		channels := client.Channels()
1633
+		channelNames := make([]string, len(channels))
1634
+		for i, channel := range channels {
1635
+			channelNames[i] = channel.Name()
1636
+		}
1637
+		client.server.accounts.saveChannels(account, channelNames)
1638
+	}
1639
+	if (dirtyBits & IncludeLastSignoff) != 0 {
1640
+		client.stateMutex.RLock()
1641
+		lastSignoff := client.lastSignoff
1642
+		client.stateMutex.RUnlock()
1643
+		client.server.accounts.saveLastSignoff(account, lastSignoff)
1644
+	}
1645
+}

+ 53
- 23
irc/client_lookup_set.go View File

@@ -105,7 +105,8 @@ func (clients *ClientManager) Resume(oldClient *Client, session *Session) (err e
105 105
 		return errNickMissing
106 106
 	}
107 107
 
108
-	if !oldClient.AddSession(session) {
108
+	success, _, _ := oldClient.AddSession(session)
109
+	if !success {
109 110
 		return errNickMissing
110 111
 	}
111 112
 
@@ -113,35 +114,51 @@ func (clients *ClientManager) Resume(oldClient *Client, session *Session) (err e
113 114
 }
114 115
 
115 116
 // SetNick sets a client's nickname, validating it against nicknames in use
116
-func (clients *ClientManager) SetNick(client *Client, session *Session, newNick string) error {
117
-	if len(newNick) > client.server.Config().Limits.NickLen {
118
-		return errNicknameInvalid
119
-	}
117
+func (clients *ClientManager) SetNick(client *Client, session *Session, newNick string) (setNick string, err error) {
118
+	config := client.server.Config()
120 119
 	newcfnick, err := CasefoldName(newNick)
121 120
 	if err != nil {
122
-		return errNicknameInvalid
121
+		return "", errNicknameInvalid
122
+	}
123
+	if len(newNick) > config.Limits.NickLen || len(newcfnick) > config.Limits.NickLen {
124
+		return "", errNicknameInvalid
123 125
 	}
124 126
 	newSkeleton, err := Skeleton(newNick)
125 127
 	if err != nil {
126
-		return errNicknameInvalid
128
+		return "", errNicknameInvalid
127 129
 	}
128 130
 
129 131
 	if restrictedCasefoldedNicks[newcfnick] || restrictedSkeletons[newSkeleton] {
130
-		return errNicknameInvalid
132
+		return "", errNicknameInvalid
131 133
 	}
132 134
 
133 135
 	reservedAccount, method := client.server.accounts.EnforcementStatus(newcfnick, newSkeleton)
134
-	account := client.Account()
135
-	config := client.server.Config()
136
+	client.stateMutex.RLock()
137
+	account := client.account
138
+	accountName := client.accountName
139
+	settings := client.accountSettings
140
+	registered := client.registered
141
+	realname := client.realname
142
+	client.stateMutex.RUnlock()
143
+
144
+	// recompute this (client.alwaysOn is not set for unregistered clients):
145
+	alwaysOn := account != "" && persistenceEnabled(config.Accounts.Multiclient.AlwaysOn, settings.AlwaysOn)
146
+
147
+	if alwaysOn && registered {
148
+		return "", errCantChangeNick
149
+	}
150
+
136 151
 	var bouncerAllowed bool
137
-	if config.Accounts.Bouncer.Enabled {
138
-		if session != nil && session.capabilities.Has(caps.Bouncer) {
152
+	if config.Accounts.Multiclient.Enabled {
153
+		if alwaysOn {
154
+			// ignore the pre-reg nick, force a reattach
155
+			newNick = accountName
156
+			newcfnick = account
139 157
 			bouncerAllowed = true
140 158
 		} else {
141
-			settings := client.AccountSettings()
142
-			if config.Accounts.Bouncer.AllowedByDefault && settings.AllowBouncer != BouncerDisallowedByUser {
159
+			if config.Accounts.Multiclient.AllowedByDefault && settings.AllowBouncer != MulticlientDisallowedByUser {
143 160
 				bouncerAllowed = true
144
-			} else if settings.AllowBouncer == BouncerAllowedByUser {
161
+			} else if settings.AllowBouncer == MulticlientAllowedByUser {
145 162
 				bouncerAllowed = true
146 163
 			}
147 164
 		}
@@ -154,28 +171,41 @@ func (clients *ClientManager) SetNick(client *Client, session *Session, newNick
154 171
 	// the client may just be changing case
155 172
 	if currentClient != nil && currentClient != client && session != nil {
156 173
 		// these conditions forbid reattaching to an existing session:
157
-		if client.Registered() || !bouncerAllowed || account == "" || account != currentClient.Account() || client.HasMode(modes.TLS) != currentClient.HasMode(modes.TLS) {
158
-			return errNicknameInUse
174
+		if registered || !bouncerAllowed || account == "" || account != currentClient.Account() || client.HasMode(modes.TLS) != currentClient.HasMode(modes.TLS) {
175
+			return "", errNicknameInUse
159 176
 		}
160
-		if !currentClient.AddSession(session) {
161
-			return errNicknameInUse
177
+		reattachSuccessful, numSessions, lastSignoff := currentClient.AddSession(session)
178
+		if !reattachSuccessful {
179
+			return "", errNicknameInUse
162 180
 		}
181
+		if numSessions == 1 {
182
+			invisible := client.HasMode(modes.Invisible)
183
+			operator := client.HasMode(modes.Operator) || client.HasMode(modes.LocalOperator)
184
+			client.server.stats.AddRegistered(invisible, operator)
185
+		}
186
+		session.lastSignoff = lastSignoff
187
+		// XXX SetNames only changes names if they are unset, so the realname change only
188
+		// takes effect on first attach to an always-on client (good), but the user/ident
189
+		// change is always a no-op (bad). we could make user/ident act the same way as
190
+		// realname, but then we'd have to send CHGHOST and i don't want to deal with that
191
+		// for performance reasons
192
+		currentClient.SetNames("user", realname, true)
163 193
 		// successful reattach!
164
-		return nil
194
+		return newNick, nil
165 195
 	}
166 196
 	// analogous checks for skeletons
167 197
 	skeletonHolder := clients.bySkeleton[newSkeleton]
168 198
 	if skeletonHolder != nil && skeletonHolder != client {
169
-		return errNicknameInUse
199
+		return "", errNicknameInUse
170 200
 	}
171 201
 	if method == NickEnforcementStrict && reservedAccount != "" && reservedAccount != account {
172
-		return errNicknameReserved
202
+		return "", errNicknameReserved
173 203
 	}
174 204
 	clients.removeInternal(client)
175 205
 	clients.byNick[newcfnick] = client
176 206
 	clients.bySkeleton[newSkeleton] = client
177 207
 	client.updateNick(newNick, newcfnick, newSkeleton)
178
-	return nil
208
+	return newNick, nil
179 209
 }
180 210
 
181 211
 func (clients *ClientManager) AllClients() (result []*Client) {

+ 7
- 6
irc/commands.go View File

@@ -54,6 +54,12 @@ func (cmd *Command) Run(server *Server, client *Client, session *Session, msg ir
54 54
 		return cmd.handler(server, client, msg, rb)
55 55
 	}()
56 56
 
57
+	// most servers do this only for PING/PONG, but we'll do it for any command:
58
+	if client.registered {
59
+		// touch even if `exiting`, so we record the time of a QUIT accurately
60
+		session.idletimer.Touch()
61
+	}
62
+
57 63
 	if exiting {
58 64
 		return
59 65
 	}
@@ -63,11 +69,6 @@ func (cmd *Command) Run(server *Server, client *Client, session *Session, msg ir
63 69
 		exiting = server.tryRegister(client, session)
64 70
 	}
65 71
 
66
-	// most servers do this only for PING/PONG, but we'll do it for any command:
67
-	if client.registered {
68
-		session.idletimer.Touch()
69
-	}
70
-
71 72
 	if client.registered && !cmd.leaveClientIdle {
72 73
 		client.Active(session)
73 74
 	}
@@ -109,7 +110,7 @@ func init() {
109 110
 		},
110 111
 		"CHATHISTORY": {
111 112
 			handler:   chathistoryHandler,
112
-			minParams: 3,
113
+			minParams: 4,
113 114
 		},
114 115
 		"DEBUG": {
115 116
 			handler:   debugHandler,

+ 212
- 29
irc/config.go View File

@@ -28,6 +28,7 @@ import (
28 28
 	"github.com/oragono/oragono/irc/ldap"
29 29
 	"github.com/oragono/oragono/irc/logger"
30 30
 	"github.com/oragono/oragono/irc/modes"
31
+	"github.com/oragono/oragono/irc/mysql"
31 32
 	"github.com/oragono/oragono/irc/passwd"
32 33
 	"github.com/oragono/oragono/irc/utils"
33 34
 	"gopkg.in/yaml.v2"
@@ -61,6 +62,157 @@ type listenerConfig struct {
61 62
 	ProxyBeforeTLS bool
62 63
 }
63 64
 
65
+type PersistentStatus uint
66
+
67
+const (
68
+	PersistentUnspecified PersistentStatus = iota
69
+	PersistentDisabled
70
+	PersistentOptIn
71
+	PersistentOptOut
72
+	PersistentMandatory
73
+)
74
+
75
+func persistentStatusToString(status PersistentStatus) string {
76
+	switch status {
77
+	case PersistentUnspecified:
78
+		return "default"
79
+	case PersistentDisabled:
80
+		return "disabled"
81
+	case PersistentOptIn:
82
+		return "opt-in"
83
+	case PersistentOptOut:
84
+		return "opt-out"
85
+	case PersistentMandatory:
86
+		return "mandatory"
87
+	default:
88
+		return ""
89
+	}
90
+}
91
+
92
+func persistentStatusFromString(status string) (PersistentStatus, error) {
93
+	switch strings.ToLower(status) {
94
+	case "default":
95
+		return PersistentUnspecified, nil
96
+	case "":
97
+		return PersistentDisabled, nil
98
+	case "opt-in":
99
+		return PersistentOptIn, nil
100
+	case "opt-out":
101
+		return PersistentOptOut, nil
102
+	case "mandatory":
103
+		return PersistentMandatory, nil
104
+	default:
105
+		b, err := utils.StringToBool(status)
106
+		if b {
107
+			return PersistentMandatory, err
108
+		} else {
109
+			return PersistentDisabled, err
110
+		}
111
+	}
112
+}
113
+
114
+func (ps *PersistentStatus) UnmarshalYAML(unmarshal func(interface{}) error) error {
115
+	var orig string
116
+	var err error
117
+	if err = unmarshal(&orig); err != nil {
118
+		return err
119
+	}
120
+	result, err := persistentStatusFromString(orig)
121
+	if err == nil {
122
+		if result == PersistentUnspecified {
123
+			result = PersistentDisabled
124
+		}
125
+		*ps = result
126
+	}
127
+	return err
128
+}
129
+
130
+func persistenceEnabled(serverSetting, clientSetting PersistentStatus) (enabled bool) {
131
+	if serverSetting == PersistentDisabled {
132
+		return false
133
+	} else if serverSetting == PersistentMandatory {
134
+		return true
135
+	} else if clientSetting == PersistentDisabled {
136
+		return false
137
+	} else if clientSetting == PersistentMandatory {
138
+		return true
139
+	} else if serverSetting == PersistentOptOut {
140
+		return true
141
+	} else {
142
+		return false
143
+	}
144
+}
145
+
146
+type HistoryStatus uint
147
+
148
+const (
149
+	HistoryDefault HistoryStatus = iota
150
+	HistoryDisabled
151
+	HistoryEphemeral
152
+	HistoryPersistent
153
+)
154
+
155
+func historyStatusFromString(str string) (status HistoryStatus, err error) {
156
+	switch strings.ToLower(str) {
157
+	case "default":
158
+		return HistoryDefault, nil
159
+	case "ephemeral":
160
+		return HistoryEphemeral, nil
161
+	case "persistent":
162
+		return HistoryPersistent, nil
163
+	default:
164
+		b, err := utils.StringToBool(str)
165
+		if b {
166
+			return HistoryPersistent, err
167
+		} else {
168
+			return HistoryDisabled, err
169
+		}
170
+	}
171
+}
172
+
173
+func historyStatusToString(status HistoryStatus) string {
174
+	switch status {
175
+	case HistoryDefault:
176
+		return "default"
177
+	case HistoryDisabled:
178
+		return "disabled"
179
+	case HistoryEphemeral:
180
+		return "ephemeral"
181
+	case HistoryPersistent:
182
+		return "persistent"
183
+	default:
184
+		return ""
185
+	}
186
+}
187
+
188
+func historyEnabled(serverSetting PersistentStatus, localSetting HistoryStatus) (result HistoryStatus) {
189
+	if serverSetting == PersistentDisabled {
190
+		return HistoryDisabled
191
+	} else if serverSetting == PersistentMandatory {
192
+		return HistoryPersistent
193
+	} else if serverSetting == PersistentOptOut {
194
+		if localSetting == HistoryDefault {
195
+			return HistoryPersistent
196
+		} else {
197
+			return localSetting
198
+		}
199
+	} else if serverSetting == PersistentOptIn {
200
+		if localSetting >= HistoryEphemeral {
201
+			return localSetting
202
+		} else {
203
+			return HistoryDisabled
204
+		}
205
+	} else {
206
+		return HistoryDisabled
207
+	}
208
+}
209
+
210
+type MulticlientConfig struct {
211
+	Enabled          bool
212
+	AllowedByDefault bool             `yaml:"allowed-by-default"`
213
+	AlwaysOn         PersistentStatus `yaml:"always-on"`
214
+}
215
+
64 216
 type AccountConfig struct {
65 217
 	Registration          AccountRegistrationConfig
66 218
 	AuthenticationEnabled bool `yaml:"authentication-enabled"`
@@ -77,19 +229,17 @@ type AccountConfig struct {
77 229
 	} `yaml:"login-throttling"`
78 230
 	SkipServerPassword bool                  `yaml:"skip-server-password"`
79 231
 	NickReservation    NickReservationConfig `yaml:"nick-reservation"`
80
-	Bouncer            struct {
81
-		Enabled          bool
82
-		AllowedByDefault bool `yaml:"allowed-by-default"`
83
-	}
84
-	VHosts VHostConfig
232
+	Multiclient        MulticlientConfig
233
+	Bouncer            *MulticlientConfig // # handle old name for 'multiclient'
234
+	VHosts             VHostConfig
85 235
 }
86 236
 
87 237
 // AccountRegistrationConfig controls account registration.
88 238
 type AccountRegistrationConfig struct {
89 239
 	Enabled                bool
90
-	EnabledCallbacks       []string      `yaml:"enabled-callbacks"`
91
-	EnabledCredentialTypes []string      `yaml:"-"`
92
-	VerifyTimeout          time.Duration `yaml:"verify-timeout"`
240
+	EnabledCallbacks       []string         `yaml:"enabled-callbacks"`
241
+	EnabledCredentialTypes []string         `yaml:"-"`
242
+	VerifyTimeout          custime.Duration `yaml:"verify-timeout"`
93 243
 	Callbacks              struct {
94 244
 		Mailto struct {
95 245
 			Server string
@@ -117,7 +267,7 @@ type VHostConfig struct {
117 267
 	UserRequests   struct {
118 268
 		Enabled  bool
119 269
 		Channel  string
120
-		Cooldown time.Duration
270
+		Cooldown custime.Duration
121 271
 	} `yaml:"user-requests"`
122 272
 	OfferList []string `yaml:"offer-list"`
123 273
 }
@@ -260,18 +410,17 @@ type Limits struct {
260 410
 
261 411
 // STSConfig controls the STS configuration/
262 412
 type STSConfig struct {
263
-	Enabled        bool
264
-	Duration       time.Duration `yaml:"duration-real"`
265
-	DurationString string        `yaml:"duration"`
266
-	Port           int
267
-	Preload        bool
268
-	STSOnlyBanner  string `yaml:"sts-only-banner"`
269
-	bannerLines    []string
413
+	Enabled       bool
414
+	Duration      custime.Duration
415
+	Port          int
416
+	Preload       bool
417
+	STSOnlyBanner string `yaml:"sts-only-banner"`
418
+	bannerLines   []string
270 419
 }
271 420
 
272 421
 // Value returns the STS value to advertise in CAP
273 422
 func (sts *STSConfig) Value() string {
274
-	val := fmt.Sprintf("duration=%d", int(sts.Duration.Seconds()))
423
+	val := fmt.Sprintf("duration=%d", int(time.Duration(sts.Duration).Seconds()))
275 424
 	if sts.Enabled && sts.Port > 0 {
276 425
 		val += fmt.Sprintf(",port=%d", sts.Port)
277 426
 	}
@@ -337,6 +486,8 @@ type Config struct {
337 486
 		isupport      isupport.List
338 487
 		IPLimits      connection_limits.LimiterConfig `yaml:"ip-limits"`
339 488
 		Cloaks        cloaks.CloakConfig              `yaml:"ip-cloaking"`
489
+		SecureNetDefs []string                        `yaml:"secure-nets"`
490
+		secureNets    []net.IPNet
340 491
 		supportedCaps *caps.Set
341 492
 		capValues     caps.Values
342 493
 		Casemapping   Casemapping
@@ -353,6 +504,7 @@ type Config struct {
353 504
 	Datastore struct {
354 505
 		Path        string
355 506
 		AutoUpgrade bool
507
+		MySQL       mysql.Config
356 508
 	}
357 509
 
358 510
 	Accounts AccountConfig
@@ -392,6 +544,18 @@ type Config struct {
392 544
 		AutoresizeWindow time.Duration `yaml:"autoresize-window"`
393 545
 		AutoreplayOnJoin int           `yaml:"autoreplay-on-join"`
394 546
 		ChathistoryMax   int           `yaml:"chathistory-maxmessages"`
547
+		ZNCMax           int           `yaml:"znc-maxmessages"`
548
+		Restrictions     struct {
549
+			ExpireTime              custime.Duration `yaml:"expire-time"`
550
+			EnforceRegistrationDate bool             `yaml:"enforce-registration-date"`
551
+			GracePeriod             custime.Duration `yaml:"grace-period"`
552
+		}
553
+		Persistent struct {
554
+			Enabled              bool
555
+			UnregisteredChannels bool             `yaml:"unregistered-channels"`
556
+			RegisteredChannels   PersistentStatus `yaml:"registered-channels"`
557
+			DirectMessages       PersistentStatus `yaml:"direct-messages"`
558
+		}
395 559
 	}
396 560
 
397 561
 	Filename string
@@ -626,6 +790,11 @@ func LoadConfig(filename string) (config *Config, err error) {
626 790
 	if config.Limits.RegistrationMessages == 0 {
627 791
 		config.Limits.RegistrationMessages = 1024
628 792
 	}
793
+	if config.Datastore.MySQL.Enabled {
794
+		if config.Limits.NickLen > mysql.MaxTargetLength || config.Limits.ChannelLen > mysql.MaxTargetLength {
795
+			return nil, fmt.Errorf("to use MySQL, nick and channel length limits must be %d or lower", mysql.MaxTargetLength)
796
+		}
797
+	}
629 798
 
630 799
 	config.Server.supportedCaps = caps.NewCompleteSet()
631 800
 	config.Server.capValues = make(caps.Values)
@@ -636,10 +805,6 @@ func LoadConfig(filename string) (config *Config, err error) {
636 805
 	}
637 806
 
638 807
 	if config.Server.STS.Enabled {
639
-		config.Server.STS.Duration, err = custime.ParseDuration(config.Server.STS.DurationString)
640
-		if err != nil {
641
-			return nil, fmt.Errorf("Could not parse STS duration: %s", err.Error())
642
-		}
643 808
 		if config.Server.STS.Port < 0 || config.Server.STS.Port > 65535 {
644 809
 			return nil, fmt.Errorf("STS port is incorrect, should be 0 if disabled: %d", config.Server.STS.Port)
645 810
 		}
@@ -692,8 +857,15 @@ func LoadConfig(filename string) (config *Config, err error) {
692 857
 		config.Server.capValues[caps.Multiline] = multilineCapValue
693 858
 	}
694 859
 
695
-	if !config.Accounts.Bouncer.Enabled {
696
-		config.Server.supportedCaps.Disable(caps.Bouncer)
860
+	// handle legacy name 'bouncer' for 'multiclient' section:
861
+	if config.Accounts.Bouncer != nil {
862
+		config.Accounts.Multiclient = *config.Accounts.Bouncer
863
+	}
864
+
865
+	if !config.Accounts.Multiclient.Enabled {
866
+		config.Accounts.Multiclient.AlwaysOn = PersistentDisabled
867
+	} else if config.Accounts.Multiclient.AlwaysOn >= PersistentOptOut {
868
+		config.Accounts.Multiclient.AllowedByDefault = true
697 869
 	}
698 870
 
699 871
 	var newLogConfigs []logger.LoggingConfig
@@ -762,6 +934,11 @@ func LoadConfig(filename string) (config *Config, err error) {
762 934
 		return nil, fmt.Errorf("Could not parse proxy-allowed-from nets: %v", err.Error())
763 935
 	}
764 936
 
937
+	config.Server.secureNets, err = utils.ParseNetList(config.Server.SecureNetDefs)
938
+	if err != nil {
939
+		return nil, fmt.Errorf("Could not parse secure-nets: %v\n", err.Error())
940
+	}
941
+
765 942
 	rawRegexp := config.Accounts.VHosts.ValidRegexpRaw
766 943
 	if rawRegexp != "" {
767 944
 		regexp, err := regexp.Compile(rawRegexp)
@@ -858,6 +1035,18 @@ func LoadConfig(filename string) (config *Config, err error) {
858 1035
 		config.History.ClientLength = 0
859 1036
 	}
860 1037
 
1038
+	if !config.History.Enabled || !config.History.Persistent.Enabled {
1039
+		config.History.Persistent.UnregisteredChannels = false
1040
+		config.History.Persistent.RegisteredChannels = PersistentDisabled
1041
+		config.History.Persistent.DirectMessages = PersistentDisabled
1042
+	}
1043
+
1044
+	if config.History.ZNCMax == 0 {
1045
+		config.History.ZNCMax = config.History.ChathistoryMax
1046
+	}
1047
+
1048
+	config.Datastore.MySQL.ExpireTime = time.Duration(config.History.Restrictions.ExpireTime)
1049
+
861 1050
 	config.Server.Cloaks.Initialize()
862 1051
 	if config.Server.Cloaks.Enabled {
863 1052
 		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)
942 1131
 		removedCaps.Add(caps.SASL)
943 1132
 	}
944 1133
 
945
-	if !oldConfig.Accounts.Bouncer.Enabled && config.Accounts.Bouncer.Enabled {
946
-		addedCaps.Add(caps.Bouncer)
947
-	} else if oldConfig.Accounts.Bouncer.Enabled && !config.Accounts.Bouncer.Enabled {
948
-		removedCaps.Add(caps.Bouncer)
949
-	}
950
-
951 1134
 	if oldConfig.Limits.Multiline.MaxBytes != 0 && config.Limits.Multiline.MaxBytes == 0 {
952 1135
 		removedCaps.Add(caps.Multiline)
953 1136
 	} else if oldConfig.Limits.Multiline.MaxBytes == 0 && config.Limits.Multiline.MaxBytes != 0 {

+ 17
- 1
irc/custime/parseduration.go View File

@@ -75,8 +75,9 @@ var unitMap = map[string]int64{
75 75
 	"m":  int64(time.Minute),
76 76
 	"h":  int64(time.Hour),
77 77
 	"d":  int64(time.Hour * 24),
78
+	"w":  int64(time.Hour * 24 * 7),
78 79
 	"mo": int64(time.Hour * 24 * 30),
79
-	"y":  int64(time.Hour * 24 * 265),
80
+	"y":  int64(time.Hour * 24 * 365),
80 81
 }
81 82
 
82 83
 // ParseDuration parses a duration string.
@@ -181,3 +182,18 @@ func ParseDuration(s string) (time.Duration, error) {
181 182
 	}
182 183
 	return time.Duration(d), nil
183 184
 }
185
+
186
+type Duration time.Duration
187
+
188
+func (d *Duration) UnmarshalYAML(unmarshal func(interface{}) error) error {
189
+	var orig string
190
+	var err error
191
+	if err = unmarshal(&orig); err != nil {
192
+		return err
193
+	}
194
+	result, err := ParseDuration(orig)
195
+	if err == nil {
196
+		*d = Duration(result)
197
+	}
198
+	return err
199
+}

+ 4
- 20
irc/database.go View File

@@ -36,22 +36,6 @@ type SchemaChange struct {
36 36
 // maps an initial version to a schema change capable of upgrading it
37 37
 var schemaChanges map[string]SchemaChange
38 38
 
39
-type incompatibleSchemaError struct {
40
-	currentVersion  string
41
-	requiredVersion string
42
-}
43
-
44
-func IncompatibleSchemaError(currentVersion string) (result *incompatibleSchemaError) {
45
-	return &incompatibleSchemaError{
46
-		currentVersion:  currentVersion,
47
-		requiredVersion: latestDbSchema,
48
-	}
49
-}
50
-
51
-func (err *incompatibleSchemaError) Error() string {
52
-	return fmt.Sprintf("Database requires update. Expected schema v%s, got v%s", err.requiredVersion, err.currentVersion)
53
-}
54
-
55 39
 // InitDB creates the database, implementing the `oragono initdb` command.
56 40
 func InitDB(path string) {
57 41
 	_, err := os.Stat(path)
@@ -129,7 +113,7 @@ func openDatabaseInternal(config *Config, allowAutoupgrade bool) (db *buntdb.DB,
129 113
 		// successful autoupgrade, let's try this again:
130 114
 		return openDatabaseInternal(config, false)
131 115
 	} else {
132
-		err = IncompatibleSchemaError(version)
116
+		err = &utils.IncompatibleSchemaError{CurrentVersion: version, RequiredVersion: latestDbSchema}
133 117
 		return
134 118
 	}
135 119
 }
@@ -179,7 +163,7 @@ func UpgradeDB(config *Config) (err error) {
179 163
 					break
180 164
 				}
181 165
 				// unable to upgrade to the desired version, roll back
182
-				return IncompatibleSchemaError(version)
166
+				return &utils.IncompatibleSchemaError{CurrentVersion: version, RequiredVersion: latestDbSchema}
183 167
 			}
184 168
 			log.Println("attempting to update schema from version " + version)
185 169
 			err := change.Changer(config, tx)
@@ -509,14 +493,14 @@ func schemaChangeV6ToV7(config *Config, tx *buntdb.Tx) error {
509 493
 type accountSettingsLegacyV7 struct {
510 494
 	AutoreplayLines *int
511 495
 	NickEnforcement NickEnforcementMethod
512
-	AllowBouncer    BouncerAllowedSetting
496
+	AllowBouncer    MulticlientAllowedSetting
513 497
 	AutoreplayJoins bool
514 498
 }
515 499
 
516 500
 type accountSettingsLegacyV8 struct {
517 501
 	AutoreplayLines *int
518 502
 	NickEnforcement NickEnforcementMethod
519
-	AllowBouncer    BouncerAllowedSetting
503
+	AllowBouncer    MulticlientAllowedSetting
520 504
 	ReplayJoins     ReplayJoinsSetting
521 505
 }
522 506
 

+ 2
- 0
irc/errors.go View File

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

+ 2
- 2
irc/gateways.go View File

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

+ 82
- 16
irc/getters.go View File

@@ -65,6 +65,7 @@ type SessionData struct {
65 65
 	atime    time.Time
66 66
 	ip       net.IP
67 67
 	hostname string
68
+	certfp   string
68 69
 }
69 70
 
70 71
 func (client *Client) AllSessionData(currentSession *Session) (data []SessionData, currentIndex int) {
@@ -81,6 +82,7 @@ func (client *Client) AllSessionData(currentSession *Session) (data []SessionDat
81 82
 			atime:    session.atime,
82 83
 			ctime:    session.ctime,
83 84
 			hostname: session.rawHostname,
85
+			certfp:   session.certfp,
84 86
 		}
85 87
 		if session.proxiedIP != nil {
86 88
 			data[i].ip = session.proxiedIP
@@ -91,21 +93,34 @@ func (client *Client) AllSessionData(currentSession *Session) (data []SessionDat
91 93
 	return
92 94
 }
93 95
 
94
-func (client *Client) AddSession(session *Session) (success bool) {
96
+func (client *Client) AddSession(session *Session) (success bool, numSessions int, lastSignoff time.Time) {
97
+	defer func() {
98
+		if !lastSignoff.IsZero() {
99
+			client.wakeWriter()
100
+		}
101
+	}()
102
+
95 103
 	client.stateMutex.Lock()
96 104
 	defer client.stateMutex.Unlock()
97 105
 
98 106
 	// client may be dying and ineligible to receive another session
99 107
 	if client.destroyed {
100
-		return false
108
+		return
101 109
 	}
102 110
 	// success, attach the new session to the client
103 111
 	session.client = client
104 112
 	newSessions := make([]*Session, len(client.sessions)+1)
105 113
 	copy(newSessions, client.sessions)
106 114
 	newSessions[len(newSessions)-1] = session
115
+	if len(client.sessions) == 0 && client.accountSettings.AutoreplayMissed {
116
+		// n.b. this is only possible if client is persistent and remained
117
+		// on the server with no sessions:
118
+		lastSignoff = client.lastSignoff
119
+		client.lastSignoff = time.Time{}
120
+		client.dirtyBits |= IncludeLastSignoff
121
+	}
107 122
 	client.sessions = newSessions
108
-	return true
123
+	return true, len(client.sessions), lastSignoff
109 124
 }
110 125
 
111 126
 func (client *Client) removeSession(session *Session) (success bool, length int) {
@@ -189,6 +204,13 @@ func (client *Client) SetExitedSnomaskSent() {
189 204
 	client.stateMutex.Unlock()
190 205
 }
191 206
 
207
+func (client *Client) AlwaysOn() (alwaysOn bool) {
208
+	client.stateMutex.Lock()
209
+	alwaysOn = client.alwaysOn
210
+	client.stateMutex.Unlock()
211
+	return
212
+}
213
+
192 214
 // uniqueIdentifiers returns the strings for which the server enforces per-client
193 215
 // uniqueness/ownership; no two clients can have colliding casefolded nicks or
194 216
 // skeletons.
@@ -264,23 +286,43 @@ func (client *Client) AccountName() string {
264 286
 	return client.accountName
265 287
 }
266 288
 
267
-func (client *Client) SetAccountName(account string) (changed bool) {
268
-	var casefoldedAccount string
269
-	var err error
270
-	if account != "" {
271
-		if casefoldedAccount, err = CasefoldName(account); err != nil {
272
-			return
273
-		}
289
+func (client *Client) Login(account ClientAccount) {
290
+	alwaysOn := persistenceEnabled(client.server.Config().Accounts.Multiclient.AlwaysOn, account.Settings.AlwaysOn)
291
+	client.stateMutex.Lock()
292
+	defer client.stateMutex.Unlock()
293
+	client.account = account.NameCasefolded
294
+	client.accountName = account.Name
295
+	client.accountSettings = account.Settings
296
+	// check `registered` to avoid incorrectly marking a temporary (pre-reattach),
297
+	// SASL'ing client as always-on
298
+	if client.registered {
299
+		client.alwaysOn = alwaysOn
274 300
 	}
301
+	client.accountRegDate = account.RegisteredAt
302
+	return
303
+}
275 304
 
305
+func (client *Client) historyCutoff() (cutoff time.Time) {
276 306
 	client.stateMutex.Lock()
277
-	defer client.stateMutex.Unlock()
278
-	changed = client.account != casefoldedAccount
279
-	client.account = casefoldedAccount
280
-	client.accountName = account
307
+	if client.account != "" {
308
+		cutoff = client.accountRegDate
309
+	} else {
310
+		cutoff = client.ctime
311
+	}
312
+	client.stateMutex.Unlock()
281 313
 	return
282 314
 }
283 315
 
316
+func (client *Client) Logout() {
317
+	client.stateMutex.Lock()
318
+	client.account = ""
319
+	client.accountName = ""
320
+	client.alwaysOn = false
321
+	client.accountRegDate = time.Time{}
322
+	client.accountSettings = AccountSettings{}
323
+	client.stateMutex.Unlock()
324
+}
325
+
284 326
 func (client *Client) AccountSettings() (result AccountSettings) {
285 327
 	client.stateMutex.RLock()
286 328
 	result = client.accountSettings
@@ -289,8 +331,12 @@ func (client *Client) AccountSettings() (result AccountSettings) {
289 331
 }
290 332
 
291 333
 func (client *Client) SetAccountSettings(settings AccountSettings) {
334
+	alwaysOn := persistenceEnabled(client.server.Config().Accounts.Multiclient.AlwaysOn, settings.AlwaysOn)
292 335
 	client.stateMutex.Lock()
293 336
 	client.accountSettings = settings
337
+	if client.registered {
338
+		client.alwaysOn = alwaysOn
339
+	}
294 340
 	client.stateMutex.Unlock()
295 341
 }
296 342
 
@@ -309,11 +355,17 @@ func (client *Client) SetLanguages(languages []string) {
309 355
 
310 356
 func (client *Client) HasMode(mode modes.Mode) bool {
311 357
 	// client.flags has its own synch
312
-	return client.flags.HasMode(mode)
358
+	return client.modes.HasMode(mode)
313 359
 }
314 360
 
315 361
 func (client *Client) SetMode(mode modes.Mode, on bool) bool {
316
-	return client.flags.SetMode(mode, on)
362
+	return client.modes.SetMode(mode, on)
363
+}
364
+
365
+func (client *Client) SetRealname(realname string) {
366
+	client.stateMutex.Lock()
367
+	client.realname = realname
368
+	client.stateMutex.Unlock()
317 369
 }
318 370
 
319 371
 func (client *Client) Channels() (result []*Channel) {
@@ -410,3 +462,17 @@ func (channel *Channel) HighestUserMode(client *Client) (result modes.Mode) {
410 462
 	channel.stateMutex.RUnlock()
411 463
 	return clientModes.HighestChannelUserMode()
412 464
 }
465
+
466
+func (channel *Channel) Settings() (result ChannelSettings) {
467
+	channel.stateMutex.RLock()
468
+	result = channel.settings
469
+	channel.stateMutex.RUnlock()
470
+	return result
471
+}
472
+
473
+func (channel *Channel) SetSettings(settings ChannelSettings) {
474
+	channel.stateMutex.Lock()
475
+	channel.settings = settings
476
+	channel.stateMutex.Unlock()
477
+	channel.MarkDirty(IncludeSettings)
478
+}

+ 188
- 266
irc/handlers.go View File

@@ -112,6 +112,7 @@ func sendSuccessfulAccountAuth(client *Client, rb *ResponseBuffer, forNS, forSAS
112 112
 
113 113
 // AUTHENTICATE [<mechanism>|<data>|*]
114 114
 func authenticateHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
115
+	session := rb.session
115 116
 	config := server.Config()
116 117
 	details := client.Details()
117 118
 
@@ -128,20 +129,17 @@ func authenticateHandler(server *Server, client *Client, msg ircmsg.IrcMessage,
128 129
 	// sasl abort
129 130
 	if !server.AccountConfig().AuthenticationEnabled || len(msg.Params) == 1 && msg.Params[0] == "*" {
130 131
 		rb.Add(nil, server.name, ERR_SASLABORTED, details.nick, client.t("SASL authentication aborted"))
131
-		client.saslInProgress = false
132
-		client.saslMechanism = ""
133
-		client.saslValue = ""
132
+		session.sasl.Clear()
134 133
 		return false
135 134
 	}
136 135
 
137 136
 	// start new sasl session
138
-	if !client.saslInProgress {
137
+	if session.sasl.mechanism == "" {
139 138
 		mechanism := strings.ToUpper(msg.Params[0])
140 139
 		_, mechanismIsEnabled := EnabledSaslMechanisms[mechanism]
141 140
 
142 141
 		if mechanismIsEnabled {
143
-			client.saslInProgress = true
144
-			client.saslMechanism = mechanism
142
+			session.sasl.mechanism = mechanism
145 143
 			if !config.Server.Compatibility.SendUnprefixedSasl {
146 144
 				// normal behavior
147 145
 				rb.Add(nil, server.name, "AUTHENTICATE", "+")
@@ -162,58 +160,46 @@ func authenticateHandler(server *Server, client *Client, msg ircmsg.IrcMessage,
162 160
 
163 161
 	if len(rawData) > 400 {
164 162
 		rb.Add(nil, server.name, ERR_SASLTOOLONG, details.nick, client.t("SASL message too long"))
165
-		client.saslInProgress = false
166
-		client.saslMechanism = ""
167
-		client.saslValue = ""
163
+		session.sasl.Clear()
168 164
 		return false
169 165
 	} else if len(rawData) == 400 {
170
-		client.saslValue += rawData
171 166
 		// allow 4 'continuation' lines before rejecting for length
172
-		if len(client.saslValue) > 400*4 {
167
+		if len(session.sasl.value) >= 400*4 {
173 168
 			rb.Add(nil, server.name, ERR_SASLFAIL, details.nick, client.t("SASL authentication failed: Passphrase too long"))
174
-			client.saslInProgress = false
175
-			client.saslMechanism = ""
176
-			client.saslValue = ""
169
+			session.sasl.Clear()
177 170
 			return false
178 171
 		}
172
+		session.sasl.value += rawData
179 173
 		return false
180 174
 	}
181 175
 	if rawData != "+" {
182
-		client.saslValue += rawData
176
+		session.sasl.value += rawData
183 177
 	}
184 178
 
185 179
 	var data []byte
186 180
 	var err error
187
-	if client.saslValue != "+" {
188
-		data, err = base64.StdEncoding.DecodeString(client.saslValue)
181
+	if session.sasl.value != "+" {
182
+		data, err = base64.StdEncoding.DecodeString(session.sasl.value)
189 183
 		if err != nil {
190 184
 			rb.Add(nil, server.name, ERR_SASLFAIL, details.nick, client.t("SASL authentication failed: Invalid b64 encoding"))
191
-			client.saslInProgress = false
192
-			client.saslMechanism = ""
193
-			client.saslValue = ""
185
+			session.sasl.Clear()
194 186
 			return false
195 187
 		}
196 188
 	}
197 189
 
198 190
 	// call actual handler
199
-	handler, handlerExists := EnabledSaslMechanisms[client.saslMechanism]
191
+	handler, handlerExists := EnabledSaslMechanisms[session.sasl.mechanism]
200 192
 
201 193
 	// like 100% not required, but it's good to be safe I guess
202 194
 	if !handlerExists {
203 195
 		rb.Add(nil, server.name, ERR_SASLFAIL, details.nick, client.t("SASL authentication failed"))
204
-		client.saslInProgress = false
205
-		client.saslMechanism = ""
206
-		client.saslValue = ""
196
+		session.sasl.Clear()
207 197
 		return false
208 198
 	}
209 199
 
210 200
 	// let the SASL handler do its thing
211
-	exiting := handler(server, client, client.saslMechanism, data, rb)
212
-
213
-	// wait 'til SASL is done before emptying the sasl vars
214
-	client.saslInProgress = false
215
-	client.saslMechanism = ""
216
-	client.saslValue = ""
201
+	exiting := handler(server, client, session.sasl.mechanism, data, rb)
202
+	session.sasl.Clear()
217 203
 
218 204
 	return exiting
219 205
 }
@@ -270,7 +256,7 @@ func authErrorToMessage(server *Server, err error) (msg string) {
270 256
 
271 257
 // AUTHENTICATE EXTERNAL
272 258
 func authExternalHandler(server *Server, client *Client, mechanism string, value []byte, rb *ResponseBuffer) bool {
273
-	if client.certfp == "" {
259
+	if rb.session.certfp == "" {
274 260
 		rb.Add(nil, server.name, ERR_SASLFAIL, client.nick, client.t("SASL authentication failed, you are not connecting with a certificate"))
275 261
 		return false
276 262
 	}
@@ -287,7 +273,7 @@ func authExternalHandler(server *Server, client *Client, mechanism string, value
287 273
 	}
288 274
 
289 275
 	if err == nil {
290
-		err = server.accounts.AuthenticateByCertFP(client, authzid)
276
+		err = server.accounts.AuthenticateByCertFP(client, rb.session.certfp, authzid)
291 277
 	}
292 278
 
293 279
 	if err != nil {
@@ -531,64 +517,49 @@ func capHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Respo
531 517
 // CHATHISTORY <target> BETWEEN <query> <query> <direction> [<limit>]
532 518
 // e.g., CHATHISTORY #ircv3 BETWEEN timestamp=YYYY-MM-DDThh:mm:ss.sssZ timestamp=YYYY-MM-DDThh:mm:ss.sssZ + 100
533 519
 func chathistoryHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) (exiting bool) {
534
-	config := server.Config()
535
-
536 520
 	var items []history.Item
537
-	success := false
538
-	var hist *history.Buffer
521
+	unknown_command := false
522
+	var target string
539 523
 	var channel *Channel
524
+	var sequence history.Sequence
525
+	var err error
540 526
 	defer func() {
541 527
 		// successful responses are sent as a chathistory or history batch
542
-		if success && 0 < len(items) {
543
-			if channel == nil {
544
-				client.replayPrivmsgHistory(rb, items, true)
545
-			} else {
528
+		if err == nil {
529
+			if channel != nil {
546 530
 				channel.replayHistoryItems(rb, items, false)
531
+			} else {
532
+				client.replayPrivmsgHistory(rb, items, target, true)
547 533
 			}
548 534
 			return
549 535
 		}
550 536
 
551 537
 		// errors are sent either without a batch, or in a draft/labeled-response batch as usual
552
-		// TODO: send `WARN CHATHISTORY MAX_MESSAGES_EXCEEDED` when appropriate
553
-		if hist == nil {
554
-			rb.Add(nil, server.name, "ERR", "CHATHISTORY", "NO_SUCH_CHANNEL")
555
-		} else if len(items) == 0 {
556
-			rb.Add(nil, server.name, "ERR", "CHATHISTORY", "NO_TEXT_TO_SEND")
557
-		} else if !success {
558
-			rb.Add(nil, server.name, "ERR", "CHATHISTORY", "NEED_MORE_PARAMS")
538
+		if unknown_command {
539
+			rb.Add(nil, server.name, "FAIL", "CHATHISTORY", "UNKNOWN_COMMAND", utils.SafeErrorParam(msg.Params[0]), client.t("Unknown command"))
540
+		} else if err == utils.ErrInvalidParams {
541
+			rb.Add(nil, server.name, "FAIL", "CHATHISTORY", "INVALID_PARAMETERS", msg.Params[0], client.t("Invalid parameters"))
542
+		} else if err != nil {
543
+			rb.Add(nil, server.name, "FAIL", "CHATHISTORY", "MESSAGE_ERROR", msg.Params[0], client.t("Messages could not be retrieved"))
544
+		} else if sequence == nil {
545
+			rb.Add(nil, server.name, "FAIL", "CHATHISTORY", "NO_SUCH_CHANNEL", utils.SafeErrorParam(msg.Params[1]), client.t("No such channel"))
559 546
 		}
560 547
 	}()
561 548
 
562
-	target := msg.Params[0]
563
-	channel = server.channels.Get(target)
564
-	if channel != nil && channel.hasClient(client) {
565
-		// "If [...] the user does not have permission to view the requested content, [...]
566
-		// NO_SUCH_CHANNEL SHOULD be returned"
567
-		hist = &channel.history
568
-	} else {
569
-		targetClient := server.clients.Get(target)
570
-		if targetClient != nil {
571
-			myAccount := client.Account()
572
-			targetAccount := targetClient.Account()
573
-			if myAccount != "" && targetAccount != "" && myAccount == targetAccount {
574
-				hist = &targetClient.history
575
-			}
576
-		}
577
-	}
578
-	if hist == nil {
549
+	config := server.Config()
550
+	maxChathistoryLimit := config.History.ChathistoryMax
551
+	if maxChathistoryLimit == 0 {
579 552
 		return
580 553
 	}
581 554
 
582
-	preposition := strings.ToLower(msg.Params[1])
583
-
584 555
 	parseQueryParam := func(param string) (msgid string, timestamp time.Time, err error) {
585
-		err = errInvalidParams
556
+		err = utils.ErrInvalidParams
586 557
 		pieces := strings.SplitN(param, "=", 2)
587 558
 		if len(pieces) < 2 {
588 559
 			return
589 560
 		}
590 561
 		identifier, value := strings.ToLower(pieces[0]), pieces[1]
591
-		if identifier == "id" {
562
+		if identifier == "msgid" {
592 563
 			msgid, err = value, nil
593 564
 			return
594 565
 		} else if identifier == "timestamp" {
@@ -598,10 +569,6 @@ func chathistoryHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
598 569
 		return
599 570
 	}
600 571
 
601
-	maxChathistoryLimit := config.History.ChathistoryMax
602
-	if maxChathistoryLimit == 0 {
603
-		return
604
-	}
605 572
 	parseHistoryLimit := func(paramIndex int) (limit int) {
606 573
 		if len(msg.Params) < (paramIndex + 1) {
607 574
 			return maxChathistoryLimit
@@ -613,140 +580,74 @@ func chathistoryHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
613 580
 		return
614 581
 	}
615 582
 
616
-	// TODO: as currently implemented, almost all of thes queries are worst-case O(n)
617
-	// in the number of stored history entries. Every one of them can be made O(1)
618
-	// if necessary, without too much difficulty. Some ideas:
619
-	// * Ensure that the ring buffer is sorted by time, enabling binary search for times
620
-	// * Maintain a map from msgid to position in the ring buffer
621
-
622
-	if preposition == "between" {
623
-		if len(msg.Params) >= 5 {
624
-			startMsgid, startTimestamp, startErr := parseQueryParam(msg.Params[2])
625
-			endMsgid, endTimestamp, endErr := parseQueryParam(msg.Params[3])
626
-			ascending := msg.Params[4] == "+"
627
-			limit := parseHistoryLimit(5)
628
-			if startErr != nil || endErr != nil {
629
-				success = false
630
-			} else if startMsgid != "" && endMsgid != "" {
631
-				inInterval := false
632
-				matches := func(item history.Item) (result bool) {
633
-					result = inInterval
634
-					if item.HasMsgid(startMsgid) {
635
-						if ascending {
636
-							inInterval = true
637
-						} else {
638
-							inInterval = false
639
-							return false // interval is exclusive
640
-						}
641
-					} else if item.HasMsgid(endMsgid) {
642
-						if ascending {
643
-							inInterval = false
644
-							return false
645
-						} else {
646
-							inInterval = true
647
-						}
648
-					}
649
-					return
650
-				}
651
-				items = hist.Match(matches, ascending, limit)
652
-				success = true
653
-			} else if !startTimestamp.IsZero() && !endTimestamp.IsZero() {
654
-				items, _ = hist.Between(startTimestamp, endTimestamp, ascending, limit)
655
-				if !ascending {
656
-					history.Reverse(items)
657
-				}
658
-				success = true
659
-			}
660
-			// else: mismatched params, success = false, fail
661
-		}
583
+	preposition := strings.ToLower(msg.Params[0])
584
+	target = msg.Params[1]
585
+	channel, sequence, err = server.GetHistorySequence(nil, client, target)
586
+	if err != nil || sequence == nil {
662 587
 		return
663 588
 	}
664 589
 
665
-	// before, after, latest, around
666
-	queryParam := msg.Params[2]
667
-	msgid, timestamp, err := parseQueryParam(queryParam)
668
-	limit := parseHistoryLimit(3)
669
-	before := false
590
+	roundUp := func(endpoint time.Time) (result time.Time) {
591
+		return endpoint.Truncate(time.Millisecond).Add(time.Millisecond)
592
+	}
593
+
594
+	var start, end history.Selector
595
+	var limit int
670 596
 	switch preposition {
671
-	case "before":
672
-		before = true
673
-		fallthrough
674
-	case "after":
675
-		var matches history.Predicate
597
+	case "between":
598
+		start.Msgid, start.Time, err = parseQueryParam(msg.Params[2])
676 599
 		if err != nil {
677
-			break
678
-		} else if msgid != "" {
679
-			inInterval := false
680
-			matches = func(item history.Item) (result bool) {
681
-				result = inInterval
682
-				if item.HasMsgid(msgid) {
683
-					inInterval = true
684
-				}
685
-				return
686
-			}
687
-		} else {
688
-			matches = func(item history.Item) bool {
689
-				return before == item.Message.Time.Before(timestamp)
690
-			}
600
+			return
691 601
 		}
692
-		items = hist.Match(matches, !before, limit)
693
-		success = true
694
-	case "latest":
695
-		if queryParam == "*" {
696
-			items = hist.Latest(limit)
697
-		} else if err != nil {
698
-			break
699
-		} else {
700
-			var matches history.Predicate
701
-			if msgid != "" {
702
-				shouldStop := false
703
-				matches = func(item history.Item) bool {
704
-					if shouldStop {
705
-						return false
706
-					}
707
-					shouldStop = item.HasMsgid(msgid)
708
-					return !shouldStop
709
-				}
602
+		end.Msgid, end.Time, err = parseQueryParam(msg.Params[3])
603
+		if err != nil {
604
+			return
605
+		}
606
+		// XXX preserve the ordering of the two parameters, since we might be going backwards,
607
+		// but round up the chronologically first one, whichever it is, to make it exclusive
608
+		if !start.Time.IsZero() && !end.Time.IsZero() {
609
+			if start.Time.Before(end.Time) {
610
+				start.Time = roundUp(start.Time)
710 611
 			} else {
711
-				matches = func(item history.Item) bool {
712
-					return item.Message.Time.After(timestamp)
713
-				}
612
+				end.Time = roundUp(end.Time)
714 613
 			}
715
-			items = hist.Match(matches, false, limit)
716 614
 		}
717
-		success = true
718
-	case "around":
615
+		limit = parseHistoryLimit(4)
616
+	case "before", "after", "around":
617
+		start.Msgid, start.Time, err = parseQueryParam(msg.Params[2])
719 618
 		if err != nil {
720
-			break
619
+			return
721 620
 		}
722
-		var initialMatcher history.Predicate
723
-		if msgid != "" {
724
-			inInterval := false
725
-			initialMatcher = func(item history.Item) (result bool) {
726
-				if inInterval {
727
-					return true
728
-				} else {
729
-					inInterval = item.HasMsgid(msgid)
730
-					return inInterval
731
-				}
621
+		if preposition == "after" && !start.Time.IsZero() {
622
+			start.Time = roundUp(start.Time)
623
+		}
624
+		if preposition == "before" {
625
+			end = start
626
+			start = history.Selector{}
627
+		}
628
+		limit = parseHistoryLimit(3)
629
+	case "latest":
630
+		if msg.Params[2] != "*" {
631
+			end.Msgid, end.Time, err = parseQueryParam(msg.Params[2])
632
+			if err != nil {
633
+				return
732 634
 			}
733
-		} else {
734
-			initialMatcher = func(item history.Item) (result bool) {
735
-				return item.Message.Time.Before(timestamp)
635
+			if !end.Time.IsZero() {
636
+				end.Time = roundUp(end.Time)
736 637
 			}
638
+			start.Time = time.Now().UTC()
737 639
 		}
738
-		var halfLimit int
739
-		halfLimit = (limit + 1) / 2
740
-		firstPass := hist.Match(initialMatcher, false, halfLimit)
741
-		if len(firstPass) > 0 {
742
-			timeWindowStart := firstPass[0].Message.Time
743
-			items = hist.Match(func(item history.Item) bool {
744
-				return item.Message.Time.Equal(timeWindowStart) || item.Message.Time.After(timeWindowStart)
745
-			}, true, limit)
746
-		}
747
-		success = true
640
+		limit = parseHistoryLimit(3)
641
+	default:
642
+		unknown_command = true
643
+		return
748 644
 	}
749 645
 
646
+	if preposition == "around" {
647
+		items, err = sequence.Around(start, limit)
648
+	} else {
649
+		items, _, err = sequence.Between(start, end, limit)
650
+	}
750 651
 	return
751 652
 }
752 653
 
@@ -1026,6 +927,7 @@ Get an explanation of <argument>, or "index" for a list of help topics.`), rb)
1026 927
 // HISTORY <target> [<limit>]
1027 928
 // e.g., HISTORY #ubuntu 10
1028 929
 // HISTORY me 15
930
+// HISTORY #darwin 1h
1029 931
 func historyHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
1030 932
 	config := server.Config()
1031 933
 	if !config.History.Enabled {
@@ -1034,53 +936,55 @@ func historyHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R
1034 936
 	}
1035 937
 
1036 938
 	target := msg.Params[0]
1037
-	var hist *history.Buffer
1038
-	channel := server.channels.Get(target)
1039
-	if channel != nil && channel.hasClient(client) {
1040
-		hist = &channel.history
1041
-	} else {
1042
-		if strings.ToLower(target) == "me" {
1043
-			hist = &client.history
1044
-		} else {
1045
-			targetClient := server.clients.Get(target)
1046
-			if targetClient != nil {
1047
-				myAccount, targetAccount := client.Account(), targetClient.Account()
1048
-				if myAccount != "" && targetAccount != "" && myAccount == targetAccount {
1049
-					hist = &targetClient.history
1050
-				}
1051
-			}
1052
-		}
939
+	if strings.ToLower(target) == "me" {
940
+		target = "*"
1053 941
 	}
942
+	channel, sequence, err := server.GetHistorySequence(nil, client, target)
1054 943
 
1055
-	if hist == nil {
1056
-		if channel == nil {
1057
-			rb.Add(nil, server.name, ERR_NOSUCHCHANNEL, client.Nick(), utils.SafeErrorParam(target), client.t("No such channel"))
1058
-		} else {
1059
-			rb.Add(nil, server.name, ERR_NOTONCHANNEL, client.Nick(), target, client.t("You're not on that channel"))
1060
-		}
944
+	if sequence == nil || err != nil {
945
+		// whatever
946
+		rb.Add(nil, server.name, ERR_NOSUCHCHANNEL, client.Nick(), utils.SafeErrorParam(target), client.t("No such channel"))
1061 947
 		return false
1062 948
 	}
1063 949
 
1064
-	limit := 10
950
+	var duration time.Duration
1065 951
 	maxChathistoryLimit := config.History.ChathistoryMax
952
+	limit := 100
953
+	if maxChathistoryLimit < limit {
954
+		limit = maxChathistoryLimit
955
+	}
1066 956
 	if len(msg.Params) > 1 {
1067 957
 		providedLimit, err := strconv.Atoi(msg.Params[1])
1068
-		if providedLimit > maxChathistoryLimit {
1069
-			providedLimit = maxChathistoryLimit
1070
-		}
1071 958
 		if err == nil && providedLimit != 0 {
1072 959
 			limit = providedLimit
960
+			if maxChathistoryLimit < limit {
961
+				limit = maxChathistoryLimit
962
+			}
963
+		} else if err != nil {
964
+			duration, err = time.ParseDuration(msg.Params[1])
965
+			if err == nil {
966
+				limit = maxChathistoryLimit
967
+			}
1073 968
 		}
1074 969
 	}
1075 970
 
1076
-	items := hist.Latest(limit)
1077
-
1078
-	if channel != nil {
1079
-		channel.replayHistoryItems(rb, items, false)
971
+	var items []history.Item
972
+	if duration == 0 {
973
+		items, _, err = sequence.Between(history.Selector{}, history.Selector{}, limit)
1080 974
 	} else {
1081
-		client.replayPrivmsgHistory(rb, items, true)
975
+		now := time.Now().UTC()
976
+		start := history.Selector{Time: now}
977
+		end := history.Selector{Time: now.Add(-duration)}
978
+		items, _, err = sequence.Between(start, end, limit)
1082 979
 	}
1083 980
 
981
+	if err == nil && len(items) != 0 {
982
+		if channel != nil {
983
+			channel.replayHistoryItems(rb, items, false)
984
+		} else {
985
+			client.replayPrivmsgHistory(rb, items, "", true)
986
+		}
987
+	}
1084 988
 	return false
1085 989
 }
1086 990
 
@@ -1964,7 +1868,7 @@ func messageHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R
1964 1868
 		return false
1965 1869
 	}
1966 1870
 
1967
-	if client.isTor && utils.IsRestrictedCTCPMessage(message) {
1871
+	if rb.session.isTor && utils.IsRestrictedCTCPMessage(message) {
1968 1872
 		// note that error replies are never sent for NOTICE
1969 1873
 		if histType != history.Notice {
1970 1874
 			rb.Notice(client.t("CTCP messages are disabled over Tor"))
@@ -2021,46 +1925,34 @@ func dispatchMessageToTarget(client *Client, tags map[string]string, histType hi
2021 1925
 			}
2022 1926
 			return
2023 1927
 		}
2024
-		tnick := user.Nick()
1928
+		tDetails := user.Details()
1929
+		tnick := tDetails.nick
2025 1930
 
2026
-		nickMaskString := client.NickMaskString()
2027
-		accountName := client.AccountName()
1931
+		details := client.Details()
1932
+		nickMaskString := details.nickMask
1933
+		accountName := details.accountName
1934
+		var deliverySessions []*Session
2028 1935
 		// restrict messages appropriately when +R is set
2029 1936
 		// intentionally make the sending user think the message went through fine
2030
-		allowedPlusR := !user.HasMode(modes.RegisteredOnly) || client.LoggedIntoAccount()
2031
-		allowedTor := !user.isTor || !message.IsRestrictedCTCPMessage()
2032
-		if allowedPlusR && allowedTor {
2033
-			for _, session := range user.Sessions() {
2034
-				hasTagsCap := session.capabilities.Has(caps.MessageTags)
2035
-				// don't send TAGMSG at all if they don't have the tags cap
2036
-				if histType == history.Tagmsg && hasTagsCap {
2037
-					session.sendFromClientInternal(false, message.Time, message.Msgid, nickMaskString, accountName, tags, command, tnick)
2038
-				} else if histType != history.Tagmsg {
2039
-					tagsToSend := tags
2040
-					if !hasTagsCap {
2041
-						tagsToSend = nil
2042
-					}
2043
-					session.sendSplitMsgFromClientInternal(false, nickMaskString, accountName, tagsToSend, command, tnick, message)
1937
+		allowedPlusR := details.account != "" || !user.HasMode(modes.RegisteredOnly)
1938
+		if allowedPlusR {
1939
+			deliverySessions = append(deliverySessions, user.Sessions()...)
1940
+		}
1941
+		// all sessions of the sender, except the originating session, get a copy as well:
1942
+		if client != user {
1943
+			for _, session := range client.Sessions() {
1944
+				if session != rb.session {
1945
+					deliverySessions = append(deliverySessions, session)
2044 1946
 				}
2045 1947
 			}
2046 1948
 		}
2047
-		// an echo-message may need to be included in the response:
2048
-		if rb.session.capabilities.Has(caps.EchoMessage) {
2049
-			if histType == history.Tagmsg && rb.session.capabilities.Has(caps.MessageTags) {
2050
-				rb.AddFromClient(message.Time, message.Msgid, nickMaskString, accountName, tags, command, tnick)
2051
-			} else {
2052
-				rb.AddSplitMessageFromClient(nickMaskString, accountName, tags, command, tnick, message)
2053
-			}
2054
-		}
2055
-		// an echo-message may need to go out to other client sessions:
2056
-		for _, session := range client.Sessions() {
2057
-			if session == rb.session {
2058
-				continue
2059
-			}
1949
+
1950
+		for _, session := range deliverySessions {
2060 1951
 			hasTagsCap := session.capabilities.Has(caps.MessageTags)
1952
+			// don't send TAGMSG at all if they don't have the tags cap
2061 1953
 			if histType == history.Tagmsg && hasTagsCap {
2062 1954
 				session.sendFromClientInternal(false, message.Time, message.Msgid, nickMaskString, accountName, tags, command, tnick)
2063
-			} else if histType != history.Tagmsg {
1955
+			} else if histType != history.Tagmsg && !(session.isTor && message.IsRestrictedCTCPMessage()) {
2064 1956
 				tagsToSend := tags
2065 1957
 				if !hasTagsCap {
2066 1958
 					tagsToSend = nil
@@ -2068,22 +1960,56 @@ func dispatchMessageToTarget(client *Client, tags map[string]string, histType hi
2068 1960
 				session.sendSplitMsgFromClientInternal(false, nickMaskString, accountName, tagsToSend, command, tnick, message)
2069 1961
 			}
2070 1962
 		}
1963
+
1964
+		// the originating session may get an echo message:
1965
+		if rb.session.capabilities.Has(caps.EchoMessage) {
1966
+			hasTagsCap := rb.session.capabilities.Has(caps.MessageTags)
1967
+			if histType == history.Tagmsg && hasTagsCap {
1968
+				rb.AddFromClient(message.Time, message.Msgid, nickMaskString, accountName, tags, command, tnick)
1969
+			} else {
1970
+				tagsToSend := tags
1971
+				if !hasTagsCap {
1972
+					tagsToSend = nil
1973
+				}
1974
+				rb.AddSplitMessageFromClient(nickMaskString, accountName, tagsToSend, command, tnick, message)
1975
+			}
1976
+		}
2071 1977
 		if histType != history.Notice && user.Away() {
2072 1978
 			//TODO(dan): possibly implement cooldown of away notifications to users
2073 1979
 			rb.Add(nil, server.name, RPL_AWAY, client.Nick(), tnick, user.AwayMessage())
2074 1980
 		}
2075 1981
 
1982
+		config := server.Config()
1983
+		if !config.History.Enabled {
1984
+			return
1985
+		}
2076 1986
 		item := history.Item{
2077 1987
 			Type:        histType,
2078 1988
 			Message:     message,
2079 1989
 			Nick:        nickMaskString,
2080 1990
 			AccountName: accountName,
1991
+			Tags:        tags,
1992
+		}
1993
+		if !item.IsStorable() {
1994
+			return
1995
+		}
1996
+		targetedItem := item
1997
+		targetedItem.Params[0] = tnick
1998
+		cPersistent, cEphemeral, _ := client.historyStatus(config)
1999
+		tPersistent, tEphemeral, _ := user.historyStatus(config)
2000
+		// add to ephemeral history
2001
+		if cEphemeral {
2002
+			targetedItem.CfCorrespondent = tDetails.nickCasefolded
2003
+			client.history.Add(targetedItem)
2004
+		}
2005
+		if tEphemeral && client != user {
2006
+			item.CfCorrespondent = details.nickCasefolded
2007
+			user.history.Add(item)
2008
+		}
2009
+		if cPersistent || tPersistent {
2010
+			item.CfCorrespondent = ""
2011
+			server.historyDB.AddDirectMessage(details.nickCasefolded, user.NickCasefolded(), cPersistent, tPersistent, targetedItem)
2081 2012
 		}
2082
-		// add to the target's history:
2083
-		user.history.Add(item)
2084
-		// add this to the client's history as well, recording the target:
2085
-		item.Params[0] = tnick
2086
-		client.history.Add(item)
2087 2013
 	}
2088 2014
 }
2089 2015
 
@@ -2136,7 +2062,7 @@ func operHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
2136 2062
 	oper := server.GetOperator(msg.Params[0])
2137 2063
 	if oper != nil {
2138 2064
 		if oper.Fingerprint != "" {
2139
-			if oper.Fingerprint == client.certfp {
2065
+			if oper.Fingerprint == rb.session.certfp {
2140 2066
 				checkPassed = true
2141 2067
 			} else {
2142 2068
 				checkFailed = true
@@ -2239,7 +2165,7 @@ func passHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
2239 2165
 
2240 2166
 	// check the provided password
2241 2167
 	password := []byte(msg.Params[0])
2242
-	client.sentPassCommand = bcrypt.CompareHashAndPassword(serverPassword, password) == nil
2168
+	rb.session.sentPassCommand = bcrypt.CompareHashAndPassword(serverPassword, password) == nil
2243 2169
 
2244 2170
 	// if they failed the check, we'll bounce them later when they try to complete registration
2245 2171
 	return false
@@ -2409,11 +2335,7 @@ func sceneHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res
2409 2335
 // SETNAME <realname>
2410 2336
 func setnameHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
2411 2337
 	realname := msg.Params[0]
2412
-
2413
-	client.stateMutex.Lock()
2414
-	client.realname = realname
2415
-	client.stateMutex.Unlock()
2416
-
2338
+	client.SetRealname(realname)
2417 2339
 	details := client.Details()
2418 2340
 
2419 2341
 	// alert friends
@@ -2622,7 +2544,7 @@ func webircHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
2622 2544
 			if 0 < len(info.Password) && bcrypt.CompareHashAndPassword(info.Password, givenPassword) != nil {
2623 2545
 				continue
2624 2546
 			}
2625
-			if info.Fingerprint != "" && info.Fingerprint != client.certfp {
2547
+			if info.Fingerprint != "" && info.Fingerprint != rb.session.certfp {
2626 2548
 				continue
2627 2549
 			}
2628 2550
 

+ 6
- 5
irc/help.go View File

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

+ 97
- 68
irc/history/history.go View File

@@ -6,7 +6,6 @@ package history
6 6
 import (
7 7
 	"github.com/oragono/oragono/irc/utils"
8 8
 	"sync"
9
-	"sync/atomic"
10 9
 	"time"
11 10
 )
12 11
 
@@ -46,6 +45,10 @@ type Item struct {
46 45
 	Message utils.SplitMessage
47 46
 	Tags    map[string]string
48 47
 	Params  [1]string
48
+	// for a DM, this is the casefolded nickname of the other party (whether this is
49
+	// an incoming or outgoing message). this lets us emulate the "query buffer" functionality
50
+	// required by CHATHISTORY:
51
+	CfCorrespondent string
49 52
 }
50 53
 
51 54
 // HasMsgid tests whether a message has the message id `msgid`.
@@ -53,20 +56,30 @@ func (item *Item) HasMsgid(msgid string) bool {
53 56
 	return item.Message.Msgid == msgid
54 57
 }
55 58
 
56
-func (item *Item) isStorable() bool {
57
-	if item.Type == Tagmsg {
59
+func (item *Item) IsStorable() bool {
60
+	switch item.Type {
61
+	case Tagmsg:
58 62
 		for name := range item.Tags {
59 63
 			if !transientTags[name] {
60 64
 				return true
61 65
 			}
62 66
 		}
63 67
 		return false // all tags were blacklisted
64
-	} else {
68
+	case Privmsg, Notice:
69
+		// don't store CTCP other than ACTION
70
+		return !item.Message.IsRestrictedCTCPMessage()
71
+	default:
65 72
 		return true
66 73
 	}
67 74
 }
68 75
 
69
-type Predicate func(item Item) (matches bool)
76
+type Predicate func(item *Item) (matches bool)
77
+
78
+func Reverse(results []Item) {
79
+	for i, j := 0, len(results)-1; i < j; i, j = i+1, j-1 {
80
+		results[i], results[j] = results[j], results[i]
81
+	}
82
+}
70 83
 
71 84
 // Buffer is a ring buffer holding message/event history for a channel or user
72 85
 type Buffer struct {
@@ -81,8 +94,6 @@ type Buffer struct {
81 94
 
82 95
 	lastDiscarded time.Time
83 96
 
84
-	enabled uint32
85
-
86 97
 	nowFunc func() time.Time
87 98
 }
88 99
 
@@ -99,8 +110,6 @@ func (hist *Buffer) Initialize(size int, window time.Duration) {
99 110
 	hist.window = window
100 111
 	hist.maximumSize = size
101 112
 	hist.nowFunc = time.Now
102
-
103
-	hist.setEnabled(size)
104 113
 }
105 114
 
106 115
 // 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) {
115 124
 	return
116 125
 }
117 126
 
118
-func (hist *Buffer) setEnabled(size int) {
119
-	var enabled uint32
120
-	if size != 0 {
121
-		enabled = 1
122
-	}
123
-	atomic.StoreUint32(&hist.enabled, enabled)
124
-}
125
-
126
-// Enabled returns whether the buffer is currently storing messages
127
-// (a disabled buffer blackholes everything it sees)
128
-func (list *Buffer) Enabled() bool {
129
-	return atomic.LoadUint32(&list.enabled) != 0
130
-}
131
-
132 127
 // Add adds a history item to the buffer
133 128
 func (list *Buffer) Add(item Item) {
134
-	// fast path without a lock acquisition for when we are not storing history
135
-	if !list.Enabled() {
136
-		return
137
-	}
138
-
139
-	if !item.isStorable() {
140
-		return
141
-	}
142
-
143 129
 	if item.Message.Time.IsZero() {
144 130
 		item.Message.Time = time.Now().UTC()
145 131
 	}
@@ -147,6 +133,10 @@ func (list *Buffer) Add(item Item) {
147 133
 	list.Lock()
148 134
 	defer list.Unlock()
149 135
 
136
+	if len(list.buffer) == 0 {
137
+		return
138
+	}
139
+
150 140
 	list.maybeExpand()
151 141
 
152 142
 	var pos int
@@ -170,55 +160,100 @@ func (list *Buffer) Add(item Item) {
170 160
 	list.buffer[pos] = item
171 161
 }
172 162
 
173
-// Reverse reverses an []Item, in-place.
174
-func Reverse(results []Item) {
175
-	for i, j := 0, len(results)-1; i < j; i, j = i+1, j-1 {
176
-		results[i], results[j] = results[j], results[i]
163
+func (list *Buffer) lookup(msgid string) (result Item, found bool) {
164
+	predicate := func(item *Item) bool {
165
+		return item.HasMsgid(msgid)
166
+	}
167
+	results := list.matchInternal(predicate, false, 1)
168
+	if len(results) != 0 {
169
+		return results[0], true
177 170
 	}
171
+	return
178 172
 }
179 173
 
180 174
 // Between returns all history items with a time `after` <= time <= `before`,
181 175
 // with an indication of whether the results are complete or are missing items
182 176
 // because some of that period was discarded. A zero value of `before` is considered
183 177
 // higher than all other times.
184
-func (list *Buffer) Between(after, before time.Time, ascending bool, limit int) (results []Item, complete bool) {
185
-	if !list.Enabled() {
186
-		return
187
-	}
178
+func (list *Buffer) betweenHelper(start, end Selector, cutoff time.Time, pred Predicate, limit int) (results []Item, complete bool, err error) {
179
+	var ascending bool
180
+
181
+	defer func() {
182
+		if !ascending {
183
+			Reverse(results)
184
+		}
185
+	}()
188 186
 
189 187
 	list.RLock()
190 188
 	defer list.RUnlock()
191 189
 
190
+	if len(list.buffer) == 0 {
191
+		return
192
+	}
193
+
194
+	after := start.Time
195
+	if start.Msgid != "" {
196
+		item, found := list.lookup(start.Msgid)
197
+		if !found {
198
+			return
199
+		}
200
+		after = item.Message.Time
201
+	}
202
+	before := end.Time
203
+	if end.Msgid != "" {
204
+		item, found := list.lookup(end.Msgid)
205
+		if !found {
206
+			return
207
+		}
208
+		before = item.Message.Time
209
+	}
210
+
211
+	after, before, ascending = MinMaxAsc(after, before, cutoff)
212
+
192 213
 	complete = after.Equal(list.lastDiscarded) || after.After(list.lastDiscarded)
193 214
 
194
-	satisfies := func(item Item) bool {
195
-		return (after.IsZero() || item.Message.Time.After(after)) && (before.IsZero() || item.Message.Time.Before(before))
215
+	satisfies := func(item *Item) bool {
216
+		return (after.IsZero() || item.Message.Time.After(after)) &&
217
+			(before.IsZero() || item.Message.Time.Before(before)) &&
218
+			(pred == nil || pred(item))
196 219
 	}
197 220
 
198
-	return list.matchInternal(satisfies, ascending, limit), complete
221
+	return list.matchInternal(satisfies, ascending, limit), complete, nil
199 222
 }
200 223
 
201
-// Match returns all history items such that `predicate` returns true for them.
202
-// Items are considered in reverse insertion order if `ascending` is false, or
203
-// in insertion order if `ascending` is true, up to a total of `limit` matches
204
-// if `limit` > 0 (unlimited otherwise).
205
-// `predicate` MAY be a closure that maintains its own state across invocations;
206
-// it MUST NOT acquire any locks or otherwise do anything weird.
207
-// Results are always returned in insertion order.
208
-func (list *Buffer) Match(predicate Predicate, ascending bool, limit int) (results []Item) {
209
-	if !list.Enabled() {
210
-		return
224
+// implements history.Sequence, emulating a single history buffer (for a channel,
225
+// a single user's DMs, or a DM conversation)
226
+type bufferSequence struct {
227
+	list   *Buffer
228
+	pred   Predicate
229
+	cutoff time.Time
230
+}
231
+
232
+func (list *Buffer) MakeSequence(correspondent string, cutoff time.Time) Sequence {
233
+	var pred Predicate
234
+	if correspondent != "" {
235
+		pred = func(item *Item) bool {
236
+			return item.CfCorrespondent == correspondent
237
+		}
211 238
 	}
239
+	return &bufferSequence{
240
+		list:   list,
241
+		pred:   pred,
242
+		cutoff: cutoff,
243
+	}
244
+}
212 245
 
213
-	list.RLock()
214
-	defer list.RUnlock()
246
+func (seq *bufferSequence) Between(start, end Selector, limit int) (results []Item, complete bool, err error) {
247
+	return seq.list.betweenHelper(start, end, seq.cutoff, seq.pred, limit)
248
+}
215 249
 
216
-	return list.matchInternal(predicate, ascending, limit)
250
+func (seq *bufferSequence) Around(start Selector, limit int) (results []Item, err error) {
251
+	return GenericAround(seq, start, limit)
217 252
 }
218 253
 
219 254
 // you must be holding the read lock to call this
220 255
 func (list *Buffer) matchInternal(predicate Predicate, ascending bool, limit int) (results []Item) {
221
-	if list.start == -1 {
256
+	if list.start == -1 || len(list.buffer) == 0 {
222 257
 		return
223 258
 	}
224 259
 
@@ -232,7 +267,7 @@ func (list *Buffer) matchInternal(predicate Predicate, ascending bool, limit int
232 267
 	}
233 268
 
234 269
 	for {
235
-		if predicate(list.buffer[pos]) {
270
+		if predicate(&list.buffer[pos]) {
236 271
 			results = append(results, list.buffer[pos])
237 272
 		}
238 273
 		if pos == stop || (limit != 0 && len(results) == limit) {
@@ -245,18 +280,14 @@ func (list *Buffer) matchInternal(predicate Predicate, ascending bool, limit int
245 280
 		}
246 281
 	}
247 282
 
248
-	// TODO sort by time instead?
249
-	if !ascending {
250
-		Reverse(results)
251
-	}
252 283
 	return
253 284
 }
254 285
 
255
-// Latest returns the items most recently added, up to `limit`. If `limit` is 0,
286
+// latest returns the items most recently added, up to `limit`. If `limit` is 0,
256 287
 // it returns all items.
257
-func (list *Buffer) Latest(limit int) (results []Item) {
258
-	matchAll := func(item Item) bool { return true }
259
-	return list.Match(matchAll, false, limit)
288
+func (list *Buffer) latest(limit int) (results []Item) {
289
+	results, _, _ = list.betweenHelper(Selector{}, Selector{}, time.Time{}, nil, limit)
290
+	return
260 291
 }
261 292
 
262 293
 // LastDiscarded returns the latest time of any entry that was evicted
@@ -355,8 +386,6 @@ func (list *Buffer) Resize(maximumSize int, window time.Duration) {
355 386
 func (list *Buffer) resize(size int) {
356 387
 	newbuffer := make([]Item, size)
357 388
 
358
-	list.setEnabled(size)
359
-
360 389
 	if list.start == -1 {
361 390
 		// indices are already correct and nothing needs to be copied
362 391
 	} else if size == 0 {

+ 46
- 25
irc/history/history_test.go View File

@@ -14,19 +14,21 @@ const (
14 14
 	timeFormat = "2006-01-02 15:04:05Z"
15 15
 )
16 16
 
17
+func betweenTimestamps(buf *Buffer, start, end time.Time, limit int) (result []Item, complete bool) {
18
+	result, complete, _ = buf.betweenHelper(Selector{Time: start}, Selector{Time: end}, time.Time{}, nil, limit)
19
+	return
20
+}
21
+
17 22
 func TestEmptyBuffer(t *testing.T) {
18 23
 	pastTime := easyParse(timeFormat)
19 24
 
20 25
 	buf := NewHistoryBuffer(0, 0)
21
-	if buf.Enabled() {
22
-		t.Error("the buffer of size 0 must be considered disabled")
23
-	}
24 26
 
25 27
 	buf.Add(Item{
26 28
 		Nick: "testnick",
27 29
 	})
28 30
 
29
-	since, complete := buf.Between(pastTime, time.Now(), false, 0)
31
+	since, complete := betweenTimestamps(buf, pastTime, time.Now(), 0)
30 32
 	if len(since) != 0 {
31 33
 		t.Error("shouldn't be able to add to disabled buf")
32 34
 	}
@@ -35,16 +37,13 @@ func TestEmptyBuffer(t *testing.T) {
35 37
 	}
36 38
 
37 39
 	buf.Resize(1, 0)
38
-	if !buf.Enabled() {
39
-		t.Error("the buffer of size 1 must be considered enabled")
40
-	}
41
-	since, complete = buf.Between(pastTime, time.Now(), false, 0)
40
+	since, complete = betweenTimestamps(buf, pastTime, time.Now(), 0)
42 41
 	assertEqual(complete, true, t)
43 42
 	assertEqual(len(since), 0, t)
44 43
 	buf.Add(Item{
45 44
 		Nick: "testnick",
46 45
 	})
47
-	since, complete = buf.Between(pastTime, time.Now(), false, 0)
46
+	since, complete = betweenTimestamps(buf, pastTime, time.Now(), 0)
48 47
 	if len(since) != 1 {
49 48
 		t.Error("should be able to store items in a nonempty buffer")
50 49
 	}
@@ -58,7 +57,7 @@ func TestEmptyBuffer(t *testing.T) {
58 57
 	buf.Add(Item{
59 58
 		Nick: "testnick2",
60 59
 	})
61
-	since, complete = buf.Between(pastTime, time.Now(), false, 0)
60
+	since, complete = betweenTimestamps(buf, pastTime, time.Now(), 0)
62 61
 	if len(since) != 1 {
63 62
 		t.Error("expect exactly 1 item")
64 63
 	}
@@ -68,8 +67,7 @@ func TestEmptyBuffer(t *testing.T) {
68 67
 	if since[0].Nick != "testnick2" {
69 68
 		t.Error("retrieved junk data")
70 69
 	}
71
-	matchAll := func(item Item) bool { return true }
72
-	assertEqual(toNicks(buf.Match(matchAll, false, 0)), []string{"testnick2"}, t)
70
+	assertEqual(toNicks(buf.latest(0)), []string{"testnick2"}, t)
73 71
 }
74 72
 
75 73
 func toNicks(items []Item) (result []string) {
@@ -110,27 +108,27 @@ func TestBuffer(t *testing.T) {
110 108
 
111 109
 	buf.Add(easyItem("testnick2", "2006-01-03 15:04:05Z"))
112 110
 
113
-	since, complete := buf.Between(start, time.Now(), false, 0)
111
+	since, complete := betweenTimestamps(buf, start, time.Now(), 0)
114 112
 	assertEqual(complete, true, t)
115 113
 	assertEqual(toNicks(since), []string{"testnick0", "testnick1", "testnick2"}, t)
116 114
 
117 115
 	// add another item, evicting the first
118 116
 	buf.Add(easyItem("testnick3", "2006-01-04 15:04:05Z"))
119 117
 
120
-	since, complete = buf.Between(start, time.Now(), false, 0)
118
+	since, complete = betweenTimestamps(buf, start, time.Now(), 0)
121 119
 	assertEqual(complete, false, t)
122 120
 	assertEqual(toNicks(since), []string{"testnick1", "testnick2", "testnick3"}, t)
123 121
 	// now exclude the time of the discarded entry; results should be complete again
124
-	since, complete = buf.Between(easyParse("2006-01-02 00:00:00Z"), time.Now(), false, 0)
122
+	since, complete = betweenTimestamps(buf, easyParse("2006-01-02 00:00:00Z"), time.Now(), 0)
125 123
 	assertEqual(complete, true, t)
126 124
 	assertEqual(toNicks(since), []string{"testnick1", "testnick2", "testnick3"}, t)
127
-	since, complete = buf.Between(easyParse("2006-01-02 00:00:00Z"), easyParse("2006-01-03 00:00:00Z"), false, 0)
125
+	since, complete = betweenTimestamps(buf, easyParse("2006-01-02 00:00:00Z"), easyParse("2006-01-03 00:00:00Z"), 0)
128 126
 	assertEqual(complete, true, t)
129 127
 	assertEqual(toNicks(since), []string{"testnick1"}, t)
130 128
 
131 129
 	// shrink the buffer, cutting off testnick1
132 130
 	buf.Resize(2, 0)
133
-	since, complete = buf.Between(easyParse("2006-01-02 00:00:00Z"), time.Now(), false, 0)
131
+	since, complete = betweenTimestamps(buf, easyParse("2006-01-02 00:00:00Z"), time.Now(), 0)
134 132
 	assertEqual(complete, false, t)
135 133
 	assertEqual(toNicks(since), []string{"testnick2", "testnick3"}, t)
136 134
 
@@ -138,18 +136,19 @@ func TestBuffer(t *testing.T) {
138 136
 	buf.Add(easyItem("testnick4", "2006-01-05 15:04:05Z"))
139 137
 	buf.Add(easyItem("testnick5", "2006-01-06 15:04:05Z"))
140 138
 	buf.Add(easyItem("testnick6", "2006-01-07 15:04:05Z"))
141
-	since, complete = buf.Between(easyParse("2006-01-03 00:00:00Z"), time.Now(), false, 0)
139
+	since, complete = betweenTimestamps(buf, easyParse("2006-01-03 00:00:00Z"), time.Now(), 0)
142 140
 	assertEqual(complete, true, t)
143 141
 	assertEqual(toNicks(since), []string{"testnick2", "testnick3", "testnick4", "testnick5", "testnick6"}, t)
144 142
 
145 143
 	// test ascending order
146
-	since, _ = buf.Between(easyParse("2006-01-03 00:00:00Z"), time.Now(), true, 2)
144
+	since, _ = betweenTimestamps(buf, easyParse("2006-01-03 00:00:00Z"), time.Time{}, 2)
147 145
 	assertEqual(toNicks(since), []string{"testnick2", "testnick3"}, t)
148 146
 }
149 147
 
150 148
 func autoItem(id int, t time.Time) (result Item) {
151 149
 	result.Message.Time = t
152 150
 	result.Nick = strconv.Itoa(id)
151
+	result.Message.Msgid = result.Nick
153 152
 	return
154 153
 }
155 154
 
@@ -181,7 +180,7 @@ func TestAutoresize(t *testing.T) {
181 180
 		now = now.Add(time.Minute * 10)
182 181
 		id += 1
183 182
 	}
184
-	items := buf.Latest(0)
183
+	items := buf.latest(0)
185 184
 	assertEqual(len(items), initialAutoSize, t)
186 185
 	assertEqual(atoi(items[0].Nick), 40, t)
187 186
 	assertEqual(atoi(items[len(items)-1].Nick), 71, t)
@@ -195,7 +194,7 @@ func TestAutoresize(t *testing.T) {
195 194
 	// ok, 5 items from the first batch are still in the 1-hour window;
196 195
 	// we should overwrite until only those 5 are left, then start expanding
197 196
 	// the buffer so that it retains those 5 and the 100 new items
198
-	items = buf.Latest(0)
197
+	items = buf.latest(0)
199 198
 	assertEqual(len(items), 105, t)
200 199
 	assertEqual(atoi(items[0].Nick), 67, t)
201 200
 	assertEqual(atoi(items[len(items)-1].Nick), 171, t)
@@ -207,7 +206,7 @@ func TestAutoresize(t *testing.T) {
207 206
 		id += 1
208 207
 	}
209 208
 	// should fill up to the maximum size of 128 and start overwriting
210
-	items = buf.Latest(0)
209
+	items = buf.latest(0)
211 210
 	assertEqual(len(items), 128, t)
212 211
 	assertEqual(atoi(items[0].Nick), 144, t)
213 212
 	assertEqual(atoi(items[len(items)-1].Nick), 271, t)
@@ -222,7 +221,7 @@ func TestEnabledByResize(t *testing.T) {
222 221
 	buf.Resize(128, time.Hour)
223 222
 	// add an item and test that it is stored and retrievable
224 223
 	buf.Add(autoItem(0, now))
225
-	items := buf.Latest(0)
224
+	items := buf.latest(0)
226 225
 	assertEqual(len(items), 1, t)
227 226
 	assertEqual(atoi(items[0].Nick), 0, t)
228 227
 }
@@ -232,13 +231,13 @@ func TestDisabledByResize(t *testing.T) {
232 231
 	// enabled autoresizing buffer
233 232
 	buf := NewHistoryBuffer(128, time.Hour)
234 233
 	buf.Add(autoItem(0, now))
235
-	items := buf.Latest(0)
234
+	items := buf.latest(0)
236 235
 	assertEqual(len(items), 1, t)
237 236
 	assertEqual(atoi(items[0].Nick), 0, t)
238 237
 
239 238
 	// disable as during a rehash, confirm that nothing can be retrieved
240 239
 	buf.Resize(0, time.Hour)
241
-	items = buf.Latest(0)
240
+	items = buf.latest(0)
242 241
 	assertEqual(len(items), 0, t)
243 242
 }
244 243
 
@@ -252,3 +251,25 @@ func TestRoundUp(t *testing.T) {
252 251
 	assertEqual(roundUpToPowerOfTwo(1025), 2048, t)
253 252
 	assertEqual(roundUpToPowerOfTwo(269435457), 536870912, t)
254 253
 }
254
+
255
+func BenchmarkInsert(b *testing.B) {
256
+	buf := NewHistoryBuffer(1024, 0)
257
+	b.ResetTimer()
258
+	for i := 0; i < b.N; i++ {
259
+		buf.Add(Item{})
260
+	}
261
+}
262
+
263
+func BenchmarkMatch(b *testing.B) {
264
+	buf := NewHistoryBuffer(1024, 0)
265
+	var now time.Time
266
+	for i := 0; i < 1024; i += 1 {
267
+		buf.Add(autoItem(i, now))
268
+		now = now.Add(time.Second)
269
+	}
270
+
271
+	b.ResetTimer()
272
+	for i := 0; i < b.N; i++ {
273
+		buf.lookup("512")
274
+	}
275
+}

+ 71
- 0
irc/history/queries.go View File

@@ -0,0 +1,71 @@
1
+// Copyright (c) 2020 Shivaram Lingamneni <slingamn@cs.stanford.edu>
2
+// released under the MIT license
3
+
4
+package history
5
+
6
+import (
7
+	"time"
8
+)
9
+
10
+// Selector represents a parameter to a CHATHISTORY command;
11
+// at most one of Msgid or Time may be nonzero
12
+type Selector struct {
13
+	Msgid string
14
+	Time  time.Time
15
+}
16
+
17
+// Sequence is an abstract sequence of history entries that can be queried;
18
+// it encapsulates restrictions such as registration time cutoffs, or
19
+// only looking at a single "query buffer" (DMs with a particular correspondent)
20
+type Sequence interface {
21
+	Between(start, end Selector, limit int) (results []Item, complete bool, err error)
22
+	Around(start Selector, limit int) (results []Item, err error)
23
+}
24
+
25
+// This is a bad, slow implementation of CHATHISTORY AROUND using the BETWEEN semantics
26
+func GenericAround(seq Sequence, start Selector, limit int) (results []Item, err error) {
27
+	var halfLimit int
28
+	halfLimit = (limit + 1) / 2
29
+	initialResults, _, err := seq.Between(Selector{}, start, halfLimit)
30
+	if err != nil {
31
+		return
32
+	} else if len(initialResults) == 0 {
33
+		// TODO: this fails if we're doing an AROUND on the first message in the buffer
34
+		// would be nice to fix this but whatever
35
+		return
36
+	}
37
+	newStart := Selector{Time: initialResults[0].Message.Time}
38
+	results, _, err = seq.Between(newStart, Selector{}, limit)
39
+	return
40
+}
41
+
42
+// MinMaxAsc converts CHATHISTORY arguments into time intervals, handling the most
43
+// general case (BETWEEN going forwards or backwards) natively and the other ordering
44
+// queries (AFTER, BEFORE, LATEST) as special cases.
45
+func MinMaxAsc(after, before, cutoff time.Time) (min, max time.Time, ascending bool) {
46
+	startIsZero, endIsZero := after.IsZero(), before.IsZero()
47
+	if !startIsZero && endIsZero {
48
+		// AFTER
49
+		ascending = true
50
+	} else if startIsZero && !endIsZero {
51
+		// BEFORE
52
+		ascending = false
53
+	} else if !startIsZero && !endIsZero {
54
+		if before.Before(after) {
55
+			// BETWEEN going backwards
56
+			before, after = after, before
57
+			ascending = false
58
+		} else {
59
+			// BETWEEN going forwards
60
+			ascending = true
61
+		}
62
+	} else if startIsZero && endIsZero {
63
+		// LATEST
64
+		ascending = false
65
+	}
66
+	if after.IsZero() || after.Before(cutoff) {
67
+		// this may result in an impossible query, which is fine
68
+		after = cutoff
69
+	}
70
+	return after, before, ascending
71
+}

+ 3
- 2
irc/hostserv.go View File

@@ -7,6 +7,7 @@ import (
7 7
 	"errors"
8 8
 	"fmt"
9 9
 	"regexp"
10
+	"time"
10 11
 
11 12
 	"github.com/oragono/oragono/irc/sno"
12 13
 )
@@ -214,7 +215,7 @@ func hsRequestHandler(server *Server, client *Client, command string, params []s
214 215
 	}
215 216
 
216 217
 	accountName := client.Account()
217
-	_, err := server.accounts.VHostRequest(accountName, vhost, server.Config().Accounts.VHosts.UserRequests.Cooldown)
218
+	_, err := server.accounts.VHostRequest(accountName, vhost, time.Duration(server.Config().Accounts.VHosts.UserRequests.Cooldown))
218 219
 	if err != nil {
219 220
 		if throttled, ok := err.(*vhostThrottleExceeded); ok {
220 221
 			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
411 412
 	}
412 413
 
413 414
 	account := client.Account()
414
-	_, err := server.accounts.VHostTake(account, vhost, config.Accounts.VHosts.UserRequests.Cooldown)
415
+	_, err := server.accounts.VHostTake(account, vhost, time.Duration(config.Accounts.VHosts.UserRequests.Cooldown))
415 416
 	if err != nil {
416 417
 		if throttled, ok := err.(*vhostThrottleExceeded); ok {
417 418
 			hsNotice(rb, fmt.Sprintf(client.t("You must wait an additional %v before taking a vhost"), throttled.timeRemaining))

+ 21
- 23
irc/idletimer.go View File

@@ -52,6 +52,7 @@ type IdleTimer struct {
52 52
 	quitTimeout time.Duration
53 53
 	state       TimerState
54 54
 	timer       *time.Timer
55
+	lastTouch   time.Time
55 56
 }
56 57
 
57 58
 // Initialize sets up an IdleTimer and starts counting idle time;
@@ -61,9 +62,11 @@ func (it *IdleTimer) Initialize(session *Session) {
61 62
 	it.registerTimeout = RegisterTimeout
62 63
 	it.idleTimeout, it.quitTimeout = it.recomputeDurations()
63 64
 	registered := session.client.Registered()
65
+	now := time.Now().UTC()
64 66
 
65 67
 	it.Lock()
66 68
 	defer it.Unlock()
69
+	it.lastTouch = now
67 70
 	if registered {
68 71
 		it.state = TimerActive
69 72
 	} else {
@@ -82,7 +85,7 @@ func (it *IdleTimer) recomputeDurations() (idleTimeout, quitTimeout time.Duratio
82 85
 	}
83 86
 
84 87
 	idleTimeout = DefaultIdleTimeout
85
-	if it.session.client.isTor {
88
+	if it.session.isTor {
86 89
 		idleTimeout = TorIdleTimeout
87 90
 	}
88 91
 
@@ -92,10 +95,12 @@ func (it *IdleTimer) recomputeDurations() (idleTimeout, quitTimeout time.Duratio
92 95
 
93 96
 func (it *IdleTimer) Touch() {
94 97
 	idleTimeout, quitTimeout := it.recomputeDurations()
98
+	now := time.Now().UTC()
95 99
 
96 100
 	it.Lock()
97 101
 	defer it.Unlock()
98 102
 	it.idleTimeout, it.quitTimeout = idleTimeout, quitTimeout
103
+	it.lastTouch = now
99 104
 	// a touch transitions TimerUnregistered or TimerIdle into TimerActive
100 105
 	if it.state != TimerDead {
101 106
 		it.state = TimerActive
@@ -103,6 +108,13 @@ func (it *IdleTimer) Touch() {
103 108
 	}
104 109
 }
105 110
 
111
+func (it *IdleTimer) LastTouch() (result time.Time) {
112
+	it.Lock()
113
+	result = it.lastTouch
114
+	it.Unlock()
115
+	return
116
+}
117
+
106 118
 func (it *IdleTimer) processTimeout() {
107 119
 	idleTimeout, quitTimeout := it.recomputeDurations()
108 120
 
@@ -322,9 +334,6 @@ const (
322 334
 	// BrbDead is the state of a client after its timeout has expired; it will be removed
323 335
 	// and therefore new sessions cannot be attached to it
324 336
 	BrbDead
325
-	// BrbSticky allows a client to remain online without sessions, with no timeout.
326
-	// This is not used yet.
327
-	BrbSticky
328 337
 )
329 338
 
330 339
 type BrbTimer struct {
@@ -345,16 +354,16 @@ func (bt *BrbTimer) Initialize(client *Client) {
345 354
 
346 355
 // attempts to enable BRB for a client, returns whether it succeeded
347 356
 func (bt *BrbTimer) Enable() (success bool, duration time.Duration) {
348
-	if !bt.client.Registered() || bt.client.ResumeID() == "" {
349
-		return
350
-	}
351
-
352 357
 	// TODO make this configurable
353 358
 	duration = ResumeableTotalTimeout
354 359
 
355 360
 	bt.client.stateMutex.Lock()
356 361
 	defer bt.client.stateMutex.Unlock()
357 362
 
363
+	if !bt.client.registered || bt.client.alwaysOn || bt.client.resumeID == "" {
364
+		return
365
+	}
366
+
358 367
 	switch bt.state {
359 368
 	case BrbDisabled, BrbEnabled:
360 369
 		bt.state = BrbEnabled
@@ -366,8 +375,6 @@ func (bt *BrbTimer) Enable() (success bool, duration time.Duration) {
366 375
 			bt.brbAt = time.Now().UTC()
367 376
 		}
368 377
 		success = true
369
-	case BrbSticky:
370
-		success = true
371 378
 	default:
372 379
 		// BrbDead
373 380
 		success = false
@@ -416,6 +423,10 @@ func (bt *BrbTimer) processTimeout() {
416 423
 	bt.client.stateMutex.Lock()
417 424
 	defer bt.client.stateMutex.Unlock()
418 425
 
426
+	if bt.client.alwaysOn {
427
+		return
428
+	}
429
+
419 430
 	switch bt.state {
420 431
 	case BrbDisabled, BrbEnabled:
421 432
 		if len(bt.client.sessions) == 0 {
@@ -432,16 +443,3 @@ func (bt *BrbTimer) processTimeout() {
432 443
 	}
433 444
 	bt.resetTimeout()
434 445
 }
435
-
436
-// sets a client to be "sticky", i.e., indefinitely exempt from removal for
437
-// lack of sessions
438
-func (bt *BrbTimer) SetSticky() (success bool) {
439
-	bt.client.stateMutex.Lock()
440
-	defer bt.client.stateMutex.Unlock()
441
-	if bt.state != BrbDead {
442
-		success = true
443
-		bt.state = BrbSticky
444
-	}
445
-	bt.resetTimeout()
446
-	return
447
-}

+ 5
- 5
irc/misc_test.go View File

@@ -9,9 +9,9 @@ import (
9 9
 )
10 10
 
11 11
 func TestZncTimestampParser(t *testing.T) {
12
-	assertEqual(zncWireTimeToTime("1558338348.988"), time.Unix(1558338348, 988000000), t)
13
-	assertEqual(zncWireTimeToTime("1558338348.9"), time.Unix(1558338348, 900000000), t)
14
-	assertEqual(zncWireTimeToTime("1558338348"), time.Unix(1558338348, 0), t)
15
-	assertEqual(zncWireTimeToTime(".988"), time.Unix(0, 988000000), t)
16
-	assertEqual(zncWireTimeToTime("garbage"), time.Unix(0, 0), t)
12
+	assertEqual(zncWireTimeToTime("1558338348.988"), time.Unix(1558338348, 988000000).UTC(), t)
13
+	assertEqual(zncWireTimeToTime("1558338348.9"), time.Unix(1558338348, 900000000).UTC(), t)
14
+	assertEqual(zncWireTimeToTime("1558338348"), time.Unix(1558338348, 0).UTC(), t)
15
+	assertEqual(zncWireTimeToTime(".988"), time.Unix(0, 988000000).UTC(), t)
16
+	assertEqual(zncWireTimeToTime("garbage"), time.Unix(0, 0).UTC(), t)
17 17
 }

+ 22
- 0
irc/mysql/config.go View File

@@ -0,0 +1,22 @@
1
+// Copyright (c) 2020 Shivaram Lingamneni
2
+// released under the MIT license
3
+
4
+package mysql
5
+
6
+import (
7
+	"time"
8
+)
9
+
10
+type Config struct {
11
+	// these are intended to be written directly into the config file:
12
+	Enabled         bool
13
+	Host            string
14
+	Port            int
15
+	User            string
16
+	Password        string
17
+	HistoryDatabase string `yaml:"history-database"`
18
+	Timeout         time.Duration
19
+
20
+	// XXX these are copied from elsewhere in the config:
21
+	ExpireTime time.Duration
22
+}

+ 557
- 0
irc/mysql/history.go View File

@@ -0,0 +1,557 @@
1
+// Copyright (c) 2020 Shivaram Lingamneni
2
+// released under the MIT license
3
+
4
+package mysql
5
+
6
+import (
7
+	"bytes"
8
+	"context"
9
+	"database/sql"
10
+	"fmt"
11
+	"runtime/debug"
12
+	"sync"
13
+	"sync/atomic"
14
+	"time"
15
+
16
+	_ "github.com/go-sql-driver/mysql"
17
+	"github.com/oragono/oragono/irc/history"
18
+	"github.com/oragono/oragono/irc/logger"
19
+	"github.com/oragono/oragono/irc/utils"
20
+)
21
+
22
+const (
23
+	// maximum length in bytes of any message target (nickname or channel name) in its
24
+	// canonicalized (i.e., casefolded) state:
25
+	MaxTargetLength = 64
26
+
27
+	// latest schema of the db
28
+	latestDbSchema   = "1"
29
+	keySchemaVersion = "db.version"
30
+	cleanupRowLimit  = 50
31
+	cleanupPauseTime = 10 * time.Minute
32
+)
33
+
34
+type MySQL struct {
35
+	timeout int64
36
+	db      *sql.DB
37
+	logger  *logger.Manager
38
+
39
+	insertHistory      *sql.Stmt
40
+	insertSequence     *sql.Stmt
41
+	insertConversation *sql.Stmt
42
+
43
+	stateMutex sync.Mutex
44
+	config     Config
45
+}
46
+
47
+func (mysql *MySQL) Initialize(logger *logger.Manager, config Config) {
48
+	mysql.logger = logger
49
+	mysql.SetConfig(config)
50
+}
51
+
52
+func (mysql *MySQL) SetConfig(config Config) {
53
+	atomic.StoreInt64(&mysql.timeout, int64(config.Timeout))
54
+	mysql.stateMutex.Lock()
55
+	mysql.config = config
56
+	mysql.stateMutex.Unlock()
57
+}
58
+
59
+func (mysql *MySQL) getExpireTime() (expireTime time.Duration) {
60
+	mysql.stateMutex.Lock()
61
+	expireTime = mysql.config.ExpireTime
62
+	mysql.stateMutex.Unlock()
63
+	return
64
+}
65
+
66
+func (m *MySQL) Open() (err error) {
67
+	var address string
68
+	if m.config.Port != 0 {
69
+		address = fmt.Sprintf("tcp(%s:%d)", m.config.Host, m.config.Port)
70
+	}
71
+
72
+	m.db, err = sql.Open("mysql", fmt.Sprintf("%s:%s@%s/%s", m.config.User, m.config.Password, address, m.config.HistoryDatabase))
73
+	if err != nil {
74
+		return err
75
+	}
76
+
77
+	err = m.fixSchemas()
78
+	if err != nil {
79
+		return err
80
+	}
81
+
82
+	err = m.prepareStatements()
83
+	if err != nil {
84
+		return err
85
+	}
86
+
87
+	go m.cleanupLoop()
88
+
89
+	return nil
90
+}
91
+
92
+func (mysql *MySQL) fixSchemas() (err error) {
93
+	_, err = mysql.db.Exec(`CREATE TABLE IF NOT EXISTS metadata (
94
+		key_name VARCHAR(32) primary key,
95
+		value VARCHAR(32) NOT NULL
96
+	) CHARSET=ascii COLLATE=ascii_bin;`)
97
+	if err != nil {
98
+		return err
99
+	}
100
+
101
+	var schema string
102
+	err = mysql.db.QueryRow(`select value from metadata where key_name = ?;`, keySchemaVersion).Scan(&schema)
103
+	if err == sql.ErrNoRows {
104
+		err = mysql.createTables()
105
+		if err != nil {
106
+			return
107
+		}
108
+		_, err = mysql.db.Exec(`insert into metadata (key_name, value) values (?, ?);`, keySchemaVersion, latestDbSchema)
109
+		if err != nil {
110
+			return
111
+		}
112
+	} else if err == nil && schema != latestDbSchema {
113
+		// TODO figure out what to do about schema changes
114
+		return &utils.IncompatibleSchemaError{CurrentVersion: schema, RequiredVersion: latestDbSchema}
115
+	} else {
116
+		return err
117
+	}
118
+
119
+	return nil
120
+}
121
+
122
+func (mysql *MySQL) createTables() (err error) {
123
+	_, err = mysql.db.Exec(`CREATE TABLE history (
124
+		id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
125
+		data BLOB NOT NULL,
126
+		msgid BINARY(16) NOT NULL,
127
+		KEY (msgid(4))
128
+	) CHARSET=ascii COLLATE=ascii_bin;`)
129
+	if err != nil {
130
+		return err
131
+	}
132
+
133
+	_, err = mysql.db.Exec(fmt.Sprintf(`CREATE TABLE sequence (
134
+		id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
135
+		target VARBINARY(%[1]d) NOT NULL,
136
+		nanotime BIGINT UNSIGNED NOT NULL,
137
+		history_id BIGINT NOT NULL,
138
+		KEY (target, nanotime),
139
+		KEY (history_id)
140
+	) CHARSET=ascii COLLATE=ascii_bin;`, MaxTargetLength))
141
+	if err != nil {
142
+		return err
143
+	}
144
+
145
+	_, err = mysql.db.Exec(fmt.Sprintf(`CREATE TABLE conversations (
146
+		id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
147
+		lower_target VARBINARY(%[1]d) NOT NULL,
148
+		upper_target VARBINARY(%[1]d) NOT NULL,
149
+		nanotime BIGINT UNSIGNED NOT NULL,
150
+		history_id BIGINT NOT NULL,
151
+		KEY (lower_target, upper_target, nanotime),
152
+		KEY (history_id)
153
+	) CHARSET=ascii COLLATE=ascii_bin;`, MaxTargetLength))
154
+	if err != nil {
155
+		return err
156
+	}
157
+
158
+	return nil
159
+}
160
+
161
+func (mysql *MySQL) cleanupLoop() {
162
+	defer func() {
163
+		if r := recover(); r != nil {
164
+			mysql.logger.Error("mysql",
165
+				fmt.Sprintf("Panic in cleanup routine: %v\n%s", r, debug.Stack()))
166
+			time.Sleep(cleanupPauseTime)
167
+			go mysql.cleanupLoop()
168
+		}
169
+	}()
170
+
171
+	for {
172
+		expireTime := mysql.getExpireTime()
173
+		if expireTime != 0 {
174
+			for {
175
+				startTime := time.Now()
176
+				rowsDeleted, err := mysql.doCleanup(expireTime)
177
+				elapsed := time.Now().Sub(startTime)
178
+				mysql.logError("error during row cleanup", err)
179
+				// keep going as long as we're accomplishing significant work
180
+				// (don't busy-wait on small numbers of rows expiring):
181
+				if rowsDeleted < (cleanupRowLimit / 10) {
182
+					break
183
+				}
184
+				// crude backpressure mechanism: if the database is slow,
185
+				// give it time to process other queries
186
+				time.Sleep(elapsed)
187
+			}
188
+		}
189
+		time.Sleep(cleanupPauseTime)
190
+	}
191
+}
192
+
193
+func (mysql *MySQL) doCleanup(age time.Duration) (count int, err error) {
194
+	ids, maxNanotime, err := mysql.selectCleanupIDs(age)
195
+	if len(ids) == 0 {
196
+		mysql.logger.Debug("mysql", "found no rows to clean up")
197
+		return
198
+	}
199
+
200
+	mysql.logger.Debug("mysql", fmt.Sprintf("deleting %d history rows, max age %s", len(ids), utils.NanoToTimestamp(maxNanotime)))
201
+
202
+	// can't use ? binding for a variable number of arguments, build the IN clause manually
203
+	var inBuf bytes.Buffer
204
+	inBuf.WriteByte('(')
205
+	for i, id := range ids {
206
+		if i != 0 {
207
+			inBuf.WriteRune(',')
208
+		}
209
+		fmt.Fprintf(&inBuf, "%d", id)
210
+	}
211
+	inBuf.WriteRune(')')
212
+
213
+	_, err = mysql.db.Exec(fmt.Sprintf(`DELETE FROM conversations WHERE history_id in %s;`, inBuf.Bytes()))
214
+	if err != nil {
215
+		return
216
+	}
217
+	_, err = mysql.db.Exec(fmt.Sprintf(`DELETE FROM sequence WHERE history_id in %s;`, inBuf.Bytes()))
218
+	if err != nil {
219
+		return
220
+	}
221
+	_, err = mysql.db.Exec(fmt.Sprintf(`DELETE FROM history WHERE id in %s;`, inBuf.Bytes()))
222
+	if err != nil {
223
+		return
224
+	}
225
+
226
+	count = len(ids)
227
+	return
228
+}
229
+
230
+func (mysql *MySQL) selectCleanupIDs(age time.Duration) (ids []uint64, maxNanotime int64, err error) {
231
+	rows, err := mysql.db.Query(`
232
+		SELECT history.id, sequence.nanotime
233
+		FROM history
234
+		LEFT JOIN sequence ON history.id = sequence.history_id
235
+		ORDER BY history.id LIMIT ?;`, cleanupRowLimit)
236
+	if err != nil {
237
+		return
238
+	}
239
+	defer rows.Close()
240
+
241
+	// a history ID may have 0-2 rows in sequence: 1 for a channel entry,
242
+	// 2 for a DM, 0 if the data is inconsistent. therefore, deduplicate
243
+	// and delete anything that doesn't have a sequence entry:
244
+	idset := make(map[uint64]struct{}, cleanupRowLimit)
245
+	threshold := time.Now().Add(-age).UnixNano()
246
+	for rows.Next() {
247
+		var id uint64
248
+		var nanotime sql.NullInt64
249
+		err = rows.Scan(&id, &nanotime)
250
+		if err != nil {
251
+			return
252
+		}
253
+		if !nanotime.Valid || nanotime.Int64 < threshold {
254
+			idset[id] = struct{}{}
255
+			if nanotime.Valid && nanotime.Int64 > maxNanotime {
256
+				maxNanotime = nanotime.Int64
257
+			}
258
+		}
259
+	}
260
+	ids = make([]uint64, len(idset))
261
+	i := 0
262
+	for id := range idset {
263
+		ids[i] = id
264
+		i++
265
+	}
266
+	return
267
+}
268
+
269
+func (mysql *MySQL) prepareStatements() (err error) {
270
+	mysql.insertHistory, err = mysql.db.Prepare(`INSERT INTO history
271
+		(data, msgid) VALUES (?, ?);`)
272
+	if err != nil {
273
+		return
274
+	}
275
+	mysql.insertSequence, err = mysql.db.Prepare(`INSERT INTO sequence
276
+		(target, nanotime, history_id) VALUES (?, ?, ?);`)
277
+	if err != nil {
278
+		return
279
+	}
280
+	mysql.insertConversation, err = mysql.db.Prepare(`INSERT INTO conversations
281
+		(lower_target, upper_target, nanotime, history_id) VALUES (?, ?, ?, ?);`)
282
+	if err != nil {
283
+		return
284
+	}
285
+
286
+	return
287
+}
288
+
289
+func (mysql *MySQL) getTimeout() time.Duration {
290
+	return time.Duration(atomic.LoadInt64(&mysql.timeout))
291
+}
292
+
293
+func (mysql *MySQL) logError(context string, err error) (quit bool) {
294
+	if err != nil {
295
+		mysql.logger.Error("mysql", context, err.Error())
296
+		return true
297
+	}
298
+	return false
299
+}
300
+
301
+func (mysql *MySQL) AddChannelItem(target string, item history.Item) (err error) {
302
+	if mysql.db == nil {
303
+		return
304
+	}
305
+
306
+	if target == "" {
307
+		return utils.ErrInvalidParams
308
+	}
309
+
310
+	ctx, cancel := context.WithTimeout(context.Background(), mysql.getTimeout())
311
+	defer cancel()
312
+
313
+	id, err := mysql.insertBase(ctx, item)
314
+	if err != nil {
315
+		return
316
+	}
317
+
318
+	err = mysql.insertSequenceEntry(ctx, target, item.Message.Time, id)
319
+	return
320
+}
321
+
322
+func (mysql *MySQL) insertSequenceEntry(ctx context.Context, target string, messageTime time.Time, id int64) (err error) {
323
+	_, err = mysql.insertSequence.ExecContext(ctx, target, messageTime.UnixNano(), id)
324
+	mysql.logError("could not insert sequence entry", err)
325
+	return
326
+}
327
+
328
+func (mysql *MySQL) insertConversationEntry(ctx context.Context, sender, recipient string, messageTime time.Time, id int64) (err error) {
329
+	lower, higher := stringMinMax(sender, recipient)
330
+	_, err = mysql.insertConversation.ExecContext(ctx, lower, higher, messageTime.UnixNano(), id)
331
+	mysql.logError("could not insert conversations entry", err)
332
+	return
333
+}
334
+
335
+func (mysql *MySQL) insertBase(ctx context.Context, item history.Item) (id int64, err error) {
336
+	value, err := marshalItem(&item)
337
+	if mysql.logError("could not marshal item", err) {
338
+		return
339
+	}
340
+
341
+	msgidBytes, err := decodeMsgid(item.Message.Msgid)
342
+	if mysql.logError("could not decode msgid", err) {
343
+		return
344
+	}
345
+
346
+	result, err := mysql.insertHistory.ExecContext(ctx, value, msgidBytes)
347
+	if mysql.logError("could not insert item", err) {
348
+		return
349
+	}
350
+	id, err = result.LastInsertId()
351
+	if mysql.logError("could not insert item", err) {
352
+		return
353
+	}
354
+
355
+	return
356
+}
357
+
358
+func stringMinMax(first, second string) (min, max string) {
359
+	if first < second {
360
+		return first, second
361
+	} else {
362
+		return second, first
363
+	}
364
+}
365
+
366
+func (mysql *MySQL) AddDirectMessage(sender, recipient string, senderPersistent, recipientPersistent bool, item history.Item) (err error) {
367
+	if mysql.db == nil {
368
+		return
369
+	}
370
+
371
+	if !(senderPersistent || recipientPersistent) {
372
+		return
373
+	}
374
+
375
+	if sender == "" || recipient == "" {
376
+		return utils.ErrInvalidParams
377
+	}
378
+
379
+	ctx, cancel := context.WithTimeout(context.Background(), mysql.getTimeout())
380
+	defer cancel()
381
+
382
+	id, err := mysql.insertBase(ctx, item)
383
+	if err != nil {
384
+		return
385
+	}
386
+
387
+	if senderPersistent {
388
+		mysql.insertSequenceEntry(ctx, sender, item.Message.Time, id)
389
+		if err != nil {
390
+			return
391
+		}
392
+	}
393
+
394
+	if recipientPersistent && sender != recipient {
395
+		err = mysql.insertSequenceEntry(ctx, recipient, item.Message.Time, id)
396
+		if err != nil {
397
+			return
398
+		}
399
+	}
400
+
401
+	err = mysql.insertConversationEntry(ctx, sender, recipient, item.Message.Time, id)
402
+
403
+	return
404
+}
405
+
406
+func (mysql *MySQL) msgidToTime(ctx context.Context, msgid string) (result time.Time, err error) {
407
+	// in theory, we could optimize out a roundtrip to the database by using a subquery instead:
408
+	// sequence.nanotime > (
409
+	//     SELECT sequence.nanotime FROM sequence, history
410
+	//     WHERE sequence.history_id = history.id AND history.msgid = ?
411
+	//     LIMIT 1)
412
+	// however, this doesn't handle the BETWEEN case with one or two msgids, where we
413
+	// don't initially know whether the interval is going forwards or backwards. to simplify
414
+	// the logic,  resolve msgids to timestamps "manually" in all cases, using a separate query.
415
+	decoded, err := decodeMsgid(msgid)
416
+	if err != nil {
417
+		return
418
+	}
419
+	row := mysql.db.QueryRowContext(ctx, `
420
+		SELECT sequence.nanotime FROM sequence
421
+		INNER JOIN history ON history.id = sequence.history_id
422
+		WHERE history.msgid = ? LIMIT 1;`, decoded)
423
+	var nanotime int64
424
+	err = row.Scan(&nanotime)
425
+	if mysql.logError("could not resolve msgid to time", err) {
426
+		return
427
+	}
428
+	result = time.Unix(0, nanotime).UTC()
429
+	return
430
+}
431
+
432
+func (mysql *MySQL) selectItems(ctx context.Context, query string, args ...interface{}) (results []history.Item, err error) {
433
+	rows, err := mysql.db.QueryContext(ctx, query, args...)
434
+	if mysql.logError("could not select history items", err) {
435
+		return
436
+	}
437
+
438
+	defer rows.Close()
439
+
440
+	for rows.Next() {
441
+		var blob []byte
442
+		var item history.Item
443
+		err = rows.Scan(&blob)
444
+		if mysql.logError("could not scan history item", err) {
445
+			return
446
+		}
447
+		err = unmarshalItem(blob, &item)
448
+		if mysql.logError("could not unmarshal history item", err) {
449
+			return
450
+		}
451
+		results = append(results, item)
452
+	}
453
+	return
454
+}
455
+
456
+func (mysql *MySQL) betweenTimestamps(ctx context.Context, sender, recipient string, after, before, cutoff time.Time, limit int) (results []history.Item, err error) {
457
+	useSequence := true
458
+	var lowerTarget, upperTarget string
459
+	if sender != "" {
460
+		lowerTarget, upperTarget = stringMinMax(sender, recipient)
461
+		useSequence = false
462
+	}
463
+
464
+	table := "sequence"
465
+	if !useSequence {
466
+		table = "conversations"
467
+	}
468
+
469
+	after, before, ascending := history.MinMaxAsc(after, before, cutoff)
470
+	direction := "ASC"
471
+	if !ascending {
472
+		direction = "DESC"
473
+	}
474
+
475
+	var queryBuf bytes.Buffer
476
+
477
+	args := make([]interface{}, 0, 6)
478
+	fmt.Fprintf(&queryBuf,
479
+		"SELECT history.data from history INNER JOIN %[1]s ON history.id = %[1]s.history_id WHERE", table)
480
+	if useSequence {
481
+		fmt.Fprintf(&queryBuf, " sequence.target = ?")
482
+		args = append(args, recipient)
483
+	} else {
484
+		fmt.Fprintf(&queryBuf, " conversations.lower_target = ? AND conversations.upper_target = ?")
485
+		args = append(args, lowerTarget)
486
+		args = append(args, upperTarget)
487
+	}
488
+	if !after.IsZero() {
489
+		fmt.Fprintf(&queryBuf, " AND %s.nanotime > ?", table)
490
+		args = append(args, after.UnixNano())
491
+	}
492
+	if !before.IsZero() {
493
+		fmt.Fprintf(&queryBuf, " AND %s.nanotime < ?", table)
494
+		args = append(args, before.UnixNano())
495
+	}
496
+	fmt.Fprintf(&queryBuf, " ORDER BY %[1]s.nanotime %[2]s LIMIT ?;", table, direction)
497
+	args = append(args, limit)
498
+
499
+	results, err = mysql.selectItems(ctx, queryBuf.String(), args...)
500
+	if err == nil && !ascending {
501
+		history.Reverse(results)
502
+	}
503
+	return
504
+}
505
+
506
+func (mysql *MySQL) Close() {
507
+	// closing the database will close our prepared statements as well
508
+	if mysql.db != nil {
509
+		mysql.db.Close()
510
+	}
511
+	mysql.db = nil
512
+}
513
+
514
+// implements history.Sequence, emulating a single history buffer (for a channel,
515
+// a single user's DMs, or a DM conversation)
516
+type mySQLHistorySequence struct {
517
+	mysql     *MySQL
518
+	sender    string
519
+	recipient string
520
+	cutoff    time.Time
521
+}
522
+
523
+func (s *mySQLHistorySequence) Between(start, end history.Selector, limit int) (results []history.Item, complete bool, err error) {
524
+	ctx, cancel := context.WithTimeout(context.Background(), s.mysql.getTimeout())
525
+	defer cancel()
526
+
527
+	startTime := start.Time
528
+	if start.Msgid != "" {
529
+		startTime, err = s.mysql.msgidToTime(ctx, start.Msgid)
530
+		if err != nil {
531
+			return nil, false, err
532
+		}
533
+	}
534
+	endTime := end.Time
535
+	if end.Msgid != "" {
536
+		endTime, err = s.mysql.msgidToTime(ctx, end.Msgid)
537
+		if err != nil {
538
+			return nil, false, err
539
+		}
540
+	}
541
+
542
+	results, err = s.mysql.betweenTimestamps(ctx, s.sender, s.recipient, startTime, endTime, s.cutoff, limit)
543
+	return results, (err == nil), err
544
+}
545
+
546
+func (s *mySQLHistorySequence) Around(start history.Selector, limit int) (results []history.Item, err error) {
547
+	return history.GenericAround(s, start, limit)
548
+}
549
+
550
+func (mysql *MySQL) MakeSequence(sender, recipient string, cutoff time.Time) history.Sequence {
551
+	return &mySQLHistorySequence{
552
+		sender:    sender,
553
+		recipient: recipient,
554
+		mysql:     mysql,
555
+		cutoff:    cutoff,
556
+	}
557
+}

+ 23
- 0
irc/mysql/serialization.go View File

@@ -0,0 +1,23 @@
1
+package mysql
2
+
3
+import (
4
+	"encoding/json"
5
+
6
+	"github.com/oragono/oragono/irc/history"
7
+	"github.com/oragono/oragono/irc/utils"
8
+)
9
+
10
+// 123 / '{' is the magic number that means JSON;
11
+// if we want to do a binary encoding later, we just have to add different magic version numbers
12
+
13
+func marshalItem(item *history.Item) (result []byte, err error) {
14
+	return json.Marshal(item)
15
+}
16
+
17
+func unmarshalItem(data []byte, result *history.Item) (err error) {
18
+	return json.Unmarshal(data, result)
19
+}
20
+
21
+func decodeMsgid(msgid string) ([]byte, error) {
22
+	return utils.B32Encoder.DecodeString(msgid)
23
+}

+ 10
- 8
irc/nickname.go View File

@@ -43,13 +43,15 @@ func performNickChange(server *Server, client *Client, target *Client, session *
43 43
 	hadNick := target.HasNick()
44 44
 	origNickMask := target.NickMaskString()
45 45
 	details := target.Details()
46
-	err := client.server.clients.SetNick(target, session, nickname)
46
+	assignedNickname, err := client.server.clients.SetNick(target, session, nickname)
47 47
 	if err == errNicknameInUse {
48 48
 		rb.Add(nil, server.name, ERR_NICKNAMEINUSE, currentNick, nickname, client.t("Nickname is already in use"))
49 49
 	} else if err == errNicknameReserved {
50 50
 		rb.Add(nil, server.name, ERR_NICKNAMEINUSE, currentNick, nickname, client.t("Nickname is reserved by a different account"))
51 51
 	} else if err == errNicknameInvalid {
52 52
 		rb.Add(nil, server.name, ERR_ERRONEUSNICKNAME, currentNick, utils.SafeErrorParam(nickname), client.t("Erroneous nickname"))
53
+	} else if err == errCantChangeNick {
54
+		rb.Add(nil, server.name, ERR_NICKNAMEINUSE, currentNick, utils.SafeErrorParam(nickname), client.t(err.Error()))
53 55
 	} else if err != nil {
54 56
 		rb.Add(nil, server.name, ERR_UNKNOWNERROR, currentNick, "NICK", fmt.Sprintf(client.t("Could not set or change nickname: %s"), err.Error()))
55 57
 	}
@@ -64,26 +66,26 @@ func performNickChange(server *Server, client *Client, target *Client, session *
64 66
 		AccountName: details.accountName,
65 67
 		Message:     message,
66 68
 	}
67
-	histItem.Params[0] = nickname
69
+	histItem.Params[0] = assignedNickname
68 70
 
69
-	client.server.logger.Debug("nick", fmt.Sprintf("%s changed nickname to %s [%s]", origNickMask, nickname, client.NickCasefolded()))
71
+	client.server.logger.Debug("nick", fmt.Sprintf("%s changed nickname to %s [%s]", origNickMask, assignedNickname, client.NickCasefolded()))
70 72
 	if hadNick {
71 73
 		if client == target {
72
-			target.server.snomasks.Send(sno.LocalNicks, fmt.Sprintf(ircfmt.Unescape("$%s$r changed nickname to %s"), details.nick, nickname))
74
+			target.server.snomasks.Send(sno.LocalNicks, fmt.Sprintf(ircfmt.Unescape("$%s$r changed nickname to %s"), details.nick, assignedNickname))
73 75
 		} else {
74
-			target.server.snomasks.Send(sno.LocalNicks, fmt.Sprintf(ircfmt.Unescape("Operator %s changed nickname of $%s$r to %s"), client.Nick(), details.nick, nickname))
76
+			target.server.snomasks.Send(sno.LocalNicks, fmt.Sprintf(ircfmt.Unescape("Operator %s changed nickname of $%s$r to %s"), client.Nick(), details.nick, assignedNickname))
75 77
 		}
76 78
 		target.server.whoWas.Append(details.WhoWas)
77
-		rb.AddFromClient(message.Time, message.Msgid, origNickMask, details.accountName, nil, "NICK", nickname)
79
+		rb.AddFromClient(message.Time, message.Msgid, origNickMask, details.accountName, nil, "NICK", assignedNickname)
78 80
 		for session := range target.Friends() {
79 81
 			if session != rb.session {
80
-				session.sendFromClientInternal(false, message.Time, message.Msgid, origNickMask, details.accountName, nil, "NICK", nickname)
82
+				session.sendFromClientInternal(false, message.Time, message.Msgid, origNickMask, details.accountName, nil, "NICK", assignedNickname)
81 83
 			}
82 84
 		}
83 85
 	}
84 86
 
85 87
 	for _, channel := range client.Channels() {
86
-		channel.history.Add(histItem)
88
+		channel.AddHistoryItem(histItem)
87 89
 	}
88 90
 
89 91
 	if target.Registered() {

+ 103
- 25
irc/nickserv.go View File

@@ -31,7 +31,7 @@ func servCmdRequiresNickRes(config *Config) bool {
31 31
 }
32 32
 
33 33
 func servCmdRequiresBouncerEnabled(config *Config) bool {
34
-	return config.Accounts.Bouncer.Enabled
34
+	return config.Accounts.Multiclient.Enabled
35 35
 }
36 36
 
37 37
 const (
@@ -147,7 +147,7 @@ an administrator can set use this command to set up user accounts.`,
147 147
 			help: `Syntax: $bSESSIONS [nickname]$b
148 148
 
149 149
 SESSIONS lists information about the sessions currently attached, via
150
-the server's bouncer functionality, to your nickname. An administrator
150
+the server's multiclient functionality, to your nickname. An administrator
151 151
 can use this command to list another user's sessions.`,
152 152
 			helpShort: `$bSESSIONS$b lists the sessions attached to a nickname.`,
153 153
 			enabled:   servCmdRequiresBouncerEnabled,
@@ -217,7 +217,7 @@ information on the settings and their possible values, see HELP SET.`,
217 217
 			helpStrings: []string{
218 218
 				`Syntax $bSET <setting> <value>$b
219 219
 
220
-Set modifies your account settings. The following settings are available:`,
220
+SET modifies your account settings. The following settings are available:`,
221 221
 
222 222
 				`$bENFORCE$b
223 223
 'enforce' lets you specify a custom enforcement mechanism for your registered
@@ -228,8 +228,8 @@ nicknames. Your options are:
228 228
 3. 'strict'  [you must already be authenticated to use the nick]
229 229
 4. 'default' [use the server default]`,
230 230
 
231
-				`$bBOUNCER$b
232
-If 'bouncer' is enabled and you are already logged in and using a nick, a
231
+				`$bMULTICLIENT$b
232
+If 'multiclient' is enabled and you are already logged in and using a nick, a
233 233
 second client of yours that authenticates with SASL and requests the same nick
234 234
 is allowed to attach to the nick as well (this is comparable to the behavior
235 235
 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
247 247
 messages, but may be spammy. Your options are 'always', 'never', and the default
248 248
 of 'commands-only' (the messages will be replayed in /HISTORY output, but not
249 249
 during autoreplay).`,
250
+				`$bALWAYS-ON$b
251
+'always-on' controls whether your nickname/identity will remain active
252
+even while you are disconnected from the server. Your options are 'true',
253
+'false', and 'default' (use the server default value).`,
254
+				`$bAUTOREPLAY-MISSED$b
255
+'autoreplay-missed' is only effective for always-on clients. If enabled,
256
+if you have at most one active session, the server will remember the time
257
+you disconnect and then replay missed messages to you when you reconnect.
258
+Your options are 'on' and 'off'.`,
259
+				`$bDM-HISTORY$b
260
+'dm-history' is only effective for always-on clients. It lets you control
261
+how the history of your direct messages is stored. Your options are:
262
+1. 'off'        [no history]
263
+2. 'ephemeral'  [a limited amount of temporary history, not stored on disk]
264
+3. 'on'         [history stored in a permanent database, if available]
265
+4. 'default'    [use the server default]`,
250 266
 			},
251 267
 			authRequired: true,
252 268
 			enabled:      servCmdRequiresAccreg,
@@ -332,23 +348,48 @@ func displaySetting(settingName string, settings AccountSettings, client *Client
332 348
 		case ReplayJoinsNever:
333 349
 			nsNotice(rb, client.t("You will not see JOINs and PARTs in /HISTORY output or in autoreplay"))
334 350
 		}
335
-	case "bouncer":
336
-		if !config.Accounts.Bouncer.Enabled {
351
+	case "multiclient":
352
+		if !config.Accounts.Multiclient.Enabled {
337 353
 			nsNotice(rb, client.t("This feature has been disabled by the server administrators"))
338 354
 		} else {
339 355
 			switch settings.AllowBouncer {
340
-			case BouncerAllowedServerDefault:
341
-				if config.Accounts.Bouncer.AllowedByDefault {
342
-					nsNotice(rb, client.t("Bouncer functionality is currently enabled for your account, but you can opt out"))
356
+			case MulticlientAllowedServerDefault:
357
+				if config.Accounts.Multiclient.AllowedByDefault {
358
+					nsNotice(rb, client.t("Multiclient functionality is currently enabled for your account, but you can opt out"))
343 359
 				} else {
344
-					nsNotice(rb, client.t("Bouncer functionality is currently disabled for your account, but you can opt in"))
360
+					nsNotice(rb, client.t("Multiclient functionality is currently disabled for your account, but you can opt in"))
345 361
 				}
346
-			case BouncerDisallowedByUser:
347
-				nsNotice(rb, client.t("Bouncer functionality is currently disabled for your account"))
348
-			case BouncerAllowedByUser:
349
-				nsNotice(rb, client.t("Bouncer functionality is currently enabled for your account"))
362
+			case MulticlientDisallowedByUser:
363
+				nsNotice(rb, client.t("Multiclient functionality is currently disabled for your account"))
364
+			case MulticlientAllowedByUser:
365
+				nsNotice(rb, client.t("Multiclient functionality is currently enabled for your account"))
350 366
 			}
351 367
 		}
368
+	case "always-on":
369
+		stored := settings.AlwaysOn
370
+		actual := client.AlwaysOn()
371
+		nsNotice(rb, fmt.Sprintf(client.t("Your stored always-on setting is: %s"), persistentStatusToString(stored)))
372
+		if actual {
373
+			nsNotice(rb, client.t("Given current server settings, your client is always-on"))
374
+		} else {
375
+			nsNotice(rb, client.t("Given current server settings, your client is not always-on"))
376
+		}
377
+	case "autoreplay-missed":
378
+		stored := settings.AutoreplayMissed
379
+		if stored {
380
+			if client.AlwaysOn() {
381
+				nsNotice(rb, client.t("Autoreplay of missed messages is enabled"))
382
+			} else {
383
+				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"))
384
+			}
385
+		} else {
386
+			nsNotice(rb, client.t("Your account is not configured to receive autoreplayed missed messages"))
387
+		}
388
+	case "dm-history":
389
+		effectiveValue := historyEnabled(config.History.Persistent.DirectMessages, settings.DMHistory)
390
+		csNotice(rb, fmt.Sprintf(client.t("Your stored direct message history setting is: %s"), historyStatusToString(settings.DMHistory)))
391
+		csNotice(rb, fmt.Sprintf(client.t("Given current server settings, your direct message history setting is: %s"), historyStatusToString(effectiveValue)))
392
+
352 393
 	default:
353 394
 		nsNotice(rb, client.t("No such setting"))
354 395
 	}
@@ -399,17 +440,17 @@ func nsSetHandler(server *Server, client *Client, command string, params []strin
399 440
 			out.AutoreplayLines = newValue
400 441
 			return
401 442
 		}
402
-	case "bouncer":
403
-		var newValue BouncerAllowedSetting
443
+	case "multiclient":
444
+		var newValue MulticlientAllowedSetting
404 445
 		if strings.ToLower(params[1]) == "default" {
405
-			newValue = BouncerAllowedServerDefault
446
+			newValue = MulticlientAllowedServerDefault
406 447
 		} else {
407 448
 			var enabled bool
408 449
 			enabled, err = utils.StringToBool(params[1])
409 450
 			if enabled {
410
-				newValue = BouncerAllowedByUser
451
+				newValue = MulticlientAllowedByUser
411 452
 			} else {
412
-				newValue = BouncerDisallowedByUser
453
+				newValue = MulticlientDisallowedByUser
413 454
 			}
414 455
 		}
415 456
 		if err == nil {
@@ -429,6 +470,37 @@ func nsSetHandler(server *Server, client *Client, command string, params []strin
429 470
 				return
430 471
 			}
431 472
 		}
473
+	case "always-on":
474
+		var newValue PersistentStatus
475
+		newValue, err = persistentStatusFromString(params[1])
476
+		// "opt-in" and "opt-out" don't make sense as user preferences
477
+		if err == nil && newValue != PersistentOptIn && newValue != PersistentOptOut {
478
+			munger = func(in AccountSettings) (out AccountSettings, err error) {
479
+				out = in
480
+				out.AlwaysOn = newValue
481
+				return
482
+			}
483
+		}
484
+	case "autoreplay-missed":
485
+		var newValue bool
486
+		newValue, err = utils.StringToBool(params[1])
487
+		if err == nil {
488
+			munger = func(in AccountSettings) (out AccountSettings, err error) {
489
+				out = in
490
+				out.AutoreplayMissed = newValue
491
+				return
492
+			}
493
+		}
494
+	case "dm-history":
495
+		var newValue HistoryStatus
496
+		newValue, err = historyStatusFromString(params[1])
497
+		if err == nil {
498
+			munger = func(in AccountSettings) (out AccountSettings, err error) {
499
+				out = in
500
+				out.DMHistory = newValue
501
+				return
502
+			}
503
+		}
432 504
 	default:
433 505
 		err = errInvalidParams
434 506
 	}
@@ -480,6 +552,9 @@ func nsGhostHandler(server *Server, client *Client, command string, params []str
480 552
 	} else if ghost == client {
481 553
 		nsNotice(rb, client.t("You can't GHOST yourself (try /QUIT instead)"))
482 554
 		return
555
+	} else if ghost.AlwaysOn() {
556
+		nsNotice(rb, client.t("You can't GHOST an always-on client"))
557
+		return
483 558
 	}
484 559
 
485 560
 	authorized := false
@@ -530,7 +605,7 @@ func nsIdentifyHandler(server *Server, client *Client, command string, params []
530 605
 
531 606
 	var username, passphrase string
532 607
 	if len(params) == 1 {
533
-		if client.certfp != "" {
608
+		if rb.session.certfp != "" {
534 609
 			username = params[0]
535 610
 		} else {
536 611
 			// XXX undocumented compatibility mode with other nickservs, allowing
@@ -553,8 +628,8 @@ func nsIdentifyHandler(server *Server, client *Client, command string, params []
553 628
 	}
554 629
 
555 630
 	// try certfp
556
-	if !loginSuccessful && client.certfp != "" {
557
-		err := server.accounts.AuthenticateByCertFP(client, "")
631
+	if !loginSuccessful && rb.session.certfp != "" {
632
+		err := server.accounts.AuthenticateByCertFP(client, rb.session.certfp, "")
558 633
 		loginSuccessful = (err == nil)
559 634
 	}
560 635
 
@@ -618,7 +693,7 @@ func nsRegisterHandler(server *Server, client *Client, command string, params []
618 693
 		email = params[1]
619 694
 	}
620 695
 
621
-	certfp := client.certfp
696
+	certfp := rb.session.certfp
622 697
 	if passphrase == "*" {
623 698
 		if certfp == "" {
624 699
 			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 []
658 733
 		}
659 734
 	}
660 735
 
661
-	err := server.accounts.Register(client, account, callbackNamespace, callbackValue, passphrase, client.certfp)
736
+	err := server.accounts.Register(client, account, callbackNamespace, callbackValue, passphrase, rb.session.certfp)
662 737
 	if err == nil {
663 738
 		if callbackNamespace == "*" {
664 739
 			err = server.accounts.Verify(client, account, "")
@@ -876,6 +951,9 @@ func nsSessionsHandler(server *Server, client *Client, command string, params []
876 951
 		nsNotice(rb, fmt.Sprintf(client.t("Hostname:    %s"), session.hostname))
877 952
 		nsNotice(rb, fmt.Sprintf(client.t("Created at:  %s"), session.ctime.Format(time.RFC1123)))
878 953
 		nsNotice(rb, fmt.Sprintf(client.t("Last active: %s"), session.atime.Format(time.RFC1123)))
954
+		if session.certfp != "" {
955
+			nsNotice(rb, fmt.Sprintf(client.t("Certfp:      %s"), session.certfp))
956
+		}
879 957
 	}
880 958
 }
881 959
 

+ 2
- 2
irc/responsebuffer.go View File

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

+ 104
- 12
irc/server.go View File

@@ -24,8 +24,10 @@ import (
24 24
 	"github.com/goshuirc/irc-go/ircfmt"
25 25
 	"github.com/oragono/oragono/irc/caps"
26 26
 	"github.com/oragono/oragono/irc/connection_limits"
27
+	"github.com/oragono/oragono/irc/history"
27 28
 	"github.com/oragono/oragono/irc/logger"
28 29
 	"github.com/oragono/oragono/irc/modes"
30
+	"github.com/oragono/oragono/irc/mysql"
29 31
 	"github.com/oragono/oragono/irc/sno"
30 32
 	"github.com/tidwall/buntdb"
31 33
 )
@@ -84,6 +86,7 @@ type Server struct {
84 86
 	signals           chan os.Signal
85 87
 	snomasks          SnoManager
86 88
 	store             *buntdb.DB
89
+	historyDB         mysql.MySQL
87 90
 	torLimiter        connection_limits.TorLimiter
88 91
 	whoWas            WhoWasList
89 92
 	stats             Stats
@@ -122,7 +125,7 @@ func NewServer(config *Config, logger *logger.Manager) (*Server, error) {
122 125
 	server.monitorManager.Initialize()
123 126
 	server.snomasks.Initialize()
124 127
 
125
-	if err := server.applyConfig(config, true); err != nil {
128
+	if err := server.applyConfig(config); err != nil {
126 129
 		return nil, err
127 130
 	}
128 131
 
@@ -143,6 +146,8 @@ func (server *Server) Shutdown() {
143 146
 	if err := server.store.Close(); err != nil {
144 147
 		server.logger.Error("shutdown", fmt.Sprintln("Could not close datastore:", err))
145 148
 	}
149
+
150
+	server.historyDB.Close()
146 151
 }
147 152
 
148 153
 // Run starts the server.
@@ -316,7 +321,7 @@ func (server *Server) tryRegister(c *Client, session *Session) (exiting bool) {
316 321
 
317 322
 	// client MUST send PASS if necessary, or authenticate with SASL if necessary,
318 323
 	// before completing the other registration commands
319
-	authOutcome := c.isAuthorized(server.Config())
324
+	authOutcome := c.isAuthorized(server.Config(), session)
320 325
 	var quitMessage string
321 326
 	switch authOutcome {
322 327
 	case authFailPass:
@@ -376,7 +381,7 @@ func (server *Server) playRegistrationBurst(session *Session) {
376 381
 	// continue registration
377 382
 	d := c.Details()
378 383
 	server.logger.Info("localconnect", fmt.Sprintf("Client connected [%s] [u:%s] [r:%s]", d.nick, d.username, d.realname))
379
-	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))
384
+	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))
380 385
 
381 386
 	// send welcome text
382 387
 	//NOTE(dan): we specifically use the NICK here instead of the nickmask
@@ -503,8 +508,12 @@ func (client *Client) getWhoisOf(target *Client, rb *ResponseBuffer) {
503 508
 		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)))
504 509
 	}
505 510
 
506
-	if target.certfp != "" && (client.HasMode(modes.Operator) || client == target) {
507
-		rb.Add(nil, client.server.name, RPL_WHOISCERTFP, cnick, tnick, fmt.Sprintf(client.t("has client certificate fingerprint %s"), target.certfp))
511
+	if client == target || client.HasMode(modes.Operator) {
512
+		for _, session := range target.Sessions() {
513
+			if session.certfp != "" {
514
+				rb.Add(nil, client.server.name, RPL_WHOISCERTFP, cnick, tnick, fmt.Sprintf(client.t("has client certificate fingerprint %s"), session.certfp))
515
+			}
516
+		}
508 517
 	}
509 518
 	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"))
510 519
 }
@@ -550,7 +559,7 @@ func (server *Server) rehash() error {
550 559
 		return fmt.Errorf("Error loading config file config: %s", err.Error())
551 560
 	}
552 561
 
553
-	err = server.applyConfig(config, false)
562
+	err = server.applyConfig(config)
554 563
 	if err != nil {
555 564
 		return fmt.Errorf("Error applying config changes: %s", err.Error())
556 565
 	}
@@ -558,7 +567,10 @@ func (server *Server) rehash() error {
558 567
 	return nil
559 568
 }
560 569
 
561
-func (server *Server) applyConfig(config *Config, initial bool) (err error) {
570
+func (server *Server) applyConfig(config *Config) (err error) {
571
+	oldConfig := server.Config()
572
+	initial := oldConfig == nil
573
+
562 574
 	if initial {
563 575
 		server.configFilename = config.Filename
564 576
 		server.name = config.Server.Name
@@ -568,7 +580,7 @@ func (server *Server) applyConfig(config *Config, initial bool) (err error) {
568 580
 		// enforce configs that can't be changed after launch:
569 581
 		if server.name != config.Server.Name {
570 582
 			return fmt.Errorf("Server name cannot be changed after launching the server, rehash aborted")
571
-		} else if server.Config().Datastore.Path != config.Datastore.Path {
583
+		} else if oldConfig.Datastore.Path != config.Datastore.Path {
572 584
 			return fmt.Errorf("Datastore path cannot be changed after launching the server, rehash aborted")
573 585
 		} else if globalCasemappingSetting != config.Server.Casemapping {
574 586
 			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) {
576 588
 	}
577 589
 
578 590
 	server.logger.Info("server", "Using config file", server.configFilename)
579
-	oldConfig := server.Config()
580 591
 
581 592
 	// first, reload config sections for functionality implemented in subpackages:
582 593
 	wasLoggingRawIO := !initial && server.logger.IsLoggingRawIO()
@@ -609,14 +620,13 @@ func (server *Server) applyConfig(config *Config, initial bool) (err error) {
609 620
 		if !oldConfig.Channels.Registration.Enabled {
610 621
 			server.channels.loadRegisteredChannels(config)
611 622
 		}
612
-
613 623
 		// resize history buffers as needed
614 624
 		if oldConfig.History != config.History {
615 625
 			for _, channel := range server.channels.Channels() {
616
-				channel.history.Resize(config.History.ChannelLength, config.History.AutoresizeWindow)
626
+				channel.resizeHistory(config)
617 627
 			}
618 628
 			for _, client := range server.clients.AllClients() {
619
-				client.history.Resize(config.History.ClientLength, config.History.AutoresizeWindow)
629
+				client.resizeHistory(config)
620 630
 			}
621 631
 		}
622 632
 	}
@@ -658,6 +668,10 @@ func (server *Server) applyConfig(config *Config, initial bool) (err error) {
658 668
 		if err := server.loadDatastore(config); err != nil {
659 669
 			return err
660 670
 		}
671
+	} else {
672
+		if config.Datastore.MySQL.Enabled && config.Datastore.MySQL != oldConfig.Datastore.MySQL {
673
+			server.historyDB.SetConfig(config.Datastore.MySQL)
674
+		}
661 675
 	}
662 676
 
663 677
 	server.setupPprofListener(config)
@@ -778,6 +792,15 @@ func (server *Server) loadDatastore(config *Config) error {
778 792
 	server.channels.Initialize(server)
779 793
 	server.accounts.Initialize(server)
780 794
 
795
+	if config.Datastore.MySQL.Enabled {
796
+		server.historyDB.Initialize(server.logger, config.Datastore.MySQL)
797
+		err = server.historyDB.Open()
798
+		if err != nil {
799
+			server.logger.Error("internal", "could not connect to mysql", err.Error())
800
+			return err
801
+		}
802
+	}
803
+
781 804
 	return nil
782 805
 }
783 806
 
@@ -835,6 +858,75 @@ func (server *Server) setupListeners(config *Config) (err error) {
835 858
 	return
836 859
 }
837 860
 
861
+// Gets the abstract sequence from which we're going to query history;
862
+// we may already know the channel we're querying, or we may have
863
+// to look it up via a string target. This function is responsible for
864
+// privilege checking.
865
+func (server *Server) GetHistorySequence(providedChannel *Channel, client *Client, target string) (channel *Channel, sequence history.Sequence, err error) {
866
+	config := server.Config()
867
+	var sender, recipient string
868
+	var hist *history.Buffer
869
+	if target == "*" {
870
+		persistent, ephemeral, target := client.historyStatus(config)
871
+		if persistent {
872
+			recipient = target
873
+		} else if ephemeral {
874
+			hist = &client.history
875
+		} else {
876
+			return
877
+		}
878
+	} else {
879
+		channel = providedChannel
880
+		if channel == nil {
881
+			channel = server.channels.Get(target)
882
+		}
883
+		if channel != nil {
884
+			if !channel.hasClient(client) {
885
+				err = errInsufficientPrivs
886
+				return
887
+			}
888
+			persistent, ephemeral, cfTarget := channel.historyStatus(config)
889
+			if persistent {
890
+				recipient = cfTarget
891
+			} else if ephemeral {
892
+				hist = &channel.history
893
+			} else {
894
+				return
895
+			}
896
+		} else {
897
+			sender = client.NickCasefolded()
898
+			var cfTarget string
899
+			cfTarget, err = CasefoldName(target)
900
+			if err != nil {
901
+				return
902
+			}
903
+			recipient = cfTarget
904
+			if !client.AlwaysOn() {
905
+				hist = &client.history
906
+			}
907
+		}
908
+	}
909
+
910
+	var cutoff time.Time
911
+	if config.History.Restrictions.ExpireTime != 0 {
912
+		cutoff = time.Now().UTC().Add(-time.Duration(config.History.Restrictions.ExpireTime))
913
+	}
914
+	if config.History.Restrictions.EnforceRegistrationDate {
915
+		regCutoff := client.historyCutoff()
916
+		regCutoff.Add(-time.Duration(config.History.Restrictions.GracePeriod))
917
+		// take the earlier of the two cutoffs
918
+		if regCutoff.After(cutoff) {
919
+			cutoff = regCutoff
920
+		}
921
+	}
922
+	if hist != nil {
923
+		sequence = hist.MakeSequence(recipient, cutoff)
924
+	} else if recipient != "" {
925
+		sequence = server.historyDB.MakeSequence(sender, recipient, cutoff)
926
+	}
927
+	return
928
+}
929
+
838 930
 // elistMatcher takes and matches ELIST conditions
839 931
 type elistMatcher struct {
840 932
 	MinClientsActive bool

+ 19
- 1
irc/stats.go View File

@@ -26,15 +26,33 @@ func (s *Stats) Add() {
26 26
 	s.mutex.Unlock()
27 27
 }
28 28
 
29
+// Activates a registered client, e.g., for the initial attach to a persistent client
30
+func (s *Stats) AddRegistered(invisible, operator bool) {
31
+	s.mutex.Lock()
32
+	if invisible {
33
+		s.Invisible += 1
34
+	}
35
+	if operator {
36
+		s.Operators += 1
37
+	}
38
+	s.Total += 1
39
+	s.setMax()
40
+	s.mutex.Unlock()
41
+}
42
+
29 43
 // Transition a client from unregistered to registered
30 44
 func (s *Stats) Register() {
31 45
 	s.mutex.Lock()
32 46
 	s.Unknown -= 1
33 47
 	s.Total += 1
48
+	s.setMax()
49
+	s.mutex.Unlock()
50
+}
51
+
52
+func (s *Stats) setMax() {
34 53
 	if s.Max < s.Total {
35 54
 		s.Max = s.Total
36 55
 	}
37
-	s.mutex.Unlock()
38 56
 }
39 57
 
40 58
 // Modify the Invisible count

+ 21
- 2
irc/utils/args.go View File

@@ -5,7 +5,13 @@ package utils
5 5
 
6 6
 import (
7 7
 	"errors"
8
+	"fmt"
8 9
 	"strings"
10
+	"time"
11
+)
12
+
13
+const (
14
+	IRCv3TimestampFormat = "2006-01-02T15:04:05.000Z"
9 15
 )
10 16
 
11 17
 var (
@@ -45,9 +51,9 @@ func ArgsToStrings(maxLength int, arguments []string, delim string) []string {
45 51
 
46 52
 func StringToBool(str string) (result bool, err error) {
47 53
 	switch strings.ToLower(str) {
48
-	case "on", "true", "t", "yes", "y":
54
+	case "on", "true", "t", "yes", "y", "enabled":
49 55
 		result = true
50
-	case "off", "false", "f", "no", "n":
56
+	case "off", "false", "f", "no", "n", "disabled":
51 57
 		result = false
52 58
 	default:
53 59
 		err = ErrInvalidParams
@@ -63,3 +69,16 @@ func SafeErrorParam(param string) string {
63 69
 	}
64 70
 	return param
65 71
 }
72
+
73
+type IncompatibleSchemaError struct {
74
+	CurrentVersion  string
75
+	RequiredVersion string
76
+}
77
+
78
+func (err *IncompatibleSchemaError) Error() string {
79
+	return fmt.Sprintf("Database requires update. Expected schema v%s, got v%s", err.RequiredVersion, err.CurrentVersion)
80
+}
81
+
82
+func NanoToTimestamp(nanotime int64) string {
83
+	return time.Unix(0, nanotime).UTC().Format(IRCv3TimestampFormat)
84
+}

+ 0
- 8
irc/utils/net.go View File

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

+ 5
- 8
irc/utils/text.go View File

@@ -23,9 +23,9 @@ type MessagePair struct {
23 23
 // SplitMessage represents a message that's been split for sending.
24 24
 // Two possibilities:
25 25
 // (a) Standard message that can be relayed on a single 512-byte line
26
-//     (MessagePair contains the message, Wrapped == nil)
26
+//     (MessagePair contains the message, Split == nil)
27 27
 // (b) multiline message that was split on the client side
28
-//     (Message == "", Wrapped contains the split lines)
28
+//     (Message == "", Split contains the split lines)
29 29
 type SplitMessage struct {
30 30
 	Message string
31 31
 	Msgid   string
@@ -36,7 +36,7 @@ type SplitMessage struct {
36 36
 func MakeMessage(original string) (result SplitMessage) {
37 37
 	result.Message = original
38 38
 	result.Msgid = GenerateSecretToken()
39
-	result.Time = time.Now().UTC()
39
+	result.SetTime()
40 40
 
41 41
 	return
42 42
 }
@@ -52,7 +52,8 @@ func (sm *SplitMessage) Append(message string, concat bool) {
52 52
 }
53 53
 
54 54
 func (sm *SplitMessage) SetTime() {
55
-	sm.Time = time.Now().UTC()
55
+	// strip the monotonic time, it's a potential source of problems:
56
+	sm.Time = time.Now().UTC().Round(0)
56 57
 }
57 58
 
58 59
 func (sm *SplitMessage) LenLines() int {
@@ -88,10 +89,6 @@ func (sm *SplitMessage) IsRestrictedCTCPMessage() bool {
88 89
 	return false
89 90
 }
90 91
 
91
-func (sm *SplitMessage) IsMultiline() bool {
92
-	return sm.Message == "" && len(sm.Split) != 0
93
-}
94
-
95 92
 func (sm *SplitMessage) Is512() bool {
96 93
 	return sm.Message != ""
97 94
 }

+ 16
- 4
irc/znc.go View File

@@ -8,6 +8,8 @@ import (
8 8
 	"strconv"
9 9
 	"strings"
10 10
 	"time"
11
+
12
+	"github.com/oragono/oragono/irc/history"
11 13
 )
12 14
 
13 15
 type zncCommandHandler func(client *Client, command string, params []string, rb *ResponseBuffer)
@@ -43,7 +45,7 @@ func zncWireTimeToTime(str string) (result time.Time) {
43 45
 	}
44 46
 	seconds, _ := strconv.ParseInt(secondsPortion, 10, 64)
45 47
 	fraction, _ := strconv.ParseFloat(fracPortion, 64)
46
-	return time.Unix(seconds, int64(fraction*1000000000))
48
+	return time.Unix(seconds, int64(fraction*1000000000)).UTC()
47 49
 }
48 50
 
49 51
 type zncPlaybackTimes struct {
@@ -89,10 +91,8 @@ func zncPlaybackHandler(client *Client, command string, params []string, rb *Res
89 91
 	//     3.3  When the client sends a subsequent redundant JOIN line for those
90 92
 	//          channels; redundant JOIN is a complete no-op so we won't replay twice
91 93
 
92
-	config := client.server.Config()
93 94
 	if params[1] == "*" {
94
-		items, _ := client.history.Between(after, before, false, config.History.ChathistoryMax)
95
-		client.replayPrivmsgHistory(rb, items, true)
95
+		zncPlayPrivmsgs(client, rb, after, before)
96 96
 	} else {
97 97
 		targets = make(StringSet)
98 98
 		// TODO actually handle nickname targets
@@ -116,3 +116,15 @@ func zncPlaybackHandler(client *Client, command string, params []string, rb *Res
116 116
 		}
117 117
 	}
118 118
 }
119
+
120
+func zncPlayPrivmsgs(client *Client, rb *ResponseBuffer, after, before time.Time) {
121
+	_, sequence, _ := client.server.GetHistorySequence(nil, client, "*")
122
+	if sequence == nil {
123
+		return
124
+	}
125
+	zncMax := client.server.Config().History.ZNCMax
126
+	items, _, err := sequence.Between(history.Selector{Time: after}, history.Selector{Time: before}, zncMax)
127
+	if err == nil && len(items) != 0 {
128
+		client.replayPrivmsgHistory(rb, items, "", true)
129
+	}
130
+}

+ 68
- 4
oragono.yaml View File

@@ -245,6 +245,15 @@ server:
245 245
         # all users will receive simply `netname` as their cloaked hostname.
246 246
         num-bits: 80
247 247
 
248
+    # secure-nets identifies IPs and CIDRs which are secure at layer 3,
249
+    # for example, because they are on a trusted internal LAN or a VPN.
250
+    # plaintext connections from these IPs and CIDRs will be considered
251
+    # secure (clients will receive the +Z mode and be allowed to resume
252
+    # or reattach to secure connections). note that loopback IPs are always
253
+    # considered secure:
254
+    secure-nets:
255
+        # - "10.0.0.0/8"
256
+
248 257
 
249 258
 # account options
250 259
 accounts:
@@ -337,9 +346,10 @@ accounts:
337 346
         # rename-prefix - this is the prefix to use when renaming clients (e.g. Guest-AB54U31)
338 347
         rename-prefix: Guest-
339 348
 
340
-    # bouncer controls whether oragono can act as a bouncer, i.e., allowing
341
-    # multiple connections to attach to the same client/nickname identity
342
-    bouncer:
349
+    # multiclient controls whether oragono allows multiple connections to
350
+    # attach to the same client/nickname identity; this is part of the
351
+    # functionality traditionally provided by a bouncer like ZNC
352
+    multiclient:
343 353
         # when disabled, each connection must use a separate nickname (as is the
344 354
         # typical behavior of IRC servers). when enabled, a new connection that
345 355
         # has authenticated with SASL can associate itself with an existing
@@ -351,6 +361,11 @@ accounts:
351 361
         # via nickserv
352 362
         allowed-by-default: true
353 363
 
364
+        # whether to allow clients that remain on the server even
365
+        # when they have no active connections. The possible values are:
366
+        # "disabled", "opt-in", "opt-out", or "mandatory".
367
+        always-on: "disabled"
368
+
354 369
     # vhosts controls the assignment of vhosts (strings displayed in place of the user's
355 370
     # hostname/IP) by the HostServ service
356 371
     vhosts:
@@ -585,6 +600,17 @@ datastore:
585 600
     # up, and if the upgrade fails, the original database will be restored.
586 601
     autoupgrade: true
587 602
 
603
+    # connection information for MySQL (currently only used for persistent history):
604
+    mysql:
605
+        enabled: false
606
+        host: "localhost"
607
+        # port is unnecessary for connections via unix domain socket:
608
+        #port: 3306
609
+        user: "oragono"
610
+        password: "KOHw8WSaRwaoo-avo0qVpQ"
611
+        history-database: "oragono_history"
612
+        timeout: 3s
613
+
588 614
 # languages config
589 615
 languages:
590 616
     # whether to load languages
@@ -657,7 +683,7 @@ fakelag:
657 683
 # message history tracking, for the RESUME extension and possibly other uses in future
658 684
 history:
659 685
     # should we store messages for later playback?
660
-    # the current implementation stores messages in RAM only; they do not persist
686
+    # by default, messages are stored in RAM only; they do not persist
661 687
     # across server restarts. however, you should not enable this unless you understand
662 688
     # how it interacts with the GDPR and/or any data privacy laws that apply
663 689
     # in your country and the countries of your users.
@@ -683,3 +709,41 @@ history:
683 709
     # maximum number of CHATHISTORY messages that can be
684 710
     # requested at once (0 disables support for CHATHISTORY)
685 711
     chathistory-maxmessages: 100
712
+
713
+    # maximum number of messages that can be replayed at once during znc emulation
714
+    # (znc.in/playback, or automatic replay on initial reattach to a persistent client):
715
+    znc-maxmessages: 2048
716
+
717
+    # options to delete old messages, or prevent them from being retrieved
718
+    restrictions:
719
+        # if this is set, messages older than this cannot be retrieved by anyone
720
+        # (and will eventually be deleted from persistent storage, if that's enabled)
721
+        #expire-time: 1w
722
+
723
+        # if this is set, logged-in users cannot retrieve messages older than their
724
+        # account registration date, and logged-out users cannot retrieve messages
725
+        # older than their sign-on time (modulo grace-period, see below):
726
+        enforce-registration-date: false
727
+
728
+        # but if this is set, you can retrieve messages that are up to `grace-period`
729
+        # older than the above cutoff time. this is recommended to allow logged-out
730
+        # users to do session resumption / query history after disconnections.
731
+        grace-period: 1h
732
+
733
+    # options to store history messages in a persistent database (currently only MySQL):
734
+    persistent:
735
+        enabled: false
736
+
737
+        # store unregistered channel messages in the persistent database?
738
+        unregistered-channels: false
739
+
740
+        # for a registered channel, the channel owner can potentially customize
741
+        # the history storage setting. as the server operator, your options are
742
+        # 'disabled' (no persistent storage, regardless of per-channel setting),
743
+        # 'opt-in', 'opt-out', and 'mandatory' (force persistent storage, ignoring
744
+        # per-channel setting):
745
+        registered-channels: "opt-out"
746
+
747
+        # direct messages are only stored in the database for persistent clients;
748
+        # you can control how they are stored here (same options as above)
749
+        direct-messages: "opt-out"

+ 9
- 0
vendor/github.com/go-sql-driver/mysql/.gitignore View File

@@ -0,0 +1,9 @@
1
+.DS_Store
2
+.DS_Store?
3
+._*
4
+.Spotlight-V100
5
+.Trashes
6
+Icon?
7
+ehthumbs.db
8
+Thumbs.db
9
+.idea

+ 129
- 0
vendor/github.com/go-sql-driver/mysql/.travis.yml View File

@@ -0,0 +1,129 @@
1
+sudo: false
2
+language: go
3
+go:
4
+  - 1.10.x
5
+  - 1.11.x
6
+  - 1.12.x
7
+  - 1.13.x
8
+  - master
9
+
10
+before_install:
11
+  - go get golang.org/x/tools/cmd/cover
12
+  - go get github.com/mattn/goveralls
13
+
14
+before_script:
15
+  - echo -e "[server]\ninnodb_log_file_size=256MB\ninnodb_buffer_pool_size=512MB\nmax_allowed_packet=16MB" | sudo tee -a /etc/mysql/my.cnf
16
+  - sudo service mysql restart
17
+  - .travis/wait_mysql.sh
18
+  - mysql -e 'create database gotest;'
19
+
20
+matrix:
21
+  include:
22
+    - env: DB=MYSQL8
23
+      sudo: required
24
+      dist: trusty
25
+      go: 1.10.x
26
+      services:
27
+        - docker
28
+      before_install:
29
+        - go get golang.org/x/tools/cmd/cover
30
+        - go get github.com/mattn/goveralls
31
+        - docker pull mysql:8.0
32
+        - 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
33
+          mysql:8.0 --innodb_log_file_size=256MB --innodb_buffer_pool_size=512MB --max_allowed_packet=16MB --local-infile=1
34
+        - cp .travis/docker.cnf ~/.my.cnf
35
+        - .travis/wait_mysql.sh
36
+      before_script:
37
+        - export MYSQL_TEST_USER=gotest
38
+        - export MYSQL_TEST_PASS=secret
39
+        - export MYSQL_TEST_ADDR=127.0.0.1:3307
40
+        - export MYSQL_TEST_CONCURRENT=1
41
+
42
+    - env: DB=MYSQL57
43
+      sudo: required
44
+      dist: trusty
45
+      go: 1.10.x
46
+      services:
47
+        - docker
48
+      before_install:
49
+        - go get golang.org/x/tools/cmd/cover
50
+        - go get github.com/mattn/goveralls
51
+        - docker pull mysql:5.7
52
+        - 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
53
+          mysql:5.7 --innodb_log_file_size=256MB --innodb_buffer_pool_size=512MB --max_allowed_packet=16MB --local-infile=1
54
+        - cp .travis/docker.cnf ~/.my.cnf
55
+        - .travis/wait_mysql.sh
56
+      before_script:
57
+        - export MYSQL_TEST_USER=gotest
58
+        - export MYSQL_TEST_PASS=secret
59
+        - export MYSQL_TEST_ADDR=127.0.0.1:3307
60
+        - export MYSQL_TEST_CONCURRENT=1
61
+
62
+    - env: DB=MARIA55
63
+      sudo: required
64
+      dist: trusty
65
+      go: 1.10.x
66
+      services:
67
+        - docker
68
+      before_install:
69
+        - go get golang.org/x/tools/cmd/cover
70
+        - go get github.com/mattn/goveralls
71
+        - docker pull mariadb:5.5
72
+        - 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
73
+          mariadb:5.5 --innodb_log_file_size=256MB --innodb_buffer_pool_size=512MB --max_allowed_packet=16MB --local-infile=1
74
+        - cp .travis/docker.cnf ~/.my.cnf
75
+        - .travis/wait_mysql.sh
76
+      before_script:
77
+        - export MYSQL_TEST_USER=gotest
78
+        - export MYSQL_TEST_PASS=secret
79
+        - export MYSQL_TEST_ADDR=127.0.0.1:3307
80
+        - export MYSQL_TEST_CONCURRENT=1
81
+
82
+    - env: DB=MARIA10_1
83
+      sudo: required
84
+      dist: trusty
85
+      go: 1.10.x
86
+      services:
87
+        - docker
88
+      before_install:
89
+        - go get golang.org/x/tools/cmd/cover
90
+        - go get github.com/mattn/goveralls
91
+        - docker pull mariadb:10.1
92
+        - 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
93
+          mariadb:10.1 --innodb_log_file_size=256MB --innodb_buffer_pool_size=512MB --max_allowed_packet=16MB --local-infile=1
94
+        - cp .travis/docker.cnf ~/.my.cnf
95
+        - .travis/wait_mysql.sh
96
+      before_script:
97
+        - export MYSQL_TEST_USER=gotest
98
+        - export MYSQL_TEST_PASS=secret
99
+        - export MYSQL_TEST_ADDR=127.0.0.1:3307
100
+        - export MYSQL_TEST_CONCURRENT=1
101
+
102
+    - os: osx
103
+      osx_image: xcode10.1
104
+      addons:
105
+        homebrew:
106
+          packages:
107
+            - mysql
108
+          update: true
109
+      go: 1.12.x
110
+      before_install:
111
+        - go get golang.org/x/tools/cmd/cover
112
+        - go get github.com/mattn/goveralls
113
+      before_script:
114
+        - 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
115
+        - mysql.server start
116
+        - mysql -uroot -e 'CREATE USER gotest IDENTIFIED BY "secret"'
117
+        - mysql -uroot -e 'GRANT ALL ON *.* TO gotest'
118
+        - mysql -uroot -e 'create database gotest;'
119
+        - export MYSQL_TEST_USER=gotest
120
+        - export MYSQL_TEST_PASS=secret
121
+        - export MYSQL_TEST_ADDR=127.0.0.1:3306
122
+        - export MYSQL_TEST_CONCURRENT=1
123
+
124
+script:
125
+  - go test -v -covermode=count -coverprofile=coverage.out
126
+  - go vet ./...
127
+  - .travis/gofmt.sh
128
+after_script:
129
+  - $HOME/gopath/bin/goveralls -coverprofile=coverage.out -service=travis-ci

+ 105
- 0
vendor/github.com/go-sql-driver/mysql/AUTHORS View File

@@ -0,0 +1,105 @@
1
+# This is the official list of Go-MySQL-Driver authors for copyright purposes.
2
+
3
+# If you are submitting a patch, please add your name or the name of the
4
+# organization which holds the copyright to this list in alphabetical order.
5
+
6
+# Names should be added to this file as
7
+#	Name <email address>
8
+# The email address is not required for organizations.
9
+# Please keep the list sorted.
10
+
11
+
12
+# Individual Persons
13
+
14
+Aaron Hopkins <go-sql-driver at die.net>
15
+Achille Roussel <achille.roussel at gmail.com>
16
+Alexey Palazhchenko <alexey.palazhchenko at gmail.com>
17
+Andrew Reid <andrew.reid at tixtrack.com>
18
+Arne Hormann <arnehormann at gmail.com>
19
+Asta Xie <xiemengjun at gmail.com>
20
+Bulat Gaifullin <gaifullinbf at gmail.com>
21
+Carlos Nieto <jose.carlos at menteslibres.net>
22
+Chris Moos <chris at tech9computers.com>
23
+Craig Wilson <craiggwilson at gmail.com>
24
+Daniel Montoya <dsmontoyam at gmail.com>
25
+Daniel Nichter <nil at codenode.com>
26
+Daniël van Eeden <git at myname.nl>
27
+Dave Protasowski <dprotaso at gmail.com>
28
+DisposaBoy <disposaboy at dby.me>
29
+Egor Smolyakov <egorsmkv at gmail.com>
30
+Erwan Martin <hello at erwan.io>
31
+Evan Shaw <evan at vendhq.com>
32
+Frederick Mayle <frederickmayle at gmail.com>
33
+Gustavo Kristic <gkristic at gmail.com>
34
+Hajime Nakagami <nakagami at gmail.com>
35
+Hanno Braun <mail at hannobraun.com>
36
+Henri Yandell <flamefew at gmail.com>
37
+Hirotaka Yamamoto <ymmt2005 at gmail.com>
38
+Huyiguang <hyg at webterren.com>
39
+ICHINOSE Shogo <shogo82148 at gmail.com>
40
+Ilia Cimpoes <ichimpoesh at gmail.com>
41
+INADA Naoki <songofacandy at gmail.com>
42
+Jacek Szwec <szwec.jacek at gmail.com>
43
+James Harr <james.harr at gmail.com>
44
+Jeff Hodges <jeff at somethingsimilar.com>
45
+Jeffrey Charles <jeffreycharles at gmail.com>
46
+Jerome Meyer <jxmeyer at gmail.com>
47
+Jiajia Zhong <zhong2plus at gmail.com>
48
+Jian Zhen <zhenjl at gmail.com>
49
+Joshua Prunier <joshua.prunier at gmail.com>
50
+Julien Lefevre <julien.lefevr at gmail.com>
51
+Julien Schmidt <go-sql-driver at julienschmidt.com>
52
+Justin Li <jli at j-li.net>
53
+Justin Nuß <nuss.justin at gmail.com>
54
+Kamil Dziedzic <kamil at klecza.pl>
55
+Kevin Malachowski <kevin at chowski.com>
56
+Kieron Woodhouse <kieron.woodhouse at infosum.com>
57
+Lennart Rudolph <lrudolph at hmc.edu>
58
+Leonardo YongUk Kim <dalinaum at gmail.com>
59
+Linh Tran Tuan <linhduonggnu at gmail.com>
60
+Lion Yang <lion at aosc.xyz>
61
+Luca Looz <luca.looz92 at gmail.com>
62
+Lucas Liu <extrafliu at gmail.com>
63
+Luke Scott <luke at webconnex.com>
64
+Maciej Zimnoch <maciej.zimnoch at codilime.com>
65
+Michael Woolnough <michael.woolnough at gmail.com>
66
+Nathanial Murphy <nathanial.murphy at gmail.com>
67
+Nicola Peduzzi <thenikso at gmail.com>
68
+Olivier Mengué <dolmen at cpan.org>
69
+oscarzhao <oscarzhaosl at gmail.com>
70
+Paul Bonser <misterpib at gmail.com>
71
+Peter Schultz <peter.schultz at classmarkets.com>
72
+Rebecca Chin <rchin at pivotal.io>
73
+Reed Allman <rdallman10 at gmail.com>
74
+Richard Wilkes <wilkes at me.com>
75
+Robert Russell <robert at rrbrussell.com>
76
+Runrioter Wung <runrioter at gmail.com>
77
+Shuode Li <elemount at qq.com>
78
+Simon J Mudd <sjmudd at pobox.com>
79
+Soroush Pour <me at soroushjp.com>
80
+Stan Putrya <root.vagner at gmail.com>
81
+Stanley Gunawan <gunawan.stanley at gmail.com>
82
+Steven Hartland <steven.hartland at multiplay.co.uk>
83
+Thomas Wodarek <wodarekwebpage at gmail.com>
84
+Tim Ruffles <timruffles at gmail.com>
85
+Tom Jenkinson <tom at tjenkinson.me>
86
+Vladimir Kovpak <cn007b at gmail.com>
87
+Xiangyu Hu <xiangyu.hu at outlook.com>
88
+Xiaobing Jiang <s7v7nislands at gmail.com>
89
+Xiuming Chen <cc at cxm.cc>
90
+Zhenye Xie <xiezhenye at gmail.com>
91
+
92
+# Organizations
93
+
94
+Barracuda Networks, Inc.
95
+Counting Ltd.
96
+DigitalOcean Inc.
97
+Facebook Inc.
98
+GitHub Inc.
99
+Google Inc.
100
+InfoSum Ltd.
101
+Keybase Inc.
102
+Multiplay Ltd.
103
+Percona LLC
104
+Pivotal Inc.
105
+Stripe Inc.

+ 206
- 0
vendor/github.com/go-sql-driver/mysql/CHANGELOG.md View File

@@ -0,0 +1,206 @@
1
+## Version 1.5 (2020-01-07)
2
+
3
+Changes:
4
+
5
+  - Dropped support Go 1.9 and lower (#823, #829, #886, #1016, #1017)
6
+  - Improve buffer handling (#890)
7
+  - Document potentially insecure TLS configs (#901)
8
+  - Use a double-buffering scheme to prevent data races (#943)
9
+  - Pass uint64 values without converting them to string (#838, #955)
10
+  - Update collations and make utf8mb4 default (#877, #1054)
11
+  - Make NullTime compatible with sql.NullTime in Go 1.13+ (#995)
12
+  - Removed CloudSQL support (#993, #1007)
13
+  - Add Go Module support (#1003)
14
+
15
+New Features:
16
+
17
+  - Implement support of optional TLS (#900)
18
+  - Check connection liveness (#934, #964, #997, #1048, #1051, #1052)
19
+  - Implement Connector Interface (#941, #958, #1020, #1035)
20
+
21
+Bugfixes:
22
+
23
+  - Mark connections as bad on error during ping (#875)
24
+  - Mark connections as bad on error during dial (#867)
25
+  - Fix connection leak caused by rapid context cancellation (#1024)
26
+  - Mark connections as bad on error during Conn.Prepare (#1030)
27
+
28
+
29
+## Version 1.4.1 (2018-11-14)
30
+
31
+Bugfixes:
32
+
33
+ - Fix TIME format for binary columns (#818)
34
+ - Fix handling of empty auth plugin names (#835)
35
+ - Fix caching_sha2_password with empty password (#826)
36
+ - Fix canceled context broke mysqlConn (#862)
37
+ - Fix OldAuthSwitchRequest support (#870)
38
+ - Fix Auth Response packet for cleartext password (#887)
39
+
40
+## Version 1.4 (2018-06-03)
41
+
42
+Changes:
43
+
44
+ - Documentation fixes (#530, #535, #567)
45
+ - Refactoring (#575, #579, #580, #581, #603, #615, #704)
46
+ - Cache column names (#444)
47
+ - Sort the DSN parameters in DSNs generated from a config (#637)
48
+ - Allow native password authentication by default (#644)
49
+ - Use the default port if it is missing in the DSN (#668)
50
+ - Removed the `strict` mode (#676)
51
+ - Do not query `max_allowed_packet` by default (#680)
52
+ - Dropped support Go 1.6 and lower (#696)
53
+ - Updated `ConvertValue()` to match the database/sql/driver implementation (#760)
54
+ - Document the usage of `0000-00-00T00:00:00` as the time.Time zero value (#783)
55
+ - Improved the compatibility of the authentication system (#807)
56
+
57
+New Features:
58
+
59
+ - Multi-Results support (#537)
60
+ - `rejectReadOnly` DSN option (#604)
61
+ - `context.Context` support (#608, #612, #627, #761)
62
+ - Transaction isolation level support (#619, #744)
63
+ - Read-Only transactions support (#618, #634)
64
+ - `NewConfig` function which initializes a config with default values (#679)
65
+ - Implemented the `ColumnType` interfaces (#667, #724)
66
+ - Support for custom string types in `ConvertValue` (#623)
67
+ - Implemented `NamedValueChecker`, improving support for uint64 with high bit set (#690, #709, #710)
68
+ - `caching_sha2_password` authentication plugin support (#794, #800, #801, #802)
69
+ - Implemented `driver.SessionResetter` (#779)
70
+ - `sha256_password` authentication plugin support (#808)
71
+
72
+Bugfixes:
73
+
74
+ - Use the DSN hostname as TLS default ServerName if `tls=true` (#564, #718)
75
+ - Fixed LOAD LOCAL DATA INFILE for empty files (#590)
76
+ - Removed columns definition cache since it sometimes cached invalid data (#592)
77
+ - Don't mutate registered TLS configs (#600)
78
+ - Make RegisterTLSConfig concurrency-safe (#613)
79
+ - Handle missing auth data in the handshake packet correctly (#646)
80
+ - Do not retry queries when data was written to avoid data corruption (#302, #736)
81
+ - Cache the connection pointer for error handling before invalidating it (#678)
82
+ - Fixed imports for appengine/cloudsql (#700)
83
+ - Fix sending STMT_LONG_DATA for 0 byte data (#734)
84
+ - Set correct capacity for []bytes read from length-encoded strings (#766)
85
+ - Make RegisterDial concurrency-safe (#773)
86
+
87
+
88
+## Version 1.3 (2016-12-01)
89
+
90
+Changes:
91
+
92
+ - Go 1.1 is no longer supported
93
+ - Use decimals fields in MySQL to format time types (#249)
94
+ - Buffer optimizations (#269)
95
+ - TLS ServerName defaults to the host (#283)
96
+ - Refactoring (#400, #410, #437)
97
+ - Adjusted documentation for second generation CloudSQL (#485)
98
+ - Documented DSN system var quoting rules (#502)
99
+ - Made statement.Close() calls idempotent to avoid errors in Go 1.6+ (#512)
100
+
101
+New Features:
102
+
103
+ - Enable microsecond resolution on TIME, DATETIME and TIMESTAMP (#249)
104
+ - Support for returning table alias on Columns() (#289, #359, #382)
105
+ - Placeholder interpolation, can be actived with the DSN parameter `interpolateParams=true` (#309, #318, #490)
106
+ - Support for uint64 parameters with high bit set (#332, #345)
107
+ - Cleartext authentication plugin support (#327)
108
+ - Exported ParseDSN function and the Config struct (#403, #419, #429)
109
+ - Read / Write timeouts (#401)
110
+ - Support for JSON field type (#414)
111
+ - Support for multi-statements and multi-results (#411, #431)
112
+ - DSN parameter to set the driver-side max_allowed_packet value manually (#489)
113
+ - Native password authentication plugin support (#494, #524)
114
+
115
+Bugfixes:
116
+
117
+ - Fixed handling of queries without columns and rows (#255)
118
+ - Fixed a panic when SetKeepAlive() failed (#298)
119
+ - Handle ERR packets while reading rows (#321)
120
+ - Fixed reading NULL length-encoded integers in MySQL 5.6+ (#349)
121
+ - Fixed absolute paths support in LOAD LOCAL DATA INFILE (#356)
122
+ - Actually zero out bytes in handshake response (#378)
123
+ - Fixed race condition in registering LOAD DATA INFILE handler (#383)
124
+ - Fixed tests with MySQL 5.7.9+ (#380)
125
+ - QueryUnescape TLS config names (#397)
126
+ - Fixed "broken pipe" error by writing to closed socket (#390)
127
+ - Fixed LOAD LOCAL DATA INFILE buffering (#424)
128
+ - Fixed parsing of floats into float64 when placeholders are used (#434)
129
+ - Fixed DSN tests with Go 1.7+ (#459)
130
+ - Handle ERR packets while waiting for EOF (#473)
131
+ - Invalidate connection on error while discarding additional results (#513)
132
+ - Allow terminating packets of length 0 (#516)
133
+
134
+
135
+## Version 1.2 (2014-06-03)
136
+
137
+Changes:
138
+
139
+ - We switched back to a "rolling release". `go get` installs the current master branch again
140
+ - Version v1 of the driver will not be maintained anymore. Go 1.0 is no longer supported by this driver
141
+ - Exported errors to allow easy checking from application code
142
+ - Enabled TCP Keepalives on TCP connections
143
+ - Optimized INFILE handling (better buffer size calculation, lazy init, ...)
144
+ - The DSN parser also checks for a missing separating slash
145
+ - Faster binary date / datetime to string formatting
146
+ - Also exported the MySQLWarning type
147
+ - mysqlConn.Close returns the first error encountered instead of ignoring all errors
148
+ - writePacket() automatically writes the packet size to the header
149
+ - readPacket() uses an iterative approach instead of the recursive approach to merge splitted packets
150
+
151
+New Features:
152
+
153
+ - `RegisterDial` allows the usage of a custom dial function to establish the network connection
154
+ - Setting the connection collation is possible with the `collation` DSN parameter. This parameter should be preferred over the `charset` parameter
155
+ - Logging of critical errors is configurable with `SetLogger`
156
+ - Google CloudSQL support
157
+
158
+Bugfixes:
159
+
160
+ - Allow more than 32 parameters in prepared statements
161
+ - Various old_password fixes
162
+ - Fixed TestConcurrent test to pass Go's race detection
163
+ - Fixed appendLengthEncodedInteger for large numbers
164
+ - Renamed readLengthEnodedString to readLengthEncodedString and skipLengthEnodedString to skipLengthEncodedString (fixed typo)
165
+
166
+
167
+## Version 1.1 (2013-11-02)
168
+
169
+Changes:
170
+
171
+  - Go-MySQL-Driver now requires Go 1.1
172
+  - Connections now use the collation `utf8_general_ci` by default. Adding `&charset=UTF8` to the DSN should not be necessary anymore
173
+  - Made closing rows and connections error tolerant. This allows for example deferring rows.Close() without checking for errors
174
+  - `[]byte(nil)` is now treated as a NULL value. Before, it was treated like an empty string / `[]byte("")`
175
+  - DSN parameter values must now be url.QueryEscape'ed. This allows text values to contain special characters, such as '&'.
176
+  - Use the IO buffer also for writing. This results in zero allocations (by the driver) for most queries
177
+  - Optimized the buffer for reading
178
+  - stmt.Query now caches column metadata
179
+  - New Logo
180
+  - Changed the copyright header to include all contributors
181
+  - Improved the LOAD INFILE documentation
182
+  - The driver struct is now exported to make the driver directly accessible
183
+  - Refactored the driver tests
184
+  - Added more benchmarks and moved all to a separate file
185
+  - Other small refactoring
186
+
187
+New Features:
188
+
189
+  - Added *old_passwords* support: Required in some cases, but must be enabled by adding `allowOldPasswords=true` to the DSN since it is insecure
190
+  - Added a `clientFoundRows` parameter: Return the number of matching rows instead of the number of rows changed on UPDATEs
191
+  - Added TLS/SSL support: Use a TLS/SSL encrypted connection to the server. Custom TLS configs can be registered and used
192
+
193
+Bugfixes:
194
+
195
+  - Fixed MySQL 4.1 support: MySQL 4.1 sends packets with lengths which differ from the specification
196
+  - Convert to DB timezone when inserting `time.Time`
197
+  - Splitted packets (more than 16MB) are now merged correctly
198
+  - Fixed false positive `io.EOF` errors when the data was fully read
199
+  - Avoid panics on reuse of closed connections
200
+  - Fixed empty string producing false nil values
201
+  - Fixed sign byte for positive TIME fields
202
+
203
+
204
+## Version 1.0 (2013-05-14)
205
+
206
+Initial Release

+ 373
- 0
vendor/github.com/go-sql-driver/mysql/LICENSE View File

@@ -0,0 +1,373 @@
1
+Mozilla Public License Version 2.0
2
+==================================
3
+
4
+1. Definitions
5
+--------------
6
+
7
+1.1. "Contributor"
8
+    means each individual or legal entity that creates, contributes to
9
+    the creation of, or owns Covered Software.
10
+
11
+1.2. "Contributor Version"
12
+    means the combination of the Contributions of others (if any) used
13
+    by a Contributor and that particular Contributor's Contribution.
14
+
15
+1.3. "Contribution"
16
+    means Covered Software of a particular Contributor.
17
+
18
+1.4. "Covered Software"
19
+    means Source Code Form to which the initial Contributor has attached
20
+    the notice in Exhibit A, the Executable Form of such Source Code
21
+    Form, and Modifications of such Source Code Form, in each case
22
+    including portions thereof.
23
+
24
+1.5. "Incompatible With Secondary Licenses"
25
+    means
26
+
27
+    (a) that the initial Contributor has attached the notice described
28
+        in Exhibit B to the Covered Software; or
29
+
30
+    (b) that the Covered Software was made available under the terms of
31
+        version 1.1 or earlier of the License, but not also under the
32
+        terms of a Secondary License.
33
+
34
+1.6. "Executable Form"
35
+    means any form of the work other than Source Code Form.
36
+
37
+1.7. "Larger Work"
38
+    means a work that combines Covered Software with other material, in 
39
+    a separate file or files, that is not Covered Software.
40
+
41
+1.8. "License"
42
+    means this document.
43
+
44
+1.9. "Licensable"
45
+    means having the right to grant, to the maximum extent possible,
46
+    whether at the time of the initial grant or subsequently, any and
47
+    all of the rights conveyed by this License.
48
+
49
+1.10. "Modifications"
50
+    means any of the following:
51
+
52
+    (a) any file in Source Code Form that results from an addition to,
53
+        deletion from, or modification of the contents of Covered
54
+        Software; or
55
+
56
+    (b) any new file in Source Code Form that contains any Covered
57
+        Software.
58
+
59
+1.11. "Patent Claims" of a Contributor
60
+    means any patent claim(s), including without limitation, method,
61
+    process, and apparatus claims, in any patent Licensable by such
62
+    Contributor that would be infringed, but for the grant of the
63
+    License, by the making, using, selling, offering for sale, having
64
+    made, import, or transfer of either its Contributions or its
65
+    Contributor Version.
66
+
67
+1.12. "Secondary License"
68
+    means either the GNU General Public License, Version 2.0, the GNU
69
+    Lesser General Public License, Version 2.1, the GNU Affero General
70
+    Public License, Version 3.0, or any later versions of those
71
+    licenses.
72
+
73
+1.13. "Source Code Form"
74
+    means the form of the work preferred for making modifications.
75
+
76
+1.14. "You" (or "Your")
77
+    means an individual or a legal entity exercising rights under this
78
+    License. For legal entities, "You" includes any entity that
79
+    controls, is controlled by, or is under common control with You. For
80
+    purposes of this definition, "control" means (a) the power, direct
81
+    or indirect, to cause the direction or management of such entity,
82
+    whether by contract or otherwise, or (b) ownership of more than
83
+    fifty percent (50%) of the outstanding shares or beneficial
84
+    ownership of such entity.
85
+
86
+2. License Grants and Conditions
87
+--------------------------------
88
+
89
+2.1. Grants
90
+
91
+Each Contributor hereby grants You a world-wide, royalty-free,
92
+non-exclusive license:
93
+
94
+(a) under intellectual property rights (other than patent or trademark)
95
+    Licensable by such Contributor to use, reproduce, make available,
96
+    modify, display, perform, distribute, and otherwise exploit its
97
+    Contributions, either on an unmodified basis, with Modifications, or
98
+    as part of a Larger Work; and
99
+
100
+(b) under Patent Claims of such Contributor to make, use, sell, offer
101
+    for sale, have made, import, and otherwise transfer either its
102
+    Contributions or its Contributor Version.
103
+
104
+2.2. Effective Date
105
+
106
+The licenses granted in Section 2.1 with respect to any Contribution
107
+become effective for each Contribution on the date the Contributor first
108
+distributes such Contribution.
109
+
110
+2.3. Limitations on Grant Scope
111
+
112
+The licenses granted in this Section 2 are the only rights granted under
113
+this License. No additional rights or licenses will be implied from the
114
+distribution or licensing of Covered Software under this License.
115
+Notwithstanding Section 2.1(b) above, no patent license is granted by a
116
+Contributor:
117
+
118
+(a) for any code that a Contributor has removed from Covered Software;
119
+    or
120
+
121
+(b) for infringements caused by: (i) Your and any other third party's
122
+    modifications of Covered Software, or (ii) the combination of its
123
+    Contributions with other software (except as part of its Contributor
124
+    Version); or
125
+
126
+(c) under Patent Claims infringed by Covered Software in the absence of
127
+    its Contributions.
128
+
129
+This License does not grant any rights in the trademarks, service marks,
130
+or logos of any Contributor (except as may be necessary to comply with
131
+the notice requirements in Section 3.4).
132
+
133
+2.4. Subsequent Licenses
134
+
135
+No Contributor makes additional grants as a result of Your choice to
136
+distribute the Covered Software under a subsequent version of this
137
+License (see Section 10.2) or under the terms of a Secondary License (if
138
+permitted under the terms of Section 3.3).
139
+
140
+2.5. Representation
141
+
142
+Each Contributor represents that the Contributor believes its
143
+Contributions are its original creation(s) or it has sufficient rights
144
+to grant the rights to its Contributions conveyed by this License.
145
+
146
+2.6. Fair Use
147
+
148
+This License is not intended to limit any rights You have under
149
+applicable copyright doctrines of fair use, fair dealing, or other
150
+equivalents.
151
+
152
+2.7. Conditions
153
+
154
+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
155
+in Section 2.1.
156
+
157
+3. Responsibilities
158
+-------------------
159
+
160
+3.1. Distribution of Source Form
161
+
162
+All distribution of Covered Software in Source Code Form, including any
163
+Modifications that You create or to which You contribute, must be under
164
+the terms of this License. You must inform recipients that the Source
165
+Code Form of the Covered Software is governed by the terms of this
166
+License, and how they can obtain a copy of this License. You may not
167
+attempt to alter or restrict the recipients' rights in the Source Code
168
+Form.
169
+
170
+3.2. Distribution of Executable Form
171
+
172
+If You distribute Covered Software in Executable Form then:
173
+
174
+(a) such Covered Software must also be made available in Source Code
175
+    Form, as described in Section 3.1, and You must inform recipients of
176
+    the Executable Form how they can obtain a copy of such Source Code
177
+    Form by reasonable means in a timely manner, at a charge no more
178
+    than the cost of distribution to the recipient; and
179
+
180
+(b) You may distribute such Executable Form under the terms of this
181
+    License, or sublicense it under different terms, provided that the
182
+    license for the Executable Form does not attempt to limit or alter
183
+    the recipients' rights in the Source Code Form under this License.
184
+
185
+3.3. Distribution of a Larger Work
186
+
187
+You may create and distribute a Larger Work under terms of Your choice,
188
+provided that You also comply with the requirements of this License for
189
+the Covered Software. If the Larger Work is a combination of Covered
190
+Software with a work governed by one or more Secondary Licenses, and the
191
+Covered Software is not Incompatible With Secondary Licenses, this
192
+License permits You to additionally distribute such Covered Software
193
+under the terms of such Secondary License(s), so that the recipient of
194
+the Larger Work may, at their option, further distribute the Covered
195
+Software under the terms of either this License or such Secondary
196
+License(s).
197
+
198
+3.4. Notices
199
+
200
+You may not remove or alter the substance of any license notices
201
+(including copyright notices, patent notices, disclaimers of warranty,
202
+or limitations of liability) contained within the Source Code Form of
203
+the Covered Software, except that You may alter any license notices to
204
+the extent required to remedy known factual inaccuracies.
205
+
206
+3.5. Application of Additional Terms
207
+
208
+You may choose to offer, and to charge a fee for, warranty, support,
209
+indemnity or liability obligations to one or more recipients of Covered
210
+Software. However, You may do so only on Your own behalf, and not on
211
+behalf of any Contributor. You must make it absolutely clear that any
212
+such warranty, support, indemnity, or liability obligation is offered by
213
+You alone, and You hereby agree to indemnify every Contributor for any
214
+liability incurred by such Contributor as a result of warranty, support,
215
+indemnity or liability terms You offer. You may include additional
216
+disclaimers of warranty and limitations of liability specific to any
217
+jurisdiction.
218
+
219
+4. Inability to Comply Due to Statute or Regulation
220
+---------------------------------------------------
221
+
222
+If it is impossible for You to comply with any of the terms of this
223
+License with respect to some or all of the Covered Software due to
224
+statute, judicial order, or regulation then You must: (a) comply with
225
+the terms of this License to the maximum extent possible; and (b)
226
+describe the limitations and the code they affect. Such description must
227
+be placed in a text file included with all distributions of the Covered
228
+Software under this License. Except to the extent prohibited by statute
229
+or regulation, such description must be sufficiently detailed for a
230
+recipient of ordinary skill to be able to understand it.
231
+
232
+5. Termination
233
+--------------
234
+
235
+5.1. The rights granted under this License will terminate automatically
236
+if You fail to comply with any of its terms. However, if You become
237
+compliant, then the rights granted under this License from a particular
238
+Contributor are reinstated (a) provisionally, unless and until such
239
+Contributor explicitly and finally terminates Your grants, and (b) on an
240
+ongoing basis, if such Contributor fails to notify You of the
241
+non-compliance by some reasonable means prior to 60 days after You have
242
+come back into compliance. Moreover, Your grants from a particular
243
+Contributor are reinstated on an ongoing basis if such Contributor
244
+notifies You of the non-compliance by some reasonable means, this is the
245
+first time You have received notice of non-compliance with this License
246
+from such Contributor, and You become compliant prior to 30 days after
247
+Your receipt of the notice.
248
+
249
+5.2. If You initiate litigation against any entity by asserting a patent
250
+infringement claim (excluding declaratory judgment actions,
251
+counter-claims, and cross-claims) alleging that a Contributor Version
252
+directly or indirectly infringes any patent, then the rights granted to
253
+You by any and all Contributors for the Covered Software under Section
254
+2.1 of this License shall terminate.
255
+
256
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all
257
+end user license agreements (excluding distributors and resellers) which
258
+have been validly granted by You or Your distributors under this License
259
+prior to termination shall survive termination.
260
+
261
+************************************************************************
262
+*                                                                      *
263
+*  6. Disclaimer of Warranty                                           *
264
+*  -------------------------                                           *
265
+*                                                                      *
266
+*  Covered Software is provided under this License on an "as is"       *
267
+*  basis, without warranty of any kind, either expressed, implied, or  *
268
+*  statutory, including, without limitation, warranties that the       *
269
+*  Covered Software is free of defects, merchantable, fit for a        *
270
+*  particular purpose or non-infringing. The entire risk as to the     *
271
+*  quality and performance of the Covered Software is with You.        *
272
+*  Should any Covered Software prove defective in any respect, You     *
273
+*  (not any Contributor) assume the cost of any necessary servicing,   *
274
+*  repair, or correction. This disclaimer of warranty constitutes an   *
275
+*  essential part of this License. No use of any Covered Software is   *
276
+*  authorized under this License except under this disclaimer.         *
277
+*                                                                      *
278
+************************************************************************
279
+
280
+************************************************************************
281
+*                                                                      *
282
+*  7. Limitation of Liability                                          *
283
+*  --------------------------                                          *
284
+*                                                                      *
285
+*  Under no circumstances and under no legal theory, whether tort      *
286
+*  (including negligence), contract, or otherwise, shall any           *
287
+*  Contributor, or anyone who distributes Covered Software as          *
288
+*  permitted above, be liable to You for any direct, indirect,         *
289
+*  special, incidental, or consequential damages of any character      *
290
+*  including, without limitation, damages for lost profits, loss of    *
291
+*  goodwill, work stoppage, computer failure or malfunction, or any    *
292
+*  and all other commercial damages or losses, even if such party      *
293
+*  shall have been informed of the possibility of such damages. This   *
294
+*  limitation of liability shall not apply to liability for death or   *
295
+*  personal injury resulting from such party's negligence to the       *
296
+*  extent applicable law prohibits such limitation. Some               *
297
+*  jurisdictions do not allow the exclusion or limitation of           *
298
+*  incidental or consequential damages, so this exclusion and          *
299
+*  limitation may not apply to You.                                    *
300
+*                                                                      *
301
+************************************************************************
302
+
303
+8. Litigation
304
+-------------
305
+
306
+Any litigation relating to this License may be brought only in the
307
+courts of a jurisdiction where the defendant maintains its principal
308
+place of business and such litigation shall be governed by laws of that
309
+jurisdiction, without reference to its conflict-of-law provisions.
310
+Nothing in this Section shall prevent a party's ability to bring
311
+cross-claims or counter-claims.
312
+
313
+9. Miscellaneous
314
+----------------
315
+
316
+This License represents the complete agreement concerning the subject
317
+matter hereof. If any provision of this License is held to be
318
+unenforceable, such provision shall be reformed only to the extent
319
+necessary to make it enforceable. Any law or regulation which provides
320
+that the language of a contract shall be construed against the drafter
321
+shall not be used to construe this License against a Contributor.
322
+
323
+10. Versions of the License
324
+---------------------------
325
+
326
+10.1. New Versions
327
+
328
+Mozilla Foundation is the license steward. Except as provided in Section
329
+10.3, no one other than the license steward has the right to modify or
330
+publish new versions of this License. Each version will be given a
331
+distinguishing version number.
332
+
333
+10.2. Effect of New Versions
334
+
335
+You may distribute the Covered Software under the terms of the version
336
+of the License under which You originally received the Covered Software,
337
+or under the terms of any subsequent version published by the license
338
+steward.
339
+
340
+10.3. Modified Versions
341
+
342
+If you create software not governed by this License, and you want to
343
+create a new license for such software, you may create and use a
344
+modified version of this License if you rename the license and remove
345
+any references to the name of the license steward (except to note that
346
+such modified license differs from this License).
347
+
348
+10.4. Distributing Source Code Form that is Incompatible With Secondary
349
+Licenses
350
+
351
+If You choose to distribute Source Code Form that is Incompatible With
352
+Secondary Licenses under the terms of this version of the License, the
353
+notice described in Exhibit B of this License must be attached.
354
+
355
+Exhibit A - Source Code Form License Notice
356
+-------------------------------------------
357
+
358
+  This Source Code Form is subject to the terms of the Mozilla Public
359
+  License, v. 2.0. If a copy of the MPL was not distributed with this
360
+  file, You can obtain one at http://mozilla.org/MPL/2.0/.
361
+
362
+If it is not possible or desirable to put the notice in a particular
363
+file, then You may include the notice in a location (such as a LICENSE
364
+file in a relevant directory) where a recipient would be likely to look
365
+for such a notice.
366
+
367
+You may add additional accurate notices of copyright ownership.
368
+
369
+Exhibit B - "Incompatible With Secondary Licenses" Notice
370
+---------------------------------------------------------
371
+
372
+  This Source Code Form is "Incompatible With Secondary Licenses", as
373
+  defined by the Mozilla Public License, v. 2.0.

+ 501
- 0
vendor/github.com/go-sql-driver/mysql/README.md View File

@@ -0,0 +1,501 @@
1
+# Go-MySQL-Driver
2
+
3
+A MySQL-Driver for Go's [database/sql](https://golang.org/pkg/database/sql/) package
4
+
5
+![Go-MySQL-Driver logo](https://raw.github.com/wiki/go-sql-driver/mysql/gomysql_m.png "Golang Gopher holding the MySQL Dolphin")
6
+
7
+---------------------------------------
8
+  * [Features](#features)
9
+  * [Requirements](#requirements)
10
+  * [Installation](#installation)
11
+  * [Usage](#usage)
12
+    * [DSN (Data Source Name)](#dsn-data-source-name)
13
+      * [Password](#password)
14
+      * [Protocol](#protocol)
15
+      * [Address](#address)
16
+      * [Parameters](#parameters)
17
+      * [Examples](#examples)
18
+    * [Connection pool and timeouts](#connection-pool-and-timeouts)
19
+    * [context.Context Support](#contextcontext-support)
20
+    * [ColumnType Support](#columntype-support)
21
+    * [LOAD DATA LOCAL INFILE support](#load-data-local-infile-support)
22
+    * [time.Time support](#timetime-support)
23
+    * [Unicode support](#unicode-support)
24
+  * [Testing / Development](#testing--development)
25
+  * [License](#license)
26
+
27
+---------------------------------------
28
+
29
+## Features
30
+  * Lightweight and [fast](https://github.com/go-sql-driver/sql-benchmark "golang MySQL-Driver performance")
31
+  * Native Go implementation. No C-bindings, just pure Go
32
+  * Connections over TCP/IPv4, TCP/IPv6, Unix domain sockets or [custom protocols](https://godoc.org/github.com/go-sql-driver/mysql#DialFunc)
33
+  * Automatic handling of broken connections
34
+  * Automatic Connection Pooling *(by database/sql package)*
35
+  * Supports queries larger than 16MB
36
+  * Full [`sql.RawBytes`](https://golang.org/pkg/database/sql/#RawBytes) support.
37
+  * Intelligent `LONG DATA` handling in prepared statements
38
+  * Secure `LOAD DATA LOCAL INFILE` support with file Whitelisting and `io.Reader` support
39
+  * Optional `time.Time` parsing
40
+  * Optional placeholder interpolation
41
+
42
+## Requirements
43
+  * Go 1.10 or higher. We aim to support the 3 latest versions of Go.
44
+  * MySQL (4.1+), MariaDB, Percona Server, Google CloudSQL or Sphinx (2.2.3+)
45
+
46
+---------------------------------------
47
+
48
+## Installation
49
+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:
50
+```bash
51
+$ go get -u github.com/go-sql-driver/mysql
52
+```
53
+Make sure [Git is installed](https://git-scm.com/downloads) on your machine and in your system's `PATH`.
54
+
55
+## Usage
56
+_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.
57
+
58
+Use `mysql` as `driverName` and a valid [DSN](#dsn-data-source-name)  as `dataSourceName`:
59
+```go
60
+import "database/sql"
61
+import _ "github.com/go-sql-driver/mysql"
62
+
63
+db, err := sql.Open("mysql", "user:password@/dbname")
64
+```
65
+
66
+[Examples are available in our Wiki](https://github.com/go-sql-driver/mysql/wiki/Examples "Go-MySQL-Driver Examples").
67
+
68
+
69
+### DSN (Data Source Name)
70
+
71
+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):
72
+```
73
+[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...&paramN=valueN]
74
+```
75
+
76
+A DSN in its fullest form:
77
+```
78
+username:password@protocol(address)/dbname?param=value
79
+```
80
+
81
+Except for the databasename, all values are optional. So the minimal DSN is:
82
+```
83
+/dbname
84
+```
85
+
86
+If you do not want to preselect a database, leave `dbname` empty:
87
+```
88
+/
89
+```
90
+This has the same effect as an empty DSN string:
91
+```
92
+
93
+```
94
+
95
+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.
96
+
97
+#### Password
98
+Passwords can consist of any character. Escaping is **not** necessary.
99
+
100
+#### Protocol
101
+See [net.Dial](https://golang.org/pkg/net/#Dial) for more information which networks are available.
102
+In general you should use an Unix domain socket if available and TCP otherwise for best performance.
103
+
104
+#### Address
105
+For TCP and UDP networks, addresses have the form `host[:port]`.
106
+If `port` is omitted, the default port will be used.
107
+If `host` is a literal IPv6 address, it must be enclosed in square brackets.
108
+The functions [net.JoinHostPort](https://golang.org/pkg/net/#JoinHostPort) and [net.SplitHostPort](https://golang.org/pkg/net/#SplitHostPort) manipulate addresses in this form.
109
+
110
+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`.
111
+
112
+#### Parameters
113
+*Parameters are case-sensitive!*
114
+
115
+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`.
116
+
117
+##### `allowAllFiles`
118
+
119
+```
120
+Type:           bool
121
+Valid Values:   true, false
122
+Default:        false
123
+```
124
+
125
+`allowAllFiles=true` disables the file Whitelist for `LOAD DATA LOCAL INFILE` and allows *all* files.
126
+[*Might be insecure!*](http://dev.mysql.com/doc/refman/5.7/en/load-data-local.html)
127
+
128
+##### `allowCleartextPasswords`
129
+
130
+```
131
+Type:           bool
132
+Valid Values:   true, false
133
+Default:        false
134
+```
135
+
136
+`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.
137
+
138
+##### `allowNativePasswords`
139
+
140
+```
141
+Type:           bool
142
+Valid Values:   true, false
143
+Default:        true
144
+```
145
+`allowNativePasswords=false` disallows the usage of MySQL native password method.
146
+
147
+##### `allowOldPasswords`
148
+
149
+```
150
+Type:           bool
151
+Valid Values:   true, false
152
+Default:        false
153
+```
154
+`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).
155
+
156
+##### `charset`
157
+
158
+```
159
+Type:           string
160
+Valid Values:   <name>
161
+Default:        none
162
+```
163
+
164
+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`).
165
+
166
+Usage of the `charset` parameter is discouraged because it issues additional queries to the server.
167
+Unless you need the fallback behavior, please use `collation` instead.
168
+
169
+##### `checkConnLiveness`
170
+
171
+```
172
+Type:           bool
173
+Valid Values:   true, false
174
+Default:        true
175
+```
176
+
177
+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.
178
+`checkConnLiveness=false` disables this liveness check of connections.
179
+
180
+##### `collation`
181
+
182
+```
183
+Type:           string
184
+Valid Values:   <name>
185
+Default:        utf8mb4_general_ci
186
+```
187
+
188
+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.
189
+
190
+A list of valid charsets for a server is retrievable with `SHOW COLLATION`.
191
+
192
+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.
193
+
194
+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)).
195
+
196
+
197
+##### `clientFoundRows`
198
+
199
+```
200
+Type:           bool
201
+Valid Values:   true, false
202
+Default:        false
203
+```
204
+
205
+`clientFoundRows=true` causes an UPDATE to return the number of matching rows instead of the number of rows changed.
206
+
207
+##### `columnsWithAlias`
208
+
209
+```
210
+Type:           bool
211
+Valid Values:   true, false
212
+Default:        false
213
+```
214
+
215
+When `columnsWithAlias` is true, calls to `sql.Rows.Columns()` will return the table alias and the column name separated by a dot. For example:
216
+
217
+```
218
+SELECT u.id FROM users as u
219
+```
220
+
221
+will return `u.id` instead of just `id` if `columnsWithAlias=true`.
222
+
223
+##### `interpolateParams`
224
+
225
+```
226
+Type:           bool
227
+Valid Values:   true, false
228
+Default:        false
229
+```
230
+
231
+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`.
232
+
233
+*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)!*
234
+
235
+##### `loc`
236
+
237
+```
238
+Type:           string
239
+Valid Values:   <escaped name>
240
+Default:        UTC
241
+```
242
+
243
+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.
244
+
245
+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.
246
+
247
+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`.
248
+
249
+##### `maxAllowedPacket`
250
+```
251
+Type:          decimal number
252
+Default:       4194304
253
+```
254
+
255
+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*.
256
+
257
+##### `multiStatements`
258
+
259
+```
260
+Type:           bool
261
+Valid Values:   true, false
262
+Default:        false
263
+```
264
+
265
+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.
266
+
267
+When `multiStatements` is used, `?` parameters must only be used in the first statement.
268
+
269
+##### `parseTime`
270
+
271
+```
272
+Type:           bool
273
+Valid Values:   true, false
274
+Default:        false
275
+```
276
+
277
+`parseTime=true` changes the output type of `DATE` and `DATETIME` values to `time.Time` instead of `[]byte` / `string`
278
+The date or datetime like `0000-00-00 00:00:00` is converted into zero value of `time.Time`.
279
+
280
+
281
+##### `readTimeout`
282
+
283
+```
284
+Type:           duration
285
+Default:        0
286
+```
287
+
288
+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"*.
289
+
290
+##### `rejectReadOnly`
291
+
292
+```
293
+Type:           bool
294
+Valid Values:   true, false
295
+Default:        false
296
+```
297
+
298
+
299
+`rejectReadOnly=true` causes the driver to reject read-only connections. This
300
+is for a possible race condition during an automatic failover, where the mysql
301
+client gets connected to a read-only replica after the failover.
302
+
303
+Note that this should be a fairly rare case, as an automatic failover normally
304
+happens when the primary is down, and the race condition shouldn't happen
305
+unless it comes back up online as soon as the failover is kicked off. On the
306
+other hand, when this happens, a MySQL application can get stuck on a
307
+read-only connection until restarted. It is however fairly easy to reproduce,
308
+for example, using a manual failover on AWS Aurora's MySQL-compatible cluster.
309
+
310
+If you are not relying on read-only transactions to reject writes that aren't
311
+supposed to happen, setting this on some MySQL providers (such as AWS Aurora)
312
+is safer for failovers.
313
+
314
+Note that ERROR 1290 can be returned for a `read-only` server and this option will
315
+cause a retry for that error. However the same error number is used for some
316
+other cases. You should ensure your application will never cause an ERROR 1290
317
+except for `read-only` mode when enabling this option.
318
+
319
+
320
+##### `serverPubKey`
321
+
322
+```
323
+Type:           string
324
+Valid Values:   <name>
325
+Default:        none
326
+```
327
+
328
+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.
329
+Public keys are used to transmit encrypted data, e.g. for authentication.
330
+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.
331
+
332
+
333
+##### `timeout`
334
+
335
+```
336
+Type:           duration
337
+Default:        OS default
338
+```
339
+
340
+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"*.
341
+
342
+
343
+##### `tls`
344
+
345
+```
346
+Type:           bool / string
347
+Valid Values:   true, false, skip-verify, preferred, <name>
348
+Default:        false
349
+```
350
+
351
+`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).
352
+
353
+
354
+##### `writeTimeout`
355
+
356
+```
357
+Type:           duration
358
+Default:        0
359
+```
360
+
361
+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"*.
362
+
363
+
364
+##### System Variables
365
+
366
+Any other parameters are interpreted as system variables:
367
+  * `<boolean_var>=<value>`: `SET <boolean_var>=<value>`
368
+  * `<enum_var>=<value>`: `SET <enum_var>=<value>`
369
+  * `<string_var>=%27<value>%27`: `SET <string_var>='<value>'`
370
+
371
+Rules:
372
+* The values for string variables must be quoted with `'`.
373
+* The values must also be [url.QueryEscape](http://golang.org/pkg/net/url/#QueryEscape)'ed!
374
+ (which implies values of string variables must be wrapped with `%27`).
375
+
376
+Examples:
377
+  * `autocommit=1`: `SET autocommit=1`
378
+  * [`time_zone=%27Europe%2FParis%27`](https://dev.mysql.com/doc/refman/5.5/en/time-zone-support.html): `SET time_zone='Europe/Paris'`
379
+  * [`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'`
380
+
381
+
382
+#### Examples
383
+```
384
+user@unix(/path/to/socket)/dbname
385
+```
386
+
387
+```
388
+root:pw@unix(/tmp/mysql.sock)/myDatabase?loc=Local
389
+```
390
+
391
+```
392
+user:password@tcp(localhost:5555)/dbname?tls=skip-verify&autocommit=true
393
+```
394
+
395
+Treat warnings as errors by setting the system variable [`sql_mode`](https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html):
396
+```
397
+user:password@/dbname?sql_mode=TRADITIONAL
398
+```
399
+
400
+TCP via IPv6:
401
+```
402
+user:password@tcp([de:ad:be:ef::ca:fe]:80)/dbname?timeout=90s&collation=utf8mb4_unicode_ci
403
+```
404
+
405
+TCP on a remote host, e.g. Amazon RDS:
406
+```
407
+id:password@tcp(your-amazonaws-uri.com:3306)/dbname
408
+```
409
+
410
+Google Cloud SQL on App Engine:
411
+```
412
+user:password@unix(/cloudsql/project-id:region-name:instance-name)/dbname
413
+```
414
+
415
+TCP using default port (3306) on localhost:
416
+```
417
+user:password@tcp/dbname?charset=utf8mb4,utf8&sys_var=esc%40ped
418
+```
419
+
420
+Use the default protocol (tcp) and host (localhost:3306):
421
+```
422
+user:password@/dbname
423
+```
424
+
425
+No Database preselected:
426
+```
427
+user:password@/
428
+```
429
+
430
+
431
+### Connection pool and timeouts
432
+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.
433
+
434
+## `ColumnType` Support
435
+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.
436
+
437
+## `context.Context` Support
438
+Go 1.8 added `database/sql` support for `context.Context`. This driver supports query timeouts and cancellation via contexts.
439
+See [context support in the database/sql package](https://golang.org/doc/go1.8#database_sql) for more details.
440
+
441
+
442
+### `LOAD DATA LOCAL INFILE` support
443
+For this feature you need direct access to the package. Therefore you must change the import path (no `_`):
444
+```go
445
+import "github.com/go-sql-driver/mysql"
446
+```
447
+
448
+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)).
449
+
450
+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.
451
+
452
+See the [godoc of Go-MySQL-Driver](https://godoc.org/github.com/go-sql-driver/mysql "golang mysql driver documentation") for details.
453
+
454
+
455
+### `time.Time` support
456
+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.
457
+
458
+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.
459
+
460
+**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).
461
+
462
+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`.
463
+
464
+
465
+### Unicode support
466
+Since version 1.5 Go-MySQL-Driver automatically uses the collation ` utf8mb4_general_ci` by default.
467
+
468
+Other collations / charsets can be set using the [`collation`](#collation) DSN parameter.
469
+
470
+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.
471
+
472
+See http://dev.mysql.com/doc/refman/8.0/en/charset-unicode.html for more details on MySQL's Unicode support.
473
+
474
+## Testing / Development
475
+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.
476
+
477
+Go-MySQL-Driver is not feature-complete yet. Your help is very appreciated.
478
+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).
479
+
480
+See the [Contribution Guidelines](https://github.com/go-sql-driver/mysql/blob/master/CONTRIBUTING.md) for details.
481
+
482
+---------------------------------------
483
+
484
+## License
485
+Go-MySQL-Driver is licensed under the [Mozilla Public License Version 2.0](https://raw.github.com/go-sql-driver/mysql/master/LICENSE)
486
+
487
+Mozilla summarizes the license scope as follows:
488
+> MPL: The copyleft applies to any files containing MPLed code.
489
+
490
+
491
+That means:
492
+  * You can **use** the **unchanged** source code both in private and commercially.
493
+  * 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).
494
+  * You **needn't publish** the source code of your library as long as the files licensed under the MPL 2.0 are **unchanged**.
495
+
496
+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.
497
+
498
+You can read the full terms here: [LICENSE](https://raw.github.com/go-sql-driver/mysql/master/LICENSE).
499
+
500
+![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")
501
+

+ 422
- 0
vendor/github.com/go-sql-driver/mysql/auth.go View File

@@ -0,0 +1,422 @@
1
+// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
2
+//
3
+// Copyright 2018 The Go-MySQL-Driver Authors. All rights reserved.
4
+//
5
+// This Source Code Form is subject to the terms of the Mozilla Public
6
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
7
+// You can obtain one at http://mozilla.org/MPL/2.0/.
8
+
9
+package mysql
10
+
11
+import (
12
+	"crypto/rand"
13
+	"crypto/rsa"
14
+	"crypto/sha1"
15
+	"crypto/sha256"
16
+	"crypto/x509"
17
+	"encoding/pem"
18
+	"sync"
19
+)
20
+
21
+// server pub keys registry
22
+var (
23
+	serverPubKeyLock     sync.RWMutex
24
+	serverPubKeyRegistry map[string]*rsa.PublicKey
25
+)
26
+
27
+// RegisterServerPubKey registers a server RSA public key which can be used to
28
+// send data in a secure manner to the server without receiving the public key
29
+// in a potentially insecure way from the server first.
30
+// Registered keys can afterwards be used adding serverPubKey=<name> to the DSN.
31
+//
32
+// Note: The provided rsa.PublicKey instance is exclusively owned by the driver
33
+// after registering it and may not be modified.
34
+//
35
+//  data, err := ioutil.ReadFile("mykey.pem")
36
+//  if err != nil {
37
+//  	log.Fatal(err)
38
+//  }
39
+//
40
+//  block, _ := pem.Decode(data)
41
+//  if block == nil || block.Type != "PUBLIC KEY" {
42
+//  	log.Fatal("failed to decode PEM block containing public key")
43
+//  }
44
+//
45
+//  pub, err := x509.ParsePKIXPublicKey(block.Bytes)
46
+//  if err != nil {
47
+//  	log.Fatal(err)
48
+//  }
49
+//
50
+//  if rsaPubKey, ok := pub.(*rsa.PublicKey); ok {
51
+//  	mysql.RegisterServerPubKey("mykey", rsaPubKey)
52
+//  } else {
53
+//  	log.Fatal("not a RSA public key")
54
+//  }
55
+//
56
+func RegisterServerPubKey(name string, pubKey *rsa.PublicKey) {
57
+	serverPubKeyLock.Lock()
58
+	if serverPubKeyRegistry == nil {
59
+		serverPubKeyRegistry = make(map[string]*rsa.PublicKey)
60
+	}
61
+
62
+	serverPubKeyRegistry[name] = pubKey
63
+	serverPubKeyLock.Unlock()
64
+}
65
+
66
+// DeregisterServerPubKey removes the public key registered with the given name.
67
+func DeregisterServerPubKey(name string) {
68
+	serverPubKeyLock.Lock()
69
+	if serverPubKeyRegistry != nil {
70
+		delete(serverPubKeyRegistry, name)
71
+	}
72
+	serverPubKeyLock.Unlock()
73
+}
74
+
75
+func getServerPubKey(name string) (pubKey *rsa.PublicKey) {
76
+	serverPubKeyLock.RLock()
77
+	if v, ok := serverPubKeyRegistry[name]; ok {
78
+		pubKey = v
79
+	}
80
+	serverPubKeyLock.RUnlock()
81
+	return
82
+}
83
+
84
+// Hash password using pre 4.1 (old password) method
85
+// https://github.com/atcurtis/mariadb/blob/master/mysys/my_rnd.c
86
+type myRnd struct {
87
+	seed1, seed2 uint32
88
+}
89
+
90
+const myRndMaxVal = 0x3FFFFFFF
91
+
92
+// Pseudo random number generator
93
+func newMyRnd(seed1, seed2 uint32) *myRnd {
94
+	return &myRnd{
95
+		seed1: seed1 % myRndMaxVal,
96
+		seed2: seed2 % myRndMaxVal,
97
+	}
98
+}
99
+
100
+// Tested to be equivalent to MariaDB's floating point variant
101
+// http://play.golang.org/p/QHvhd4qved
102
+// http://play.golang.org/p/RG0q4ElWDx
103
+func (r *myRnd) NextByte() byte {
104
+	r.seed1 = (r.seed1*3 + r.seed2) % myRndMaxVal
105
+	r.seed2 = (r.seed1 + r.seed2 + 33) % myRndMaxVal
106
+
107
+	return byte(uint64(r.seed1) * 31 / myRndMaxVal)
108
+}
109
+
110
+// Generate binary hash from byte string using insecure pre 4.1 method
111
+func pwHash(password []byte) (result [2]uint32) {
112
+	var add uint32 = 7
113
+	var tmp uint32
114
+
115
+	result[0] = 1345345333
116
+	result[1] = 0x12345671
117
+
118
+	for _, c := range password {
119
+		// skip spaces and tabs in password
120
+		if c == ' ' || c == '\t' {
121
+			continue
122
+		}
123
+
124
+		tmp = uint32(c)
125
+		result[0] ^= (((result[0] & 63) + add) * tmp) + (result[0] << 8)
126
+		result[1] += (result[1] << 8) ^ result[0]
127
+		add += tmp
128
+	}
129
+
130
+	// Remove sign bit (1<<31)-1)
131
+	result[0] &= 0x7FFFFFFF
132
+	result[1] &= 0x7FFFFFFF
133
+
134
+	return
135
+}
136
+
137
+// Hash password using insecure pre 4.1 method
138
+func scrambleOldPassword(scramble []byte, password string) []byte {
139
+	if len(password) == 0 {
140
+		return nil
141
+	}
142
+
143
+	scramble = scramble[:8]
144
+
145
+	hashPw := pwHash([]byte(password))
146
+	hashSc := pwHash(scramble)
147
+
148
+	r := newMyRnd(hashPw[0]^hashSc[0], hashPw[1]^hashSc[1])
149
+
150
+	var out [8]byte
151
+	for i := range out {
152
+		out[i] = r.NextByte() + 64
153
+	}
154
+
155
+	mask := r.NextByte()
156
+	for i := range out {
157
+		out[i] ^= mask
158
+	}
159
+
160
+	return out[:]
161
+}
162
+
163
+// Hash password using 4.1+ method (SHA1)
164
+func scramblePassword(scramble []byte, password string) []byte {
165
+	if len(password) == 0 {
166
+		return nil
167
+	}
168
+
169
+	// stage1Hash = SHA1(password)
170
+	crypt := sha1.New()
171
+	crypt.Write([]byte(password))
172
+	stage1 := crypt.Sum(nil)
173
+
174
+	// scrambleHash = SHA1(scramble + SHA1(stage1Hash))
175
+	// inner Hash
176
+	crypt.Reset()
177
+	crypt.Write(stage1)
178
+	hash := crypt.Sum(nil)
179
+
180
+	// outer Hash
181
+	crypt.Reset()
182
+	crypt.Write(scramble)
183
+	crypt.Write(hash)
184
+	scramble = crypt.Sum(nil)
185
+
186
+	// token = scrambleHash XOR stage1Hash
187
+	for i := range scramble {
188
+		scramble[i] ^= stage1[i]
189
+	}
190
+	return scramble
191
+}
192
+
193
+// Hash password using MySQL 8+ method (SHA256)
194
+func scrambleSHA256Password(scramble []byte, password string) []byte {
195
+	if len(password) == 0 {
196
+		return nil
197
+	}
198
+
199
+	// XOR(SHA256(password), SHA256(SHA256(SHA256(password)), scramble))
200
+
201
+	crypt := sha256.New()
202
+	crypt.Write([]byte(password))
203
+	message1 := crypt.Sum(nil)
204
+
205
+	crypt.Reset()
206
+	crypt.Write(message1)
207
+	message1Hash := crypt.Sum(nil)
208
+
209
+	crypt.Reset()
210
+	crypt.Write(message1Hash)
211
+	crypt.Write(scramble)
212
+	message2 := crypt.Sum(nil)
213
+
214
+	for i := range message1 {
215
+		message1[i] ^= message2[i]
216
+	}
217
+
218
+	return message1
219
+}
220
+
221
+func encryptPassword(password string, seed []byte, pub *rsa.PublicKey) ([]byte, error) {
222
+	plain := make([]byte, len(password)+1)
223
+	copy(plain, password)
224
+	for i := range plain {
225
+		j := i % len(seed)
226
+		plain[i] ^= seed[j]
227
+	}
228
+	sha1 := sha1.New()
229
+	return rsa.EncryptOAEP(sha1, rand.Reader, pub, plain, nil)
230
+}
231
+
232
+func (mc *mysqlConn) sendEncryptedPassword(seed []byte, pub *rsa.PublicKey) error {
233
+	enc, err := encryptPassword(mc.cfg.Passwd, seed, pub)
234
+	if err != nil {
235
+		return err
236
+	}
237
+	return mc.writeAuthSwitchPacket(enc)
238
+}
239
+
240
+func (mc *mysqlConn) auth(authData []byte, plugin string) ([]byte, error) {
241
+	switch plugin {
242
+	case "caching_sha2_password":
243
+		authResp := scrambleSHA256Password(authData, mc.cfg.Passwd)
244
+		return authResp, nil
245
+
246
+	case "mysql_old_password":
247
+		if !mc.cfg.AllowOldPasswords {
248
+			return nil, ErrOldPassword
249
+		}
250
+		// Note: there are edge cases where this should work but doesn't;
251
+		// this is currently "wontfix":
252
+		// https://github.com/go-sql-driver/mysql/issues/184
253
+		authResp := append(scrambleOldPassword(authData[:8], mc.cfg.Passwd), 0)
254
+		return authResp, nil
255
+
256
+	case "mysql_clear_password":
257
+		if !mc.cfg.AllowCleartextPasswords {
258
+			return nil, ErrCleartextPassword
259
+		}
260
+		// http://dev.mysql.com/doc/refman/5.7/en/cleartext-authentication-plugin.html
261
+		// http://dev.mysql.com/doc/refman/5.7/en/pam-authentication-plugin.html
262
+		return append([]byte(mc.cfg.Passwd), 0), nil
263
+
264
+	case "mysql_native_password":
265
+		if !mc.cfg.AllowNativePasswords {
266
+			return nil, ErrNativePassword
267
+		}
268
+		// https://dev.mysql.com/doc/internals/en/secure-password-authentication.html
269
+		// Native password authentication only need and will need 20-byte challenge.
270
+		authResp := scramblePassword(authData[:20], mc.cfg.Passwd)
271
+		return authResp, nil
272
+
273
+	case "sha256_password":
274
+		if len(mc.cfg.Passwd) == 0 {
275
+			return []byte{0}, nil
276
+		}
277
+		if mc.cfg.tls != nil || mc.cfg.Net == "unix" {
278
+			// write cleartext auth packet
279
+			return append([]byte(mc.cfg.Passwd), 0), nil
280
+		}
281
+
282
+		pubKey := mc.cfg.pubKey
283
+		if pubKey == nil {
284
+			// request public key from server
285
+			return []byte{1}, nil
286
+		}
287
+
288
+		// encrypted password
289
+		enc, err := encryptPassword(mc.cfg.Passwd, authData, pubKey)
290
+		return enc, err
291
+
292
+	default:
293
+		errLog.Print("unknown auth plugin:", plugin)
294
+		return nil, ErrUnknownPlugin
295
+	}
296
+}
297
+
298
+func (mc *mysqlConn) handleAuthResult(oldAuthData []byte, plugin string) error {
299
+	// Read Result Packet
300
+	authData, newPlugin, err := mc.readAuthResult()
301
+	if err != nil {
302
+		return err
303
+	}
304
+
305
+	// handle auth plugin switch, if requested
306
+	if newPlugin != "" {
307
+		// If CLIENT_PLUGIN_AUTH capability is not supported, no new cipher is
308
+		// sent and we have to keep using the cipher sent in the init packet.
309
+		if authData == nil {
310
+			authData = oldAuthData
311
+		} else {
312
+			// copy data from read buffer to owned slice
313
+			copy(oldAuthData, authData)
314
+		}
315
+
316
+		plugin = newPlugin
317
+
318
+		authResp, err := mc.auth(authData, plugin)
319
+		if err != nil {
320
+			return err
321
+		}
322
+		if err = mc.writeAuthSwitchPacket(authResp); err != nil {
323
+			return err
324
+		}
325
+
326
+		// Read Result Packet
327
+		authData, newPlugin, err = mc.readAuthResult()
328
+		if err != nil {
329
+			return err
330
+		}
331
+
332
+		// Do not allow to change the auth plugin more than once
333
+		if newPlugin != "" {
334
+			return ErrMalformPkt
335
+		}
336
+	}
337
+
338
+	switch plugin {
339
+
340
+	// https://insidemysql.com/preparing-your-community-connector-for-mysql-8-part-2-sha256/
341
+	case "caching_sha2_password":
342
+		switch len(authData) {
343
+		case 0:
344
+			return nil // auth successful
345
+		case 1:
346
+			switch authData[0] {
347
+			case cachingSha2PasswordFastAuthSuccess:
348
+				if err = mc.readResultOK(); err == nil {
349
+					return nil // auth successful
350
+				}
351
+
352
+			case cachingSha2PasswordPerformFullAuthentication:
353
+				if mc.cfg.tls != nil || mc.cfg.Net == "unix" {
354
+					// write cleartext auth packet
355
+					err = mc.writeAuthSwitchPacket(append([]byte(mc.cfg.Passwd), 0))
356
+					if err != nil {
357
+						return err
358
+					}
359
+				} else {
360
+					pubKey := mc.cfg.pubKey
361
+					if pubKey == nil {
362
+						// request public key from server
363
+						data, err := mc.buf.takeSmallBuffer(4 + 1)
364
+						if err != nil {
365
+							return err
366
+						}
367
+						data[4] = cachingSha2PasswordRequestPublicKey
368
+						mc.writePacket(data)
369
+
370
+						// parse public key
371
+						if data, err = mc.readPacket(); err != nil {
372
+							return err
373
+						}
374
+
375
+						block, _ := pem.Decode(data[1:])
376
+						pkix, err := x509.ParsePKIXPublicKey(block.Bytes)
377
+						if err != nil {
378
+							return err
379
+						}
380
+						pubKey = pkix.(*rsa.PublicKey)
381
+					}
382
+
383
+					// send encrypted password
384
+					err = mc.sendEncryptedPassword(oldAuthData, pubKey)
385
+					if err != nil {
386
+						return err
387
+					}
388
+				}
389
+				return mc.readResultOK()
390
+
391
+			default:
392
+				return ErrMalformPkt
393
+			}
394
+		default:
395
+			return ErrMalformPkt
396
+		}
397
+
398
+	case "sha256_password":
399
+		switch len(authData) {
400
+		case 0:
401
+			return nil // auth successful
402
+		default:
403
+			block, _ := pem.Decode(authData)
404
+			pub, err := x509.ParsePKIXPublicKey(block.Bytes)
405
+			if err != nil {
406
+				return err
407
+			}
408
+
409
+			// send encrypted password
410
+			err = mc.sendEncryptedPassword(oldAuthData, pub.(*rsa.PublicKey))
411
+			if err != nil {
412
+				return err
413
+			}
414
+			return mc.readResultOK()
415
+		}
416
+
417
+	default:
418
+		return nil // auth successful
419
+	}
420
+
421
+	return err
422
+}

+ 182
- 0
vendor/github.com/go-sql-driver/mysql/buffer.go View File

@@ -0,0 +1,182 @@
1
+// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
2
+//
3
+// Copyright 2013 The Go-MySQL-Driver Authors. All rights reserved.
4
+//
5
+// This Source Code Form is subject to the terms of the Mozilla Public
6
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
7
+// You can obtain one at http://mozilla.org/MPL/2.0/.
8
+
9
+package mysql
10
+
11
+import (
12
+	"io"
13
+	"net"
14
+	"time"
15
+)
16
+
17
+const defaultBufSize = 4096
18
+const maxCachedBufSize = 256 * 1024
19
+
20
+// A buffer which is used for both reading and writing.
21
+// This is possible since communication on each connection is synchronous.
22
+// In other words, we can't write and read simultaneously on the same connection.
23
+// The buffer is similar to bufio.Reader / Writer but zero-copy-ish
24
+// Also highly optimized for this particular use case.
25
+// This buffer is backed by two byte slices in a double-buffering scheme
26
+type buffer struct {
27
+	buf     []byte // buf is a byte buffer who's length and capacity are equal.
28
+	nc      net.Conn
29
+	idx     int
30
+	length  int
31
+	timeout time.Duration
32
+	dbuf    [2][]byte // dbuf is an array with the two byte slices that back this buffer
33
+	flipcnt uint      // flipccnt is the current buffer counter for double-buffering
34
+}
35
+
36
+// newBuffer allocates and returns a new buffer.
37
+func newBuffer(nc net.Conn) buffer {
38
+	fg := make([]byte, defaultBufSize)
39
+	return buffer{
40
+		buf:  fg,
41
+		nc:   nc,
42
+		dbuf: [2][]byte{fg, nil},
43
+	}
44
+}
45
+
46
+// flip replaces the active buffer with the background buffer
47
+// this is a delayed flip that simply increases the buffer counter;
48
+// the actual flip will be performed the next time we call `buffer.fill`
49
+func (b *buffer) flip() {
50
+	b.flipcnt += 1
51
+}
52
+
53
+// fill reads into the buffer until at least _need_ bytes are in it
54
+func (b *buffer) fill(need int) error {
55
+	n := b.length
56
+	// fill data into its double-buffering target: if we've called
57
+	// flip on this buffer, we'll be copying to the background buffer,
58
+	// and then filling it with network data; otherwise we'll just move
59
+	// the contents of the current buffer to the front before filling it
60
+	dest := b.dbuf[b.flipcnt&1]
61
+
62
+	// grow buffer if necessary to fit the whole packet.
63
+	if need > len(dest) {
64
+		// Round up to the next multiple of the default size
65
+		dest = make([]byte, ((need/defaultBufSize)+1)*defaultBufSize)
66
+
67
+		// if the allocated buffer is not too large, move it to backing storage
68
+		// to prevent extra allocations on applications that perform large reads
69
+		if len(dest) <= maxCachedBufSize {
70
+			b.dbuf[b.flipcnt&1] = dest
71
+		}
72
+	}
73
+
74
+	// if we're filling the fg buffer, move the existing data to the start of it.
75
+	// if we're filling the bg buffer, copy over the data
76
+	if n > 0 {
77
+		copy(dest[:n], b.buf[b.idx:])
78
+	}
79
+
80
+	b.buf = dest
81
+	b.idx = 0
82
+
83
+	for {
84
+		if b.timeout > 0 {
85
+			if err := b.nc.SetReadDeadline(time.Now().Add(b.timeout)); err != nil {
86
+				return err
87
+			}
88
+		}
89
+
90
+		nn, err := b.nc.Read(b.buf[n:])
91
+		n += nn
92
+
93
+		switch err {
94
+		case nil:
95
+			if n < need {
96
+				continue
97
+			}
98
+			b.length = n
99
+			return nil
100
+
101
+		case io.EOF:
102
+			if n >= need {
103
+				b.length = n
104
+				return nil
105
+			}
106
+			return io.ErrUnexpectedEOF
107
+
108
+		default:
109
+			return err
110
+		}
111
+	}
112
+}
113
+
114
+// returns next N bytes from buffer.
115
+// The returned slice is only guaranteed to be valid until the next read
116
+func (b *buffer) readNext(need int) ([]byte, error) {
117
+	if b.length < need {
118
+		// refill
119
+		if err := b.fill(need); err != nil {
120
+			return nil, err
121
+		}
122
+	}
123
+
124
+	offset := b.idx
125
+	b.idx += need
126
+	b.length -= need
127
+	return b.buf[offset:b.idx], nil
128
+}
129
+
130
+// takeBuffer returns a buffer with the requested size.
131
+// If possible, a slice from the existing buffer is returned.
132
+// Otherwise a bigger buffer is made.
133
+// Only one buffer (total) can be used at a time.
134
+func (b *buffer) takeBuffer(length int) ([]byte, error) {
135
+	if b.length > 0 {
136
+		return nil, ErrBusyBuffer
137
+	}
138
+
139
+	// test (cheap) general case first
140
+	if length <= cap(b.buf) {
141
+		return b.buf[:length], nil
142
+	}
143
+
144
+	if length < maxPacketSize {
145
+		b.buf = make([]byte, length)
146
+		return b.buf, nil
147
+	}
148
+
149
+	// buffer is larger than we want to store.
150
+	return make([]byte, length), nil
151
+}
152
+
153
+// takeSmallBuffer is shortcut which can be used if length is
154
+// known to be smaller than defaultBufSize.
155
+// Only one buffer (total) can be used at a time.
156
+func (b *buffer) takeSmallBuffer(length int) ([]byte, error) {
157
+	if b.length > 0 {
158
+		return nil, ErrBusyBuffer
159
+	}
160
+	return b.buf[:length], nil
161
+}
162
+
163
+// takeCompleteBuffer returns the complete existing buffer.
164
+// This can be used if the necessary buffer size is unknown.
165
+// cap and len of the returned buffer will be equal.
166
+// Only one buffer (total) can be used at a time.
167
+func (b *buffer) takeCompleteBuffer() ([]byte, error) {
168
+	if b.length > 0 {
169
+		return nil, ErrBusyBuffer
170
+	}
171
+	return b.buf, nil
172
+}
173
+
174
+// store stores buf, an updated buffer, if its suitable to do so.
175
+func (b *buffer) store(buf []byte) error {
176
+	if b.length > 0 {
177
+		return ErrBusyBuffer
178
+	} else if cap(buf) <= maxPacketSize && cap(buf) > cap(b.buf) {
179
+		b.buf = buf[:cap(buf)]
180
+	}
181
+	return nil
182
+}

+ 265
- 0
vendor/github.com/go-sql-driver/mysql/collations.go View File

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

+ 54
- 0
vendor/github.com/go-sql-driver/mysql/conncheck.go View File

@@ -0,0 +1,54 @@
1
+// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
2
+//
3
+// Copyright 2019 The Go-MySQL-Driver Authors. All rights reserved.
4
+//
5
+// This Source Code Form is subject to the terms of the Mozilla Public
6
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
7
+// You can obtain one at http://mozilla.org/MPL/2.0/.
8
+
9
+// +build linux darwin dragonfly freebsd netbsd openbsd solaris illumos
10
+
11
+package mysql
12
+
13
+import (
14
+	"errors"
15
+	"io"
16
+	"net"
17
+	"syscall"
18
+)
19
+
20
+var errUnexpectedRead = errors.New("unexpected read from socket")
21
+
22
+func connCheck(conn net.Conn) error {
23
+	var sysErr error
24
+
25
+	sysConn, ok := conn.(syscall.Conn)
26
+	if !ok {
27
+		return nil
28
+	}
29
+	rawConn, err := sysConn.SyscallConn()
30
+	if err != nil {
31
+		return err
32
+	}
33
+
34
+	err = rawConn.Read(func(fd uintptr) bool {
35
+		var buf [1]byte
36
+		n, err := syscall.Read(int(fd), buf[:])
37
+		switch {
38
+		case n == 0 && err == nil:
39
+			sysErr = io.EOF
40
+		case n > 0:
41
+			sysErr = errUnexpectedRead
42
+		case err == syscall.EAGAIN || err == syscall.EWOULDBLOCK:
43
+			sysErr = nil
44
+		default:
45
+			sysErr = err
46
+		}
47
+		return true
48
+	})
49
+	if err != nil {
50
+		return err
51
+	}
52
+
53
+	return sysErr
54
+}

+ 17
- 0
vendor/github.com/go-sql-driver/mysql/conncheck_dummy.go View File

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

+ 651
- 0
vendor/github.com/go-sql-driver/mysql/connection.go View File

@@ -0,0 +1,651 @@
1
+// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
2
+//
3
+// Copyright 2012 The Go-MySQL-Driver Authors. All rights reserved.
4
+//
5
+// This Source Code Form is subject to the terms of the Mozilla Public
6
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
7
+// You can obtain one at http://mozilla.org/MPL/2.0/.
8
+
9
+package mysql
10
+
11
+import (
12
+	"context"
13
+	"database/sql"
14
+	"database/sql/driver"
15
+	"io"
16
+	"net"
17
+	"strconv"
18
+	"strings"
19
+	"time"
20
+)
21
+
22
+type mysqlConn struct {
23
+	buf              buffer
24
+	netConn          net.Conn
25
+	rawConn          net.Conn // underlying connection when netConn is TLS connection.
26
+	affectedRows     uint64
27
+	insertId         uint64
28
+	cfg              *Config
29
+	maxAllowedPacket int
30
+	maxWriteSize     int
31
+	writeTimeout     time.Duration
32
+	flags            clientFlag
33
+	status           statusFlag
34
+	sequence         uint8
35
+	parseTime        bool
36
+	reset            bool // set when the Go SQL package calls ResetSession
37
+
38
+	// for context support (Go 1.8+)
39
+	watching bool
40
+	watcher  chan<- context.Context
41
+	closech  chan struct{}
42
+	finished chan<- struct{}
43
+	canceled atomicError // set non-nil if conn is canceled
44
+	closed   atomicBool  // set when conn is closed, before closech is closed
45
+}
46
+
47
+// Handles parameters set in DSN after the connection is established
48
+func (mc *mysqlConn) handleParams() (err error) {
49
+	for param, val := range mc.cfg.Params {
50
+		switch param {
51
+		// Charset
52
+		case "charset":
53
+			charsets := strings.Split(val, ",")
54
+			for i := range charsets {
55
+				// ignore errors here - a charset may not exist
56
+				err = mc.exec("SET NAMES " + charsets[i])
57
+				if err == nil {
58
+					break
59
+				}
60
+			}
61
+			if err != nil {
62
+				return
63
+			}
64
+
65
+		// System Vars
66
+		default:
67
+			err = mc.exec("SET " + param + "=" + val + "")
68
+			if err != nil {
69
+				return
70
+			}
71
+		}
72
+	}
73
+
74
+	return
75
+}
76
+
77
+func (mc *mysqlConn) markBadConn(err error) error {
78
+	if mc == nil {
79
+		return err
80
+	}
81
+	if err != errBadConnNoWrite {
82
+		return err
83
+	}
84
+	return driver.ErrBadConn
85
+}
86
+
87
+func (mc *mysqlConn) Begin() (driver.Tx, error) {
88
+	return mc.begin(false)
89
+}
90
+
91
+func (mc *mysqlConn) begin(readOnly bool) (driver.Tx, error) {
92
+	if mc.closed.IsSet() {
93
+		errLog.Print(ErrInvalidConn)
94
+		return nil, driver.ErrBadConn
95
+	}
96
+	var q string
97
+	if readOnly {
98
+		q = "START TRANSACTION READ ONLY"
99
+	} else {
100
+		q = "START TRANSACTION"
101
+	}
102
+	err := mc.exec(q)
103
+	if err == nil {
104
+		return &mysqlTx{mc}, err
105
+	}
106
+	return nil, mc.markBadConn(err)
107
+}
108
+
109
+func (mc *mysqlConn) Close() (err error) {
110
+	// Makes Close idempotent
111
+	if !mc.closed.IsSet() {
112
+		err = mc.writeCommandPacket(comQuit)
113
+	}
114
+
115
+	mc.cleanup()
116
+
117
+	return
118
+}
119
+
120
+// Closes the network connection and unsets internal variables. Do not call this
121
+// function after successfully authentication, call Close instead. This function
122
+// is called before auth or on auth failure because MySQL will have already
123
+// closed the network connection.
124
+func (mc *mysqlConn) cleanup() {
125
+	if !mc.closed.TrySet(true) {
126
+		return
127
+	}
128
+
129
+	// Makes cleanup idempotent
130
+	close(mc.closech)
131
+	if mc.netConn == nil {
132
+		return
133
+	}
134
+	if err := mc.netConn.Close(); err != nil {
135
+		errLog.Print(err)
136
+	}
137
+}
138
+
139
+func (mc *mysqlConn) error() error {
140
+	if mc.closed.IsSet() {
141
+		if err := mc.canceled.Value(); err != nil {
142
+			return err
143
+		}
144
+		return ErrInvalidConn
145
+	}
146
+	return nil
147
+}
148
+
149
+func (mc *mysqlConn) Prepare(query string) (driver.Stmt, error) {
150
+	if mc.closed.IsSet() {
151
+		errLog.Print(ErrInvalidConn)
152
+		return nil, driver.ErrBadConn
153
+	}
154
+	// Send command
155
+	err := mc.writeCommandPacketStr(comStmtPrepare, query)
156
+	if err != nil {
157
+		// STMT_PREPARE is safe to retry.  So we can return ErrBadConn here.
158
+		errLog.Print(err)
159
+		return nil, driver.ErrBadConn
160
+	}
161
+
162
+	stmt := &mysqlStmt{
163
+		mc: mc,
164
+	}
165
+
166
+	// Read Result
167
+	columnCount, err := stmt.readPrepareResultPacket()
168
+	if err == nil {
169
+		if stmt.paramCount > 0 {
170
+			if err = mc.readUntilEOF(); err != nil {
171
+				return nil, err
172
+			}
173
+		}
174
+
175
+		if columnCount > 0 {
176
+			err = mc.readUntilEOF()
177
+		}
178
+	}
179
+
180
+	return stmt, err
181
+}
182
+
183
+func (mc *mysqlConn) interpolateParams(query string, args []driver.Value) (string, error) {
184
+	// Number of ? should be same to len(args)
185
+	if strings.Count(query, "?") != len(args) {
186
+		return "", driver.ErrSkip
187
+	}
188
+
189
+	buf, err := mc.buf.takeCompleteBuffer()
190
+	if err != nil {
191
+		// can not take the buffer. Something must be wrong with the connection
192
+		errLog.Print(err)
193
+		return "", ErrInvalidConn
194
+	}
195
+	buf = buf[:0]
196
+	argPos := 0
197
+
198
+	for i := 0; i < len(query); i++ {
199
+		q := strings.IndexByte(query[i:], '?')
200
+		if q == -1 {
201
+			buf = append(buf, query[i:]...)
202
+			break
203
+		}
204
+		buf = append(buf, query[i:i+q]...)
205
+		i += q
206
+
207
+		arg := args[argPos]
208
+		argPos++
209
+
210
+		if arg == nil {
211
+			buf = append(buf, "NULL"...)
212
+			continue
213
+		}
214
+
215
+		switch v := arg.(type) {
216
+		case int64:
217
+			buf = strconv.AppendInt(buf, v, 10)
218
+		case uint64:
219
+			// Handle uint64 explicitly because our custom ConvertValue emits unsigned values
220
+			buf = strconv.AppendUint(buf, v, 10)
221
+		case float64:
222
+			buf = strconv.AppendFloat(buf, v, 'g', -1, 64)
223
+		case bool:
224
+			if v {
225
+				buf = append(buf, '1')
226
+			} else {
227
+				buf = append(buf, '0')
228
+			}
229
+		case time.Time:
230
+			if v.IsZero() {
231
+				buf = append(buf, "'0000-00-00'"...)
232
+			} else {
233
+				v := v.In(mc.cfg.Loc)
234
+				v = v.Add(time.Nanosecond * 500) // To round under microsecond
235
+				year := v.Year()
236
+				year100 := year / 100
237
+				year1 := year % 100
238
+				month := v.Month()
239
+				day := v.Day()
240
+				hour := v.Hour()
241
+				minute := v.Minute()
242
+				second := v.Second()
243
+				micro := v.Nanosecond() / 1000
244
+
245
+				buf = append(buf, []byte{
246
+					'\'',
247
+					digits10[year100], digits01[year100],
248
+					digits10[year1], digits01[year1],
249
+					'-',
250
+					digits10[month], digits01[month],
251
+					'-',
252
+					digits10[day], digits01[day],
253
+					' ',
254
+					digits10[hour], digits01[hour],
255
+					':',
256
+					digits10[minute], digits01[minute],
257
+					':',
258
+					digits10[second], digits01[second],
259
+				}...)
260
+
261
+				if micro != 0 {
262
+					micro10000 := micro / 10000
263
+					micro100 := micro / 100 % 100
264
+					micro1 := micro % 100
265
+					buf = append(buf, []byte{
266
+						'.',
267
+						digits10[micro10000], digits01[micro10000],
268
+						digits10[micro100], digits01[micro100],
269
+						digits10[micro1], digits01[micro1],
270
+					}...)
271
+				}
272
+				buf = append(buf, '\'')
273
+			}
274
+		case []byte:
275
+			if v == nil {
276
+				buf = append(buf, "NULL"...)
277
+			} else {
278
+				buf = append(buf, "_binary'"...)
279
+				if mc.status&statusNoBackslashEscapes == 0 {
280
+					buf = escapeBytesBackslash(buf, v)
281
+				} else {
282
+					buf = escapeBytesQuotes(buf, v)
283
+				}
284
+				buf = append(buf, '\'')
285
+			}
286
+		case string:
287
+			buf = append(buf, '\'')
288
+			if mc.status&statusNoBackslashEscapes == 0 {
289
+				buf = escapeStringBackslash(buf, v)
290
+			} else {
291
+				buf = escapeStringQuotes(buf, v)
292
+			}
293
+			buf = append(buf, '\'')
294
+		default:
295
+			return "", driver.ErrSkip
296
+		}
297
+
298
+		if len(buf)+4 > mc.maxAllowedPacket {
299
+			return "", driver.ErrSkip
300
+		}
301
+	}
302
+	if argPos != len(args) {
303
+		return "", driver.ErrSkip
304
+	}
305
+	return string(buf), nil
306
+}
307
+
308
+func (mc *mysqlConn) Exec(query string, args []driver.Value) (driver.Result, error) {
309
+	if mc.closed.IsSet() {
310
+		errLog.Print(ErrInvalidConn)
311
+		return nil, driver.ErrBadConn
312
+	}
313
+	if len(args) != 0 {
314
+		if !mc.cfg.InterpolateParams {
315
+			return nil, driver.ErrSkip
316
+		}
317
+		// try to interpolate the parameters to save extra roundtrips for preparing and closing a statement
318
+		prepared, err := mc.interpolateParams(query, args)
319
+		if err != nil {
320
+			return nil, err
321
+		}
322
+		query = prepared
323
+	}
324
+	mc.affectedRows = 0
325
+	mc.insertId = 0
326
+
327
+	err := mc.exec(query)
328
+	if err == nil {
329
+		return &mysqlResult{
330
+			affectedRows: int64(mc.affectedRows),
331
+			insertId:     int64(mc.insertId),
332
+		}, err
333
+	}
334
+	return nil, mc.markBadConn(err)
335
+}
336
+
337
+// Internal function to execute commands
338
+func (mc *mysqlConn) exec(query string) error {
339
+	// Send command
340
+	if err := mc.writeCommandPacketStr(comQuery, query); err != nil {
341
+		return mc.markBadConn(err)
342
+	}
343
+
344
+	// Read Result
345
+	resLen, err := mc.readResultSetHeaderPacket()
346
+	if err != nil {
347
+		return err
348
+	}
349
+
350
+	if resLen > 0 {
351
+		// columns
352
+		if err := mc.readUntilEOF(); err != nil {
353
+			return err
354
+		}
355
+
356
+		// rows
357
+		if err := mc.readUntilEOF(); err != nil {
358
+			return err
359
+		}
360
+	}
361
+
362
+	return mc.discardResults()
363
+}
364
+
365
+func (mc *mysqlConn) Query(query string, args []driver.Value) (driver.Rows, error) {
366
+	return mc.query(query, args)
367
+}
368
+
369
+func (mc *mysqlConn) query(query string, args []driver.Value) (*textRows, error) {
370
+	if mc.closed.IsSet() {
371
+		errLog.Print(ErrInvalidConn)
372
+		return nil, driver.ErrBadConn
373
+	}
374
+	if len(args) != 0 {
375
+		if !mc.cfg.InterpolateParams {
376
+			return nil, driver.ErrSkip
377
+		}
378
+		// try client-side prepare to reduce roundtrip
379
+		prepared, err := mc.interpolateParams(query, args)
380
+		if err != nil {
381
+			return nil, err
382
+		}
383
+		query = prepared
384
+	}
385
+	// Send command
386
+	err := mc.writeCommandPacketStr(comQuery, query)
387
+	if err == nil {
388
+		// Read Result
389
+		var resLen int
390
+		resLen, err = mc.readResultSetHeaderPacket()
391
+		if err == nil {
392
+			rows := new(textRows)
393
+			rows.mc = mc
394
+
395
+			if resLen == 0 {
396
+				rows.rs.done = true
397
+
398
+				switch err := rows.NextResultSet(); err {
399
+				case nil, io.EOF:
400
+					return rows, nil
401
+				default:
402
+					return nil, err
403
+				}
404
+			}
405
+
406
+			// Columns
407
+			rows.rs.columns, err = mc.readColumns(resLen)
408
+			return rows, err
409
+		}
410
+	}
411
+	return nil, mc.markBadConn(err)
412
+}
413
+
414
+// Gets the value of the given MySQL System Variable
415
+// The returned byte slice is only valid until the next read
416
+func (mc *mysqlConn) getSystemVar(name string) ([]byte, error) {
417
+	// Send command
418
+	if err := mc.writeCommandPacketStr(comQuery, "SELECT @@"+name); err != nil {
419
+		return nil, err
420
+	}
421
+
422
+	// Read Result
423
+	resLen, err := mc.readResultSetHeaderPacket()
424
+	if err == nil {
425
+		rows := new(textRows)
426
+		rows.mc = mc
427
+		rows.rs.columns = []mysqlField{{fieldType: fieldTypeVarChar}}
428
+
429
+		if resLen > 0 {
430
+			// Columns
431
+			if err := mc.readUntilEOF(); err != nil {
432
+				return nil, err
433
+			}
434
+		}
435
+
436
+		dest := make([]driver.Value, resLen)
437
+		if err = rows.readRow(dest); err == nil {
438
+			return dest[0].([]byte), mc.readUntilEOF()
439
+		}
440
+	}
441
+	return nil, err
442
+}
443
+
444
+// finish is called when the query has canceled.
445
+func (mc *mysqlConn) cancel(err error) {
446
+	mc.canceled.Set(err)
447
+	mc.cleanup()
448
+}
449
+
450
+// finish is called when the query has succeeded.
451
+func (mc *mysqlConn) finish() {
452
+	if !mc.watching || mc.finished == nil {
453
+		return
454
+	}
455
+	select {
456
+	case mc.finished <- struct{}{}:
457
+		mc.watching = false
458
+	case <-mc.closech:
459
+	}
460
+}
461
+
462
+// Ping implements driver.Pinger interface
463
+func (mc *mysqlConn) Ping(ctx context.Context) (err error) {
464
+	if mc.closed.IsSet() {
465
+		errLog.Print(ErrInvalidConn)
466
+		return driver.ErrBadConn
467
+	}
468
+
469
+	if err = mc.watchCancel(ctx); err != nil {
470
+		return
471
+	}
472
+	defer mc.finish()
473
+
474
+	if err = mc.writeCommandPacket(comPing); err != nil {
475
+		return mc.markBadConn(err)
476
+	}
477
+
478
+	return mc.readResultOK()
479
+}
480
+
481
+// BeginTx implements driver.ConnBeginTx interface
482
+func (mc *mysqlConn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) {
483
+	if err := mc.watchCancel(ctx); err != nil {
484
+		return nil, err
485
+	}
486
+	defer mc.finish()
487
+
488
+	if sql.IsolationLevel(opts.Isolation) != sql.LevelDefault {
489
+		level, err := mapIsolationLevel(opts.Isolation)
490
+		if err != nil {
491
+			return nil, err
492
+		}
493
+		err = mc.exec("SET TRANSACTION ISOLATION LEVEL " + level)
494
+		if err != nil {
495
+			return nil, err
496
+		}
497
+	}
498
+
499
+	return mc.begin(opts.ReadOnly)
500
+}
501
+
502
+func (mc *mysqlConn) QueryContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Rows, error) {
503
+	dargs, err := namedValueToValue(args)
504
+	if err != nil {
505
+		return nil, err
506
+	}
507
+
508
+	if err := mc.watchCancel(ctx); err != nil {
509
+		return nil, err
510
+	}
511
+
512
+	rows, err := mc.query(query, dargs)
513
+	if err != nil {
514
+		mc.finish()
515
+		return nil, err
516
+	}
517
+	rows.finish = mc.finish
518
+	return rows, err
519
+}
520
+
521
+func (mc *mysqlConn) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) {
522
+	dargs, err := namedValueToValue(args)
523
+	if err != nil {
524
+		return nil, err
525
+	}
526
+
527
+	if err := mc.watchCancel(ctx); err != nil {
528
+		return nil, err
529
+	}
530
+	defer mc.finish()
531
+
532
+	return mc.Exec(query, dargs)
533
+}
534
+
535
+func (mc *mysqlConn) PrepareContext(ctx context.Context, query string) (driver.Stmt, error) {
536
+	if err := mc.watchCancel(ctx); err != nil {
537
+		return nil, err
538
+	}
539
+
540
+	stmt, err := mc.Prepare(query)
541
+	mc.finish()
542
+	if err != nil {
543
+		return nil, err
544
+	}
545
+
546
+	select {
547
+	default:
548
+	case <-ctx.Done():
549
+		stmt.Close()
550
+		return nil, ctx.Err()
551
+	}
552
+	return stmt, nil
553
+}
554
+
555
+func (stmt *mysqlStmt) QueryContext(ctx context.Context, args []driver.NamedValue) (driver.Rows, error) {
556
+	dargs, err := namedValueToValue(args)
557
+	if err != nil {
558
+		return nil, err
559
+	}
560
+
561
+	if err := stmt.mc.watchCancel(ctx); err != nil {
562
+		return nil, err
563
+	}
564
+
565
+	rows, err := stmt.query(dargs)
566
+	if err != nil {
567
+		stmt.mc.finish()
568
+		return nil, err
569
+	}
570
+	rows.finish = stmt.mc.finish
571
+	return rows, err
572
+}
573
+
574
+func (stmt *mysqlStmt) ExecContext(ctx context.Context, args []driver.NamedValue) (driver.Result, error) {
575
+	dargs, err := namedValueToValue(args)
576
+	if err != nil {
577
+		return nil, err
578
+	}
579
+
580
+	if err := stmt.mc.watchCancel(ctx); err != nil {
581
+		return nil, err
582
+	}
583
+	defer stmt.mc.finish()
584
+
585
+	return stmt.Exec(dargs)
586
+}
587
+
588
+func (mc *mysqlConn) watchCancel(ctx context.Context) error {
589
+	if mc.watching {
590
+		// Reach here if canceled,
591
+		// so the connection is already invalid
592
+		mc.cleanup()
593
+		return nil
594
+	}
595
+	// When ctx is already cancelled, don't watch it.
596
+	if err := ctx.Err(); err != nil {
597
+		return err
598
+	}
599
+	// When ctx is not cancellable, don't watch it.
600
+	if ctx.Done() == nil {
601
+		return nil
602
+	}
603
+	// When watcher is not alive, can't watch it.
604
+	if mc.watcher == nil {
605
+		return nil
606
+	}
607
+
608
+	mc.watching = true
609
+	mc.watcher <- ctx
610
+	return nil
611
+}
612
+
613
+func (mc *mysqlConn) startWatcher() {
614
+	watcher := make(chan context.Context, 1)
615
+	mc.watcher = watcher
616
+	finished := make(chan struct{})
617
+	mc.finished = finished
618
+	go func() {
619
+		for {
620
+			var ctx context.Context
621
+			select {
622
+			case ctx = <-watcher:
623
+			case <-mc.closech:
624
+				return
625
+			}
626
+
627
+			select {
628
+			case <-ctx.Done():
629
+				mc.cancel(ctx.Err())
630
+			case <-finished:
631
+			case <-mc.closech:
632
+				return
633
+			}
634
+		}
635
+	}()
636
+}
637
+
638
+func (mc *mysqlConn) CheckNamedValue(nv *driver.NamedValue) (err error) {
639
+	nv.Value, err = converter{}.ConvertValue(nv.Value)
640
+	return
641
+}
642
+
643
+// ResetSession implements driver.SessionResetter.
644
+// (From Go 1.10)
645
+func (mc *mysqlConn) ResetSession(ctx context.Context) error {
646
+	if mc.closed.IsSet() {
647
+		return driver.ErrBadConn
648
+	}
649
+	mc.reset = true
650
+	return nil
651
+}

+ 146
- 0
vendor/github.com/go-sql-driver/mysql/connector.go View File

@@ -0,0 +1,146 @@
1
+// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
2
+//
3
+// Copyright 2018 The Go-MySQL-Driver Authors. All rights reserved.
4
+//
5
+// This Source Code Form is subject to the terms of the Mozilla Public
6
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
7
+// You can obtain one at http://mozilla.org/MPL/2.0/.
8
+
9
+package mysql
10
+
11
+import (
12
+	"context"
13
+	"database/sql/driver"
14
+	"net"
15
+)
16
+
17
+type connector struct {
18
+	cfg *Config // immutable private copy.
19
+}
20
+
21
+// Connect implements driver.Connector interface.
22
+// Connect returns a connection to the database.
23
+func (c *connector) Connect(ctx context.Context) (driver.Conn, error) {
24
+	var err error
25
+
26
+	// New mysqlConn
27
+	mc := &mysqlConn{
28
+		maxAllowedPacket: maxPacketSize,
29
+		maxWriteSize:     maxPacketSize - 1,
30
+		closech:          make(chan struct{}),
31
+		cfg:              c.cfg,
32
+	}
33
+	mc.parseTime = mc.cfg.ParseTime
34
+
35
+	// Connect to Server
36
+	dialsLock.RLock()
37
+	dial, ok := dials[mc.cfg.Net]
38
+	dialsLock.RUnlock()
39
+	if ok {
40
+		dctx := ctx
41
+		if mc.cfg.Timeout > 0 {
42
+			var cancel context.CancelFunc
43
+			dctx, cancel = context.WithTimeout(ctx, c.cfg.Timeout)
44
+			defer cancel()
45
+		}
46
+		mc.netConn, err = dial(dctx, mc.cfg.Addr)
47
+	} else {
48
+		nd := net.Dialer{Timeout: mc.cfg.Timeout}
49
+		mc.netConn, err = nd.DialContext(ctx, mc.cfg.Net, mc.cfg.Addr)
50
+	}
51
+
52
+	if err != nil {
53
+		return nil, err
54
+	}
55
+
56
+	// Enable TCP Keepalives on TCP connections
57
+	if tc, ok := mc.netConn.(*net.TCPConn); ok {
58
+		if err := tc.SetKeepAlive(true); err != nil {
59
+			// Don't send COM_QUIT before handshake.
60
+			mc.netConn.Close()
61
+			mc.netConn = nil
62
+			return nil, err
63
+		}
64
+	}
65
+
66
+	// Call startWatcher for context support (From Go 1.8)
67
+	mc.startWatcher()
68
+	if err := mc.watchCancel(ctx); err != nil {
69
+		mc.cleanup()
70
+		return nil, err
71
+	}
72
+	defer mc.finish()
73
+
74
+	mc.buf = newBuffer(mc.netConn)
75
+
76
+	// Set I/O timeouts
77
+	mc.buf.timeout = mc.cfg.ReadTimeout
78
+	mc.writeTimeout = mc.cfg.WriteTimeout
79
+
80
+	// Reading Handshake Initialization Packet
81
+	authData, plugin, err := mc.readHandshakePacket()
82
+	if err != nil {
83
+		mc.cleanup()
84
+		return nil, err
85
+	}
86
+
87
+	if plugin == "" {
88
+		plugin = defaultAuthPlugin
89
+	}
90
+
91
+	// Send Client Authentication Packet
92
+	authResp, err := mc.auth(authData, plugin)
93
+	if err != nil {
94
+		// try the default auth plugin, if using the requested plugin failed
95
+		errLog.Print("could not use requested auth plugin '"+plugin+"': ", err.Error())
96
+		plugin = defaultAuthPlugin
97
+		authResp, err = mc.auth(authData, plugin)
98
+		if err != nil {
99
+			mc.cleanup()
100
+			return nil, err
101
+		}
102
+	}
103
+	if err = mc.writeHandshakeResponsePacket(authResp, plugin); err != nil {
104
+		mc.cleanup()
105
+		return nil, err
106
+	}
107
+
108
+	// Handle response to auth packet, switch methods if possible
109
+	if err = mc.handleAuthResult(authData, plugin); err != nil {
110
+		// Authentication failed and MySQL has already closed the connection
111
+		// (https://dev.mysql.com/doc/internals/en/authentication-fails.html).
112
+		// Do not send COM_QUIT, just cleanup and return the error.
113
+		mc.cleanup()
114
+		return nil, err
115
+	}
116
+
117
+	if mc.cfg.MaxAllowedPacket > 0 {
118
+		mc.maxAllowedPacket = mc.cfg.MaxAllowedPacket
119
+	} else {
120
+		// Get max allowed packet size
121
+		maxap, err := mc.getSystemVar("max_allowed_packet")
122
+		if err != nil {
123
+			mc.Close()
124
+			return nil, err
125
+		}
126
+		mc.maxAllowedPacket = stringToInt(maxap) - 1
127
+	}
128
+	if mc.maxAllowedPacket < maxPacketSize {
129
+		mc.maxWriteSize = mc.maxAllowedPacket
130
+	}
131
+
132
+	// Handle DSN Params
133
+	err = mc.handleParams()
134
+	if err != nil {
135
+		mc.Close()
136
+		return nil, err
137
+	}
138
+
139
+	return mc, nil
140
+}
141
+
142
+// Driver implements driver.Connector interface.
143
+// Driver returns &MySQLDriver{}.
144
+func (c *connector) Driver() driver.Driver {
145
+	return &MySQLDriver{}
146
+}

+ 174
- 0
vendor/github.com/go-sql-driver/mysql/const.go View File

@@ -0,0 +1,174 @@
1
+// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
2
+//
3
+// Copyright 2012 The Go-MySQL-Driver Authors. All rights reserved.
4
+//
5
+// This Source Code Form is subject to the terms of the Mozilla Public
6
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
7
+// You can obtain one at http://mozilla.org/MPL/2.0/.
8
+
9
+package mysql
10
+
11
+const (
12
+	defaultAuthPlugin       = "mysql_native_password"
13
+	defaultMaxAllowedPacket = 4 << 20 // 4 MiB
14
+	minProtocolVersion      = 10
15
+	maxPacketSize           = 1<<24 - 1
16
+	timeFormat              = "2006-01-02 15:04:05.999999"
17
+)
18
+
19
+// MySQL constants documentation:
20
+// http://dev.mysql.com/doc/internals/en/client-server-protocol.html
21
+
22
+const (
23
+	iOK           byte = 0x00
24
+	iAuthMoreData byte = 0x01
25
+	iLocalInFile  byte = 0xfb
26
+	iEOF          byte = 0xfe
27
+	iERR          byte = 0xff
28
+)
29
+
30
+// https://dev.mysql.com/doc/internals/en/capability-flags.html#packet-Protocol::CapabilityFlags
31
+type clientFlag uint32
32
+
33
+const (
34
+	clientLongPassword clientFlag = 1 << iota
35
+	clientFoundRows
36
+	clientLongFlag
37
+	clientConnectWithDB
38
+	clientNoSchema
39
+	clientCompress
40
+	clientODBC
41
+	clientLocalFiles
42
+	clientIgnoreSpace
43
+	clientProtocol41
44
+	clientInteractive
45
+	clientSSL
46
+	clientIgnoreSIGPIPE
47
+	clientTransactions
48
+	clientReserved
49
+	clientSecureConn
50
+	clientMultiStatements
51
+	clientMultiResults
52
+	clientPSMultiResults
53
+	clientPluginAuth
54
+	clientConnectAttrs
55
+	clientPluginAuthLenEncClientData
56
+	clientCanHandleExpiredPasswords
57
+	clientSessionTrack
58
+	clientDeprecateEOF
59
+)
60
+
61
+const (
62
+	comQuit byte = iota + 1
63
+	comInitDB
64
+	comQuery
65
+	comFieldList
66
+	comCreateDB
67
+	comDropDB
68
+	comRefresh
69
+	comShutdown
70
+	comStatistics
71
+	comProcessInfo
72
+	comConnect
73
+	comProcessKill
74
+	comDebug
75
+	comPing
76
+	comTime
77
+	comDelayedInsert
78
+	comChangeUser
79
+	comBinlogDump
80
+	comTableDump
81
+	comConnectOut
82
+	comRegisterSlave
83
+	comStmtPrepare
84
+	comStmtExecute
85
+	comStmtSendLongData
86
+	comStmtClose
87
+	comStmtReset
88
+	comSetOption
89
+	comStmtFetch
90
+)
91
+
92
+// https://dev.mysql.com/doc/internals/en/com-query-response.html#packet-Protocol::ColumnType
93
+type fieldType byte
94
+
95
+const (
96
+	fieldTypeDecimal fieldType = iota
97
+	fieldTypeTiny
98
+	fieldTypeShort
99
+	fieldTypeLong
100
+	fieldTypeFloat
101
+	fieldTypeDouble
102
+	fieldTypeNULL
103
+	fieldTypeTimestamp
104
+	fieldTypeLongLong
105
+	fieldTypeInt24
106
+	fieldTypeDate
107
+	fieldTypeTime
108
+	fieldTypeDateTime
109
+	fieldTypeYear
110
+	fieldTypeNewDate
111
+	fieldTypeVarChar
112
+	fieldTypeBit
113
+)
114
+const (
115
+	fieldTypeJSON fieldType = iota + 0xf5
116
+	fieldTypeNewDecimal
117
+	fieldTypeEnum
118
+	fieldTypeSet
119
+	fieldTypeTinyBLOB
120
+	fieldTypeMediumBLOB
121
+	fieldTypeLongBLOB
122
+	fieldTypeBLOB
123
+	fieldTypeVarString
124
+	fieldTypeString
125
+	fieldTypeGeometry
126
+)
127
+
128
+type fieldFlag uint16
129
+
130
+const (
131
+	flagNotNULL fieldFlag = 1 << iota
132
+	flagPriKey
133
+	flagUniqueKey
134
+	flagMultipleKey
135
+	flagBLOB
136
+	flagUnsigned
137
+	flagZeroFill
138
+	flagBinary
139
+	flagEnum
140
+	flagAutoIncrement
141
+	flagTimestamp
142
+	flagSet
143
+	flagUnknown1
144
+	flagUnknown2
145
+	flagUnknown3
146
+	flagUnknown4
147
+)
148
+
149
+// http://dev.mysql.com/doc/internals/en/status-flags.html
150
+type statusFlag uint16
151
+
152
+const (
153
+	statusInTrans statusFlag = 1 << iota
154
+	statusInAutocommit
155
+	statusReserved // Not in documentation
156
+	statusMoreResultsExists
157
+	statusNoGoodIndexUsed
158
+	statusNoIndexUsed
159
+	statusCursorExists
160
+	statusLastRowSent
161
+	statusDbDropped
162
+	statusNoBackslashEscapes
163
+	statusMetadataChanged
164
+	statusQueryWasSlow
165
+	statusPsOutParams
166
+	statusInTransReadonly
167
+	statusSessionStateChanged
168
+)
169
+
170
+const (
171
+	cachingSha2PasswordRequestPublicKey          = 2
172
+	cachingSha2PasswordFastAuthSuccess           = 3
173
+	cachingSha2PasswordPerformFullAuthentication = 4
174
+)

+ 107
- 0
vendor/github.com/go-sql-driver/mysql/driver.go View File

@@ -0,0 +1,107 @@
1
+// Copyright 2012 The Go-MySQL-Driver Authors. All rights reserved.
2
+//
3
+// This Source Code Form is subject to the terms of the Mozilla Public
4
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
5
+// You can obtain one at http://mozilla.org/MPL/2.0/.
6
+
7
+// Package mysql provides a MySQL driver for Go's database/sql package.
8
+//
9
+// The driver should be used via the database/sql package:
10
+//
11
+//  import "database/sql"
12
+//  import _ "github.com/go-sql-driver/mysql"
13
+//
14
+//  db, err := sql.Open("mysql", "user:password@/dbname")
15
+//
16
+// See https://github.com/go-sql-driver/mysql#usage for details
17
+package mysql
18
+
19
+import (
20
+	"context"
21
+	"database/sql"
22
+	"database/sql/driver"
23
+	"net"
24
+	"sync"
25
+)
26
+
27
+// MySQLDriver is exported to make the driver directly accessible.
28
+// In general the driver is used via the database/sql package.
29
+type MySQLDriver struct{}
30
+
31
+// DialFunc is a function which can be used to establish the network connection.
32
+// Custom dial functions must be registered with RegisterDial
33
+//
34
+// Deprecated: users should register a DialContextFunc instead
35
+type DialFunc func(addr string) (net.Conn, error)
36
+
37
+// DialContextFunc is a function which can be used to establish the network connection.
38
+// Custom dial functions must be registered with RegisterDialContext
39
+type DialContextFunc func(ctx context.Context, addr string) (net.Conn, error)
40
+
41
+var (
42
+	dialsLock sync.RWMutex
43
+	dials     map[string]DialContextFunc
44
+)
45
+
46
+// RegisterDialContext registers a custom dial function. It can then be used by the
47
+// network address mynet(addr), where mynet is the registered new network.
48
+// The current context for the connection and its address is passed to the dial function.
49
+func RegisterDialContext(net string, dial DialContextFunc) {
50
+	dialsLock.Lock()
51
+	defer dialsLock.Unlock()
52
+	if dials == nil {
53
+		dials = make(map[string]DialContextFunc)
54
+	}
55
+	dials[net] = dial
56
+}
57
+
58
+// RegisterDial registers a custom dial function. It can then be used by the
59
+// network address mynet(addr), where mynet is the registered new network.
60
+// addr is passed as a parameter to the dial function.
61
+//
62
+// Deprecated: users should call RegisterDialContext instead
63
+func RegisterDial(network string, dial DialFunc) {
64
+	RegisterDialContext(network, func(_ context.Context, addr string) (net.Conn, error) {
65
+		return dial(addr)
66
+	})
67
+}
68
+
69
+// Open new Connection.
70
+// See https://github.com/go-sql-driver/mysql#dsn-data-source-name for how
71
+// the DSN string is formatted
72
+func (d MySQLDriver) Open(dsn string) (driver.Conn, error) {
73
+	cfg, err := ParseDSN(dsn)
74
+	if err != nil {
75
+		return nil, err
76
+	}
77
+	c := &connector{
78
+		cfg: cfg,
79
+	}
80
+	return c.Connect(context.Background())
81
+}
82
+
83
+func init() {
84
+	sql.Register("mysql", &MySQLDriver{})
85
+}
86
+
87
+// NewConnector returns new driver.Connector.
88
+func NewConnector(cfg *Config) (driver.Connector, error) {
89
+	cfg = cfg.Clone()
90
+	// normalize the contents of cfg so calls to NewConnector have the same
91
+	// behavior as MySQLDriver.OpenConnector
92
+	if err := cfg.normalize(); err != nil {
93
+		return nil, err
94
+	}
95
+	return &connector{cfg: cfg}, nil
96
+}
97
+
98
+// OpenConnector implements driver.DriverContext.
99
+func (d MySQLDriver) OpenConnector(dsn string) (driver.Connector, error) {
100
+	cfg, err := ParseDSN(dsn)
101
+	if err != nil {
102
+		return nil, err
103
+	}
104
+	return &connector{
105
+		cfg: cfg,
106
+	}, nil
107
+}

+ 560
- 0
vendor/github.com/go-sql-driver/mysql/dsn.go View File

@@ -0,0 +1,560 @@
1
+// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
2
+//
3
+// Copyright 2016 The Go-MySQL-Driver Authors. All rights reserved.
4
+//
5
+// This Source Code Form is subject to the terms of the Mozilla Public
6
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
7
+// You can obtain one at http://mozilla.org/MPL/2.0/.
8
+
9
+package mysql
10
+
11
+import (
12
+	"bytes"
13
+	"crypto/rsa"
14
+	"crypto/tls"
15
+	"errors"
16
+	"fmt"
17
+	"math/big"
18
+	"net"
19
+	"net/url"
20
+	"sort"
21
+	"strconv"
22
+	"strings"
23
+	"time"
24
+)
25
+
26
+var (
27
+	errInvalidDSNUnescaped       = errors.New("invalid DSN: did you forget to escape a param value?")
28
+	errInvalidDSNAddr            = errors.New("invalid DSN: network address not terminated (missing closing brace)")
29
+	errInvalidDSNNoSlash         = errors.New("invalid DSN: missing the slash separating the database name")
30
+	errInvalidDSNUnsafeCollation = errors.New("invalid DSN: interpolateParams can not be used with unsafe collations")
31
+)
32
+
33
+// Config is a configuration parsed from a DSN string.
34
+// If a new Config is created instead of being parsed from a DSN string,
35
+// the NewConfig function should be used, which sets default values.
36
+type Config struct {
37
+	User             string            // Username
38
+	Passwd           string            // Password (requires User)
39
+	Net              string            // Network type
40
+	Addr             string            // Network address (requires Net)
41
+	DBName           string            // Database name
42
+	Params           map[string]string // Connection parameters
43
+	Collation        string            // Connection collation
44
+	Loc              *time.Location    // Location for time.Time values
45
+	MaxAllowedPacket int               // Max packet size allowed
46
+	ServerPubKey     string            // Server public key name
47
+	pubKey           *rsa.PublicKey    // Server public key
48
+	TLSConfig        string            // TLS configuration name
49
+	tls              *tls.Config       // TLS configuration
50
+	Timeout          time.Duration     // Dial timeout
51
+	ReadTimeout      time.Duration     // I/O read timeout
52
+	WriteTimeout     time.Duration     // I/O write timeout
53
+
54
+	AllowAllFiles           bool // Allow all files to be used with LOAD DATA LOCAL INFILE
55
+	AllowCleartextPasswords bool // Allows the cleartext client side plugin
56
+	AllowNativePasswords    bool // Allows the native password authentication method
57
+	AllowOldPasswords       bool // Allows the old insecure password method
58
+	CheckConnLiveness       bool // Check connections for liveness before using them
59
+	ClientFoundRows         bool // Return number of matching rows instead of rows changed
60
+	ColumnsWithAlias        bool // Prepend table alias to column names
61
+	InterpolateParams       bool // Interpolate placeholders into query string
62
+	MultiStatements         bool // Allow multiple statements in one query
63
+	ParseTime               bool // Parse time values to time.Time
64
+	RejectReadOnly          bool // Reject read-only connections
65
+}
66
+
67
+// NewConfig creates a new Config and sets default values.
68
+func NewConfig() *Config {
69
+	return &Config{
70
+		Collation:            defaultCollation,
71
+		Loc:                  time.UTC,
72
+		MaxAllowedPacket:     defaultMaxAllowedPacket,
73
+		AllowNativePasswords: true,
74
+		CheckConnLiveness:    true,
75
+	}
76
+}
77
+
78
+func (cfg *Config) Clone() *Config {
79
+	cp := *cfg
80
+	if cp.tls != nil {
81
+		cp.tls = cfg.tls.Clone()
82
+	}
83
+	if len(cp.Params) > 0 {
84
+		cp.Params = make(map[string]string, len(cfg.Params))
85
+		for k, v := range cfg.Params {
86
+			cp.Params[k] = v
87
+		}
88
+	}
89
+	if cfg.pubKey != nil {
90
+		cp.pubKey = &rsa.PublicKey{
91
+			N: new(big.Int).Set(cfg.pubKey.N),
92
+			E: cfg.pubKey.E,
93
+		}
94
+	}
95
+	return &cp
96
+}
97
+
98
+func (cfg *Config) normalize() error {
99
+	if cfg.InterpolateParams && unsafeCollations[cfg.Collation] {
100
+		return errInvalidDSNUnsafeCollation
101
+	}
102
+
103
+	// Set default network if empty
104
+	if cfg.Net == "" {
105
+		cfg.Net = "tcp"
106
+	}
107
+
108
+	// Set default address if empty
109
+	if cfg.Addr == "" {
110
+		switch cfg.Net {
111
+		case "tcp":
112
+			cfg.Addr = "127.0.0.1:3306"
113
+		case "unix":
114
+			cfg.Addr = "/tmp/mysql.sock"
115
+		default:
116
+			return errors.New("default addr for network '" + cfg.Net + "' unknown")
117
+		}
118
+	} else if cfg.Net == "tcp" {
119
+		cfg.Addr = ensureHavePort(cfg.Addr)
120
+	}
121
+
122
+	switch cfg.TLSConfig {
123
+	case "false", "":
124
+		// don't set anything
125
+	case "true":
126
+		cfg.tls = &tls.Config{}
127
+	case "skip-verify", "preferred":
128
+		cfg.tls = &tls.Config{InsecureSkipVerify: true}
129
+	default:
130
+		cfg.tls = getTLSConfigClone(cfg.TLSConfig)
131
+		if cfg.tls == nil {
132
+			return errors.New("invalid value / unknown config name: " + cfg.TLSConfig)
133
+		}
134
+	}
135
+
136
+	if cfg.tls != nil && cfg.tls.ServerName == "" && !cfg.tls.InsecureSkipVerify {
137
+		host, _, err := net.SplitHostPort(cfg.Addr)
138
+		if err == nil {
139
+			cfg.tls.ServerName = host
140
+		}
141
+	}
142
+
143
+	if cfg.ServerPubKey != "" {
144
+		cfg.pubKey = getServerPubKey(cfg.ServerPubKey)
145
+		if cfg.pubKey == nil {
146
+			return errors.New("invalid value / unknown server pub key name: " + cfg.ServerPubKey)
147
+		}
148
+	}
149
+
150
+	return nil
151
+}
152
+
153
+func writeDSNParam(buf *bytes.Buffer, hasParam *bool, name, value string) {
154
+	buf.Grow(1 + len(name) + 1 + len(value))
155
+	if !*hasParam {
156
+		*hasParam = true
157
+		buf.WriteByte('?')
158
+	} else {
159
+		buf.WriteByte('&')
160
+	}
161
+	buf.WriteString(name)
162
+	buf.WriteByte('=')
163
+	buf.WriteString(value)
164
+}
165
+
166
+// FormatDSN formats the given Config into a DSN string which can be passed to
167
+// the driver.
168
+func (cfg *Config) FormatDSN() string {
169
+	var buf bytes.Buffer
170
+
171
+	// [username[:password]@]
172
+	if len(cfg.User) > 0 {
173
+		buf.WriteString(cfg.User)
174
+		if len(cfg.Passwd) > 0 {
175
+			buf.WriteByte(':')
176
+			buf.WriteString(cfg.Passwd)
177
+		}
178
+		buf.WriteByte('@')
179
+	}
180
+
181
+	// [protocol[(address)]]
182
+	if len(cfg.Net) > 0 {
183
+		buf.WriteString(cfg.Net)
184
+		if len(cfg.Addr) > 0 {
185
+			buf.WriteByte('(')
186
+			buf.WriteString(cfg.Addr)
187
+			buf.WriteByte(')')
188
+		}
189
+	}
190
+
191
+	// /dbname
192
+	buf.WriteByte('/')
193
+	buf.WriteString(cfg.DBName)
194
+
195
+	// [?param1=value1&...&paramN=valueN]
196
+	hasParam := false
197
+
198
+	if cfg.AllowAllFiles {
199
+		hasParam = true
200
+		buf.WriteString("?allowAllFiles=true")
201
+	}
202
+
203
+	if cfg.AllowCleartextPasswords {
204
+		writeDSNParam(&buf, &hasParam, "allowCleartextPasswords", "true")
205
+	}
206
+
207
+	if !cfg.AllowNativePasswords {
208
+		writeDSNParam(&buf, &hasParam, "allowNativePasswords", "false")
209
+	}
210
+
211
+	if cfg.AllowOldPasswords {
212
+		writeDSNParam(&buf, &hasParam, "allowOldPasswords", "true")
213
+	}
214
+
215
+	if !cfg.CheckConnLiveness {
216
+		writeDSNParam(&buf, &hasParam, "checkConnLiveness", "false")
217
+	}
218
+
219
+	if cfg.ClientFoundRows {
220
+		writeDSNParam(&buf, &hasParam, "clientFoundRows", "true")
221
+	}
222
+
223
+	if col := cfg.Collation; col != defaultCollation && len(col) > 0 {
224
+		writeDSNParam(&buf, &hasParam, "collation", col)
225
+	}
226
+
227
+	if cfg.ColumnsWithAlias {
228
+		writeDSNParam(&buf, &hasParam, "columnsWithAlias", "true")
229
+	}
230
+
231
+	if cfg.InterpolateParams {
232
+		writeDSNParam(&buf, &hasParam, "interpolateParams", "true")
233
+	}
234
+
235
+	if cfg.Loc != time.UTC && cfg.Loc != nil {
236
+		writeDSNParam(&buf, &hasParam, "loc", url.QueryEscape(cfg.Loc.String()))
237
+	}
238
+
239
+	if cfg.MultiStatements {
240
+		writeDSNParam(&buf, &hasParam, "multiStatements", "true")
241
+	}
242
+
243
+	if cfg.ParseTime {
244
+		writeDSNParam(&buf, &hasParam, "parseTime", "true")
245
+	}
246
+
247
+	if cfg.ReadTimeout > 0 {
248
+		writeDSNParam(&buf, &hasParam, "readTimeout", cfg.ReadTimeout.String())
249
+	}
250
+
251
+	if cfg.RejectReadOnly {
252
+		writeDSNParam(&buf, &hasParam, "rejectReadOnly", "true")
253
+	}
254
+
255
+	if len(cfg.ServerPubKey) > 0 {
256
+		writeDSNParam(&buf, &hasParam, "serverPubKey", url.QueryEscape(cfg.ServerPubKey))
257
+	}
258
+
259
+	if cfg.Timeout > 0 {
260
+		writeDSNParam(&buf, &hasParam, "timeout", cfg.Timeout.String())
261
+	}
262
+
263
+	if len(cfg.TLSConfig) > 0 {
264
+		writeDSNParam(&buf, &hasParam, "tls", url.QueryEscape(cfg.TLSConfig))
265
+	}
266
+
267
+	if cfg.WriteTimeout > 0 {
268
+		writeDSNParam(&buf, &hasParam, "writeTimeout", cfg.WriteTimeout.String())
269
+	}
270
+
271
+	if cfg.MaxAllowedPacket != defaultMaxAllowedPacket {
272
+		writeDSNParam(&buf, &hasParam, "maxAllowedPacket", strconv.Itoa(cfg.MaxAllowedPacket))
273
+	}
274
+
275
+	// other params
276
+	if cfg.Params != nil {
277
+		var params []string
278
+		for param := range cfg.Params {
279
+			params = append(params, param)
280
+		}
281
+		sort.Strings(params)
282
+		for _, param := range params {
283
+			writeDSNParam(&buf, &hasParam, param, url.QueryEscape(cfg.Params[param]))
284
+		}
285
+	}
286
+
287
+	return buf.String()
288
+}
289
+
290
+// ParseDSN parses the DSN string to a Config
291
+func ParseDSN(dsn string) (cfg *Config, err error) {
292
+	// New config with some default values
293
+	cfg = NewConfig()
294
+
295
+	// [user[:password]@][net[(addr)]]/dbname[?param1=value1&paramN=valueN]
296
+	// Find the last '/' (since the password or the net addr might contain a '/')
297
+	foundSlash := false
298
+	for i := len(dsn) - 1; i >= 0; i-- {
299
+		if dsn[i] == '/' {
300
+			foundSlash = true
301
+			var j, k int
302
+
303
+			// left part is empty if i <= 0
304
+			if i > 0 {
305
+				// [username[:password]@][protocol[(address)]]
306
+				// Find the last '@' in dsn[:i]
307
+				for j = i; j >= 0; j-- {
308
+					if dsn[j] == '@' {
309
+						// username[:password]
310
+						// Find the first ':' in dsn[:j]
311
+						for k = 0; k < j; k++ {
312
+							if dsn[k] == ':' {
313
+								cfg.Passwd = dsn[k+1 : j]
314
+								break
315
+							}
316
+						}
317
+						cfg.User = dsn[:k]
318
+
319
+						break
320
+					}
321
+				}
322
+
323
+				// [protocol[(address)]]
324
+				// Find the first '(' in dsn[j+1:i]
325
+				for k = j + 1; k < i; k++ {
326
+					if dsn[k] == '(' {
327
+						// dsn[i-1] must be == ')' if an address is specified
328
+						if dsn[i-1] != ')' {
329
+							if strings.ContainsRune(dsn[k+1:i], ')') {
330
+								return nil, errInvalidDSNUnescaped
331
+							}
332
+							return nil, errInvalidDSNAddr
333
+						}
334
+						cfg.Addr = dsn[k+1 : i-1]
335
+						break
336
+					}
337
+				}
338
+				cfg.Net = dsn[j+1 : k]
339
+			}
340
+
341
+			// dbname[?param1=value1&...&paramN=valueN]
342
+			// Find the first '?' in dsn[i+1:]
343
+			for j = i + 1; j < len(dsn); j++ {
344
+				if dsn[j] == '?' {
345
+					if err = parseDSNParams(cfg, dsn[j+1:]); err != nil {
346
+						return
347
+					}
348
+					break
349
+				}
350
+			}
351
+			cfg.DBName = dsn[i+1 : j]
352
+
353
+			break
354
+		}
355
+	}
356
+
357
+	if !foundSlash && len(dsn) > 0 {
358
+		return nil, errInvalidDSNNoSlash
359
+	}
360
+
361
+	if err = cfg.normalize(); err != nil {
362
+		return nil, err
363
+	}
364
+	return
365
+}
366
+
367
+// parseDSNParams parses the DSN "query string"
368
+// Values must be url.QueryEscape'ed
369
+func parseDSNParams(cfg *Config, params string) (err error) {
370
+	for _, v := range strings.Split(params, "&") {
371
+		param := strings.SplitN(v, "=", 2)
372
+		if len(param) != 2 {
373
+			continue
374
+		}
375
+
376
+		// cfg params
377
+		switch value := param[1]; param[0] {
378
+		// Disable INFILE whitelist / enable all files
379
+		case "allowAllFiles":
380
+			var isBool bool
381
+			cfg.AllowAllFiles, isBool = readBool(value)
382
+			if !isBool {
383
+				return errors.New("invalid bool value: " + value)
384
+			}
385
+
386
+		// Use cleartext authentication mode (MySQL 5.5.10+)
387
+		case "allowCleartextPasswords":
388
+			var isBool bool
389
+			cfg.AllowCleartextPasswords, isBool = readBool(value)
390
+			if !isBool {
391
+				return errors.New("invalid bool value: " + value)
392
+			}
393
+
394
+		// Use native password authentication
395
+		case "allowNativePasswords":
396
+			var isBool bool
397
+			cfg.AllowNativePasswords, isBool = readBool(value)
398
+			if !isBool {
399
+				return errors.New("invalid bool value: " + value)
400
+			}
401
+
402
+		// Use old authentication mode (pre MySQL 4.1)
403
+		case "allowOldPasswords":
404
+			var isBool bool
405
+			cfg.AllowOldPasswords, isBool = readBool(value)
406
+			if !isBool {
407
+				return errors.New("invalid bool value: " + value)
408
+			}
409
+
410
+		// Check connections for Liveness before using them
411
+		case "checkConnLiveness":
412
+			var isBool bool
413
+			cfg.CheckConnLiveness, isBool = readBool(value)
414
+			if !isBool {
415
+				return errors.New("invalid bool value: " + value)
416
+			}
417
+
418
+		// Switch "rowsAffected" mode
419
+		case "clientFoundRows":
420
+			var isBool bool
421
+			cfg.ClientFoundRows, isBool = readBool(value)
422
+			if !isBool {
423
+				return errors.New("invalid bool value: " + value)
424
+			}
425
+
426
+		// Collation
427
+		case "collation":
428
+			cfg.Collation = value
429
+			break
430
+
431
+		case "columnsWithAlias":
432
+			var isBool bool
433
+			cfg.ColumnsWithAlias, isBool = readBool(value)
434
+			if !isBool {
435
+				return errors.New("invalid bool value: " + value)
436
+			}
437
+
438
+		// Compression
439
+		case "compress":
440
+			return errors.New("compression not implemented yet")
441
+
442
+		// Enable client side placeholder substitution
443
+		case "interpolateParams":
444
+			var isBool bool
445
+			cfg.InterpolateParams, isBool = readBool(value)
446
+			if !isBool {
447
+				return errors.New("invalid bool value: " + value)
448
+			}
449
+
450
+		// Time Location
451
+		case "loc":
452
+			if value, err = url.QueryUnescape(value); err != nil {
453
+				return
454
+			}
455
+			cfg.Loc, err = time.LoadLocation(value)
456
+			if err != nil {
457
+				return
458
+			}
459
+
460
+		// multiple statements in one query
461
+		case "multiStatements":
462
+			var isBool bool
463
+			cfg.MultiStatements, isBool = readBool(value)
464
+			if !isBool {
465
+				return errors.New("invalid bool value: " + value)
466
+			}
467
+
468
+		// time.Time parsing
469
+		case "parseTime":
470
+			var isBool bool
471
+			cfg.ParseTime, isBool = readBool(value)
472
+			if !isBool {
473
+				return errors.New("invalid bool value: " + value)
474
+			}
475
+
476
+		// I/O read Timeout
477
+		case "readTimeout":
478
+			cfg.ReadTimeout, err = time.ParseDuration(value)
479
+			if err != nil {
480
+				return
481
+			}
482
+
483
+		// Reject read-only connections
484
+		case "rejectReadOnly":
485
+			var isBool bool
486
+			cfg.RejectReadOnly, isBool = readBool(value)
487
+			if !isBool {
488
+				return errors.New("invalid bool value: " + value)
489
+			}
490
+
491
+		// Server public key
492
+		case "serverPubKey":
493
+			name, err := url.QueryUnescape(value)
494
+			if err != nil {
495
+				return fmt.Errorf("invalid value for server pub key name: %v", err)
496
+			}
497
+			cfg.ServerPubKey = name
498
+
499
+		// Strict mode
500
+		case "strict":
501
+			panic("strict mode has been removed. See https://github.com/go-sql-driver/mysql/wiki/strict-mode")
502
+
503
+		// Dial Timeout
504
+		case "timeout":
505
+			cfg.Timeout, err = time.ParseDuration(value)
506
+			if err != nil {
507
+				return
508
+			}
509
+
510
+		// TLS-Encryption
511
+		case "tls":
512
+			boolValue, isBool := readBool(value)
513
+			if isBool {
514
+				if boolValue {
515
+					cfg.TLSConfig = "true"
516
+				} else {
517
+					cfg.TLSConfig = "false"
518
+				}
519
+			} else if vl := strings.ToLower(value); vl == "skip-verify" || vl == "preferred" {
520
+				cfg.TLSConfig = vl
521
+			} else {
522
+				name, err := url.QueryUnescape(value)
523
+				if err != nil {
524
+					return fmt.Errorf("invalid value for TLS config name: %v", err)
525
+				}
526
+				cfg.TLSConfig = name
527
+			}
528
+
529
+		// I/O write Timeout
530
+		case "writeTimeout":
531
+			cfg.WriteTimeout, err = time.ParseDuration(value)
532
+			if err != nil {
533
+				return
534
+			}
535
+		case "maxAllowedPacket":
536
+			cfg.MaxAllowedPacket, err = strconv.Atoi(value)
537
+			if err != nil {
538
+				return
539
+			}
540
+		default:
541
+			// lazy init
542
+			if cfg.Params == nil {
543
+				cfg.Params = make(map[string]string)
544
+			}
545
+
546
+			if cfg.Params[param[0]], err = url.QueryUnescape(value); err != nil {
547
+				return
548
+			}
549
+		}
550
+	}
551
+
552
+	return
553
+}
554
+
555
+func ensureHavePort(addr string) string {
556
+	if _, _, err := net.SplitHostPort(addr); err != nil {
557
+		return net.JoinHostPort(addr, "3306")
558
+	}
559
+	return addr
560
+}

+ 65
- 0
vendor/github.com/go-sql-driver/mysql/errors.go View File

@@ -0,0 +1,65 @@
1
+// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
2
+//
3
+// Copyright 2013 The Go-MySQL-Driver Authors. All rights reserved.
4
+//
5
+// This Source Code Form is subject to the terms of the Mozilla Public
6
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
7
+// You can obtain one at http://mozilla.org/MPL/2.0/.
8
+
9
+package mysql
10
+
11
+import (
12
+	"errors"
13
+	"fmt"
14
+	"log"
15
+	"os"
16
+)
17
+
18
+// Various errors the driver might return. Can change between driver versions.
19
+var (
20
+	ErrInvalidConn       = errors.New("invalid connection")
21
+	ErrMalformPkt        = errors.New("malformed packet")
22
+	ErrNoTLS             = errors.New("TLS requested but server does not support TLS")
23
+	ErrCleartextPassword = errors.New("this user requires clear text authentication. If you still want to use it, please add 'allowCleartextPasswords=1' to your DSN")
24
+	ErrNativePassword    = errors.New("this user requires mysql native password authentication.")
25
+	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")
26
+	ErrUnknownPlugin     = errors.New("this authentication plugin is not supported")
27
+	ErrOldProtocol       = errors.New("MySQL server does not support required protocol 41+")
28
+	ErrPktSync           = errors.New("commands out of sync. You can't run this command now")
29
+	ErrPktSyncMul        = errors.New("commands out of sync. Did you run multiple statements at once?")
30
+	ErrPktTooLarge       = errors.New("packet for query is too large. Try adjusting the 'max_allowed_packet' variable on the server")
31
+	ErrBusyBuffer        = errors.New("busy buffer")
32
+
33
+	// errBadConnNoWrite is used for connection errors where nothing was sent to the database yet.
34
+	// If this happens first in a function starting a database interaction, it should be replaced by driver.ErrBadConn
35
+	// to trigger a resend.
36
+	// See https://github.com/go-sql-driver/mysql/pull/302
37
+	errBadConnNoWrite = errors.New("bad connection")
38
+)
39
+
40
+var errLog = Logger(log.New(os.Stderr, "[mysql] ", log.Ldate|log.Ltime|log.Lshortfile))
41
+
42
+// Logger is used to log critical error messages.
43
+type Logger interface {
44
+	Print(v ...interface{})
45
+}
46
+
47
+// SetLogger is used to set the logger for critical errors.
48
+// The initial logger is os.Stderr.
49
+func SetLogger(logger Logger) error {
50
+	if logger == nil {
51
+		return errors.New("logger is nil")
52
+	}
53
+	errLog = logger
54
+	return nil
55
+}
56
+
57
+// MySQLError is an error type which represents a single MySQL error
58
+type MySQLError struct {
59
+	Number  uint16
60
+	Message string
61
+}
62
+
63
+func (me *MySQLError) Error() string {
64
+	return fmt.Sprintf("Error %d: %s", me.Number, me.Message)
65
+}

+ 194
- 0
vendor/github.com/go-sql-driver/mysql/fields.go View File

@@ -0,0 +1,194 @@
1
+// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
2
+//
3
+// Copyright 2017 The Go-MySQL-Driver Authors. All rights reserved.
4
+//
5
+// This Source Code Form is subject to the terms of the Mozilla Public
6
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
7
+// You can obtain one at http://mozilla.org/MPL/2.0/.
8
+
9
+package mysql
10
+
11
+import (
12
+	"database/sql"
13
+	"reflect"
14
+)
15
+
16
+func (mf *mysqlField) typeDatabaseName() string {
17
+	switch mf.fieldType {
18
+	case fieldTypeBit:
19
+		return "BIT"
20
+	case fieldTypeBLOB:
21
+		if mf.charSet != collations[binaryCollation] {
22
+			return "TEXT"
23
+		}
24
+		return "BLOB"
25
+	case fieldTypeDate:
26
+		return "DATE"
27
+	case fieldTypeDateTime:
28
+		return "DATETIME"
29
+	case fieldTypeDecimal:
30
+		return "DECIMAL"
31
+	case fieldTypeDouble:
32
+		return "DOUBLE"
33
+	case fieldTypeEnum:
34
+		return "ENUM"
35
+	case fieldTypeFloat:
36
+		return "FLOAT"
37
+	case fieldTypeGeometry:
38
+		return "GEOMETRY"
39
+	case fieldTypeInt24:
40
+		return "MEDIUMINT"
41
+	case fieldTypeJSON:
42
+		return "JSON"
43
+	case fieldTypeLong:
44
+		return "INT"
45
+	case fieldTypeLongBLOB:
46
+		if mf.charSet != collations[binaryCollation] {
47
+			return "LONGTEXT"
48
+		}
49
+		return "LONGBLOB"
50
+	case fieldTypeLongLong:
51
+		return "BIGINT"
52
+	case fieldTypeMediumBLOB:
53
+		if mf.charSet != collations[binaryCollation] {
54
+			return "MEDIUMTEXT"
55
+		}
56
+		return "MEDIUMBLOB"
57
+	case fieldTypeNewDate:
58
+		return "DATE"
59
+	case fieldTypeNewDecimal:
60
+		return "DECIMAL"
61
+	case fieldTypeNULL:
62
+		return "NULL"
63
+	case fieldTypeSet:
64
+		return "SET"
65
+	case fieldTypeShort:
66
+		return "SMALLINT"
67
+	case fieldTypeString:
68
+		if mf.charSet == collations[binaryCollation] {
69
+			return "BINARY"
70
+		}
71
+		return "CHAR"
72
+	case fieldTypeTime:
73
+		return "TIME"
74
+	case fieldTypeTimestamp:
75
+		return "TIMESTAMP"
76
+	case fieldTypeTiny:
77
+		return "TINYINT"
78
+	case fieldTypeTinyBLOB:
79
+		if mf.charSet != collations[binaryCollation] {
80
+			return "TINYTEXT"
81
+		}
82
+		return "TINYBLOB"
83
+	case fieldTypeVarChar:
84
+		if mf.charSet == collations[binaryCollation] {
85
+			return "VARBINARY"
86
+		}
87
+		return "VARCHAR"
88
+	case fieldTypeVarString:
89
+		if mf.charSet == collations[binaryCollation] {
90
+			return "VARBINARY"
91
+		}
92
+		return "VARCHAR"
93
+	case fieldTypeYear:
94
+		return "YEAR"
95
+	default:
96
+		return ""
97
+	}
98
+}
99
+
100
+var (
101
+	scanTypeFloat32   = reflect.TypeOf(float32(0))
102
+	scanTypeFloat64   = reflect.TypeOf(float64(0))
103
+	scanTypeInt8      = reflect.TypeOf(int8(0))
104
+	scanTypeInt16     = reflect.TypeOf(int16(0))
105
+	scanTypeInt32     = reflect.TypeOf(int32(0))
106
+	scanTypeInt64     = reflect.TypeOf(int64(0))
107
+	scanTypeNullFloat = reflect.TypeOf(sql.NullFloat64{})
108
+	scanTypeNullInt   = reflect.TypeOf(sql.NullInt64{})
109
+	scanTypeNullTime  = reflect.TypeOf(NullTime{})
110
+	scanTypeUint8     = reflect.TypeOf(uint8(0))
111
+	scanTypeUint16    = reflect.TypeOf(uint16(0))
112
+	scanTypeUint32    = reflect.TypeOf(uint32(0))
113
+	scanTypeUint64    = reflect.TypeOf(uint64(0))
114
+	scanTypeRawBytes  = reflect.TypeOf(sql.RawBytes{})
115
+	scanTypeUnknown   = reflect.TypeOf(new(interface{}))
116
+)
117
+
118
+type mysqlField struct {
119
+	tableName string
120
+	name      string
121
+	length    uint32
122
+	flags     fieldFlag
123
+	fieldType fieldType
124
+	decimals  byte
125
+	charSet   uint8
126
+}
127
+
128
+func (mf *mysqlField) scanType() reflect.Type {
129
+	switch mf.fieldType {
130
+	case fieldTypeTiny:
131
+		if mf.flags&flagNotNULL != 0 {
132
+			if mf.flags&flagUnsigned != 0 {
133
+				return scanTypeUint8
134
+			}
135
+			return scanTypeInt8
136
+		}
137
+		return scanTypeNullInt
138
+
139
+	case fieldTypeShort, fieldTypeYear:
140
+		if mf.flags&flagNotNULL != 0 {
141
+			if mf.flags&flagUnsigned != 0 {
142
+				return scanTypeUint16
143
+			}
144
+			return scanTypeInt16
145
+		}
146
+		return scanTypeNullInt
147
+
148
+	case fieldTypeInt24, fieldTypeLong:
149
+		if mf.flags&flagNotNULL != 0 {
150
+			if mf.flags&flagUnsigned != 0 {
151
+				return scanTypeUint32
152
+			}
153
+			return scanTypeInt32
154
+		}
155
+		return scanTypeNullInt
156
+
157
+	case fieldTypeLongLong:
158
+		if mf.flags&flagNotNULL != 0 {
159
+			if mf.flags&flagUnsigned != 0 {
160
+				return scanTypeUint64
161
+			}
162
+			return scanTypeInt64
163
+		}
164
+		return scanTypeNullInt
165
+
166
+	case fieldTypeFloat:
167
+		if mf.flags&flagNotNULL != 0 {
168
+			return scanTypeFloat32
169
+		}
170
+		return scanTypeNullFloat
171
+
172
+	case fieldTypeDouble:
173
+		if mf.flags&flagNotNULL != 0 {
174
+			return scanTypeFloat64
175
+		}
176
+		return scanTypeNullFloat
177
+
178
+	case fieldTypeDecimal, fieldTypeNewDecimal, fieldTypeVarChar,
179
+		fieldTypeBit, fieldTypeEnum, fieldTypeSet, fieldTypeTinyBLOB,
180
+		fieldTypeMediumBLOB, fieldTypeLongBLOB, fieldTypeBLOB,
181
+		fieldTypeVarString, fieldTypeString, fieldTypeGeometry, fieldTypeJSON,
182
+		fieldTypeTime:
183
+		return scanTypeRawBytes
184
+
185
+	case fieldTypeDate, fieldTypeNewDate,
186
+		fieldTypeTimestamp, fieldTypeDateTime:
187
+		// NullTime is always returned for more consistent behavior as it can
188
+		// handle both cases of parseTime regardless if the field is nullable.
189
+		return scanTypeNullTime
190
+
191
+	default:
192
+		return scanTypeUnknown
193
+	}
194
+}

+ 3
- 0
vendor/github.com/go-sql-driver/mysql/go.mod View File

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

+ 182
- 0
vendor/github.com/go-sql-driver/mysql/infile.go View File

@@ -0,0 +1,182 @@
1
+// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
2
+//
3
+// Copyright 2013 The Go-MySQL-Driver Authors. All rights reserved.
4
+//
5
+// This Source Code Form is subject to the terms of the Mozilla Public
6
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
7
+// You can obtain one at http://mozilla.org/MPL/2.0/.
8
+
9
+package mysql
10
+
11
+import (
12
+	"fmt"
13
+	"io"
14
+	"os"
15
+	"strings"
16
+	"sync"
17
+)
18
+
19
+var (
20
+	fileRegister       map[string]bool
21
+	fileRegisterLock   sync.RWMutex
22
+	readerRegister     map[string]func() io.Reader
23
+	readerRegisterLock sync.RWMutex
24
+)
25
+
26
+// RegisterLocalFile adds the given file to the file whitelist,
27
+// so that it can be used by "LOAD DATA LOCAL INFILE <filepath>".
28
+// Alternatively you can allow the use of all local files with
29
+// the DSN parameter 'allowAllFiles=true'
30
+//
31
+//  filePath := "/home/gopher/data.csv"
32
+//  mysql.RegisterLocalFile(filePath)
33
+//  err := db.Exec("LOAD DATA LOCAL INFILE '" + filePath + "' INTO TABLE foo")
34
+//  if err != nil {
35
+//  ...
36
+//
37
+func RegisterLocalFile(filePath string) {
38
+	fileRegisterLock.Lock()
39
+	// lazy map init
40
+	if fileRegister == nil {
41
+		fileRegister = make(map[string]bool)
42
+	}
43
+
44
+	fileRegister[strings.Trim(filePath, `"`)] = true
45
+	fileRegisterLock.Unlock()
46
+}
47
+
48
+// DeregisterLocalFile removes the given filepath from the whitelist.
49
+func DeregisterLocalFile(filePath string) {
50
+	fileRegisterLock.Lock()
51
+	delete(fileRegister, strings.Trim(filePath, `"`))
52
+	fileRegisterLock.Unlock()
53
+}
54
+
55
+// RegisterReaderHandler registers a handler function which is used
56
+// to receive a io.Reader.
57
+// The Reader can be used by "LOAD DATA LOCAL INFILE Reader::<name>".
58
+// If the handler returns a io.ReadCloser Close() is called when the
59
+// request is finished.
60
+//
61
+//  mysql.RegisterReaderHandler("data", func() io.Reader {
62
+//  	var csvReader io.Reader // Some Reader that returns CSV data
63
+//  	... // Open Reader here
64
+//  	return csvReader
65
+//  })
66
+//  err := db.Exec("LOAD DATA LOCAL INFILE 'Reader::data' INTO TABLE foo")
67
+//  if err != nil {
68
+//  ...
69
+//
70
+func RegisterReaderHandler(name string, handler func() io.Reader) {
71
+	readerRegisterLock.Lock()
72
+	// lazy map init
73
+	if readerRegister == nil {
74
+		readerRegister = make(map[string]func() io.Reader)
75
+	}
76
+
77
+	readerRegister[name] = handler
78
+	readerRegisterLock.Unlock()
79
+}
80
+
81
+// DeregisterReaderHandler removes the ReaderHandler function with
82
+// the given name from the registry.
83
+func DeregisterReaderHandler(name string) {
84
+	readerRegisterLock.Lock()
85
+	delete(readerRegister, name)
86
+	readerRegisterLock.Unlock()
87
+}
88
+
89
+func deferredClose(err *error, closer io.Closer) {
90
+	closeErr := closer.Close()
91
+	if *err == nil {
92
+		*err = closeErr
93
+	}
94
+}
95
+
96
+func (mc *mysqlConn) handleInFileRequest(name string) (err error) {
97
+	var rdr io.Reader
98
+	var data []byte
99
+	packetSize := 16 * 1024 // 16KB is small enough for disk readahead and large enough for TCP
100
+	if mc.maxWriteSize < packetSize {
101
+		packetSize = mc.maxWriteSize
102
+	}
103
+
104
+	if idx := strings.Index(name, "Reader::"); idx == 0 || (idx > 0 && name[idx-1] == '/') { // io.Reader
105
+		// The server might return an an absolute path. See issue #355.
106
+		name = name[idx+8:]
107
+
108
+		readerRegisterLock.RLock()
109
+		handler, inMap := readerRegister[name]
110
+		readerRegisterLock.RUnlock()
111
+
112
+		if inMap {
113
+			rdr = handler()
114
+			if rdr != nil {
115
+				if cl, ok := rdr.(io.Closer); ok {
116
+					defer deferredClose(&err, cl)
117
+				}
118
+			} else {
119
+				err = fmt.Errorf("Reader '%s' is <nil>", name)
120
+			}
121
+		} else {
122
+			err = fmt.Errorf("Reader '%s' is not registered", name)
123
+		}
124
+	} else { // File
125
+		name = strings.Trim(name, `"`)
126
+		fileRegisterLock.RLock()
127
+		fr := fileRegister[name]
128
+		fileRegisterLock.RUnlock()
129
+		if mc.cfg.AllowAllFiles || fr {
130
+			var file *os.File
131
+			var fi os.FileInfo
132
+
133
+			if file, err = os.Open(name); err == nil {
134
+				defer deferredClose(&err, file)
135
+
136
+				// get file size
137
+				if fi, err = file.Stat(); err == nil {
138
+					rdr = file
139
+					if fileSize := int(fi.Size()); fileSize < packetSize {
140
+						packetSize = fileSize
141
+					}
142
+				}
143
+			}
144
+		} else {
145
+			err = fmt.Errorf("local file '%s' is not registered", name)
146
+		}
147
+	}
148
+
149
+	// send content packets
150
+	// if packetSize == 0, the Reader contains no data
151
+	if err == nil && packetSize > 0 {
152
+		data := make([]byte, 4+packetSize)
153
+		var n int
154
+		for err == nil {
155
+			n, err = rdr.Read(data[4:])
156
+			if n > 0 {
157
+				if ioErr := mc.writePacket(data[:4+n]); ioErr != nil {
158
+					return ioErr
159
+				}
160
+			}
161
+		}
162
+		if err == io.EOF {
163
+			err = nil
164
+		}
165
+	}
166
+
167
+	// send empty packet (termination)
168
+	if data == nil {
169
+		data = make([]byte, 4)
170
+	}
171
+	if ioErr := mc.writePacket(data[:4]); ioErr != nil {
172
+		return ioErr
173
+	}
174
+
175
+	// read OK packet
176
+	if err == nil {
177
+		return mc.readResultOK()
178
+	}
179
+
180
+	mc.readPacket()
181
+	return err
182
+}

+ 50
- 0
vendor/github.com/go-sql-driver/mysql/nulltime.go View File

@@ -0,0 +1,50 @@
1
+// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
2
+//
3
+// Copyright 2013 The Go-MySQL-Driver Authors. All rights reserved.
4
+//
5
+// This Source Code Form is subject to the terms of the Mozilla Public
6
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
7
+// You can obtain one at http://mozilla.org/MPL/2.0/.
8
+
9
+package mysql
10
+
11
+import (
12
+	"database/sql/driver"
13
+	"fmt"
14
+	"time"
15
+)
16
+
17
+// Scan implements the Scanner interface.
18
+// The value type must be time.Time or string / []byte (formatted time-string),
19
+// otherwise Scan fails.
20
+func (nt *NullTime) Scan(value interface{}) (err error) {
21
+	if value == nil {
22
+		nt.Time, nt.Valid = time.Time{}, false
23
+		return
24
+	}
25
+
26
+	switch v := value.(type) {
27
+	case time.Time:
28
+		nt.Time, nt.Valid = v, true
29
+		return
30
+	case []byte:
31
+		nt.Time, err = parseDateTime(string(v), time.UTC)
32
+		nt.Valid = (err == nil)
33
+		return
34
+	case string:
35
+		nt.Time, err = parseDateTime(v, time.UTC)
36
+		nt.Valid = (err == nil)
37
+		return
38
+	}
39
+
40
+	nt.Valid = false
41
+	return fmt.Errorf("Can't convert %T to time.Time", value)
42
+}
43
+
44
+// Value implements the driver Valuer interface.
45
+func (nt NullTime) Value() (driver.Value, error) {
46
+	if !nt.Valid {
47
+		return nil, nil
48
+	}
49
+	return nt.Time, nil
50
+}

+ 31
- 0
vendor/github.com/go-sql-driver/mysql/nulltime_go113.go View File

@@ -0,0 +1,31 @@
1
+// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
2
+//
3
+// Copyright 2013 The Go-MySQL-Driver Authors. All rights reserved.
4
+//
5
+// This Source Code Form is subject to the terms of the Mozilla Public
6
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
7
+// You can obtain one at http://mozilla.org/MPL/2.0/.
8
+
9
+// +build go1.13
10
+
11
+package mysql
12
+
13
+import (
14
+	"database/sql"
15
+)
16
+
17
+// NullTime represents a time.Time that may be NULL.
18
+// NullTime implements the Scanner interface so
19
+// it can be used as a scan destination:
20
+//
21
+//  var nt NullTime
22
+//  err := db.QueryRow("SELECT time FROM foo WHERE id=?", id).Scan(&nt)
23
+//  ...
24
+//  if nt.Valid {
25
+//     // use nt.Time
26
+//  } else {
27
+//     // NULL value
28
+//  }
29
+//
30
+// This NullTime implementation is not driver-specific
31
+type NullTime sql.NullTime

+ 34
- 0
vendor/github.com/go-sql-driver/mysql/nulltime_legacy.go View File

@@ -0,0 +1,34 @@
1
+// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
2
+//
3
+// Copyright 2013 The Go-MySQL-Driver Authors. All rights reserved.
4
+//
5
+// This Source Code Form is subject to the terms of the Mozilla Public
6
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
7
+// You can obtain one at http://mozilla.org/MPL/2.0/.
8
+
9
+// +build !go1.13
10
+
11
+package mysql
12
+
13
+import (
14
+	"time"
15
+)
16
+
17
+// NullTime represents a time.Time that may be NULL.
18
+// NullTime implements the Scanner interface so
19
+// it can be used as a scan destination:
20
+//
21
+//  var nt NullTime
22
+//  err := db.QueryRow("SELECT time FROM foo WHERE id=?", id).Scan(&nt)
23
+//  ...
24
+//  if nt.Valid {
25
+//     // use nt.Time
26
+//  } else {
27
+//     // NULL value
28
+//  }
29
+//
30
+// This NullTime implementation is not driver-specific
31
+type NullTime struct {
32
+	Time  time.Time
33
+	Valid bool // Valid is true if Time is not NULL
34
+}

+ 1342
- 0
vendor/github.com/go-sql-driver/mysql/packets.go
File diff suppressed because it is too large
View File


+ 22
- 0
vendor/github.com/go-sql-driver/mysql/result.go View File

@@ -0,0 +1,22 @@
1
+// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
2
+//
3
+// Copyright 2012 The Go-MySQL-Driver Authors. All rights reserved.
4
+//
5
+// This Source Code Form is subject to the terms of the Mozilla Public
6
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
7
+// You can obtain one at http://mozilla.org/MPL/2.0/.
8
+
9
+package mysql
10
+
11
+type mysqlResult struct {
12
+	affectedRows int64
13
+	insertId     int64
14
+}
15
+
16
+func (res *mysqlResult) LastInsertId() (int64, error) {
17
+	return res.insertId, nil
18
+}
19
+
20
+func (res *mysqlResult) RowsAffected() (int64, error) {
21
+	return res.affectedRows, nil
22
+}

+ 223
- 0
vendor/github.com/go-sql-driver/mysql/rows.go View File

@@ -0,0 +1,223 @@
1
+// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
2
+//
3
+// Copyright 2012 The Go-MySQL-Driver Authors. All rights reserved.
4
+//
5
+// This Source Code Form is subject to the terms of the Mozilla Public
6
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
7
+// You can obtain one at http://mozilla.org/MPL/2.0/.
8
+
9
+package mysql
10
+
11
+import (
12
+	"database/sql/driver"
13
+	"io"
14
+	"math"
15
+	"reflect"
16
+)
17
+
18
+type resultSet struct {
19
+	columns     []mysqlField
20
+	columnNames []string
21
+	done        bool
22
+}
23
+
24
+type mysqlRows struct {
25
+	mc     *mysqlConn
26
+	rs     resultSet
27
+	finish func()
28
+}
29
+
30
+type binaryRows struct {
31
+	mysqlRows
32
+}
33
+
34
+type textRows struct {
35
+	mysqlRows
36
+}
37
+
38
+func (rows *mysqlRows) Columns() []string {
39
+	if rows.rs.columnNames != nil {
40
+		return rows.rs.columnNames
41
+	}
42
+
43
+	columns := make([]string, len(rows.rs.columns))
44
+	if rows.mc != nil && rows.mc.cfg.ColumnsWithAlias {
45
+		for i := range columns {
46
+			if tableName := rows.rs.columns[i].tableName; len(tableName) > 0 {
47
+				columns[i] = tableName + "." + rows.rs.columns[i].name
48
+			} else {
49
+				columns[i] = rows.rs.columns[i].name
50
+			}
51
+		}
52
+	} else {
53
+		for i := range columns {
54
+			columns[i] = rows.rs.columns[i].name
55
+		}
56
+	}
57
+
58
+	rows.rs.columnNames = columns
59
+	return columns
60
+}
61
+
62
+func (rows *mysqlRows) ColumnTypeDatabaseTypeName(i int) string {
63
+	return rows.rs.columns[i].typeDatabaseName()
64
+}
65
+
66
+// func (rows *mysqlRows) ColumnTypeLength(i int) (length int64, ok bool) {
67
+// 	return int64(rows.rs.columns[i].length), true
68
+// }
69
+
70
+func (rows *mysqlRows) ColumnTypeNullable(i int) (nullable, ok bool) {
71
+	return rows.rs.columns[i].flags&flagNotNULL == 0, true
72
+}
73
+
74
+func (rows *mysqlRows) ColumnTypePrecisionScale(i int) (int64, int64, bool) {
75
+	column := rows.rs.columns[i]
76
+	decimals := int64(column.decimals)
77
+
78
+	switch column.fieldType {
79
+	case fieldTypeDecimal, fieldTypeNewDecimal:
80
+		if decimals > 0 {
81
+			return int64(column.length) - 2, decimals, true
82
+		}
83
+		return int64(column.length) - 1, decimals, true
84
+	case fieldTypeTimestamp, fieldTypeDateTime, fieldTypeTime:
85
+		return decimals, decimals, true
86
+	case fieldTypeFloat, fieldTypeDouble:
87
+		if decimals == 0x1f {
88
+			return math.MaxInt64, math.MaxInt64, true
89
+		}
90
+		return math.MaxInt64, decimals, true
91
+	}
92
+
93
+	return 0, 0, false
94
+}
95
+
96
+func (rows *mysqlRows) ColumnTypeScanType(i int) reflect.Type {
97
+	return rows.rs.columns[i].scanType()
98
+}
99
+
100
+func (rows *mysqlRows) Close() (err error) {
101
+	if f := rows.finish; f != nil {
102
+		f()
103
+		rows.finish = nil
104
+	}
105
+
106
+	mc := rows.mc
107
+	if mc == nil {
108
+		return nil
109
+	}
110
+	if err := mc.error(); err != nil {
111
+		return err
112
+	}
113
+
114
+	// flip the buffer for this connection if we need to drain it.
115
+	// note that for a successful query (i.e. one where rows.next()
116
+	// has been called until it returns false), `rows.mc` will be nil
117
+	// by the time the user calls `(*Rows).Close`, so we won't reach this
118
+	// see: https://github.com/golang/go/commit/651ddbdb5056ded455f47f9c494c67b389622a47
119
+	mc.buf.flip()
120
+
121
+	// Remove unread packets from stream
122
+	if !rows.rs.done {
123
+		err = mc.readUntilEOF()
124
+	}
125
+	if err == nil {
126
+		if err = mc.discardResults(); err != nil {
127
+			return err
128
+		}
129
+	}
130
+
131
+	rows.mc = nil
132
+	return err
133
+}
134
+
135
+func (rows *mysqlRows) HasNextResultSet() (b bool) {
136
+	if rows.mc == nil {
137
+		return false
138
+	}
139
+	return rows.mc.status&statusMoreResultsExists != 0
140
+}
141
+
142
+func (rows *mysqlRows) nextResultSet() (int, error) {
143
+	if rows.mc == nil {
144
+		return 0, io.EOF
145
+	}
146
+	if err := rows.mc.error(); err != nil {
147
+		return 0, err
148
+	}
149
+
150
+	// Remove unread packets from stream
151
+	if !rows.rs.done {
152
+		if err := rows.mc.readUntilEOF(); err != nil {
153
+			return 0, err
154
+		}
155
+		rows.rs.done = true
156
+	}
157
+
158
+	if !rows.HasNextResultSet() {
159
+		rows.mc = nil
160
+		return 0, io.EOF
161
+	}
162
+	rows.rs = resultSet{}
163
+	return rows.mc.readResultSetHeaderPacket()
164
+}
165
+
166
+func (rows *mysqlRows) nextNotEmptyResultSet() (int, error) {
167
+	for {
168
+		resLen, err := rows.nextResultSet()
169
+		if err != nil {
170
+			return 0, err
171
+		}
172
+
173
+		if resLen > 0 {
174
+			return resLen, nil
175
+		}
176
+
177
+		rows.rs.done = true
178
+	}
179
+}
180
+
181
+func (rows *binaryRows) NextResultSet() error {
182
+	resLen, err := rows.nextNotEmptyResultSet()
183
+	if err != nil {
184
+		return err
185
+	}
186
+
187
+	rows.rs.columns, err = rows.mc.readColumns(resLen)
188
+	return err
189
+}
190
+
191
+func (rows *binaryRows) Next(dest []driver.Value) error {
192
+	if mc := rows.mc; mc != nil {
193
+		if err := mc.error(); err != nil {
194
+			return err
195
+		}
196
+
197
+		// Fetch next row from stream
198
+		return rows.readRow(dest)
199
+	}
200
+	return io.EOF
201
+}
202
+
203
+func (rows *textRows) NextResultSet() (err error) {
204
+	resLen, err := rows.nextNotEmptyResultSet()
205
+	if err != nil {
206
+		return err
207
+	}
208
+
209
+	rows.rs.columns, err = rows.mc.readColumns(resLen)
210
+	return err
211
+}
212
+
213
+func (rows *textRows) Next(dest []driver.Value) error {
214
+	if mc := rows.mc; mc != nil {
215
+		if err := mc.error(); err != nil {
216
+			return err
217
+		}
218
+
219
+		// Fetch next row from stream
220
+		return rows.readRow(dest)
221
+	}
222
+	return io.EOF
223
+}

+ 204
- 0
vendor/github.com/go-sql-driver/mysql/statement.go View File

@@ -0,0 +1,204 @@
1
+// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
2
+//
3
+// Copyright 2012 The Go-MySQL-Driver Authors. All rights reserved.
4
+//
5
+// This Source Code Form is subject to the terms of the Mozilla Public
6
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
7
+// You can obtain one at http://mozilla.org/MPL/2.0/.
8
+
9
+package mysql
10
+
11
+import (
12
+	"database/sql/driver"
13
+	"fmt"
14
+	"io"
15
+	"reflect"
16
+)
17
+
18
+type mysqlStmt struct {
19
+	mc         *mysqlConn
20
+	id         uint32
21
+	paramCount int
22
+}
23
+
24
+func (stmt *mysqlStmt) Close() error {
25
+	if stmt.mc == nil || stmt.mc.closed.IsSet() {
26
+		// driver.Stmt.Close can be called more than once, thus this function
27
+		// has to be idempotent.
28
+		// See also Issue #450 and golang/go#16019.
29
+		//errLog.Print(ErrInvalidConn)
30
+		return driver.ErrBadConn
31
+	}
32
+
33
+	err := stmt.mc.writeCommandPacketUint32(comStmtClose, stmt.id)
34
+	stmt.mc = nil
35
+	return err
36
+}
37
+
38
+func (stmt *mysqlStmt) NumInput() int {
39
+	return stmt.paramCount
40
+}
41
+
42
+func (stmt *mysqlStmt) ColumnConverter(idx int) driver.ValueConverter {
43
+	return converter{}
44
+}
45
+
46
+func (stmt *mysqlStmt) Exec(args []driver.Value) (driver.Result, error) {
47
+	if stmt.mc.closed.IsSet() {
48
+		errLog.Print(ErrInvalidConn)
49
+		return nil, driver.ErrBadConn
50
+	}
51
+	// Send command
52
+	err := stmt.writeExecutePacket(args)
53
+	if err != nil {
54
+		return nil, stmt.mc.markBadConn(err)
55
+	}
56
+
57
+	mc := stmt.mc
58
+
59
+	mc.affectedRows = 0
60
+	mc.insertId = 0
61
+
62
+	// Read Result
63
+	resLen, err := mc.readResultSetHeaderPacket()
64
+	if err != nil {
65
+		return nil, err
66
+	}
67
+
68
+	if resLen > 0 {
69
+		// Columns
70
+		if err = mc.readUntilEOF(); err != nil {
71
+			return nil, err
72
+		}
73
+
74
+		// Rows
75
+		if err := mc.readUntilEOF(); err != nil {
76
+			return nil, err
77
+		}
78
+	}
79
+
80
+	if err := mc.discardResults(); err != nil {
81
+		return nil, err
82
+	}
83
+
84
+	return &mysqlResult{
85
+		affectedRows: int64(mc.affectedRows),
86
+		insertId:     int64(mc.insertId),
87
+	}, nil
88
+}
89
+
90
+func (stmt *mysqlStmt) Query(args []driver.Value) (driver.Rows, error) {
91
+	return stmt.query(args)
92
+}
93
+
94
+func (stmt *mysqlStmt) query(args []driver.Value) (*binaryRows, error) {
95
+	if stmt.mc.closed.IsSet() {
96
+		errLog.Print(ErrInvalidConn)
97
+		return nil, driver.ErrBadConn
98
+	}
99
+	// Send command
100
+	err := stmt.writeExecutePacket(args)
101
+	if err != nil {
102
+		return nil, stmt.mc.markBadConn(err)
103
+	}
104
+
105
+	mc := stmt.mc
106
+
107
+	// Read Result
108
+	resLen, err := mc.readResultSetHeaderPacket()
109
+	if err != nil {
110
+		return nil, err
111
+	}
112
+
113
+	rows := new(binaryRows)
114
+
115
+	if resLen > 0 {
116
+		rows.mc = mc
117
+		rows.rs.columns, err = mc.readColumns(resLen)
118
+	} else {
119
+		rows.rs.done = true
120
+
121
+		switch err := rows.NextResultSet(); err {
122
+		case nil, io.EOF:
123
+			return rows, nil
124
+		default:
125
+			return nil, err
126
+		}
127
+	}
128
+
129
+	return rows, err
130
+}
131
+
132
+type converter struct{}
133
+
134
+// ConvertValue mirrors the reference/default converter in database/sql/driver
135
+// with _one_ exception.  We support uint64 with their high bit and the default
136
+// implementation does not.  This function should be kept in sync with
137
+// database/sql/driver defaultConverter.ConvertValue() except for that
138
+// deliberate difference.
139
+func (c converter) ConvertValue(v interface{}) (driver.Value, error) {
140
+	if driver.IsValue(v) {
141
+		return v, nil
142
+	}
143
+
144
+	if vr, ok := v.(driver.Valuer); ok {
145
+		sv, err := callValuerValue(vr)
146
+		if err != nil {
147
+			return nil, err
148
+		}
149
+		if !driver.IsValue(sv) {
150
+			return nil, fmt.Errorf("non-Value type %T returned from Value", sv)
151
+		}
152
+		return sv, nil
153
+	}
154
+
155
+	rv := reflect.ValueOf(v)
156
+	switch rv.Kind() {
157
+	case reflect.Ptr:
158
+		// indirect pointers
159
+		if rv.IsNil() {
160
+			return nil, nil
161
+		} else {
162
+			return c.ConvertValue(rv.Elem().Interface())
163
+		}
164
+	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
165
+		return rv.Int(), nil
166
+	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
167
+		return rv.Uint(), nil
168
+	case reflect.Float32, reflect.Float64:
169
+		return rv.Float(), nil
170
+	case reflect.Bool:
171
+		return rv.Bool(), nil
172
+	case reflect.Slice:
173
+		ek := rv.Type().Elem().Kind()
174
+		if ek == reflect.Uint8 {
175
+			return rv.Bytes(), nil
176
+		}
177
+		return nil, fmt.Errorf("unsupported type %T, a slice of %s", v, ek)
178
+	case reflect.String:
179
+		return rv.String(), nil
180
+	}
181
+	return nil, fmt.Errorf("unsupported type %T, a %s", v, rv.Kind())
182
+}
183
+
184
+var valuerReflectType = reflect.TypeOf((*driver.Valuer)(nil)).Elem()
185
+
186
+// callValuerValue returns vr.Value(), with one exception:
187
+// If vr.Value is an auto-generated method on a pointer type and the
188
+// pointer is nil, it would panic at runtime in the panicwrap
189
+// method. Treat it like nil instead.
190
+//
191
+// This is so people can implement driver.Value on value types and
192
+// still use nil pointers to those types to mean nil/NULL, just like
193
+// string/*string.
194
+//
195
+// This is an exact copy of the same-named unexported function from the
196
+// database/sql package.
197
+func callValuerValue(vr driver.Valuer) (v driver.Value, err error) {
198
+	if rv := reflect.ValueOf(vr); rv.Kind() == reflect.Ptr &&
199
+		rv.IsNil() &&
200
+		rv.Type().Elem().Implements(valuerReflectType) {
201
+		return nil, nil
202
+	}
203
+	return vr.Value()
204
+}

+ 31
- 0
vendor/github.com/go-sql-driver/mysql/transaction.go View File

@@ -0,0 +1,31 @@
1
+// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
2
+//
3
+// Copyright 2012 The Go-MySQL-Driver Authors. All rights reserved.
4
+//
5
+// This Source Code Form is subject to the terms of the Mozilla Public
6
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
7
+// You can obtain one at http://mozilla.org/MPL/2.0/.
8
+
9
+package mysql
10
+
11
+type mysqlTx struct {
12
+	mc *mysqlConn
13
+}
14
+
15
+func (tx *mysqlTx) Commit() (err error) {
16
+	if tx.mc == nil || tx.mc.closed.IsSet() {
17
+		return ErrInvalidConn
18
+	}
19
+	err = tx.mc.exec("COMMIT")
20
+	tx.mc = nil
21
+	return
22
+}
23
+
24
+func (tx *mysqlTx) Rollback() (err error) {
25
+	if tx.mc == nil || tx.mc.closed.IsSet() {
26
+		return ErrInvalidConn
27
+	}
28
+	err = tx.mc.exec("ROLLBACK")
29
+	tx.mc = nil
30
+	return
31
+}

+ 701
- 0
vendor/github.com/go-sql-driver/mysql/utils.go View File

@@ -0,0 +1,701 @@
1
+// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
2
+//
3
+// Copyright 2012 The Go-MySQL-Driver Authors. All rights reserved.
4
+//
5
+// This Source Code Form is subject to the terms of the Mozilla Public
6
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
7
+// You can obtain one at http://mozilla.org/MPL/2.0/.
8
+
9
+package mysql
10
+
11
+import (
12
+	"crypto/tls"
13
+	"database/sql"
14
+	"database/sql/driver"
15
+	"encoding/binary"
16
+	"errors"
17
+	"fmt"
18
+	"io"
19
+	"strconv"
20
+	"strings"
21
+	"sync"
22
+	"sync/atomic"
23
+	"time"
24
+)
25
+
26
+// Registry for custom tls.Configs
27
+var (
28
+	tlsConfigLock     sync.RWMutex
29
+	tlsConfigRegistry map[string]*tls.Config
30
+)
31
+
32
+// RegisterTLSConfig registers a custom tls.Config to be used with sql.Open.
33
+// Use the key as a value in the DSN where tls=value.
34
+//
35
+// Note: The provided tls.Config is exclusively owned by the driver after
36
+// registering it.
37
+//
38
+//  rootCertPool := x509.NewCertPool()
39
+//  pem, err := ioutil.ReadFile("/path/ca-cert.pem")
40
+//  if err != nil {
41
+//      log.Fatal(err)
42
+//  }
43
+//  if ok := rootCertPool.AppendCertsFromPEM(pem); !ok {
44
+//      log.Fatal("Failed to append PEM.")
45
+//  }
46
+//  clientCert := make([]tls.Certificate, 0, 1)
47
+//  certs, err := tls.LoadX509KeyPair("/path/client-cert.pem", "/path/client-key.pem")
48
+//  if err != nil {
49
+//      log.Fatal(err)
50
+//  }
51
+//  clientCert = append(clientCert, certs)
52
+//  mysql.RegisterTLSConfig("custom", &tls.Config{
53
+//      RootCAs: rootCertPool,
54
+//      Certificates: clientCert,
55
+//  })
56
+//  db, err := sql.Open("mysql", "user@tcp(localhost:3306)/test?tls=custom")
57
+//
58
+func RegisterTLSConfig(key string, config *tls.Config) error {
59
+	if _, isBool := readBool(key); isBool || strings.ToLower(key) == "skip-verify" || strings.ToLower(key) == "preferred" {
60
+		return fmt.Errorf("key '%s' is reserved", key)
61
+	}
62
+
63
+	tlsConfigLock.Lock()
64
+	if tlsConfigRegistry == nil {
65
+		tlsConfigRegistry = make(map[string]*tls.Config)
66
+	}
67
+
68
+	tlsConfigRegistry[key] = config
69
+	tlsConfigLock.Unlock()
70
+	return nil
71
+}
72
+
73
+// DeregisterTLSConfig removes the tls.Config associated with key.
74
+func DeregisterTLSConfig(key string) {
75
+	tlsConfigLock.Lock()
76
+	if tlsConfigRegistry != nil {
77
+		delete(tlsConfigRegistry, key)
78
+	}
79
+	tlsConfigLock.Unlock()
80
+}
81
+
82
+func getTLSConfigClone(key string) (config *tls.Config) {
83
+	tlsConfigLock.RLock()
84
+	if v, ok := tlsConfigRegistry[key]; ok {
85
+		config = v.Clone()
86
+	}
87
+	tlsConfigLock.RUnlock()
88
+	return
89
+}
90
+
91
+// Returns the bool value of the input.
92
+// The 2nd return value indicates if the input was a valid bool value
93
+func readBool(input string) (value bool, valid bool) {
94
+	switch input {
95
+	case "1", "true", "TRUE", "True":
96
+		return true, true
97
+	case "0", "false", "FALSE", "False":
98
+		return false, true
99
+	}
100
+
101
+	// Not a valid bool value
102
+	return
103
+}
104
+
105
+/******************************************************************************
106
+*                           Time related utils                                *
107
+******************************************************************************/
108
+
109
+func parseDateTime(str string, loc *time.Location) (t time.Time, err error) {
110
+	base := "0000-00-00 00:00:00.0000000"
111
+	switch len(str) {
112
+	case 10, 19, 21, 22, 23, 24, 25, 26: // up to "YYYY-MM-DD HH:MM:SS.MMMMMM"
113
+		if str == base[:len(str)] {
114
+			return
115
+		}
116
+		t, err = time.Parse(timeFormat[:len(str)], str)
117
+	default:
118
+		err = fmt.Errorf("invalid time string: %s", str)
119
+		return
120
+	}
121
+
122
+	// Adjust location
123
+	if err == nil && loc != time.UTC {
124
+		y, mo, d := t.Date()
125
+		h, mi, s := t.Clock()
126
+		t, err = time.Date(y, mo, d, h, mi, s, t.Nanosecond(), loc), nil
127
+	}
128
+
129
+	return
130
+}
131
+
132
+func parseBinaryDateTime(num uint64, data []byte, loc *time.Location) (driver.Value, error) {
133
+	switch num {
134
+	case 0:
135
+		return time.Time{}, nil
136
+	case 4:
137
+		return time.Date(
138
+			int(binary.LittleEndian.Uint16(data[:2])), // year
139
+			time.Month(data[2]),                       // month
140
+			int(data[3]),                              // day
141
+			0, 0, 0, 0,
142
+			loc,
143
+		), nil
144
+	case 7:
145
+		return time.Date(
146
+			int(binary.LittleEndian.Uint16(data[:2])), // year
147
+			time.Month(data[2]),                       // month
148
+			int(data[3]),                              // day
149
+			int(data[4]),                              // hour
150
+			int(data[5]),                              // minutes
151
+			int(data[6]),                              // seconds
152
+			0,
153
+			loc,
154
+		), nil
155
+	case 11:
156
+		return time.Date(
157
+			int(binary.LittleEndian.Uint16(data[:2])), // year
158
+			time.Month(data[2]),                       // month
159
+			int(data[3]),                              // day
160
+			int(data[4]),                              // hour
161
+			int(data[5]),                              // minutes
162
+			int(data[6]),                              // seconds
163
+			int(binary.LittleEndian.Uint32(data[7:11]))*1000, // nanoseconds
164
+			loc,
165
+		), nil
166
+	}
167
+	return nil, fmt.Errorf("invalid DATETIME packet length %d", num)
168
+}
169
+
170
+// zeroDateTime is used in formatBinaryDateTime to avoid an allocation
171
+// if the DATE or DATETIME has the zero value.
172
+// It must never be changed.
173
+// The current behavior depends on database/sql copying the result.
174
+var zeroDateTime = []byte("0000-00-00 00:00:00.000000")
175
+
176
+const digits01 = "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"
177
+const digits10 = "0000000000111111111122222222223333333333444444444455555555556666666666777777777788888888889999999999"
178
+
179
+func appendMicrosecs(dst, src []byte, decimals int) []byte {
180
+	if decimals <= 0 {
181
+		return dst
182
+	}
183
+	if len(src) == 0 {
184
+		return append(dst, ".000000"[:decimals+1]...)
185
+	}
186
+
187
+	microsecs := binary.LittleEndian.Uint32(src[:4])
188
+	p1 := byte(microsecs / 10000)
189
+	microsecs -= 10000 * uint32(p1)
190
+	p2 := byte(microsecs / 100)
191
+	microsecs -= 100 * uint32(p2)
192
+	p3 := byte(microsecs)
193
+
194
+	switch decimals {
195
+	default:
196
+		return append(dst, '.',
197
+			digits10[p1], digits01[p1],
198
+			digits10[p2], digits01[p2],
199
+			digits10[p3], digits01[p3],
200
+		)
201
+	case 1:
202
+		return append(dst, '.',
203
+			digits10[p1],
204
+		)
205
+	case 2:
206
+		return append(dst, '.',
207
+			digits10[p1], digits01[p1],
208
+		)
209
+	case 3:
210
+		return append(dst, '.',
211
+			digits10[p1], digits01[p1],
212
+			digits10[p2],
213
+		)
214
+	case 4:
215
+		return append(dst, '.',
216
+			digits10[p1], digits01[p1],
217
+			digits10[p2], digits01[p2],
218
+		)
219
+	case 5:
220
+		return append(dst, '.',
221
+			digits10[p1], digits01[p1],
222
+			digits10[p2], digits01[p2],
223
+			digits10[p3],
224
+		)
225
+	}
226
+}
227
+
228
+func formatBinaryDateTime(src []byte, length uint8) (driver.Value, error) {
229
+	// length expects the deterministic length of the zero value,
230
+	// negative time and 100+ hours are automatically added if needed
231
+	if len(src) == 0 {
232
+		return zeroDateTime[:length], nil
233
+	}
234
+	var dst []byte      // return value
235
+	var p1, p2, p3 byte // current digit pair
236
+
237
+	switch length {
238
+	case 10, 19, 21, 22, 23, 24, 25, 26:
239
+	default:
240
+		t := "DATE"
241
+		if length > 10 {
242
+			t += "TIME"
243
+		}
244
+		return nil, fmt.Errorf("illegal %s length %d", t, length)
245
+	}
246
+	switch len(src) {
247
+	case 4, 7, 11:
248
+	default:
249
+		t := "DATE"
250
+		if length > 10 {
251
+			t += "TIME"
252
+		}
253
+		return nil, fmt.Errorf("illegal %s packet length %d", t, len(src))
254
+	}
255
+	dst = make([]byte, 0, length)
256
+	// start with the date
257
+	year := binary.LittleEndian.Uint16(src[:2])
258
+	pt := year / 100
259
+	p1 = byte(year - 100*uint16(pt))
260
+	p2, p3 = src[2], src[3]
261
+	dst = append(dst,
262
+		digits10[pt], digits01[pt],
263
+		digits10[p1], digits01[p1], '-',
264
+		digits10[p2], digits01[p2], '-',
265
+		digits10[p3], digits01[p3],
266
+	)
267
+	if length == 10 {
268
+		return dst, nil
269
+	}
270
+	if len(src) == 4 {
271
+		return append(dst, zeroDateTime[10:length]...), nil
272
+	}
273
+	dst = append(dst, ' ')
274
+	p1 = src[4] // hour
275
+	src = src[5:]
276
+
277
+	// p1 is 2-digit hour, src is after hour
278
+	p2, p3 = src[0], src[1]
279
+	dst = append(dst,
280
+		digits10[p1], digits01[p1], ':',
281
+		digits10[p2], digits01[p2], ':',
282
+		digits10[p3], digits01[p3],
283
+	)
284
+	return appendMicrosecs(dst, src[2:], int(length)-20), nil
285
+}
286
+
287
+func formatBinaryTime(src []byte, length uint8) (driver.Value, error) {
288
+	// length expects the deterministic length of the zero value,
289
+	// negative time and 100+ hours are automatically added if needed
290
+	if len(src) == 0 {
291
+		return zeroDateTime[11 : 11+length], nil
292
+	}
293
+	var dst []byte // return value
294
+
295
+	switch length {
296
+	case
297
+		8,                      // time (can be up to 10 when negative and 100+ hours)
298
+		10, 11, 12, 13, 14, 15: // time with fractional seconds
299
+	default:
300
+		return nil, fmt.Errorf("illegal TIME length %d", length)
301
+	}
302
+	switch len(src) {
303
+	case 8, 12:
304
+	default:
305
+		return nil, fmt.Errorf("invalid TIME packet length %d", len(src))
306
+	}
307
+	// +2 to enable negative time and 100+ hours
308
+	dst = make([]byte, 0, length+2)
309
+	if src[0] == 1 {
310
+		dst = append(dst, '-')
311
+	}
312
+	days := binary.LittleEndian.Uint32(src[1:5])
313
+	hours := int64(days)*24 + int64(src[5])
314
+
315
+	if hours >= 100 {
316
+		dst = strconv.AppendInt(dst, hours, 10)
317
+	} else {
318
+		dst = append(dst, digits10[hours], digits01[hours])
319
+	}
320
+
321
+	min, sec := src[6], src[7]
322
+	dst = append(dst, ':',
323
+		digits10[min], digits01[min], ':',
324
+		digits10[sec], digits01[sec],
325
+	)
326
+	return appendMicrosecs(dst, src[8:], int(length)-9), nil
327
+}
328
+
329
+/******************************************************************************
330
+*                       Convert from and to bytes                             *
331
+******************************************************************************/
332
+
333
+func uint64ToBytes(n uint64) []byte {
334
+	return []byte{
335
+		byte(n),
336
+		byte(n >> 8),
337
+		byte(n >> 16),
338
+		byte(n >> 24),
339
+		byte(n >> 32),
340
+		byte(n >> 40),
341
+		byte(n >> 48),
342
+		byte(n >> 56),
343
+	}
344
+}
345
+
346
+func uint64ToString(n uint64) []byte {
347
+	var a [20]byte
348
+	i := 20
349
+
350
+	// U+0030 = 0
351
+	// ...
352
+	// U+0039 = 9
353
+
354
+	var q uint64
355
+	for n >= 10 {
356
+		i--
357
+		q = n / 10
358
+		a[i] = uint8(n-q*10) + 0x30
359
+		n = q
360
+	}
361
+
362
+	i--
363
+	a[i] = uint8(n) + 0x30
364
+
365
+	return a[i:]
366
+}
367
+
368
+// treats string value as unsigned integer representation
369
+func stringToInt(b []byte) int {
370
+	val := 0
371
+	for i := range b {
372
+		val *= 10
373
+		val += int(b[i] - 0x30)
374
+	}
375
+	return val
376
+}
377
+
378
+// returns the string read as a bytes slice, wheter the value is NULL,
379
+// the number of bytes read and an error, in case the string is longer than
380
+// the input slice
381
+func readLengthEncodedString(b []byte) ([]byte, bool, int, error) {
382
+	// Get length
383
+	num, isNull, n := readLengthEncodedInteger(b)
384
+	if num < 1 {
385
+		return b[n:n], isNull, n, nil
386
+	}
387
+
388
+	n += int(num)
389
+
390
+	// Check data length
391
+	if len(b) >= n {
392
+		return b[n-int(num) : n : n], false, n, nil
393
+	}
394
+	return nil, false, n, io.EOF
395
+}
396
+
397
+// returns the number of bytes skipped and an error, in case the string is
398
+// longer than the input slice
399
+func skipLengthEncodedString(b []byte) (int, error) {
400
+	// Get length
401
+	num, _, n := readLengthEncodedInteger(b)
402
+	if num < 1 {
403
+		return n, nil
404
+	}
405
+
406
+	n += int(num)
407
+
408
+	// Check data length
409
+	if len(b) >= n {
410
+		return n, nil
411
+	}
412
+	return n, io.EOF
413
+}
414
+
415
+// returns the number read, whether the value is NULL and the number of bytes read
416
+func readLengthEncodedInteger(b []byte) (uint64, bool, int) {
417
+	// See issue #349
418
+	if len(b) == 0 {
419
+		return 0, true, 1
420
+	}
421
+
422
+	switch b[0] {
423
+	// 251: NULL
424
+	case 0xfb:
425
+		return 0, true, 1
426
+
427
+	// 252: value of following 2
428
+	case 0xfc:
429
+		return uint64(b[1]) | uint64(b[2])<<8, false, 3
430
+
431
+	// 253: value of following 3
432
+	case 0xfd:
433
+		return uint64(b[1]) | uint64(b[2])<<8 | uint64(b[3])<<16, false, 4
434
+
435
+	// 254: value of following 8
436
+	case 0xfe:
437
+		return uint64(b[1]) | uint64(b[2])<<8 | uint64(b[3])<<16 |
438
+				uint64(b[4])<<24 | uint64(b[5])<<32 | uint64(b[6])<<40 |
439
+				uint64(b[7])<<48 | uint64(b[8])<<56,
440
+			false, 9
441
+	}
442
+
443
+	// 0-250: value of first byte
444
+	return uint64(b[0]), false, 1
445
+}
446
+
447
+// encodes a uint64 value and appends it to the given bytes slice
448
+func appendLengthEncodedInteger(b []byte, n uint64) []byte {
449
+	switch {
450
+	case n <= 250:
451
+		return append(b, byte(n))
452
+
453
+	case n <= 0xffff:
454
+		return append(b, 0xfc, byte(n), byte(n>>8))
455
+
456
+	case n <= 0xffffff:
457
+		return append(b, 0xfd, byte(n), byte(n>>8), byte(n>>16))
458
+	}
459
+	return append(b, 0xfe, byte(n), byte(n>>8), byte(n>>16), byte(n>>24),
460
+		byte(n>>32), byte(n>>40), byte(n>>48), byte(n>>56))
461
+}
462
+
463
+// reserveBuffer checks cap(buf) and expand buffer to len(buf) + appendSize.
464
+// If cap(buf) is not enough, reallocate new buffer.
465
+func reserveBuffer(buf []byte, appendSize int) []byte {
466
+	newSize := len(buf) + appendSize
467
+	if cap(buf) < newSize {
468
+		// Grow buffer exponentially
469
+		newBuf := make([]byte, len(buf)*2+appendSize)
470
+		copy(newBuf, buf)
471
+		buf = newBuf
472
+	}
473
+	return buf[:newSize]
474
+}
475
+
476
+// escapeBytesBackslash escapes []byte with backslashes (\)
477
+// This escapes the contents of a string (provided as []byte) by adding backslashes before special
478
+// characters, and turning others into specific escape sequences, such as
479
+// turning newlines into \n and null bytes into \0.
480
+// https://github.com/mysql/mysql-server/blob/mysql-5.7.5/mysys/charset.c#L823-L932
481
+func escapeBytesBackslash(buf, v []byte) []byte {
482
+	pos := len(buf)
483
+	buf = reserveBuffer(buf, len(v)*2)
484
+
485
+	for _, c := range v {
486
+		switch c {
487
+		case '\x00':
488
+			buf[pos] = '\\'
489
+			buf[pos+1] = '0'
490
+			pos += 2
491
+		case '\n':
492
+			buf[pos] = '\\'
493
+			buf[pos+1] = 'n'
494
+			pos += 2
495
+		case '\r':
496
+			buf[pos] = '\\'
497
+			buf[pos+1] = 'r'
498
+			pos += 2
499
+		case '\x1a':
500
+			buf[pos] = '\\'
501
+			buf[pos+1] = 'Z'
502
+			pos += 2
503
+		case '\'':
504
+			buf[pos] = '\\'
505
+			buf[pos+1] = '\''
506
+			pos += 2
507
+		case '"':
508
+			buf[pos] = '\\'
509
+			buf[pos+1] = '"'
510
+			pos += 2
511
+		case '\\':
512
+			buf[pos] = '\\'
513
+			buf[pos+1] = '\\'
514
+			pos += 2
515
+		default:
516
+			buf[pos] = c
517
+			pos++
518
+		}
519
+	}
520
+
521
+	return buf[:pos]
522
+}
523
+
524
+// escapeStringBackslash is similar to escapeBytesBackslash but for string.
525
+func escapeStringBackslash(buf []byte, v string) []byte {
526
+	pos := len(buf)
527
+	buf = reserveBuffer(buf, len(v)*2)
528
+
529
+	for i := 0; i < len(v); i++ {
530
+		c := v[i]
531
+		switch c {
532
+		case '\x00':
533
+			buf[pos] = '\\'
534
+			buf[pos+1] = '0'
535
+			pos += 2
536
+		case '\n':
537
+			buf[pos] = '\\'
538
+			buf[pos+1] = 'n'
539
+			pos += 2
540
+		case '\r':
541
+			buf[pos] = '\\'
542
+			buf[pos+1] = 'r'
543
+			pos += 2
544
+		case '\x1a':
545
+			buf[pos] = '\\'
546
+			buf[pos+1] = 'Z'
547
+			pos += 2
548
+		case '\'':
549
+			buf[pos] = '\\'
550
+			buf[pos+1] = '\''
551
+			pos += 2
552
+		case '"':
553
+			buf[pos] = '\\'
554
+			buf[pos+1] = '"'
555
+			pos += 2
556
+		case '\\':
557
+			buf[pos] = '\\'
558
+			buf[pos+1] = '\\'
559
+			pos += 2
560
+		default:
561
+			buf[pos] = c
562
+			pos++
563
+		}
564
+	}
565
+
566
+	return buf[:pos]
567
+}
568
+
569
+// escapeBytesQuotes escapes apostrophes in []byte by doubling them up.
570
+// This escapes the contents of a string by doubling up any apostrophes that
571
+// it contains. This is used when the NO_BACKSLASH_ESCAPES SQL_MODE is in
572
+// effect on the server.
573
+// https://github.com/mysql/mysql-server/blob/mysql-5.7.5/mysys/charset.c#L963-L1038
574
+func escapeBytesQuotes(buf, v []byte) []byte {
575
+	pos := len(buf)
576
+	buf = reserveBuffer(buf, len(v)*2)
577
+
578
+	for _, c := range v {
579
+		if c == '\'' {
580
+			buf[pos] = '\''
581
+			buf[pos+1] = '\''
582
+			pos += 2
583
+		} else {
584
+			buf[pos] = c
585
+			pos++
586
+		}
587
+	}
588
+
589
+	return buf[:pos]
590
+}
591
+
592
+// escapeStringQuotes is similar to escapeBytesQuotes but for string.
593
+func escapeStringQuotes(buf []byte, v string) []byte {
594
+	pos := len(buf)
595
+	buf = reserveBuffer(buf, len(v)*2)
596
+
597
+	for i := 0; i < len(v); i++ {
598
+		c := v[i]
599
+		if c == '\'' {
600
+			buf[pos] = '\''
601
+			buf[pos+1] = '\''
602
+			pos += 2
603
+		} else {
604
+			buf[pos] = c
605
+			pos++
606
+		}
607
+	}
608
+
609
+	return buf[:pos]
610
+}
611
+
612
+/******************************************************************************
613
+*                               Sync utils                                    *
614
+******************************************************************************/
615
+
616
+// noCopy may be embedded into structs which must not be copied
617
+// after the first use.
618
+//
619
+// See https://github.com/golang/go/issues/8005#issuecomment-190753527
620
+// for details.
621
+type noCopy struct{}
622
+
623
+// Lock is a no-op used by -copylocks checker from `go vet`.
624
+func (*noCopy) Lock() {}
625
+
626
+// atomicBool is a wrapper around uint32 for usage as a boolean value with
627
+// atomic access.
628
+type atomicBool struct {
629
+	_noCopy noCopy
630
+	value   uint32
631
+}
632
+
633
+// IsSet returns whether the current boolean value is true
634
+func (ab *atomicBool) IsSet() bool {
635
+	return atomic.LoadUint32(&ab.value) > 0
636
+}
637
+
638
+// Set sets the value of the bool regardless of the previous value
639
+func (ab *atomicBool) Set(value bool) {
640
+	if value {
641
+		atomic.StoreUint32(&ab.value, 1)
642
+	} else {
643
+		atomic.StoreUint32(&ab.value, 0)
644
+	}
645
+}
646
+
647
+// TrySet sets the value of the bool and returns whether the value changed
648
+func (ab *atomicBool) TrySet(value bool) bool {
649
+	if value {
650
+		return atomic.SwapUint32(&ab.value, 1) == 0
651
+	}
652
+	return atomic.SwapUint32(&ab.value, 0) > 0
653
+}
654
+
655
+// atomicError is a wrapper for atomically accessed error values
656
+type atomicError struct {
657
+	_noCopy noCopy
658
+	value   atomic.Value
659
+}
660
+
661
+// Set sets the error value regardless of the previous value.
662
+// The value must not be nil
663
+func (ae *atomicError) Set(value error) {
664
+	ae.value.Store(value)
665
+}
666
+
667
+// Value returns the current error value
668
+func (ae *atomicError) Value() error {
669
+	if v := ae.value.Load(); v != nil {
670
+		// this will panic if the value doesn't implement the error interface
671
+		return v.(error)
672
+	}
673
+	return nil
674
+}
675
+
676
+func namedValueToValue(named []driver.NamedValue) ([]driver.Value, error) {
677
+	dargs := make([]driver.Value, len(named))
678
+	for n, param := range named {
679
+		if len(param.Name) > 0 {
680
+			// TODO: support the use of Named Parameters #561
681
+			return nil, errors.New("mysql: driver does not support the use of Named Parameters")
682
+		}
683
+		dargs[n] = param.Value
684
+	}
685
+	return dargs, nil
686
+}
687
+
688
+func mapIsolationLevel(level driver.IsolationLevel) (string, error) {
689
+	switch sql.IsolationLevel(level) {
690
+	case sql.LevelRepeatableRead:
691
+		return "REPEATABLE READ", nil
692
+	case sql.LevelReadCommitted:
693
+		return "READ COMMITTED", nil
694
+	case sql.LevelReadUncommitted:
695
+		return "READ UNCOMMITTED", nil
696
+	case sql.LevelSerializable:
697
+		return "SERIALIZABLE", nil
698
+	default:
699
+		return "", fmt.Errorf("mysql: unsupported isolation level: %v", level)
700
+	}
701
+}

+ 1
- 0
vendor/golang.org/x/sys/unix/mkerrors.sh View File

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

+ 11
- 1
vendor/golang.org/x/sys/unix/zerrors_aix_ppc.go View File

@@ -459,6 +459,15 @@ const (
459 459
 	MAP_SHARED                    = 0x1
460 460
 	MAP_TYPE                      = 0xf0
461 461
 	MAP_VARIABLE                  = 0x0
462
+	MCAST_BLOCK_SOURCE            = 0x40
463
+	MCAST_EXCLUDE                 = 0x2
464
+	MCAST_INCLUDE                 = 0x1
465
+	MCAST_JOIN_GROUP              = 0x3e
466
+	MCAST_JOIN_SOURCE_GROUP       = 0x42
467
+	MCAST_LEAVE_GROUP             = 0x3f
468
+	MCAST_LEAVE_SOURCE_GROUP      = 0x43
469
+	MCAST_SOURCE_FILTER           = 0x49
470
+	MCAST_UNBLOCK_SOURCE          = 0x41
462 471
 	MCL_CURRENT                   = 0x100
463 472
 	MCL_FUTURE                    = 0x200
464 473
 	MSG_ANY                       = 0x4
@@ -483,6 +492,7 @@ const (
483 492
 	MS_INVALIDATE                 = 0x40
484 493
 	MS_PER_SEC                    = 0x3e8
485 494
 	MS_SYNC                       = 0x20
495
+	NFDBITS                       = 0x20
486 496
 	NL0                           = 0x0
487 497
 	NL1                           = 0x4000
488 498
 	NL2                           = 0x8000
@@ -688,7 +698,7 @@ const (
688 698
 	SIOCGHIWAT                    = 0x40047301
689 699
 	SIOCGIFADDR                   = -0x3fd796df
690 700
 	SIOCGIFADDRS                  = 0x2000698c
691
-	SIOCGIFBAUDRATE               = -0x3fd79693
701
+	SIOCGIFBAUDRATE               = -0x3fdf9669
692 702
 	SIOCGIFBRDADDR                = -0x3fd796dd
693 703
 	SIOCGIFCONF                   = -0x3ff796bb
694 704
 	SIOCGIFCONFGLOB               = -0x3ff79670

+ 11
- 1
vendor/golang.org/x/sys/unix/zerrors_aix_ppc64.go View File

@@ -459,6 +459,15 @@ const (
459 459
 	MAP_SHARED                    = 0x1
460 460
 	MAP_TYPE                      = 0xf0
461 461
 	MAP_VARIABLE                  = 0x0
462
+	MCAST_BLOCK_SOURCE            = 0x40
463
+	MCAST_EXCLUDE                 = 0x2
464
+	MCAST_INCLUDE                 = 0x1
465
+	MCAST_JOIN_GROUP              = 0x3e
466
+	MCAST_JOIN_SOURCE_GROUP       = 0x42
467
+	MCAST_LEAVE_GROUP             = 0x3f
468
+	MCAST_LEAVE_SOURCE_GROUP      = 0x43
469
+	MCAST_SOURCE_FILTER           = 0x49
470
+	MCAST_UNBLOCK_SOURCE          = 0x41
462 471
 	MCL_CURRENT                   = 0x100
463 472
 	MCL_FUTURE                    = 0x200
464 473
 	MSG_ANY                       = 0x4
@@ -483,6 +492,7 @@ const (
483 492
 	MS_INVALIDATE                 = 0x40
484 493
 	MS_PER_SEC                    = 0x3e8
485 494
 	MS_SYNC                       = 0x20
495
+	NFDBITS                       = 0x40
486 496
 	NL0                           = 0x0
487 497
 	NL1                           = 0x4000
488 498
 	NL2                           = 0x8000
@@ -688,7 +698,7 @@ const (
688 698
 	SIOCGHIWAT                    = 0x40047301
689 699
 	SIOCGIFADDR                   = -0x3fd796df
690 700
 	SIOCGIFADDRS                  = 0x2000698c
691
-	SIOCGIFBAUDRATE               = -0x3fd79693
701
+	SIOCGIFBAUDRATE               = -0x3fdf9669
692 702
 	SIOCGIFBRDADDR                = -0x3fd796dd
693 703
 	SIOCGIFCONF                   = -0x3fef96bb
694 704
 	SIOCGIFCONFGLOB               = -0x3fef9670

+ 1
- 3
vendor/golang.org/x/sys/windows/security_windows.go View File

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

+ 55
- 0
vendor/golang.org/x/sys/windows/syscall_windows.go View File

@@ -313,6 +313,10 @@ func NewCallbackCDecl(fn interface{}) uintptr {
313 313
 //sys	CoTaskMemFree(address unsafe.Pointer) = ole32.CoTaskMemFree
314 314
 //sys	rtlGetVersion(info *OsVersionInfoEx) (ret error) = ntdll.RtlGetVersion
315 315
 //sys	rtlGetNtVersionNumbers(majorVersion *uint32, minorVersion *uint32, buildNumber *uint32) = ntdll.RtlGetNtVersionNumbers
316
+//sys	getProcessPreferredUILanguages(flags uint32, numLanguages *uint32, buf *uint16, bufSize *uint32) (err error) = kernel32.GetProcessPreferredUILanguages
317
+//sys	getThreadPreferredUILanguages(flags uint32, numLanguages *uint32, buf *uint16, bufSize *uint32) (err error) = kernel32.GetThreadPreferredUILanguages
318
+//sys	getUserPreferredUILanguages(flags uint32, numLanguages *uint32, buf *uint16, bufSize *uint32) (err error) = kernel32.GetUserPreferredUILanguages
319
+//sys	getSystemPreferredUILanguages(flags uint32, numLanguages *uint32, buf *uint16, bufSize *uint32) (err error) = kernel32.GetSystemPreferredUILanguages
316 320
 
317 321
 // Process Status API (PSAPI)
318 322
 //sys	EnumProcesses(processIds []uint32, bytesReturned *uint32) (err error) = psapi.EnumProcesses
@@ -1378,3 +1382,54 @@ func RtlGetNtVersionNumbers() (majorVersion, minorVersion, buildNumber uint32) {
1378 1382
 	buildNumber &= 0xffff
1379 1383
 	return
1380 1384
 }
1385
+
1386
+// GetProcessPreferredUILanguages retrieves the process preferred UI languages.
1387
+func GetProcessPreferredUILanguages(flags uint32) ([]string, error) {
1388
+	return getUILanguages(flags, getProcessPreferredUILanguages)
1389
+}
1390
+
1391
+// GetThreadPreferredUILanguages retrieves the thread preferred UI languages for the current thread.
1392
+func GetThreadPreferredUILanguages(flags uint32) ([]string, error) {
1393
+	return getUILanguages(flags, getThreadPreferredUILanguages)
1394
+}
1395
+
1396
+// GetUserPreferredUILanguages retrieves information about the user preferred UI languages.
1397
+func GetUserPreferredUILanguages(flags uint32) ([]string, error) {
1398
+	return getUILanguages(flags, getUserPreferredUILanguages)
1399
+}
1400
+
1401
+// GetSystemPreferredUILanguages retrieves the system preferred UI languages.
1402
+func GetSystemPreferredUILanguages(flags uint32) ([]string, error) {
1403
+	return getUILanguages(flags, getSystemPreferredUILanguages)
1404
+}
1405
+
1406
+func getUILanguages(flags uint32, f func(flags uint32, numLanguages *uint32, buf *uint16, bufSize *uint32) error) ([]string, error) {
1407
+	size := uint32(128)
1408
+	for {
1409
+		var numLanguages uint32
1410
+		buf := make([]uint16, size)
1411
+		err := f(flags, &numLanguages, &buf[0], &size)
1412
+		if err == ERROR_INSUFFICIENT_BUFFER {
1413
+			continue
1414
+		}
1415
+		if err != nil {
1416
+			return nil, err
1417
+		}
1418
+		buf = buf[:size]
1419
+		if numLanguages == 0 || len(buf) == 0 { // GetProcessPreferredUILanguages may return numLanguages==0 with "\0\0"
1420
+			return []string{}, nil
1421
+		}
1422
+		if buf[len(buf)-1] == 0 {
1423
+			buf = buf[:len(buf)-1] // remove terminating null
1424
+		}
1425
+		languages := make([]string, 0, numLanguages)
1426
+		from := 0
1427
+		for i, c := range buf {
1428
+			if c == 0 {
1429
+				languages = append(languages, string(utf16.Decode(buf[from:i])))
1430
+				from = i + 1
1431
+			}
1432
+		}
1433
+		return languages, nil
1434
+	}
1435
+}

+ 33
- 0
vendor/golang.org/x/sys/windows/types_windows.go View File

@@ -1742,3 +1742,36 @@ const (
1742 1742
 	GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT = 2
1743 1743
 	GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS       = 4
1744 1744
 )
1745
+
1746
+// MUI function flag values
1747
+const (
1748
+	MUI_LANGUAGE_ID                    = 0x4
1749
+	MUI_LANGUAGE_NAME                  = 0x8
1750
+	MUI_MERGE_SYSTEM_FALLBACK          = 0x10
1751
+	MUI_MERGE_USER_FALLBACK            = 0x20
1752
+	MUI_UI_FALLBACK                    = MUI_MERGE_SYSTEM_FALLBACK | MUI_MERGE_USER_FALLBACK
1753
+	MUI_THREAD_LANGUAGES               = 0x40
1754
+	MUI_CONSOLE_FILTER                 = 0x100
1755
+	MUI_COMPLEX_SCRIPT_FILTER          = 0x200
1756
+	MUI_RESET_FILTERS                  = 0x001
1757
+	MUI_USER_PREFERRED_UI_LANGUAGES    = 0x10
1758
+	MUI_USE_INSTALLED_LANGUAGES        = 0x20
1759
+	MUI_USE_SEARCH_ALL_LANGUAGES       = 0x40
1760
+	MUI_LANG_NEUTRAL_PE_FILE           = 0x100
1761
+	MUI_NON_LANG_NEUTRAL_FILE          = 0x200
1762
+	MUI_MACHINE_LANGUAGE_SETTINGS      = 0x400
1763
+	MUI_FILETYPE_NOT_LANGUAGE_NEUTRAL  = 0x001
1764
+	MUI_FILETYPE_LANGUAGE_NEUTRAL_MAIN = 0x002
1765
+	MUI_FILETYPE_LANGUAGE_NEUTRAL_MUI  = 0x004
1766
+	MUI_QUERY_TYPE                     = 0x001
1767
+	MUI_QUERY_CHECKSUM                 = 0x002
1768
+	MUI_QUERY_LANGUAGE_NAME            = 0x004
1769
+	MUI_QUERY_RESOURCE_TYPES           = 0x008
1770
+	MUI_FILEINFO_VERSION               = 0x001
1771
+
1772
+	MUI_FULL_LANGUAGE      = 0x01
1773
+	MUI_PARTIAL_LANGUAGE   = 0x02
1774
+	MUI_LIP_LANGUAGE       = 0x04
1775
+	MUI_LANGUAGE_INSTALLED = 0x20
1776
+	MUI_LANGUAGE_LICENSED  = 0x40
1777
+)

+ 52
- 0
vendor/golang.org/x/sys/windows/zsyscall_windows.go View File

@@ -248,6 +248,10 @@ var (
248 248
 	procCoTaskMemFree                                        = modole32.NewProc("CoTaskMemFree")
249 249
 	procRtlGetVersion                                        = modntdll.NewProc("RtlGetVersion")
250 250
 	procRtlGetNtVersionNumbers                               = modntdll.NewProc("RtlGetNtVersionNumbers")
251
+	procGetProcessPreferredUILanguages                       = modkernel32.NewProc("GetProcessPreferredUILanguages")
252
+	procGetThreadPreferredUILanguages                        = modkernel32.NewProc("GetThreadPreferredUILanguages")
253
+	procGetUserPreferredUILanguages                          = modkernel32.NewProc("GetUserPreferredUILanguages")
254
+	procGetSystemPreferredUILanguages                        = modkernel32.NewProc("GetSystemPreferredUILanguages")
251 255
 	procEnumProcesses                                        = modpsapi.NewProc("EnumProcesses")
252 256
 	procWSAStartup                                           = modws2_32.NewProc("WSAStartup")
253 257
 	procWSACleanup                                           = modws2_32.NewProc("WSACleanup")
@@ -2760,6 +2764,54 @@ func rtlGetNtVersionNumbers(majorVersion *uint32, minorVersion *uint32, buildNum
2760 2764
 	return
2761 2765
 }
2762 2766
 
2767
+func getProcessPreferredUILanguages(flags uint32, numLanguages *uint32, buf *uint16, bufSize *uint32) (err error) {
2768
+	r1, _, e1 := syscall.Syscall6(procGetProcessPreferredUILanguages.Addr(), 4, uintptr(flags), uintptr(unsafe.Pointer(numLanguages)), uintptr(unsafe.Pointer(buf)), uintptr(unsafe.Pointer(bufSize)), 0, 0)
2769
+	if r1 == 0 {
2770
+		if e1 != 0 {
2771
+			err = errnoErr(e1)
2772
+		} else {
2773
+			err = syscall.EINVAL
2774
+		}
2775
+	}
2776
+	return
2777
+}
2778
+
2779
+func getThreadPreferredUILanguages(flags uint32, numLanguages *uint32, buf *uint16, bufSize *uint32) (err error) {
2780
+	r1, _, e1 := syscall.Syscall6(procGetThreadPreferredUILanguages.Addr(), 4, uintptr(flags), uintptr(unsafe.Pointer(numLanguages)), uintptr(unsafe.Pointer(buf)), uintptr(unsafe.Pointer(bufSize)), 0, 0)
2781
+	if r1 == 0 {
2782
+		if e1 != 0 {
2783
+			err = errnoErr(e1)
2784
+		} else {
2785
+			err = syscall.EINVAL
2786
+		}
2787
+	}
2788
+	return
2789
+}
2790
+
2791
+func getUserPreferredUILanguages(flags uint32, numLanguages *uint32, buf *uint16, bufSize *uint32) (err error) {
2792
+	r1, _, e1 := syscall.Syscall6(procGetUserPreferredUILanguages.Addr(), 4, uintptr(flags), uintptr(unsafe.Pointer(numLanguages)), uintptr(unsafe.Pointer(buf)), uintptr(unsafe.Pointer(bufSize)), 0, 0)
2793
+	if r1 == 0 {
2794
+		if e1 != 0 {
2795
+			err = errnoErr(e1)
2796
+		} else {
2797
+			err = syscall.EINVAL
2798
+		}
2799
+	}
2800
+	return
2801
+}
2802
+
2803
+func getSystemPreferredUILanguages(flags uint32, numLanguages *uint32, buf *uint16, bufSize *uint32) (err error) {
2804
+	r1, _, e1 := syscall.Syscall6(procGetSystemPreferredUILanguages.Addr(), 4, uintptr(flags), uintptr(unsafe.Pointer(numLanguages)), uintptr(unsafe.Pointer(buf)), uintptr(unsafe.Pointer(bufSize)), 0, 0)
2805
+	if r1 == 0 {
2806
+		if e1 != 0 {
2807
+			err = errnoErr(e1)
2808
+		} else {
2809
+			err = syscall.EINVAL
2810
+		}
2811
+	}
2812
+	return
2813
+}
2814
+
2763 2815
 func EnumProcesses(processIds []uint32, bytesReturned *uint32) (err error) {
2764 2816
 	var _p0 *uint32
2765 2817
 	if len(processIds) > 0 {

+ 10
- 9
vendor/modules.txt View File

@@ -9,6 +9,9 @@ github.com/go-asn1-ber/asn1-ber
9 9
 # github.com/go-ldap/ldap/v3 v3.1.6
10 10
 ## explicit
11 11
 github.com/go-ldap/ldap/v3
12
+# github.com/go-sql-driver/mysql v1.5.0
13
+## explicit
14
+github.com/go-sql-driver/mysql
12 15
 # github.com/goshuirc/e-nfa v0.0.0-20160917075329-7071788e3940
13 16
 ## explicit
14 17
 github.com/goshuirc/e-nfa
@@ -26,36 +29,35 @@ github.com/mattn/go-isatty
26 29
 # github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b
27 30
 ## explicit
28 31
 github.com/mgutz/ansi
32
+# github.com/onsi/ginkgo v1.12.0
33
+## explicit
34
+# github.com/onsi/gomega v1.9.0
35
+## explicit
29 36
 # github.com/oragono/confusables v0.0.0-20190624102032-fe1cf31a24b0
30 37
 ## explicit
31 38
 github.com/oragono/confusables
32 39
 # github.com/oragono/go-ident v0.0.0-20170110123031-337fed0fd21a
33 40
 ## explicit
34 41
 github.com/oragono/go-ident
35
-# github.com/tidwall/btree v0.0.0-20191029221954-400434d76274
42
+# github.com/stretchr/testify v1.4.0
36 43
 ## explicit
44
+# github.com/tidwall/btree v0.0.0-20191029221954-400434d76274
37 45
 github.com/tidwall/btree
38 46
 # github.com/tidwall/buntdb v1.1.2
39 47
 ## explicit
40 48
 github.com/tidwall/buntdb
41 49
 # github.com/tidwall/gjson v1.3.4
42
-## explicit
43 50
 github.com/tidwall/gjson
44 51
 # github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb
45
-## explicit
46 52
 github.com/tidwall/grect
47 53
 # github.com/tidwall/match v1.0.1
48
-## explicit
49 54
 github.com/tidwall/match
50 55
 # github.com/tidwall/pretty v1.0.0
51
-## explicit
52 56
 github.com/tidwall/pretty
53 57
 # github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e
54
-## explicit
55 58
 github.com/tidwall/rtree
56 59
 github.com/tidwall/rtree/base
57 60
 # github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563
58
-## explicit
59 61
 github.com/tidwall/tinyqueue
60 62
 # golang.org/x/crypto v0.0.0-20191112222119-e1110fd1c708
61 63
 ## explicit
@@ -63,8 +65,7 @@ golang.org/x/crypto/bcrypt
63 65
 golang.org/x/crypto/blowfish
64 66
 golang.org/x/crypto/sha3
65 67
 golang.org/x/crypto/ssh/terminal
66
-# golang.org/x/sys v0.0.0-20191115151921-52ab43148777
67
-## explicit
68
+# golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e
68 69
 golang.org/x/sys/cpu
69 70
 golang.org/x/sys/unix
70 71
 golang.org/x/sys/windows

Loading…
Cancel
Save