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
 	cd irc/history && go test . && go vet .
25
 	cd irc/history && go test . && go vet .
26
 	cd irc/isupport && go test . && go vet .
26
 	cd irc/isupport && go test . && go vet .
27
 	cd irc/modes && go test . && go vet .
27
 	cd irc/modes && go test . && go vet .
28
+	cd irc/mysql && go test . && go vet .
28
 	cd irc/passwd && go test . && go vet .
29
 	cd irc/passwd && go test . && go vet .
29
 	cd irc/utils && go test . && go vet .
30
 	cd irc/utils && go test . && go vet .
30
 	./.check-gofmt.sh
31
 	./.check-gofmt.sh

+ 6
- 6
gencapdefs.py View File

135
         url="https://ircv3.net/specs/extensions/userhost-in-names-3.2.html",
135
         url="https://ircv3.net/specs/extensions/userhost-in-names-3.2.html",
136
         standard="IRCv3",
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
     CapDef(
138
     CapDef(
145
         identifier="ZNCSelfMessage",
139
         identifier="ZNCSelfMessage",
146
         name="znc.in/self-message",
140
         name="znc.in/self-message",
171
         url="https://github.com/ircv3/ircv3-specifications/pull/398",
165
         url="https://github.com/ircv3/ircv3-specifications/pull/398",
172
         standard="proposed IRCv3",
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
 def validate_defs():
176
 def validate_defs():

+ 4
- 8
go.mod View File

6
 	code.cloudfoundry.org/bytefmt v0.0.0-20190819182555-854d396b647c
6
 	code.cloudfoundry.org/bytefmt v0.0.0-20190819182555-854d396b647c
7
 	github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815
7
 	github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815
8
 	github.com/go-ldap/ldap/v3 v3.1.6
8
 	github.com/go-ldap/ldap/v3 v3.1.6
9
+	github.com/go-sql-driver/mysql v1.5.0
9
 	github.com/goshuirc/e-nfa v0.0.0-20160917075329-7071788e3940 // indirect
10
 	github.com/goshuirc/e-nfa v0.0.0-20160917075329-7071788e3940 // indirect
10
 	github.com/goshuirc/irc-go v0.0.0-20190713001546-05ecc95249a0
11
 	github.com/goshuirc/irc-go v0.0.0-20190713001546-05ecc95249a0
11
 	github.com/mattn/go-colorable v0.1.4
12
 	github.com/mattn/go-colorable v0.1.4
12
 	github.com/mattn/go-isatty v0.0.10 // indirect
13
 	github.com/mattn/go-isatty v0.0.10 // indirect
13
 	github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b
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
 	github.com/oragono/confusables v0.0.0-20190624102032-fe1cf31a24b0
17
 	github.com/oragono/confusables v0.0.0-20190624102032-fe1cf31a24b0
15
 	github.com/oragono/go-ident v0.0.0-20170110123031-337fed0fd21a
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
 	github.com/tidwall/buntdb v1.1.2
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
 	golang.org/x/crypto v0.0.0-20191112222119-e1110fd1c708
21
 	golang.org/x/crypto v0.0.0-20191112222119-e1110fd1c708
25
-	golang.org/x/sys v0.0.0-20191115151921-52ab43148777 // indirect
26
 	golang.org/x/text v0.3.2
22
 	golang.org/x/text v0.3.2
27
 	gopkg.in/yaml.v2 v2.2.5
23
 	gopkg.in/yaml.v2 v2.2.5
28
 )
24
 )

+ 91
- 0
go.sum View File

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
 	keyAccountSettings         = "account.settings %s"
33
 	keyAccountSettings         = "account.settings %s"
34
 	keyAccountVHost            = "account.vhost %s"
34
 	keyAccountVHost            = "account.vhost %s"
35
 	keyCertToAccount           = "account.creds.certfp %s"
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
 	keyVHostQueueAcctToId = "vhostQueue %s"
40
 	keyVHostQueueAcctToId = "vhostQueue %s"
39
 	vhostRequestIdx       = "vhostQueue"
41
 	vhostRequestIdx       = "vhostQueue"
71
 	config := server.Config()
73
 	config := server.Config()
72
 	am.buildNickToAccountIndex(config)
74
 	am.buildNickToAccountIndex(config)
73
 	am.initVHostRequestQueue(config)
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
 func (am *AccountManager) buildNickToAccountIndex(config *Config) {
112
 func (am *AccountManager) buildNickToAccountIndex(config *Config) {
346
 	callbackSpec := fmt.Sprintf("%s:%s", callbackNamespace, callbackValue)
382
 	callbackSpec := fmt.Sprintf("%s:%s", callbackNamespace, callbackValue)
347
 
383
 
348
 	var setOptions *buntdb.SetOptions
384
 	var setOptions *buntdb.SetOptions
349
-	ttl := config.Registration.VerifyTimeout
385
+	ttl := time.Duration(config.Registration.VerifyTimeout)
350
 	if ttl != 0 {
386
 	if ttl != 0 {
351
 		setOptions = &buntdb.SetOptions{Expires: true, TTL: ttl}
387
 		setOptions = &buntdb.SetOptions{Expires: true, TTL: ttl}
352
 	}
388
 	}
477
 	return err
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
 func (am *AccountManager) addRemoveCertfp(account, certfp string, add bool, hasPrivs bool) (err error) {
568
 func (am *AccountManager) addRemoveCertfp(account, certfp string, add bool, hasPrivs bool) (err error) {
481
 	certfp, err = utils.NormalizeCertfp(certfp)
569
 	certfp, err = utils.NormalizeCertfp(certfp)
482
 	if err != nil {
570
 	if err != nil {
685
 	}
773
 	}
686
 	am.server.logger.Info("accounts", "client", nick, "registered account", casefoldedAccount)
774
 	am.server.logger.Info("accounts", "client", nick, "registered account", casefoldedAccount)
687
 	raw.Verified = true
775
 	raw.Verified = true
688
-	clientAccount, err := am.deserializeRawAccount(raw)
776
+	clientAccount, err := am.deserializeRawAccount(raw, casefoldedAccount)
689
 	if err != nil {
777
 	if err != nil {
690
 		return err
778
 		return err
691
 	}
779
 	}
892
 		return
980
 		return
893
 	}
981
 	}
894
 
982
 
895
-	result, err = am.deserializeRawAccount(raw)
896
-	result.NameCasefolded = casefoldedAccount
983
+	result, err = am.deserializeRawAccount(raw, casefoldedAccount)
897
 	return
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
 	result.Name = raw.Name
988
 	result.Name = raw.Name
989
+	result.NameCasefolded = cfName
902
 	regTimeInt, _ := strconv.ParseInt(raw.RegisteredAt, 10, 64)
990
 	regTimeInt, _ := strconv.ParseInt(raw.RegisteredAt, 10, 64)
903
 	result.RegisteredAt = time.Unix(regTimeInt, 0).UTC()
991
 	result.RegisteredAt = time.Unix(regTimeInt, 0).UTC()
904
 	e := json.Unmarshal([]byte(raw.Credentials), &result.Credentials)
992
 	e := json.Unmarshal([]byte(raw.Credentials), &result.Credentials)
976
 	vhostKey := fmt.Sprintf(keyAccountVHost, casefoldedAccount)
1064
 	vhostKey := fmt.Sprintf(keyAccountVHost, casefoldedAccount)
977
 	vhostQueueKey := fmt.Sprintf(keyVHostQueueAcctToId, casefoldedAccount)
1065
 	vhostQueueKey := fmt.Sprintf(keyVHostQueueAcctToId, casefoldedAccount)
978
 	channelsKey := fmt.Sprintf(keyAccountChannels, casefoldedAccount)
1066
 	channelsKey := fmt.Sprintf(keyAccountChannels, casefoldedAccount)
1067
+	joinedChannelsKey := fmt.Sprintf(keyAccountJoinedChannels, casefoldedAccount)
1068
+	lastSignoffKey := fmt.Sprintf(keyAccountLastSignoff, casefoldedAccount)
979
 
1069
 
980
 	var clients []*Client
1070
 	var clients []*Client
981
 
1071
 
1011
 		tx.Delete(vhostKey)
1101
 		tx.Delete(vhostKey)
1012
 		channelsStr, _ = tx.Get(channelsKey)
1102
 		channelsStr, _ = tx.Get(channelsKey)
1013
 		tx.Delete(channelsKey)
1103
 		tx.Delete(channelsKey)
1104
+		tx.Delete(joinedChannelsKey)
1105
+		tx.Delete(lastSignoffKey)
1014
 
1106
 
1015
 		_, err := tx.Delete(vhostQueueKey)
1107
 		_, err := tx.Delete(vhostQueueKey)
1016
 		am.decrementVHostQueueCount(casefoldedAccount, err)
1108
 		am.decrementVHostQueueCount(casefoldedAccount, err)
1087
 	return unmarshalRegisteredChannels(channelStr)
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
 		return errAccountInvalidCredentials
1184
 		return errAccountInvalidCredentials
1093
 	}
1185
 	}
1094
 
1186
 
1095
 	var account string
1187
 	var account string
1096
-	certFPKey := fmt.Sprintf(keyCertToAccount, client.certfp)
1188
+	certFPKey := fmt.Sprintf(keyCertToAccount, certfp)
1097
 
1189
 
1098
 	err := am.server.store.View(func(tx *buntdb.Tx) error {
1190
 	err := am.server.store.View(func(tx *buntdb.Tx) error {
1099
 		account, _ = tx.Get(certFPKey)
1191
 		account, _ = tx.Get(certFPKey)
1455
 }
1547
 }
1456
 
1548
 
1457
 func (am *AccountManager) Login(client *Client, account ClientAccount) {
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
 	client.nickTimer.Touch(nil)
1552
 	client.nickTimer.Touch(nil)
1464
 
1553
 
1468
 	am.Lock()
1557
 	am.Lock()
1469
 	defer am.Unlock()
1558
 	defer am.Unlock()
1470
 	am.accountToClients[casefoldedAccount] = append(am.accountToClients[casefoldedAccount], client)
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
 func (am *AccountManager) Logout(client *Client) {
1562
 func (am *AccountManager) Logout(client *Client) {
1590
 	return nil
1676
 	return nil
1591
 }
1677
 }
1592
 
1678
 
1593
-type BouncerAllowedSetting int
1679
+type MulticlientAllowedSetting int
1594
 
1680
 
1595
 const (
1681
 const (
1596
-	BouncerAllowedServerDefault BouncerAllowedSetting = iota
1597
-	BouncerDisallowedByUser
1598
-	BouncerAllowedByUser
1682
+	MulticlientAllowedServerDefault MulticlientAllowedSetting = iota
1683
+	MulticlientDisallowedByUser
1684
+	MulticlientAllowedByUser
1599
 )
1685
 )
1600
 
1686
 
1601
 // controls whether/when clients without event-playback support see fake
1687
 // controls whether/when clients without event-playback support see fake
1622
 	return
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
 type AccountSettings struct {
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
 // ClientAccount represents a user account.
1723
 // ClientAccount represents a user account.
1661
 		return
1752
 		return
1662
 	}
1753
 	}
1663
 
1754
 
1664
-	client.SetAccountName("")
1755
+	client.Logout()
1665
 	go client.nickTimer.Touch(nil)
1756
 	go client.nickTimer.Touch(nil)
1666
 
1757
 
1667
 	// dispatch account-notify
1758
 	// dispatch account-notify

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

37
 	// https://ircv3.net/specs/extensions/chghost-3.2.html
37
 	// https://ircv3.net/specs/extensions/chghost-3.2.html
38
 	ChgHost Capability = iota
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
 	// EventPlayback is the proposed IRCv3 capability named "draft/event-playback":
44
 	// EventPlayback is the proposed IRCv3 capability named "draft/event-playback":
41
 	// https://github.com/ircv3/ircv3-specifications/pull/362
45
 	// https://github.com/ircv3/ircv3-specifications/pull/362
42
 	EventPlayback Capability = iota
46
 	EventPlayback Capability = iota
85
 	// https://ircv3.net/specs/extensions/multi-prefix-3.1.html
89
 	// https://ircv3.net/specs/extensions/multi-prefix-3.1.html
86
 	MultiPrefix Capability = iota
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
 	// Nope is the Oragono vendor capability named "oragono.io/nope":
92
 	// Nope is the Oragono vendor capability named "oragono.io/nope":
93
 	// https://oragono.io/nope
93
 	// https://oragono.io/nope
94
 	Nope Capability = iota
94
 	Nope Capability = iota
127
 		"batch",
127
 		"batch",
128
 		"cap-notify",
128
 		"cap-notify",
129
 		"chghost",
129
 		"chghost",
130
+		"draft/chathistory",
130
 		"draft/event-playback",
131
 		"draft/event-playback",
131
 		"draft/languages",
132
 		"draft/languages",
132
 		"draft/multiline",
133
 		"draft/multiline",
139
 		"labeled-response",
140
 		"labeled-response",
140
 		"message-tags",
141
 		"message-tags",
141
 		"multi-prefix",
142
 		"multi-prefix",
142
-		"oragono.io/bnc",
143
 		"oragono.io/nope",
143
 		"oragono.io/nope",
144
 		"sasl",
144
 		"sasl",
145
 		"server-time",
145
 		"server-time",

+ 121
- 31
irc/channel.go View File

24
 	histServMask = "HistServ!HistServ@localhost"
24
 	histServMask = "HistServ!HistServ@localhost"
25
 )
25
 )
26
 
26
 
27
+type ChannelSettings struct {
28
+	History HistoryStatus
29
+}
30
+
27
 // Channel represents a channel that clients can join.
31
 // Channel represents a channel that clients can join.
28
 type Channel struct {
32
 type Channel struct {
29
 	flags             modes.ModeSet
33
 	flags             modes.ModeSet
49
 	joinPartMutex     sync.Mutex      // tier 3
53
 	joinPartMutex     sync.Mutex      // tier 3
50
 	ensureLoaded      utils.Once      // manages loading stored registration info from the database
54
 	ensureLoaded      utils.Once      // manages loading stored registration info from the database
51
 	dirtyBits         uint
55
 	dirtyBits         uint
56
+	settings          ChannelSettings
52
 }
57
 }
53
 
58
 
54
 // NewChannel creates a new channel from a `Server` and a `name`
59
 // NewChannel creates a new channel from a `Server` and a `name`
66
 
71
 
67
 	channel.initializeLists()
72
 	channel.initializeLists()
68
 	channel.writerSemaphore.Initialize(1)
73
 	channel.writerSemaphore.Initialize(1)
69
-	channel.history.Initialize(config.History.ChannelLength, config.History.AutoresizeWindow)
74
+	channel.history.Initialize(0, 0)
70
 
75
 
71
 	if !registered {
76
 	if !registered {
77
+		channel.resizeHistory(config)
72
 		for _, mode := range config.Channels.defaultModes {
78
 		for _, mode := range config.Channels.defaultModes {
73
 			channel.flags.SetMode(mode, true)
79
 			channel.flags.SetMode(mode, true)
74
 		}
80
 		}
106
 	return channel.ensureLoaded.Done()
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
 // read in channel state that was persisted in the DB
124
 // read in channel state that was persisted in the DB
110
 func (channel *Channel) applyRegInfo(chanReg RegisteredChannel) {
125
 func (channel *Channel) applyRegInfo(chanReg RegisteredChannel) {
126
+	defer channel.resizeHistory(channel.server.Config())
127
+
111
 	channel.stateMutex.Lock()
128
 	channel.stateMutex.Lock()
112
 	defer channel.stateMutex.Unlock()
129
 	defer channel.stateMutex.Unlock()
113
 
130
 
120
 	channel.createdTime = chanReg.RegisteredAt
137
 	channel.createdTime = chanReg.RegisteredAt
121
 	channel.key = chanReg.Key
138
 	channel.key = chanReg.Key
122
 	channel.userLimit = chanReg.UserLimit
139
 	channel.userLimit = chanReg.UserLimit
140
+	channel.settings = chanReg.Settings
123
 
141
 
124
 	for _, mode := range chanReg.Modes {
142
 	for _, mode := range chanReg.Modes {
125
 		channel.flags.SetMode(mode, true)
143
 		channel.flags.SetMode(mode, true)
164
 		}
182
 		}
165
 	}
183
 	}
166
 
184
 
185
+	if includeFlags&IncludeSettings != 0 {
186
+		info.Settings = channel.settings
187
+	}
188
+
167
 	return
189
 	return
168
 }
190
 }
169
 
191
 
434
 			if modeSet == nil {
456
 			if modeSet == nil {
435
 				continue
457
 				continue
436
 			}
458
 			}
437
-			if !isJoined && target.flags.HasMode(modes.Invisible) && !isOper {
459
+			if !isJoined && target.HasMode(modes.Invisible) && !isOper {
438
 				continue
460
 				continue
439
 			}
461
 			}
440
 			prefix := modeSet.Prefixes(isMultiPrefix)
462
 			prefix := modeSet.Prefixes(isMultiPrefix)
564
 	return len(channel.members) == 0
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
 // Join joins the given client to this channel (if they can be joined).
631
 // Join joins the given client to this channel (if they can be joined).
568
 func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *ResponseBuffer) {
632
 func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *ResponseBuffer) {
569
 	details := client.Details()
633
 	details := client.Details()
618
 
682
 
619
 	client.server.logger.Debug("join", fmt.Sprintf("%s joined channel %s", details.nick, chname))
683
 	client.server.logger.Debug("join", fmt.Sprintf("%s joined channel %s", details.nick, chname))
620
 
684
 
621
-	var message utils.SplitMessage
622
-
623
 	givenMode := func() (givenMode modes.Mode) {
685
 	givenMode := func() (givenMode modes.Mode) {
624
 		channel.joinPartMutex.Lock()
686
 		channel.joinPartMutex.Lock()
625
 		defer channel.joinPartMutex.Unlock()
687
 		defer channel.joinPartMutex.Unlock()
643
 
705
 
644
 		channel.regenerateMembersCache()
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
 		message = utils.MakeMessage("")
714
 		message = utils.MakeMessage("")
647
 		histItem := history.Item{
715
 		histItem := history.Item{
648
 			Type:        history.Join,
716
 			Type:        history.Join,
651
 			Message:     message,
719
 			Message:     message,
652
 		}
720
 		}
653
 		histItem.Params[0] = details.realname
721
 		histItem.Params[0] = details.realname
654
-		channel.history.Add(histItem)
655
-
656
-		return
657
-	}()
722
+		channel.AddHistoryItem(histItem)
723
+	}
658
 
724
 
659
 	client.addChannel(channel)
725
 	client.addChannel(channel)
660
 
726
 
727
+	if rb == nil {
728
+		return
729
+	}
730
+
661
 	var modestr string
731
 	var modestr string
662
 	if givenMode != 0 {
732
 	if givenMode != 0 {
663
 		modestr = fmt.Sprintf("+%v", givenMode)
733
 		modestr = fmt.Sprintf("+%v", givenMode)
702
 
772
 
703
 func (channel *Channel) autoReplayHistory(client *Client, rb *ResponseBuffer, skipMsgid string) {
773
 func (channel *Channel) autoReplayHistory(client *Client, rb *ResponseBuffer, skipMsgid string) {
704
 	// autoreplay any messages as necessary
774
 	// autoreplay any messages as necessary
705
-	config := channel.server.Config()
706
 	var items []history.Item
775
 	var items []history.Item
776
+
777
+	var after, before time.Time
707
 	if rb.session.zncPlaybackTimes != nil && (rb.session.zncPlaybackTimes.targets == nil || rb.session.zncPlaybackTimes.targets.Has(channel.NameCasefolded())) {
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
 	} else if !rb.session.HasHistoryCaps() {
791
 	} else if !rb.session.HasHistoryCaps() {
710
 		var replayLimit int
792
 		var replayLimit int
711
 		customReplayLimit := client.AccountSettings().AutoreplayLines
793
 		customReplayLimit := client.AccountSettings().AutoreplayLines
719
 			replayLimit = channel.server.Config().History.AutoreplayOnJoin
801
 			replayLimit = channel.server.Config().History.AutoreplayOnJoin
720
 		}
802
 		}
721
 		if 0 < replayLimit {
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
 	// remove the client's own JOIN line from the replay
810
 	// remove the client's own JOIN line from the replay
784
 		}
869
 		}
785
 	}
870
 	}
786
 
871
 
787
-	channel.history.Add(history.Item{
872
+	channel.AddHistoryItem(history.Item{
788
 		Type:        history.Part,
873
 		Type:        history.Part,
789
 		Nick:        details.nickMask,
874
 		Nick:        details.nickMask,
790
 		AccountName: details.accountName,
875
 		AccountName: details.accountName,
799
 // 2. Send JOIN and MODE lines to channel participants (including the new client)
884
 // 2. Send JOIN and MODE lines to channel participants (including the new client)
800
 // 3. Replay missed message history to the client
885
 // 3. Replay missed message history to the client
801
 func (channel *Channel) Resume(session *Session, timestamp time.Time) {
886
 func (channel *Channel) Resume(session *Session, timestamp time.Time) {
802
-	now := time.Now().UTC()
803
 	channel.resumeAndAnnounce(session)
887
 	channel.resumeAndAnnounce(session)
804
 	if !timestamp.IsZero() {
888
 	if !timestamp.IsZero() {
805
-		channel.replayHistoryForResume(session, timestamp, now)
889
+		channel.replayHistoryForResume(session, timestamp, time.Time{})
806
 	}
890
 	}
807
 }
891
 }
808
 
892
 
852
 }
936
 }
853
 
937
 
854
 func (channel *Channel) replayHistoryForResume(session *Session, after time.Time, before time.Time) {
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
 	rb := NewResponseBuffer(session)
946
 	rb := NewResponseBuffer(session)
857
-	channel.replayHistoryItems(rb, items, false)
947
+	if len(items) != 0 {
948
+		channel.replayHistoryItems(rb, items, false)
949
+	}
858
 	if !complete && !session.resumeDetails.HistoryIncomplete {
950
 	if !complete && !session.resumeDetails.HistoryIncomplete {
859
 		// warn here if we didn't warn already
951
 		// warn here if we didn't warn already
860
 		rb.Add(nil, histServMask, "NOTICE", channel.Name(), session.client.t("Some additional message history may have been lost"))
952
 		rb.Add(nil, histServMask, "NOTICE", channel.Name(), session.client.t("Some additional message history may have been lost"))
871
 }
963
 }
872
 
964
 
873
 func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.Item, autoreplay bool) {
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
 	chname := channel.Name()
967
 	chname := channel.Name()
879
 	client := rb.target
968
 	client := rb.target
880
 	eventPlayback := rb.session.capabilities.Has(caps.EventPlayback)
969
 	eventPlayback := rb.session.capabilities.Has(caps.EventPlayback)
908
 		case history.Join:
997
 		case history.Join:
909
 			if eventPlayback {
998
 			if eventPlayback {
910
 				if extendedJoin {
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
 				} else {
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
 			} else {
1004
 			} else {
916
 				if !playJoinsAsPrivmsg {
1005
 				if !playJoinsAsPrivmsg {
926
 			}
1015
 			}
927
 		case history.Part:
1016
 		case history.Part:
928
 			if eventPlayback {
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
 			} else {
1019
 			} else {
931
 				if !playJoinsAsPrivmsg {
1020
 				if !playJoinsAsPrivmsg {
932
 					continue // #474
1021
 					continue // #474
936
 			}
1025
 			}
937
 		case history.Kick:
1026
 		case history.Kick:
938
 			if eventPlayback {
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
 			} else {
1029
 			} else {
941
 				message := fmt.Sprintf(client.t("%[1]s kicked %[2]s (%[3]s)"), nick, item.Params[0], item.Message.Message)
1030
 				message := fmt.Sprintf(client.t("%[1]s kicked %[2]s (%[3]s)"), nick, item.Params[0], item.Message.Message)
942
 				rb.AddFromClient(item.Message.Time, utils.MungeSecretToken(item.Message.Msgid), histServMask, "*", nil, "PRIVMSG", chname, message)
1031
 				rb.AddFromClient(item.Message.Time, utils.MungeSecretToken(item.Message.Msgid), histServMask, "*", nil, "PRIVMSG", chname, message)
943
 			}
1032
 			}
944
 		case history.Quit:
1033
 		case history.Quit:
945
 			if eventPlayback {
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
 			} else {
1036
 			} else {
948
 				if !playJoinsAsPrivmsg {
1037
 				if !playJoinsAsPrivmsg {
949
 					continue // #474
1038
 					continue // #474
953
 			}
1042
 			}
954
 		case history.Nick:
1043
 		case history.Nick:
955
 			if eventPlayback {
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
 			} else {
1046
 			} else {
958
 				message := fmt.Sprintf(client.t("%[1]s changed nick to %[2]s"), nick, item.Params[0])
1047
 				message := fmt.Sprintf(client.t("%[1]s changed nick to %[2]s"), nick, item.Params[0])
959
 				rb.AddFromClient(item.Message.Time, utils.MungeSecretToken(item.Message.Msgid), histServMask, "*", nil, "PRIVMSG", chname, message)
1048
 				rb.AddFromClient(item.Message.Time, utils.MungeSecretToken(item.Message.Msgid), histServMask, "*", nil, "PRIVMSG", chname, message)
1124
 			// STATUSMSG
1213
 			// STATUSMSG
1125
 			continue
1214
 			continue
1126
 		}
1215
 		}
1127
-		if isCTCP && member.isTor {
1128
-			continue // #753
1129
-		}
1130
 
1216
 
1131
 		for _, session := range member.Sessions() {
1217
 		for _, session := range member.Sessions() {
1218
+			if isCTCP && session.isTor {
1219
+				continue // #753
1220
+			}
1221
+
1132
 			var tagsToUse map[string]string
1222
 			var tagsToUse map[string]string
1133
 			if session.capabilities.Has(caps.MessageTags) {
1223
 			if session.capabilities.Has(caps.MessageTags) {
1134
 				tagsToUse = clientOnlyTags
1224
 				tagsToUse = clientOnlyTags
1144
 		}
1234
 		}
1145
 	}
1235
 	}
1146
 
1236
 
1147
-	channel.history.Add(history.Item{
1237
+	channel.AddHistoryItem(history.Item{
1148
 		Type:        histType,
1238
 		Type:        histType,
1149
 		Message:     message,
1239
 		Message:     message,
1150
 		Nick:        nickmask,
1240
 		Nick:        nickmask,
1266
 		Message:     message,
1356
 		Message:     message,
1267
 	}
1357
 	}
1268
 	histItem.Params[0] = targetNick
1358
 	histItem.Params[0] = targetNick
1269
-	channel.history.Add(histItem)
1359
+	channel.AddHistoryItem(histItem)
1270
 
1360
 
1271
 	channel.Quit(target)
1361
 	channel.Quit(target)
1272
 }
1362
 }

+ 16
- 1
irc/channelreg.go View File

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

+ 116
- 21
irc/chanserv.go View File

135
 			enabled:   chanregEnabled,
135
 			enabled:   chanregEnabled,
136
 			minParams: 1,
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
 	return
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
 func csUnregisterHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
366
 func csUnregisterHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
322
 	channelName := params[0]
367
 	channelName := params[0]
323
 	var verificationCode string
368
 	var verificationCode string
325
 		verificationCode = params[1]
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
 	if channel == nil {
374
 	if channel == nil {
336
 		csNotice(rb, client.t("No such channel"))
375
 		csNotice(rb, client.t("No such channel"))
337
 		return
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
 		return
382
 		return
350
 	}
383
 	}
351
 
384
 
352
-	info := channel.ExportRegistration(0)
353
 	expectedCode := utils.ConfirmationCode(info.Name, info.RegisteredAt)
385
 	expectedCode := utils.ConfirmationCode(info.Name, info.RegisteredAt)
354
 	if expectedCode != verificationCode {
386
 	if expectedCode != verificationCode {
355
 		csNotice(rb, ircfmt.Unescape(client.t("$bWarning: unregistering this channel will remove all stored channel attributes.$b")))
387
 		csNotice(rb, ircfmt.Unescape(client.t("$bWarning: unregistering this channel will remove all stored channel attributes.$b")))
357
 		return
389
 		return
358
 	}
390
 	}
359
 
391
 
360
-	server.channels.SetUnregistered(channelKey, founder)
392
+	server.channels.SetUnregistered(channelKey, info.Founder)
361
 	csNotice(rb, fmt.Sprintf(client.t("Channel %s is now unregistered"), channelKey))
393
 	csNotice(rb, fmt.Sprintf(client.t("Channel %s is now unregistered"), channelKey))
362
 }
394
 }
363
 
395
 
367
 		csNotice(rb, client.t("Channel does not exist"))
399
 		csNotice(rb, client.t("Channel does not exist"))
368
 		return
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
 		return
403
 		return
374
 	}
404
 	}
375
 
405
 
563
 	csNotice(rb, fmt.Sprintf(client.t("Founder: %s"), chinfo.Founder))
593
 	csNotice(rb, fmt.Sprintf(client.t("Founder: %s"), chinfo.Founder))
564
 	csNotice(rb, fmt.Sprintf(client.t("Registered at: %s"), chinfo.RegisteredAt.Format(time.RFC1123)))
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
 const (
29
 const (
30
 	// IdentTimeoutSeconds is how many seconds before our ident (username) check times out.
30
 	// IdentTimeoutSeconds is how many seconds before our ident (username) check times out.
31
 	IdentTimeoutSeconds  = 1.5
31
 	IdentTimeoutSeconds  = 1.5
32
-	IRCv3TimestampFormat = "2006-01-02T15:04:05.000Z"
32
+	IRCv3TimestampFormat = utils.IRCv3TimestampFormat
33
 )
33
 )
34
 
34
 
35
 // ResumeDetails is a place to stash data at various stages of
35
 // ResumeDetails is a place to stash data at various stages of
45
 type Client struct {
45
 type Client struct {
46
 	account            string
46
 	account            string
47
 	accountName        string // display name of the account: uncasefolded, '*' if not logged in
47
 	accountName        string // display name of the account: uncasefolded, '*' if not logged in
48
+	accountRegDate     time.Time
48
 	accountSettings    AccountSettings
49
 	accountSettings    AccountSettings
49
 	atime              time.Time
50
 	atime              time.Time
50
 	away               bool
51
 	away               bool
51
 	awayMessage        string
52
 	awayMessage        string
52
 	brbTimer           BrbTimer
53
 	brbTimer           BrbTimer
53
-	certfp             string
54
 	channels           ChannelSet
54
 	channels           ChannelSet
55
 	ctime              time.Time
55
 	ctime              time.Time
56
 	destroyed          bool
56
 	destroyed          bool
57
 	exitedSnomaskSent  bool
57
 	exitedSnomaskSent  bool
58
-	flags              modes.ModeSet
58
+	modes              modes.ModeSet
59
 	hostname           string
59
 	hostname           string
60
 	invitedTo          map[string]bool
60
 	invitedTo          map[string]bool
61
 	isSTSOnly          bool
61
 	isSTSOnly          bool
62
-	isTor              bool
63
 	languages          []string
62
 	languages          []string
63
+	lastSignoff        time.Time // for always-on clients, the time their last session quit
64
 	loginThrottle      connection_limits.GenericThrottle
64
 	loginThrottle      connection_limits.GenericThrottle
65
 	nick               string
65
 	nick               string
66
 	nickCasefolded     string
66
 	nickCasefolded     string
76
 	realIP             net.IP
76
 	realIP             net.IP
77
 	registered         bool
77
 	registered         bool
78
 	resumeID           string
78
 	resumeID           string
79
-	saslInProgress     bool
80
-	saslMechanism      string
81
-	saslValue          string
82
-	sentPassCommand    bool
83
 	server             *Server
79
 	server             *Server
84
 	skeleton           string
80
 	skeleton           string
85
 	sessions           []*Session
81
 	sessions           []*Session
86
 	stateMutex         sync.RWMutex // tier 1
82
 	stateMutex         sync.RWMutex // tier 1
83
+	alwaysOn           bool
87
 	username           string
84
 	username           string
88
 	vhost              string
85
 	vhost              string
89
 	history            history.Buffer
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
 // Session is an individual client connection to the server (TCP connection
100
 // Session is an individual client connection to the server (TCP connection
102
 	realIP      net.IP
110
 	realIP      net.IP
103
 	proxiedIP   net.IP
111
 	proxiedIP   net.IP
104
 	rawHostname string
112
 	rawHostname string
113
+	isTor       bool
105
 
114
 
106
 	idletimer IdleTimer
115
 	idletimer IdleTimer
107
 	fakelag   Fakelag
116
 	fakelag   Fakelag
108
 	destroyed uint32
117
 	destroyed uint32
109
 
118
 
119
+	certfp          string
120
+	sasl            saslStatus
121
+	sentPassCommand bool
122
+
110
 	batchCounter uint32
123
 	batchCounter uint32
111
 
124
 
112
 	quitMessage string
125
 	quitMessage string
120
 	resumeID         string
133
 	resumeID         string
121
 	resumeDetails    *ResumeDetails
134
 	resumeDetails    *ResumeDetails
122
 	zncPlaybackTimes *zncPlaybackTimes
135
 	zncPlaybackTimes *zncPlaybackTimes
136
+	lastSignoff      time.Time
123
 
137
 
124
 	batch MultilineBatch
138
 	batch MultilineBatch
125
 }
139
 }
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
 // returns whether the session was actively destroyed (for example, by ping
171
 // returns whether the session was actively destroyed (for example, by ping
151
 // timeout or NS GHOST).
172
 // timeout or NS GHOST).
152
 // avoids a race condition between asynchronous idle-timing-out of sessions,
173
 // avoids a race condition between asynchronous idle-timing-out of sessions,
164
 // returns whether the client supports a smart history replay cap,
185
 // returns whether the client supports a smart history replay cap,
165
 // and therefore autoreplay-on-join and similar should be suppressed
186
 // and therefore autoreplay-on-join and similar should be suppressed
166
 func (session *Session) HasHistoryCaps() bool {
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
 // generates a batch ID. the uniqueness requirements for this are fairly weak:
191
 // generates a batch ID. the uniqueness requirements for this are fairly weak:
231
 		channels:  make(ChannelSet),
251
 		channels:  make(ChannelSet),
232
 		ctime:     now,
252
 		ctime:     now,
233
 		isSTSOnly: conn.Config.STSOnly,
253
 		isSTSOnly: conn.Config.STSOnly,
234
-		isTor:     conn.Config.Tor,
235
 		languages: server.Languages().Default(),
254
 		languages: server.Languages().Default(),
236
 		loginThrottle: connection_limits.GenericThrottle{
255
 		loginThrottle: connection_limits.GenericThrottle{
237
 			Duration: config.Accounts.LoginThrottling.Duration,
256
 			Duration: config.Accounts.LoginThrottling.Duration,
253
 		ctime:      now,
272
 		ctime:      now,
254
 		atime:      now,
273
 		atime:      now,
255
 		realIP:     realIP,
274
 		realIP:     realIP,
275
+		isTor:      conn.Config.Tor,
256
 	}
276
 	}
257
 	client.sessions = []*Session{session}
277
 	client.sessions = []*Session{session}
258
 
278
 
259
 	if conn.Config.TLSConfig != nil {
279
 	if conn.Config.TLSConfig != nil {
260
 		client.SetMode(modes.TLS, true)
280
 		client.SetMode(modes.TLS, true)
261
 		// error is not useful to us here anyways so we can ignore it
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
 	if conn.Config.Tor {
285
 	if conn.Config.Tor {
272
 		client.rawHostname = session.rawHostname
292
 		client.rawHostname = session.rawHostname
273
 	} else {
293
 	} else {
274
 		remoteAddr := conn.Conn.RemoteAddr()
294
 		remoteAddr := conn.Conn.RemoteAddr()
275
-		if utils.AddrIsLocal(remoteAddr) {
295
+		if realIP.IsLoopback() || utils.IPInNets(realIP, config.Server.secureNets) {
276
 			// treat local connections as secure (may be overridden later by WEBIRC)
296
 			// treat local connections as secure (may be overridden later by WEBIRC)
277
 			client.SetMode(modes.TLS, true)
297
 			client.SetMode(modes.TLS, true)
278
 		}
298
 		}
286
 	client.run(session, proxyLine)
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
 // resolve an IP to an IRC-ready hostname, using reverse DNS, forward-confirming if necessary,
365
 // resolve an IP to an IRC-ready hostname, using reverse DNS, forward-confirming if necessary,
290
 // and sending appropriate notices to the client
366
 // and sending appropriate notices to the client
291
 func (client *Client) lookupHostname(session *Session, overwrite bool) {
367
 func (client *Client) lookupHostname(session *Session, overwrite bool) {
292
-	if client.isTor {
368
+	if session.isTor {
293
 		return
369
 		return
294
 	} // else: even if cloaking is enabled, look up the real hostname to show to operators
370
 	} // else: even if cloaking is enabled, look up the real hostname to show to operators
295
 
371
 
384
 	authFailSaslRequired
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
 	saslSent := client.account != ""
464
 	saslSent := client.account != ""
389
 	// PASS requirement
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
 		return authFailPass
467
 		return authFailPass
392
 	}
468
 	}
393
 	// Tor connections may be required to authenticate with SASL
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
 		return authFailTorSaslRequired
471
 		return authFailTorSaslRequired
396
 	}
472
 	}
397
 	// finally, enforce require-sasl
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
 		return authFailSaslRequired
475
 		return authFailSaslRequired
400
 	}
476
 	}
401
 	return authSuccess
477
 	return authSuccess
572
 
648
 
573
 func (client *Client) playReattachMessages(session *Session) {
649
 func (client *Client) playReattachMessages(session *Session) {
574
 	client.server.playRegistrationBurst(session)
650
 	client.server.playRegistrationBurst(session)
651
+	hasHistoryCaps := session.HasHistoryCaps()
575
 	for _, channel := range session.client.Channels() {
652
 	for _, channel := range session.client.Channels() {
576
 		channel.playJoinForSession(session)
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
 		// if they negotiated znc.in/playback or chathistory, they will receive nothing,
658
 		// if they negotiated znc.in/playback or chathistory, they will receive nothing,
579
 		// because those caps disable autoreplay-on-join and they haven't sent the relevant
659
 		// because those caps disable autoreplay-on-join and they haven't sent the relevant
580
 		// *playback PRIVMSG or CHATHISTORY command yet
660
 		// *playback PRIVMSG or CHATHISTORY command yet
582
 		channel.autoReplayHistory(client, rb, "")
662
 		channel.autoReplayHistory(client, rb, "")
583
 		rb.Send(true)
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
 		return
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
 	err := server.clients.Resume(oldClient, session)
723
 	err := server.clients.Resume(oldClient, session)
643
 	if err != nil {
724
 	if err != nil {
644
 		session.Send(nil, server.name, "FAIL", "RESUME", "CANNOT_RESUME", client.t("Cannot resume connection"))
725
 		session.Send(nil, server.name, "FAIL", "RESUME", "CANNOT_RESUME", client.t("Cannot resume connection"))
657
 func (session *Session) playResume() {
738
 func (session *Session) playResume() {
658
 	client := session.client
739
 	client := session.client
659
 	server := client.server
740
 	server := client.server
741
+	config := server.Config()
660
 
742
 
661
 	friends := make(ClientSet)
743
 	friends := make(ClientSet)
662
-	oldestLostMessage := time.Now().UTC()
744
+	var oldestLostMessage time.Time
663
 
745
 
664
 	// work out how much time, if any, is not covered by history buffers
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
 	for _, channel := range client.Channels() {
748
 	for _, channel := range client.Channels() {
666
 		for _, member := range channel.Members() {
749
 		for _, member := range channel.Members() {
667
 			friends.Add(member)
750
 			friends.Add(member)
751
+		}
752
+		_, ephemeral, _ := channel.historyStatus(config)
753
+		if ephemeral {
668
 			lastDiscarded := channel.history.LastDiscarded()
754
 			lastDiscarded := channel.history.LastDiscarded()
669
-			if lastDiscarded.Before(oldestLostMessage) {
755
+			if oldestLostMessage.Before(lastDiscarded) {
670
 				oldestLostMessage = lastDiscarded
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
 	timestamp := session.resumeDetails.Timestamp
778
 	timestamp := session.resumeDetails.Timestamp
690
-	gap := lastDiscarded.Sub(timestamp)
779
+	gap := oldestLostMessage.Sub(timestamp)
691
 	session.resumeDetails.HistoryIncomplete = gap > 0 || timestamp.IsZero()
780
 	session.resumeDetails.HistoryIncomplete = gap > 0 || timestamp.IsZero()
692
 	gapSeconds := int(gap.Seconds()) + 1 // round up to avoid confusion
781
 	gapSeconds := int(gap.Seconds()) + 1 // round up to avoid confusion
693
 
782
 
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
 	session.Send(nil, client.server.name, "RESUME", "SUCCESS", details.nick)
823
 	session.Send(nil, client.server.name, "RESUME", "SUCCESS", details.nick)
738
 	}
829
 	}
739
 
830
 
740
 	// replay direct PRIVSMG history
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
 	session.resumeDetails = nil
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
 	var batchID string
846
 	var batchID string
754
 	details := client.Details()
847
 	details := client.Details()
755
 	nick := details.nick
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
 	allowTags := rb.session.capabilities.Has(caps.MessageTags)
854
 	allowTags := rb.session.capabilities.Has(caps.MessageTags)
761
 	for _, item := range items {
855
 	for _, item := range items {
778
 		if allowTags {
872
 		if allowTags {
779
 			tags = item.Tags
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
 			rb.AddSplitMessageFromClient(item.Nick, item.AccountName, tags, command, nick, item.Message)
881
 			rb.AddSplitMessageFromClient(item.Nick, item.AccountName, tags, command, nick, item.Message)
784
 		} else {
882
 		} else {
785
 			// this message was sent *from* the client to another nick; the target is item.Params[0]
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
 			rb.AddSplitMessageFromClient(details.nickMask, item.AccountName, tags, command, item.Params[0], item.Message)
885
 			rb.AddSplitMessageFromClient(details.nickMask, item.AccountName, tags, command, item.Params[0], item.Message)
788
 		}
886
 		}
789
 	}
887
 	}
875
 
973
 
876
 // ModeString returns the mode string for this client.
974
 // ModeString returns the mode string for this client.
877
 func (client *Client) ModeString() (str string) {
975
 func (client *Client) ModeString() (str string) {
878
-	return "+" + client.flags.String()
976
+	return "+" + client.modes.String()
879
 }
977
 }
880
 
978
 
881
 // Friends refers to clients that share a channel with this client.
979
 // Friends refers to clients that share a channel with this client.
1053
 // has no more sessions.
1151
 // has no more sessions.
1054
 func (client *Client) destroy(session *Session) {
1152
 func (client *Client) destroy(session *Session) {
1055
 	var sessionsToDestroy []*Session
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
 	client.stateMutex.Lock()
1161
 	client.stateMutex.Lock()
1058
 	details := client.detailsNoMutex()
1162
 	details := client.detailsNoMutex()
1060
 	brbAt := client.brbTimer.brbAt
1164
 	brbAt := client.brbTimer.brbAt
1061
 	wasReattach := session != nil && session.client != client
1165
 	wasReattach := session != nil && session.client != client
1062
 	sessionRemoved := false
1166
 	sessionRemoved := false
1167
+	registered := client.registered
1168
+	alwaysOn := client.alwaysOn
1063
 	var remainingSessions int
1169
 	var remainingSessions int
1064
 	if session == nil {
1170
 	if session == nil {
1065
 		sessionsToDestroy = client.sessions
1171
 		sessionsToDestroy = client.sessions
1074
 
1180
 
1075
 	// should we destroy the whole client this time?
1181
 	// should we destroy the whole client this time?
1076
 	// BRB is not respected if this is a destroy of the whole client (i.e., session == nil)
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
 	shouldDestroy := !client.destroyed && remainingSessions == 0 && !brbEligible
1184
 	shouldDestroy := !client.destroyed && remainingSessions == 0 && !brbEligible
1079
 	if shouldDestroy {
1185
 	if shouldDestroy {
1080
 		// if it's our job to destroy it, don't let anyone else try
1186
 		// if it's our job to destroy it, don't let anyone else try
1081
 		client.destroyed = true
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
 	exitedSnomaskSent := client.exitedSnomaskSent
1195
 	exitedSnomaskSent := client.exitedSnomaskSent
1084
 	client.stateMutex.Unlock()
1196
 	client.stateMutex.Unlock()
1085
 
1197
 
1198
+	if !lastSignoff.IsZero() {
1199
+		client.wakeWriter()
1200
+	}
1201
+
1086
 	// destroy all applicable sessions:
1202
 	// destroy all applicable sessions:
1087
 	var quitMessage string
1203
 	var quitMessage string
1088
 	for _, session := range sessionsToDestroy {
1204
 	for _, session := range sessionsToDestroy {
1099
 
1215
 
1100
 		// remove from connection limits
1216
 		// remove from connection limits
1101
 		var source string
1217
 		var source string
1102
-		if client.isTor {
1218
+		if session.isTor {
1103
 			client.server.torLimiter.RemoveClient()
1219
 			client.server.torLimiter.RemoveClient()
1104
 			source = "tor"
1220
 			source = "tor"
1105
 		} else {
1221
 		} else {
1113
 		client.server.logger.Info("localconnect-ip", fmt.Sprintf("disconnecting session of %s from %s", details.nick, source))
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
 	// do not destroy the client if it has either remaining sessions, or is BRB'ed
1239
 	// do not destroy the client if it has either remaining sessions, or is BRB'ed
1117
 	if !shouldDestroy {
1240
 	if !shouldDestroy {
1118
 		return
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
 	// see #235: deduplicating the list of PART recipients uses (comparatively speaking)
1259
 	// see #235: deduplicating the list of PART recipients uses (comparatively speaking)
1122
 	// a lot of RAM, so limit concurrency to avoid thrashing
1260
 	// a lot of RAM, so limit concurrency to avoid thrashing
1123
 	client.server.semaphores.ClientDestroy.Acquire()
1261
 	client.server.semaphores.ClientDestroy.Acquire()
1127
 		client.server.logger.Debug("quit", fmt.Sprintf("%s is no longer on the server", details.nick))
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
 	if registered {
1268
 	if registered {
1132
 		client.server.whoWas.Append(client.WhoWas())
1269
 		client.server.whoWas.Append(client.WhoWas())
1133
 	}
1270
 	}
1141
 	// clean up monitor state
1278
 	// clean up monitor state
1142
 	client.server.monitorManager.RemoveAll(client)
1279
 	client.server.monitorManager.RemoveAll(client)
1143
 
1280
 
1144
-	splitQuitMessage := utils.MakeMessage(quitMessage)
1145
 	// clean up channels
1281
 	// clean up channels
1146
 	// (note that if this is a reattach, client has no channels and therefore no friends)
1282
 	// (note that if this is a reattach, client has no channels and therefore no friends)
1147
 	friends := make(ClientSet)
1283
 	friends := make(ClientSet)
1148
-	for _, channel := range client.Channels() {
1284
+	channels = client.Channels()
1285
+	for _, channel := range channels {
1149
 		channel.Quit(client)
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
 		for _, member := range channel.Members() {
1287
 		for _, member := range channel.Members() {
1157
 			friends.Add(member)
1288
 			friends.Add(member)
1158
 		}
1289
 		}
1168
 
1299
 
1169
 	client.server.accounts.Logout(client)
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
 	// this happens under failure to return from BRB
1302
 	// this happens under failure to return from BRB
1175
 	if quitMessage == "" {
1303
 	if quitMessage == "" {
1176
 		if brbState == BrbDead && !brbAt.IsZero() {
1304
 		if brbState == BrbDead && !brbAt.IsZero() {
1196
 // SendSplitMsgFromClient sends an IRC PRIVMSG/NOTICE coming from a specific client.
1324
 // SendSplitMsgFromClient sends an IRC PRIVMSG/NOTICE coming from a specific client.
1197
 // Adds account-tag to the line as well.
1325
 // Adds account-tag to the line as well.
1198
 func (session *Session) sendSplitMsgFromClientInternal(blocking bool, nickmask, accountName string, tags map[string]string, command, target string, message utils.SplitMessage) {
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
 	if message.Is512() {
1327
 	if message.Is512() {
1201
 		session.sendFromClientInternal(blocking, message.Time, message.Msgid, nickmask, accountName, tags, command, target, message.Message)
1328
 		session.sendFromClientInternal(blocking, message.Time, message.Msgid, nickmask, accountName, tags, command, target, message.Message)
1202
 	} else {
1329
 	} else {
1203
-		if message.IsMultiline() && session.capabilities.Has(caps.Multiline) {
1330
+		if session.capabilities.Has(caps.Multiline) {
1204
 			for _, msg := range session.composeMultilineBatch(nickmask, accountName, tags, command, target, message) {
1331
 			for _, msg := range session.composeMultilineBatch(nickmask, accountName, tags, command, target, message) {
1205
 				session.SendRawMessage(msg, blocking)
1332
 				session.SendRawMessage(msg, blocking)
1206
 			}
1333
 			}
1366
 func (client *Client) addChannel(channel *Channel) {
1493
 func (client *Client) addChannel(channel *Channel) {
1367
 	client.stateMutex.Lock()
1494
 	client.stateMutex.Lock()
1368
 	client.channels[channel] = true
1495
 	client.channels[channel] = true
1496
+	alwaysOn := client.alwaysOn
1369
 	client.stateMutex.Unlock()
1497
 	client.stateMutex.Unlock()
1498
+
1499
+	if alwaysOn {
1500
+		client.markDirty(IncludeChannels)
1501
+	}
1370
 }
1502
 }
1371
 
1503
 
1372
 func (client *Client) removeChannel(channel *Channel) {
1504
 func (client *Client) removeChannel(channel *Channel) {
1373
 	client.stateMutex.Lock()
1505
 	client.stateMutex.Lock()
1374
 	delete(client.channels, channel)
1506
 	delete(client.channels, channel)
1507
+	alwaysOn := client.alwaysOn
1375
 	client.stateMutex.Unlock()
1508
 	client.stateMutex.Unlock()
1509
+
1510
+	if alwaysOn {
1511
+		client.markDirty(IncludeChannels)
1512
+	}
1376
 }
1513
 }
1377
 
1514
 
1378
 // Records that the client has been invited to join an invite-only channel
1515
 // Records that the client has been invited to join an invite-only channel
1401
 // Implements auto-oper by certfp (scans for an auto-eligible operator block that matches
1538
 // Implements auto-oper by certfp (scans for an auto-eligible operator block that matches
1402
 // the client's cert, then applies it).
1539
 // the client's cert, then applies it).
1403
 func (client *Client) attemptAutoOper(session *Session) {
1540
 func (client *Client) attemptAutoOper(session *Session) {
1404
-	if client.certfp == "" || client.HasMode(modes.Operator) {
1541
+	if session.certfp == "" || client.HasMode(modes.Operator) {
1405
 		return
1542
 		return
1406
 	}
1543
 	}
1407
 	for _, oper := range client.server.Config().operators {
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
 			rb := NewResponseBuffer(session)
1546
 			rb := NewResponseBuffer(session)
1410
 			applyOper(client, oper, rb)
1547
 			applyOper(client, oper, rb)
1411
 			rb.Send(true)
1548
 			rb.Send(true)
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
 		return errNickMissing
105
 		return errNickMissing
106
 	}
106
 	}
107
 
107
 
108
-	if !oldClient.AddSession(session) {
108
+	success, _, _ := oldClient.AddSession(session)
109
+	if !success {
109
 		return errNickMissing
110
 		return errNickMissing
110
 	}
111
 	}
111
 
112
 
113
 }
114
 }
114
 
115
 
115
 // SetNick sets a client's nickname, validating it against nicknames in use
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
 	newcfnick, err := CasefoldName(newNick)
119
 	newcfnick, err := CasefoldName(newNick)
121
 	if err != nil {
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
 	newSkeleton, err := Skeleton(newNick)
126
 	newSkeleton, err := Skeleton(newNick)
125
 	if err != nil {
127
 	if err != nil {
126
-		return errNicknameInvalid
128
+		return "", errNicknameInvalid
127
 	}
129
 	}
128
 
130
 
129
 	if restrictedCasefoldedNicks[newcfnick] || restrictedSkeletons[newSkeleton] {
131
 	if restrictedCasefoldedNicks[newcfnick] || restrictedSkeletons[newSkeleton] {
130
-		return errNicknameInvalid
132
+		return "", errNicknameInvalid
131
 	}
133
 	}
132
 
134
 
133
 	reservedAccount, method := client.server.accounts.EnforcementStatus(newcfnick, newSkeleton)
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
 	var bouncerAllowed bool
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
 			bouncerAllowed = true
157
 			bouncerAllowed = true
140
 		} else {
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
 				bouncerAllowed = true
160
 				bouncerAllowed = true
144
-			} else if settings.AllowBouncer == BouncerAllowedByUser {
161
+			} else if settings.AllowBouncer == MulticlientAllowedByUser {
145
 				bouncerAllowed = true
162
 				bouncerAllowed = true
146
 			}
163
 			}
147
 		}
164
 		}
154
 	// the client may just be changing case
171
 	// the client may just be changing case
155
 	if currentClient != nil && currentClient != client && session != nil {
172
 	if currentClient != nil && currentClient != client && session != nil {
156
 		// these conditions forbid reattaching to an existing session:
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
 		// successful reattach!
193
 		// successful reattach!
164
-		return nil
194
+		return newNick, nil
165
 	}
195
 	}
166
 	// analogous checks for skeletons
196
 	// analogous checks for skeletons
167
 	skeletonHolder := clients.bySkeleton[newSkeleton]
197
 	skeletonHolder := clients.bySkeleton[newSkeleton]
168
 	if skeletonHolder != nil && skeletonHolder != client {
198
 	if skeletonHolder != nil && skeletonHolder != client {
169
-		return errNicknameInUse
199
+		return "", errNicknameInUse
170
 	}
200
 	}
171
 	if method == NickEnforcementStrict && reservedAccount != "" && reservedAccount != account {
201
 	if method == NickEnforcementStrict && reservedAccount != "" && reservedAccount != account {
172
-		return errNicknameReserved
202
+		return "", errNicknameReserved
173
 	}
203
 	}
174
 	clients.removeInternal(client)
204
 	clients.removeInternal(client)
175
 	clients.byNick[newcfnick] = client
205
 	clients.byNick[newcfnick] = client
176
 	clients.bySkeleton[newSkeleton] = client
206
 	clients.bySkeleton[newSkeleton] = client
177
 	client.updateNick(newNick, newcfnick, newSkeleton)
207
 	client.updateNick(newNick, newcfnick, newSkeleton)
178
-	return nil
208
+	return newNick, nil
179
 }
209
 }
180
 
210
 
181
 func (clients *ClientManager) AllClients() (result []*Client) {
211
 func (clients *ClientManager) AllClients() (result []*Client) {

+ 7
- 6
irc/commands.go View File

54
 		return cmd.handler(server, client, msg, rb)
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
 	if exiting {
63
 	if exiting {
58
 		return
64
 		return
59
 	}
65
 	}
63
 		exiting = server.tryRegister(client, session)
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
 	if client.registered && !cmd.leaveClientIdle {
72
 	if client.registered && !cmd.leaveClientIdle {
72
 		client.Active(session)
73
 		client.Active(session)
73
 	}
74
 	}
109
 		},
110
 		},
110
 		"CHATHISTORY": {
111
 		"CHATHISTORY": {
111
 			handler:   chathistoryHandler,
112
 			handler:   chathistoryHandler,
112
-			minParams: 3,
113
+			minParams: 4,
113
 		},
114
 		},
114
 		"DEBUG": {
115
 		"DEBUG": {
115
 			handler:   debugHandler,
116
 			handler:   debugHandler,

+ 212
- 29
irc/config.go View File

28
 	"github.com/oragono/oragono/irc/ldap"
28
 	"github.com/oragono/oragono/irc/ldap"
29
 	"github.com/oragono/oragono/irc/logger"
29
 	"github.com/oragono/oragono/irc/logger"
30
 	"github.com/oragono/oragono/irc/modes"
30
 	"github.com/oragono/oragono/irc/modes"
31
+	"github.com/oragono/oragono/irc/mysql"
31
 	"github.com/oragono/oragono/irc/passwd"
32
 	"github.com/oragono/oragono/irc/passwd"
32
 	"github.com/oragono/oragono/irc/utils"
33
 	"github.com/oragono/oragono/irc/utils"
33
 	"gopkg.in/yaml.v2"
34
 	"gopkg.in/yaml.v2"
61
 	ProxyBeforeTLS bool
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
 type AccountConfig struct {
216
 type AccountConfig struct {
65
 	Registration          AccountRegistrationConfig
217
 	Registration          AccountRegistrationConfig
66
 	AuthenticationEnabled bool `yaml:"authentication-enabled"`
218
 	AuthenticationEnabled bool `yaml:"authentication-enabled"`
77
 	} `yaml:"login-throttling"`
229
 	} `yaml:"login-throttling"`
78
 	SkipServerPassword bool                  `yaml:"skip-server-password"`
230
 	SkipServerPassword bool                  `yaml:"skip-server-password"`
79
 	NickReservation    NickReservationConfig `yaml:"nick-reservation"`
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
 // AccountRegistrationConfig controls account registration.
237
 // AccountRegistrationConfig controls account registration.
88
 type AccountRegistrationConfig struct {
238
 type AccountRegistrationConfig struct {
89
 	Enabled                bool
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
 	Callbacks              struct {
243
 	Callbacks              struct {
94
 		Mailto struct {
244
 		Mailto struct {
95
 			Server string
245
 			Server string
117
 	UserRequests   struct {
267
 	UserRequests   struct {
118
 		Enabled  bool
268
 		Enabled  bool
119
 		Channel  string
269
 		Channel  string
120
-		Cooldown time.Duration
270
+		Cooldown custime.Duration
121
 	} `yaml:"user-requests"`
271
 	} `yaml:"user-requests"`
122
 	OfferList []string `yaml:"offer-list"`
272
 	OfferList []string `yaml:"offer-list"`
123
 }
273
 }
260
 
410
 
261
 // STSConfig controls the STS configuration/
411
 // STSConfig controls the STS configuration/
262
 type STSConfig struct {
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
 // Value returns the STS value to advertise in CAP
421
 // Value returns the STS value to advertise in CAP
273
 func (sts *STSConfig) Value() string {
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
 	if sts.Enabled && sts.Port > 0 {
424
 	if sts.Enabled && sts.Port > 0 {
276
 		val += fmt.Sprintf(",port=%d", sts.Port)
425
 		val += fmt.Sprintf(",port=%d", sts.Port)
277
 	}
426
 	}
337
 		isupport      isupport.List
486
 		isupport      isupport.List
338
 		IPLimits      connection_limits.LimiterConfig `yaml:"ip-limits"`
487
 		IPLimits      connection_limits.LimiterConfig `yaml:"ip-limits"`
339
 		Cloaks        cloaks.CloakConfig              `yaml:"ip-cloaking"`
488
 		Cloaks        cloaks.CloakConfig              `yaml:"ip-cloaking"`
489
+		SecureNetDefs []string                        `yaml:"secure-nets"`
490
+		secureNets    []net.IPNet
340
 		supportedCaps *caps.Set
491
 		supportedCaps *caps.Set
341
 		capValues     caps.Values
492
 		capValues     caps.Values
342
 		Casemapping   Casemapping
493
 		Casemapping   Casemapping
353
 	Datastore struct {
504
 	Datastore struct {
354
 		Path        string
505
 		Path        string
355
 		AutoUpgrade bool
506
 		AutoUpgrade bool
507
+		MySQL       mysql.Config
356
 	}
508
 	}
357
 
509
 
358
 	Accounts AccountConfig
510
 	Accounts AccountConfig
392
 		AutoresizeWindow time.Duration `yaml:"autoresize-window"`
544
 		AutoresizeWindow time.Duration `yaml:"autoresize-window"`
393
 		AutoreplayOnJoin int           `yaml:"autoreplay-on-join"`
545
 		AutoreplayOnJoin int           `yaml:"autoreplay-on-join"`
394
 		ChathistoryMax   int           `yaml:"chathistory-maxmessages"`
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
 	Filename string
561
 	Filename string
626
 	if config.Limits.RegistrationMessages == 0 {
790
 	if config.Limits.RegistrationMessages == 0 {
627
 		config.Limits.RegistrationMessages = 1024
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
 	config.Server.supportedCaps = caps.NewCompleteSet()
799
 	config.Server.supportedCaps = caps.NewCompleteSet()
631
 	config.Server.capValues = make(caps.Values)
800
 	config.Server.capValues = make(caps.Values)
636
 	}
805
 	}
637
 
806
 
638
 	if config.Server.STS.Enabled {
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
 		if config.Server.STS.Port < 0 || config.Server.STS.Port > 65535 {
808
 		if config.Server.STS.Port < 0 || config.Server.STS.Port > 65535 {
644
 			return nil, fmt.Errorf("STS port is incorrect, should be 0 if disabled: %d", config.Server.STS.Port)
809
 			return nil, fmt.Errorf("STS port is incorrect, should be 0 if disabled: %d", config.Server.STS.Port)
645
 		}
810
 		}
692
 		config.Server.capValues[caps.Multiline] = multilineCapValue
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
 	var newLogConfigs []logger.LoggingConfig
871
 	var newLogConfigs []logger.LoggingConfig
762
 		return nil, fmt.Errorf("Could not parse proxy-allowed-from nets: %v", err.Error())
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
 	rawRegexp := config.Accounts.VHosts.ValidRegexpRaw
942
 	rawRegexp := config.Accounts.VHosts.ValidRegexpRaw
766
 	if rawRegexp != "" {
943
 	if rawRegexp != "" {
767
 		regexp, err := regexp.Compile(rawRegexp)
944
 		regexp, err := regexp.Compile(rawRegexp)
858
 		config.History.ClientLength = 0
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
 	config.Server.Cloaks.Initialize()
1050
 	config.Server.Cloaks.Initialize()
862
 	if config.Server.Cloaks.Enabled {
1051
 	if config.Server.Cloaks.Enabled {
863
 		if config.Server.Cloaks.Secret == "" || config.Server.Cloaks.Secret == "siaELnk6Kaeo65K3RCrwJjlWaZ-Bt3WuZ2L8MXLbNb4" {
1052
 		if config.Server.Cloaks.Secret == "" || config.Server.Cloaks.Secret == "siaELnk6Kaeo65K3RCrwJjlWaZ-Bt3WuZ2L8MXLbNb4" {
942
 		removedCaps.Add(caps.SASL)
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
 	if oldConfig.Limits.Multiline.MaxBytes != 0 && config.Limits.Multiline.MaxBytes == 0 {
1134
 	if oldConfig.Limits.Multiline.MaxBytes != 0 && config.Limits.Multiline.MaxBytes == 0 {
952
 		removedCaps.Add(caps.Multiline)
1135
 		removedCaps.Add(caps.Multiline)
953
 	} else if oldConfig.Limits.Multiline.MaxBytes == 0 && config.Limits.Multiline.MaxBytes != 0 {
1136
 	} else if oldConfig.Limits.Multiline.MaxBytes == 0 && config.Limits.Multiline.MaxBytes != 0 {

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

75
 	"m":  int64(time.Minute),
75
 	"m":  int64(time.Minute),
76
 	"h":  int64(time.Hour),
76
 	"h":  int64(time.Hour),
77
 	"d":  int64(time.Hour * 24),
77
 	"d":  int64(time.Hour * 24),
78
+	"w":  int64(time.Hour * 24 * 7),
78
 	"mo": int64(time.Hour * 24 * 30),
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
 // ParseDuration parses a duration string.
83
 // ParseDuration parses a duration string.
181
 	}
182
 	}
182
 	return time.Duration(d), nil
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
 // maps an initial version to a schema change capable of upgrading it
36
 // maps an initial version to a schema change capable of upgrading it
37
 var schemaChanges map[string]SchemaChange
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
 // InitDB creates the database, implementing the `oragono initdb` command.
39
 // InitDB creates the database, implementing the `oragono initdb` command.
56
 func InitDB(path string) {
40
 func InitDB(path string) {
57
 	_, err := os.Stat(path)
41
 	_, err := os.Stat(path)
129
 		// successful autoupgrade, let's try this again:
113
 		// successful autoupgrade, let's try this again:
130
 		return openDatabaseInternal(config, false)
114
 		return openDatabaseInternal(config, false)
131
 	} else {
115
 	} else {
132
-		err = IncompatibleSchemaError(version)
116
+		err = &utils.IncompatibleSchemaError{CurrentVersion: version, RequiredVersion: latestDbSchema}
133
 		return
117
 		return
134
 	}
118
 	}
135
 }
119
 }
179
 					break
163
 					break
180
 				}
164
 				}
181
 				// unable to upgrade to the desired version, roll back
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
 			log.Println("attempting to update schema from version " + version)
168
 			log.Println("attempting to update schema from version " + version)
185
 			err := change.Changer(config, tx)
169
 			err := change.Changer(config, tx)
509
 type accountSettingsLegacyV7 struct {
493
 type accountSettingsLegacyV7 struct {
510
 	AutoreplayLines *int
494
 	AutoreplayLines *int
511
 	NickEnforcement NickEnforcementMethod
495
 	NickEnforcement NickEnforcementMethod
512
-	AllowBouncer    BouncerAllowedSetting
496
+	AllowBouncer    MulticlientAllowedSetting
513
 	AutoreplayJoins bool
497
 	AutoreplayJoins bool
514
 }
498
 }
515
 
499
 
516
 type accountSettingsLegacyV8 struct {
500
 type accountSettingsLegacyV8 struct {
517
 	AutoreplayLines *int
501
 	AutoreplayLines *int
518
 	NickEnforcement NickEnforcementMethod
502
 	NickEnforcement NickEnforcementMethod
519
-	AllowBouncer    BouncerAllowedSetting
503
+	AllowBouncer    MulticlientAllowedSetting
520
 	ReplayJoins     ReplayJoinsSetting
504
 	ReplayJoins     ReplayJoinsSetting
521
 }
505
 }
522
 
506
 

+ 2
- 0
irc/errors.go View File

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

+ 2
- 2
irc/gateways.go View File

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

+ 82
- 16
irc/getters.go View File

65
 	atime    time.Time
65
 	atime    time.Time
66
 	ip       net.IP
66
 	ip       net.IP
67
 	hostname string
67
 	hostname string
68
+	certfp   string
68
 }
69
 }
69
 
70
 
70
 func (client *Client) AllSessionData(currentSession *Session) (data []SessionData, currentIndex int) {
71
 func (client *Client) AllSessionData(currentSession *Session) (data []SessionData, currentIndex int) {
81
 			atime:    session.atime,
82
 			atime:    session.atime,
82
 			ctime:    session.ctime,
83
 			ctime:    session.ctime,
83
 			hostname: session.rawHostname,
84
 			hostname: session.rawHostname,
85
+			certfp:   session.certfp,
84
 		}
86
 		}
85
 		if session.proxiedIP != nil {
87
 		if session.proxiedIP != nil {
86
 			data[i].ip = session.proxiedIP
88
 			data[i].ip = session.proxiedIP
91
 	return
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
 	client.stateMutex.Lock()
103
 	client.stateMutex.Lock()
96
 	defer client.stateMutex.Unlock()
104
 	defer client.stateMutex.Unlock()
97
 
105
 
98
 	// client may be dying and ineligible to receive another session
106
 	// client may be dying and ineligible to receive another session
99
 	if client.destroyed {
107
 	if client.destroyed {
100
-		return false
108
+		return
101
 	}
109
 	}
102
 	// success, attach the new session to the client
110
 	// success, attach the new session to the client
103
 	session.client = client
111
 	session.client = client
104
 	newSessions := make([]*Session, len(client.sessions)+1)
112
 	newSessions := make([]*Session, len(client.sessions)+1)
105
 	copy(newSessions, client.sessions)
113
 	copy(newSessions, client.sessions)
106
 	newSessions[len(newSessions)-1] = session
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
 	client.sessions = newSessions
122
 	client.sessions = newSessions
108
-	return true
123
+	return true, len(client.sessions), lastSignoff
109
 }
124
 }
110
 
125
 
111
 func (client *Client) removeSession(session *Session) (success bool, length int) {
126
 func (client *Client) removeSession(session *Session) (success bool, length int) {
189
 	client.stateMutex.Unlock()
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
 // uniqueIdentifiers returns the strings for which the server enforces per-client
214
 // uniqueIdentifiers returns the strings for which the server enforces per-client
193
 // uniqueness/ownership; no two clients can have colliding casefolded nicks or
215
 // uniqueness/ownership; no two clients can have colliding casefolded nicks or
194
 // skeletons.
216
 // skeletons.
264
 	return client.accountName
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
 	client.stateMutex.Lock()
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
 	return
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
 func (client *Client) AccountSettings() (result AccountSettings) {
326
 func (client *Client) AccountSettings() (result AccountSettings) {
285
 	client.stateMutex.RLock()
327
 	client.stateMutex.RLock()
286
 	result = client.accountSettings
328
 	result = client.accountSettings
289
 }
331
 }
290
 
332
 
291
 func (client *Client) SetAccountSettings(settings AccountSettings) {
333
 func (client *Client) SetAccountSettings(settings AccountSettings) {
334
+	alwaysOn := persistenceEnabled(client.server.Config().Accounts.Multiclient.AlwaysOn, settings.AlwaysOn)
292
 	client.stateMutex.Lock()
335
 	client.stateMutex.Lock()
293
 	client.accountSettings = settings
336
 	client.accountSettings = settings
337
+	if client.registered {
338
+		client.alwaysOn = alwaysOn
339
+	}
294
 	client.stateMutex.Unlock()
340
 	client.stateMutex.Unlock()
295
 }
341
 }
296
 
342
 
309
 
355
 
310
 func (client *Client) HasMode(mode modes.Mode) bool {
356
 func (client *Client) HasMode(mode modes.Mode) bool {
311
 	// client.flags has its own synch
357
 	// client.flags has its own synch
312
-	return client.flags.HasMode(mode)
358
+	return client.modes.HasMode(mode)
313
 }
359
 }
314
 
360
 
315
 func (client *Client) SetMode(mode modes.Mode, on bool) bool {
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
 func (client *Client) Channels() (result []*Channel) {
371
 func (client *Client) Channels() (result []*Channel) {
410
 	channel.stateMutex.RUnlock()
462
 	channel.stateMutex.RUnlock()
411
 	return clientModes.HighestChannelUserMode()
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
 
112
 
113
 // AUTHENTICATE [<mechanism>|<data>|*]
113
 // AUTHENTICATE [<mechanism>|<data>|*]
114
 func authenticateHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
114
 func authenticateHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
115
+	session := rb.session
115
 	config := server.Config()
116
 	config := server.Config()
116
 	details := client.Details()
117
 	details := client.Details()
117
 
118
 
128
 	// sasl abort
129
 	// sasl abort
129
 	if !server.AccountConfig().AuthenticationEnabled || len(msg.Params) == 1 && msg.Params[0] == "*" {
130
 	if !server.AccountConfig().AuthenticationEnabled || len(msg.Params) == 1 && msg.Params[0] == "*" {
130
 		rb.Add(nil, server.name, ERR_SASLABORTED, details.nick, client.t("SASL authentication aborted"))
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
 		return false
133
 		return false
135
 	}
134
 	}
136
 
135
 
137
 	// start new sasl session
136
 	// start new sasl session
138
-	if !client.saslInProgress {
137
+	if session.sasl.mechanism == "" {
139
 		mechanism := strings.ToUpper(msg.Params[0])
138
 		mechanism := strings.ToUpper(msg.Params[0])
140
 		_, mechanismIsEnabled := EnabledSaslMechanisms[mechanism]
139
 		_, mechanismIsEnabled := EnabledSaslMechanisms[mechanism]
141
 
140
 
142
 		if mechanismIsEnabled {
141
 		if mechanismIsEnabled {
143
-			client.saslInProgress = true
144
-			client.saslMechanism = mechanism
142
+			session.sasl.mechanism = mechanism
145
 			if !config.Server.Compatibility.SendUnprefixedSasl {
143
 			if !config.Server.Compatibility.SendUnprefixedSasl {
146
 				// normal behavior
144
 				// normal behavior
147
 				rb.Add(nil, server.name, "AUTHENTICATE", "+")
145
 				rb.Add(nil, server.name, "AUTHENTICATE", "+")
162
 
160
 
163
 	if len(rawData) > 400 {
161
 	if len(rawData) > 400 {
164
 		rb.Add(nil, server.name, ERR_SASLTOOLONG, details.nick, client.t("SASL message too long"))
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
 		return false
164
 		return false
169
 	} else if len(rawData) == 400 {
165
 	} else if len(rawData) == 400 {
170
-		client.saslValue += rawData
171
 		// allow 4 'continuation' lines before rejecting for length
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
 			rb.Add(nil, server.name, ERR_SASLFAIL, details.nick, client.t("SASL authentication failed: Passphrase too long"))
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
 			return false
170
 			return false
178
 		}
171
 		}
172
+		session.sasl.value += rawData
179
 		return false
173
 		return false
180
 	}
174
 	}
181
 	if rawData != "+" {
175
 	if rawData != "+" {
182
-		client.saslValue += rawData
176
+		session.sasl.value += rawData
183
 	}
177
 	}
184
 
178
 
185
 	var data []byte
179
 	var data []byte
186
 	var err error
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
 		if err != nil {
183
 		if err != nil {
190
 			rb.Add(nil, server.name, ERR_SASLFAIL, details.nick, client.t("SASL authentication failed: Invalid b64 encoding"))
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
 			return false
186
 			return false
195
 		}
187
 		}
196
 	}
188
 	}
197
 
189
 
198
 	// call actual handler
190
 	// call actual handler
199
-	handler, handlerExists := EnabledSaslMechanisms[client.saslMechanism]
191
+	handler, handlerExists := EnabledSaslMechanisms[session.sasl.mechanism]
200
 
192
 
201
 	// like 100% not required, but it's good to be safe I guess
193
 	// like 100% not required, but it's good to be safe I guess
202
 	if !handlerExists {
194
 	if !handlerExists {
203
 		rb.Add(nil, server.name, ERR_SASLFAIL, details.nick, client.t("SASL authentication failed"))
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
 		return false
197
 		return false
208
 	}
198
 	}
209
 
199
 
210
 	// let the SASL handler do its thing
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
 	return exiting
204
 	return exiting
219
 }
205
 }
270
 
256
 
271
 // AUTHENTICATE EXTERNAL
257
 // AUTHENTICATE EXTERNAL
272
 func authExternalHandler(server *Server, client *Client, mechanism string, value []byte, rb *ResponseBuffer) bool {
258
 func authExternalHandler(server *Server, client *Client, mechanism string, value []byte, rb *ResponseBuffer) bool {
273
-	if client.certfp == "" {
259
+	if rb.session.certfp == "" {
274
 		rb.Add(nil, server.name, ERR_SASLFAIL, client.nick, client.t("SASL authentication failed, you are not connecting with a certificate"))
260
 		rb.Add(nil, server.name, ERR_SASLFAIL, client.nick, client.t("SASL authentication failed, you are not connecting with a certificate"))
275
 		return false
261
 		return false
276
 	}
262
 	}
287
 	}
273
 	}
288
 
274
 
289
 	if err == nil {
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
 	if err != nil {
279
 	if err != nil {
531
 // CHATHISTORY <target> BETWEEN <query> <query> <direction> [<limit>]
517
 // CHATHISTORY <target> BETWEEN <query> <query> <direction> [<limit>]
532
 // e.g., CHATHISTORY #ircv3 BETWEEN timestamp=YYYY-MM-DDThh:mm:ss.sssZ timestamp=YYYY-MM-DDThh:mm:ss.sssZ + 100
518
 // e.g., CHATHISTORY #ircv3 BETWEEN timestamp=YYYY-MM-DDThh:mm:ss.sssZ timestamp=YYYY-MM-DDThh:mm:ss.sssZ + 100
533
 func chathistoryHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) (exiting bool) {
519
 func chathistoryHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) (exiting bool) {
534
-	config := server.Config()
535
-
536
 	var items []history.Item
520
 	var items []history.Item
537
-	success := false
538
-	var hist *history.Buffer
521
+	unknown_command := false
522
+	var target string
539
 	var channel *Channel
523
 	var channel *Channel
524
+	var sequence history.Sequence
525
+	var err error
540
 	defer func() {
526
 	defer func() {
541
 		// successful responses are sent as a chathistory or history batch
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
 				channel.replayHistoryItems(rb, items, false)
530
 				channel.replayHistoryItems(rb, items, false)
531
+			} else {
532
+				client.replayPrivmsgHistory(rb, items, target, true)
547
 			}
533
 			}
548
 			return
534
 			return
549
 		}
535
 		}
550
 
536
 
551
 		// errors are sent either without a batch, or in a draft/labeled-response batch as usual
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
 		return
552
 		return
580
 	}
553
 	}
581
 
554
 
582
-	preposition := strings.ToLower(msg.Params[1])
583
-
584
 	parseQueryParam := func(param string) (msgid string, timestamp time.Time, err error) {
555
 	parseQueryParam := func(param string) (msgid string, timestamp time.Time, err error) {
585
-		err = errInvalidParams
556
+		err = utils.ErrInvalidParams
586
 		pieces := strings.SplitN(param, "=", 2)
557
 		pieces := strings.SplitN(param, "=", 2)
587
 		if len(pieces) < 2 {
558
 		if len(pieces) < 2 {
588
 			return
559
 			return
589
 		}
560
 		}
590
 		identifier, value := strings.ToLower(pieces[0]), pieces[1]
561
 		identifier, value := strings.ToLower(pieces[0]), pieces[1]
591
-		if identifier == "id" {
562
+		if identifier == "msgid" {
592
 			msgid, err = value, nil
563
 			msgid, err = value, nil
593
 			return
564
 			return
594
 		} else if identifier == "timestamp" {
565
 		} else if identifier == "timestamp" {
598
 		return
569
 		return
599
 	}
570
 	}
600
 
571
 
601
-	maxChathistoryLimit := config.History.ChathistoryMax
602
-	if maxChathistoryLimit == 0 {
603
-		return
604
-	}
605
 	parseHistoryLimit := func(paramIndex int) (limit int) {
572
 	parseHistoryLimit := func(paramIndex int) (limit int) {
606
 		if len(msg.Params) < (paramIndex + 1) {
573
 		if len(msg.Params) < (paramIndex + 1) {
607
 			return maxChathistoryLimit
574
 			return maxChathistoryLimit
613
 		return
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
 		return
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
 	switch preposition {
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
 		if err != nil {
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
 			} else {
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
 		if err != nil {
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
 	return
651
 	return
751
 }
652
 }
752
 
653
 
1026
 // HISTORY <target> [<limit>]
927
 // HISTORY <target> [<limit>]
1027
 // e.g., HISTORY #ubuntu 10
928
 // e.g., HISTORY #ubuntu 10
1028
 // HISTORY me 15
929
 // HISTORY me 15
930
+// HISTORY #darwin 1h
1029
 func historyHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
931
 func historyHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
1030
 	config := server.Config()
932
 	config := server.Config()
1031
 	if !config.History.Enabled {
933
 	if !config.History.Enabled {
1034
 	}
936
 	}
1035
 
937
 
1036
 	target := msg.Params[0]
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
 		return false
947
 		return false
1062
 	}
948
 	}
1063
 
949
 
1064
-	limit := 10
950
+	var duration time.Duration
1065
 	maxChathistoryLimit := config.History.ChathistoryMax
951
 	maxChathistoryLimit := config.History.ChathistoryMax
952
+	limit := 100
953
+	if maxChathistoryLimit < limit {
954
+		limit = maxChathistoryLimit
955
+	}
1066
 	if len(msg.Params) > 1 {
956
 	if len(msg.Params) > 1 {
1067
 		providedLimit, err := strconv.Atoi(msg.Params[1])
957
 		providedLimit, err := strconv.Atoi(msg.Params[1])
1068
-		if providedLimit > maxChathistoryLimit {
1069
-			providedLimit = maxChathistoryLimit
1070
-		}
1071
 		if err == nil && providedLimit != 0 {
958
 		if err == nil && providedLimit != 0 {
1072
 			limit = providedLimit
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
 	} else {
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
 	return false
988
 	return false
1085
 }
989
 }
1086
 
990
 
1964
 		return false
1868
 		return false
1965
 	}
1869
 	}
1966
 
1870
 
1967
-	if client.isTor && utils.IsRestrictedCTCPMessage(message) {
1871
+	if rb.session.isTor && utils.IsRestrictedCTCPMessage(message) {
1968
 		// note that error replies are never sent for NOTICE
1872
 		// note that error replies are never sent for NOTICE
1969
 		if histType != history.Notice {
1873
 		if histType != history.Notice {
1970
 			rb.Notice(client.t("CTCP messages are disabled over Tor"))
1874
 			rb.Notice(client.t("CTCP messages are disabled over Tor"))
2021
 			}
1925
 			}
2022
 			return
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
 		// restrict messages appropriately when +R is set
1935
 		// restrict messages appropriately when +R is set
2029
 		// intentionally make the sending user think the message went through fine
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
 			hasTagsCap := session.capabilities.Has(caps.MessageTags)
1951
 			hasTagsCap := session.capabilities.Has(caps.MessageTags)
1952
+			// don't send TAGMSG at all if they don't have the tags cap
2061
 			if histType == history.Tagmsg && hasTagsCap {
1953
 			if histType == history.Tagmsg && hasTagsCap {
2062
 				session.sendFromClientInternal(false, message.Time, message.Msgid, nickMaskString, accountName, tags, command, tnick)
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
 				tagsToSend := tags
1956
 				tagsToSend := tags
2065
 				if !hasTagsCap {
1957
 				if !hasTagsCap {
2066
 					tagsToSend = nil
1958
 					tagsToSend = nil
2068
 				session.sendSplitMsgFromClientInternal(false, nickMaskString, accountName, tagsToSend, command, tnick, message)
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
 		if histType != history.Notice && user.Away() {
1977
 		if histType != history.Notice && user.Away() {
2072
 			//TODO(dan): possibly implement cooldown of away notifications to users
1978
 			//TODO(dan): possibly implement cooldown of away notifications to users
2073
 			rb.Add(nil, server.name, RPL_AWAY, client.Nick(), tnick, user.AwayMessage())
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
 		item := history.Item{
1986
 		item := history.Item{
2077
 			Type:        histType,
1987
 			Type:        histType,
2078
 			Message:     message,
1988
 			Message:     message,
2079
 			Nick:        nickMaskString,
1989
 			Nick:        nickMaskString,
2080
 			AccountName: accountName,
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
 	oper := server.GetOperator(msg.Params[0])
2062
 	oper := server.GetOperator(msg.Params[0])
2137
 	if oper != nil {
2063
 	if oper != nil {
2138
 		if oper.Fingerprint != "" {
2064
 		if oper.Fingerprint != "" {
2139
-			if oper.Fingerprint == client.certfp {
2065
+			if oper.Fingerprint == rb.session.certfp {
2140
 				checkPassed = true
2066
 				checkPassed = true
2141
 			} else {
2067
 			} else {
2142
 				checkFailed = true
2068
 				checkFailed = true
2239
 
2165
 
2240
 	// check the provided password
2166
 	// check the provided password
2241
 	password := []byte(msg.Params[0])
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
 	// if they failed the check, we'll bounce them later when they try to complete registration
2170
 	// if they failed the check, we'll bounce them later when they try to complete registration
2245
 	return false
2171
 	return false
2409
 // SETNAME <realname>
2335
 // SETNAME <realname>
2410
 func setnameHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
2336
 func setnameHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
2411
 	realname := msg.Params[0]
2337
 	realname := msg.Params[0]
2412
-
2413
-	client.stateMutex.Lock()
2414
-	client.realname = realname
2415
-	client.stateMutex.Unlock()
2416
-
2338
+	client.SetRealname(realname)
2417
 	details := client.Details()
2339
 	details := client.Details()
2418
 
2340
 
2419
 	// alert friends
2341
 	// alert friends
2622
 			if 0 < len(info.Password) && bcrypt.CompareHashAndPassword(info.Password, givenPassword) != nil {
2544
 			if 0 < len(info.Password) && bcrypt.CompareHashAndPassword(info.Password, givenPassword) != nil {
2623
 				continue
2545
 				continue
2624
 			}
2546
 			}
2625
-			if info.Fingerprint != "" && info.Fingerprint != client.certfp {
2547
+			if info.Fingerprint != "" && info.Fingerprint != rb.session.certfp {
2626
 				continue
2548
 				continue
2627
 			}
2549
 			}
2628
 
2550
 

+ 6
- 5
irc/help.go View File

145
 	"chathistory": {
145
 	"chathistory": {
146
 		text: `CHATHISTORY [params]
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
 	"debug": {
152
 	"debug": {
153
 		oper: true,
153
 		oper: true,
213
 
213
 
214
 Replay message history. <target> can be a channel name, "me" to replay direct
214
 Replay message history. <target> can be a channel name, "me" to replay direct
215
 message history, or a nickname to replay another client's direct message
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
 	"info": {
220
 	"info": {
220
 		text: `INFO
221
 		text: `INFO

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

6
 import (
6
 import (
7
 	"github.com/oragono/oragono/irc/utils"
7
 	"github.com/oragono/oragono/irc/utils"
8
 	"sync"
8
 	"sync"
9
-	"sync/atomic"
10
 	"time"
9
 	"time"
11
 )
10
 )
12
 
11
 
46
 	Message utils.SplitMessage
45
 	Message utils.SplitMessage
47
 	Tags    map[string]string
46
 	Tags    map[string]string
48
 	Params  [1]string
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
 // HasMsgid tests whether a message has the message id `msgid`.
54
 // HasMsgid tests whether a message has the message id `msgid`.
53
 	return item.Message.Msgid == msgid
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
 		for name := range item.Tags {
62
 		for name := range item.Tags {
59
 			if !transientTags[name] {
63
 			if !transientTags[name] {
60
 				return true
64
 				return true
61
 			}
65
 			}
62
 		}
66
 		}
63
 		return false // all tags were blacklisted
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
 		return true
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
 // Buffer is a ring buffer holding message/event history for a channel or user
84
 // Buffer is a ring buffer holding message/event history for a channel or user
72
 type Buffer struct {
85
 type Buffer struct {
81
 
94
 
82
 	lastDiscarded time.Time
95
 	lastDiscarded time.Time
83
 
96
 
84
-	enabled uint32
85
-
86
 	nowFunc func() time.Time
97
 	nowFunc func() time.Time
87
 }
98
 }
88
 
99
 
99
 	hist.window = window
110
 	hist.window = window
100
 	hist.maximumSize = size
111
 	hist.maximumSize = size
101
 	hist.nowFunc = time.Now
112
 	hist.nowFunc = time.Now
102
-
103
-	hist.setEnabled(size)
104
 }
113
 }
105
 
114
 
106
 // compute the initial size for the buffer, taking into account autoresize
115
 // compute the initial size for the buffer, taking into account autoresize
115
 	return
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
 // Add adds a history item to the buffer
127
 // Add adds a history item to the buffer
133
 func (list *Buffer) Add(item Item) {
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
 	if item.Message.Time.IsZero() {
129
 	if item.Message.Time.IsZero() {
144
 		item.Message.Time = time.Now().UTC()
130
 		item.Message.Time = time.Now().UTC()
145
 	}
131
 	}
147
 	list.Lock()
133
 	list.Lock()
148
 	defer list.Unlock()
134
 	defer list.Unlock()
149
 
135
 
136
+	if len(list.buffer) == 0 {
137
+		return
138
+	}
139
+
150
 	list.maybeExpand()
140
 	list.maybeExpand()
151
 
141
 
152
 	var pos int
142
 	var pos int
170
 	list.buffer[pos] = item
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
 // Between returns all history items with a time `after` <= time <= `before`,
174
 // Between returns all history items with a time `after` <= time <= `before`,
181
 // with an indication of whether the results are complete or are missing items
175
 // with an indication of whether the results are complete or are missing items
182
 // because some of that period was discarded. A zero value of `before` is considered
176
 // because some of that period was discarded. A zero value of `before` is considered
183
 // higher than all other times.
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
 	list.RLock()
187
 	list.RLock()
190
 	defer list.RUnlock()
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
 	complete = after.Equal(list.lastDiscarded) || after.After(list.lastDiscarded)
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
 // you must be holding the read lock to call this
254
 // you must be holding the read lock to call this
220
 func (list *Buffer) matchInternal(predicate Predicate, ascending bool, limit int) (results []Item) {
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
 		return
257
 		return
223
 	}
258
 	}
224
 
259
 
232
 	}
267
 	}
233
 
268
 
234
 	for {
269
 	for {
235
-		if predicate(list.buffer[pos]) {
270
+		if predicate(&list.buffer[pos]) {
236
 			results = append(results, list.buffer[pos])
271
 			results = append(results, list.buffer[pos])
237
 		}
272
 		}
238
 		if pos == stop || (limit != 0 && len(results) == limit) {
273
 		if pos == stop || (limit != 0 && len(results) == limit) {
245
 		}
280
 		}
246
 	}
281
 	}
247
 
282
 
248
-	// TODO sort by time instead?
249
-	if !ascending {
250
-		Reverse(results)
251
-	}
252
 	return
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
 // it returns all items.
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
 // LastDiscarded returns the latest time of any entry that was evicted
293
 // LastDiscarded returns the latest time of any entry that was evicted
355
 func (list *Buffer) resize(size int) {
386
 func (list *Buffer) resize(size int) {
356
 	newbuffer := make([]Item, size)
387
 	newbuffer := make([]Item, size)
357
 
388
 
358
-	list.setEnabled(size)
359
-
360
 	if list.start == -1 {
389
 	if list.start == -1 {
361
 		// indices are already correct and nothing needs to be copied
390
 		// indices are already correct and nothing needs to be copied
362
 	} else if size == 0 {
391
 	} else if size == 0 {

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

14
 	timeFormat = "2006-01-02 15:04:05Z"
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
 func TestEmptyBuffer(t *testing.T) {
22
 func TestEmptyBuffer(t *testing.T) {
18
 	pastTime := easyParse(timeFormat)
23
 	pastTime := easyParse(timeFormat)
19
 
24
 
20
 	buf := NewHistoryBuffer(0, 0)
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
 	buf.Add(Item{
27
 	buf.Add(Item{
26
 		Nick: "testnick",
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
 	if len(since) != 0 {
32
 	if len(since) != 0 {
31
 		t.Error("shouldn't be able to add to disabled buf")
33
 		t.Error("shouldn't be able to add to disabled buf")
32
 	}
34
 	}
35
 	}
37
 	}
36
 
38
 
37
 	buf.Resize(1, 0)
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
 	assertEqual(complete, true, t)
41
 	assertEqual(complete, true, t)
43
 	assertEqual(len(since), 0, t)
42
 	assertEqual(len(since), 0, t)
44
 	buf.Add(Item{
43
 	buf.Add(Item{
45
 		Nick: "testnick",
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
 	if len(since) != 1 {
47
 	if len(since) != 1 {
49
 		t.Error("should be able to store items in a nonempty buffer")
48
 		t.Error("should be able to store items in a nonempty buffer")
50
 	}
49
 	}
58
 	buf.Add(Item{
57
 	buf.Add(Item{
59
 		Nick: "testnick2",
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
 	if len(since) != 1 {
61
 	if len(since) != 1 {
63
 		t.Error("expect exactly 1 item")
62
 		t.Error("expect exactly 1 item")
64
 	}
63
 	}
68
 	if since[0].Nick != "testnick2" {
67
 	if since[0].Nick != "testnick2" {
69
 		t.Error("retrieved junk data")
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
 func toNicks(items []Item) (result []string) {
73
 func toNicks(items []Item) (result []string) {
110
 
108
 
111
 	buf.Add(easyItem("testnick2", "2006-01-03 15:04:05Z"))
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
 	assertEqual(complete, true, t)
112
 	assertEqual(complete, true, t)
115
 	assertEqual(toNicks(since), []string{"testnick0", "testnick1", "testnick2"}, t)
113
 	assertEqual(toNicks(since), []string{"testnick0", "testnick1", "testnick2"}, t)
116
 
114
 
117
 	// add another item, evicting the first
115
 	// add another item, evicting the first
118
 	buf.Add(easyItem("testnick3", "2006-01-04 15:04:05Z"))
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
 	assertEqual(complete, false, t)
119
 	assertEqual(complete, false, t)
122
 	assertEqual(toNicks(since), []string{"testnick1", "testnick2", "testnick3"}, t)
120
 	assertEqual(toNicks(since), []string{"testnick1", "testnick2", "testnick3"}, t)
123
 	// now exclude the time of the discarded entry; results should be complete again
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
 	assertEqual(complete, true, t)
123
 	assertEqual(complete, true, t)
126
 	assertEqual(toNicks(since), []string{"testnick1", "testnick2", "testnick3"}, t)
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
 	assertEqual(complete, true, t)
126
 	assertEqual(complete, true, t)
129
 	assertEqual(toNicks(since), []string{"testnick1"}, t)
127
 	assertEqual(toNicks(since), []string{"testnick1"}, t)
130
 
128
 
131
 	// shrink the buffer, cutting off testnick1
129
 	// shrink the buffer, cutting off testnick1
132
 	buf.Resize(2, 0)
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
 	assertEqual(complete, false, t)
132
 	assertEqual(complete, false, t)
135
 	assertEqual(toNicks(since), []string{"testnick2", "testnick3"}, t)
133
 	assertEqual(toNicks(since), []string{"testnick2", "testnick3"}, t)
136
 
134
 
138
 	buf.Add(easyItem("testnick4", "2006-01-05 15:04:05Z"))
136
 	buf.Add(easyItem("testnick4", "2006-01-05 15:04:05Z"))
139
 	buf.Add(easyItem("testnick5", "2006-01-06 15:04:05Z"))
137
 	buf.Add(easyItem("testnick5", "2006-01-06 15:04:05Z"))
140
 	buf.Add(easyItem("testnick6", "2006-01-07 15:04:05Z"))
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
 	assertEqual(complete, true, t)
140
 	assertEqual(complete, true, t)
143
 	assertEqual(toNicks(since), []string{"testnick2", "testnick3", "testnick4", "testnick5", "testnick6"}, t)
141
 	assertEqual(toNicks(since), []string{"testnick2", "testnick3", "testnick4", "testnick5", "testnick6"}, t)
144
 
142
 
145
 	// test ascending order
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
 	assertEqual(toNicks(since), []string{"testnick2", "testnick3"}, t)
145
 	assertEqual(toNicks(since), []string{"testnick2", "testnick3"}, t)
148
 }
146
 }
149
 
147
 
150
 func autoItem(id int, t time.Time) (result Item) {
148
 func autoItem(id int, t time.Time) (result Item) {
151
 	result.Message.Time = t
149
 	result.Message.Time = t
152
 	result.Nick = strconv.Itoa(id)
150
 	result.Nick = strconv.Itoa(id)
151
+	result.Message.Msgid = result.Nick
153
 	return
152
 	return
154
 }
153
 }
155
 
154
 
181
 		now = now.Add(time.Minute * 10)
180
 		now = now.Add(time.Minute * 10)
182
 		id += 1
181
 		id += 1
183
 	}
182
 	}
184
-	items := buf.Latest(0)
183
+	items := buf.latest(0)
185
 	assertEqual(len(items), initialAutoSize, t)
184
 	assertEqual(len(items), initialAutoSize, t)
186
 	assertEqual(atoi(items[0].Nick), 40, t)
185
 	assertEqual(atoi(items[0].Nick), 40, t)
187
 	assertEqual(atoi(items[len(items)-1].Nick), 71, t)
186
 	assertEqual(atoi(items[len(items)-1].Nick), 71, t)
195
 	// ok, 5 items from the first batch are still in the 1-hour window;
194
 	// ok, 5 items from the first batch are still in the 1-hour window;
196
 	// we should overwrite until only those 5 are left, then start expanding
195
 	// we should overwrite until only those 5 are left, then start expanding
197
 	// the buffer so that it retains those 5 and the 100 new items
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
 	assertEqual(len(items), 105, t)
198
 	assertEqual(len(items), 105, t)
200
 	assertEqual(atoi(items[0].Nick), 67, t)
199
 	assertEqual(atoi(items[0].Nick), 67, t)
201
 	assertEqual(atoi(items[len(items)-1].Nick), 171, t)
200
 	assertEqual(atoi(items[len(items)-1].Nick), 171, t)
207
 		id += 1
206
 		id += 1
208
 	}
207
 	}
209
 	// should fill up to the maximum size of 128 and start overwriting
208
 	// should fill up to the maximum size of 128 and start overwriting
210
-	items = buf.Latest(0)
209
+	items = buf.latest(0)
211
 	assertEqual(len(items), 128, t)
210
 	assertEqual(len(items), 128, t)
212
 	assertEqual(atoi(items[0].Nick), 144, t)
211
 	assertEqual(atoi(items[0].Nick), 144, t)
213
 	assertEqual(atoi(items[len(items)-1].Nick), 271, t)
212
 	assertEqual(atoi(items[len(items)-1].Nick), 271, t)
222
 	buf.Resize(128, time.Hour)
221
 	buf.Resize(128, time.Hour)
223
 	// add an item and test that it is stored and retrievable
222
 	// add an item and test that it is stored and retrievable
224
 	buf.Add(autoItem(0, now))
223
 	buf.Add(autoItem(0, now))
225
-	items := buf.Latest(0)
224
+	items := buf.latest(0)
226
 	assertEqual(len(items), 1, t)
225
 	assertEqual(len(items), 1, t)
227
 	assertEqual(atoi(items[0].Nick), 0, t)
226
 	assertEqual(atoi(items[0].Nick), 0, t)
228
 }
227
 }
232
 	// enabled autoresizing buffer
231
 	// enabled autoresizing buffer
233
 	buf := NewHistoryBuffer(128, time.Hour)
232
 	buf := NewHistoryBuffer(128, time.Hour)
234
 	buf.Add(autoItem(0, now))
233
 	buf.Add(autoItem(0, now))
235
-	items := buf.Latest(0)
234
+	items := buf.latest(0)
236
 	assertEqual(len(items), 1, t)
235
 	assertEqual(len(items), 1, t)
237
 	assertEqual(atoi(items[0].Nick), 0, t)
236
 	assertEqual(atoi(items[0].Nick), 0, t)
238
 
237
 
239
 	// disable as during a rehash, confirm that nothing can be retrieved
238
 	// disable as during a rehash, confirm that nothing can be retrieved
240
 	buf.Resize(0, time.Hour)
239
 	buf.Resize(0, time.Hour)
241
-	items = buf.Latest(0)
240
+	items = buf.latest(0)
242
 	assertEqual(len(items), 0, t)
241
 	assertEqual(len(items), 0, t)
243
 }
242
 }
244
 
243
 
252
 	assertEqual(roundUpToPowerOfTwo(1025), 2048, t)
251
 	assertEqual(roundUpToPowerOfTwo(1025), 2048, t)
253
 	assertEqual(roundUpToPowerOfTwo(269435457), 536870912, t)
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

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
 	"errors"
7
 	"errors"
8
 	"fmt"
8
 	"fmt"
9
 	"regexp"
9
 	"regexp"
10
+	"time"
10
 
11
 
11
 	"github.com/oragono/oragono/irc/sno"
12
 	"github.com/oragono/oragono/irc/sno"
12
 )
13
 )
214
 	}
215
 	}
215
 
216
 
216
 	accountName := client.Account()
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
 	if err != nil {
219
 	if err != nil {
219
 		if throttled, ok := err.(*vhostThrottleExceeded); ok {
220
 		if throttled, ok := err.(*vhostThrottleExceeded); ok {
220
 			hsNotice(rb, fmt.Sprintf(client.t("You must wait an additional %v before making another request"), throttled.timeRemaining))
221
 			hsNotice(rb, fmt.Sprintf(client.t("You must wait an additional %v before making another request"), throttled.timeRemaining))
411
 	}
412
 	}
412
 
413
 
413
 	account := client.Account()
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
 	if err != nil {
416
 	if err != nil {
416
 		if throttled, ok := err.(*vhostThrottleExceeded); ok {
417
 		if throttled, ok := err.(*vhostThrottleExceeded); ok {
417
 			hsNotice(rb, fmt.Sprintf(client.t("You must wait an additional %v before taking a vhost"), throttled.timeRemaining))
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
 	quitTimeout time.Duration
52
 	quitTimeout time.Duration
53
 	state       TimerState
53
 	state       TimerState
54
 	timer       *time.Timer
54
 	timer       *time.Timer
55
+	lastTouch   time.Time
55
 }
56
 }
56
 
57
 
57
 // Initialize sets up an IdleTimer and starts counting idle time;
58
 // Initialize sets up an IdleTimer and starts counting idle time;
61
 	it.registerTimeout = RegisterTimeout
62
 	it.registerTimeout = RegisterTimeout
62
 	it.idleTimeout, it.quitTimeout = it.recomputeDurations()
63
 	it.idleTimeout, it.quitTimeout = it.recomputeDurations()
63
 	registered := session.client.Registered()
64
 	registered := session.client.Registered()
65
+	now := time.Now().UTC()
64
 
66
 
65
 	it.Lock()
67
 	it.Lock()
66
 	defer it.Unlock()
68
 	defer it.Unlock()
69
+	it.lastTouch = now
67
 	if registered {
70
 	if registered {
68
 		it.state = TimerActive
71
 		it.state = TimerActive
69
 	} else {
72
 	} else {
82
 	}
85
 	}
83
 
86
 
84
 	idleTimeout = DefaultIdleTimeout
87
 	idleTimeout = DefaultIdleTimeout
85
-	if it.session.client.isTor {
88
+	if it.session.isTor {
86
 		idleTimeout = TorIdleTimeout
89
 		idleTimeout = TorIdleTimeout
87
 	}
90
 	}
88
 
91
 
92
 
95
 
93
 func (it *IdleTimer) Touch() {
96
 func (it *IdleTimer) Touch() {
94
 	idleTimeout, quitTimeout := it.recomputeDurations()
97
 	idleTimeout, quitTimeout := it.recomputeDurations()
98
+	now := time.Now().UTC()
95
 
99
 
96
 	it.Lock()
100
 	it.Lock()
97
 	defer it.Unlock()
101
 	defer it.Unlock()
98
 	it.idleTimeout, it.quitTimeout = idleTimeout, quitTimeout
102
 	it.idleTimeout, it.quitTimeout = idleTimeout, quitTimeout
103
+	it.lastTouch = now
99
 	// a touch transitions TimerUnregistered or TimerIdle into TimerActive
104
 	// a touch transitions TimerUnregistered or TimerIdle into TimerActive
100
 	if it.state != TimerDead {
105
 	if it.state != TimerDead {
101
 		it.state = TimerActive
106
 		it.state = TimerActive
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
 func (it *IdleTimer) processTimeout() {
118
 func (it *IdleTimer) processTimeout() {
107
 	idleTimeout, quitTimeout := it.recomputeDurations()
119
 	idleTimeout, quitTimeout := it.recomputeDurations()
108
 
120
 
322
 	// BrbDead is the state of a client after its timeout has expired; it will be removed
334
 	// BrbDead is the state of a client after its timeout has expired; it will be removed
323
 	// and therefore new sessions cannot be attached to it
335
 	// and therefore new sessions cannot be attached to it
324
 	BrbDead
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
 type BrbTimer struct {
339
 type BrbTimer struct {
345
 
354
 
346
 // attempts to enable BRB for a client, returns whether it succeeded
355
 // attempts to enable BRB for a client, returns whether it succeeded
347
 func (bt *BrbTimer) Enable() (success bool, duration time.Duration) {
356
 func (bt *BrbTimer) Enable() (success bool, duration time.Duration) {
348
-	if !bt.client.Registered() || bt.client.ResumeID() == "" {
349
-		return
350
-	}
351
-
352
 	// TODO make this configurable
357
 	// TODO make this configurable
353
 	duration = ResumeableTotalTimeout
358
 	duration = ResumeableTotalTimeout
354
 
359
 
355
 	bt.client.stateMutex.Lock()
360
 	bt.client.stateMutex.Lock()
356
 	defer bt.client.stateMutex.Unlock()
361
 	defer bt.client.stateMutex.Unlock()
357
 
362
 
363
+	if !bt.client.registered || bt.client.alwaysOn || bt.client.resumeID == "" {
364
+		return
365
+	}
366
+
358
 	switch bt.state {
367
 	switch bt.state {
359
 	case BrbDisabled, BrbEnabled:
368
 	case BrbDisabled, BrbEnabled:
360
 		bt.state = BrbEnabled
369
 		bt.state = BrbEnabled
366
 			bt.brbAt = time.Now().UTC()
375
 			bt.brbAt = time.Now().UTC()
367
 		}
376
 		}
368
 		success = true
377
 		success = true
369
-	case BrbSticky:
370
-		success = true
371
 	default:
378
 	default:
372
 		// BrbDead
379
 		// BrbDead
373
 		success = false
380
 		success = false
416
 	bt.client.stateMutex.Lock()
423
 	bt.client.stateMutex.Lock()
417
 	defer bt.client.stateMutex.Unlock()
424
 	defer bt.client.stateMutex.Unlock()
418
 
425
 
426
+	if bt.client.alwaysOn {
427
+		return
428
+	}
429
+
419
 	switch bt.state {
430
 	switch bt.state {
420
 	case BrbDisabled, BrbEnabled:
431
 	case BrbDisabled, BrbEnabled:
421
 		if len(bt.client.sessions) == 0 {
432
 		if len(bt.client.sessions) == 0 {
432
 	}
443
 	}
433
 	bt.resetTimeout()
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
 )
10
 
10
 
11
 func TestZncTimestampParser(t *testing.T) {
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

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

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

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
 	hadNick := target.HasNick()
43
 	hadNick := target.HasNick()
44
 	origNickMask := target.NickMaskString()
44
 	origNickMask := target.NickMaskString()
45
 	details := target.Details()
45
 	details := target.Details()
46
-	err := client.server.clients.SetNick(target, session, nickname)
46
+	assignedNickname, err := client.server.clients.SetNick(target, session, nickname)
47
 	if err == errNicknameInUse {
47
 	if err == errNicknameInUse {
48
 		rb.Add(nil, server.name, ERR_NICKNAMEINUSE, currentNick, nickname, client.t("Nickname is already in use"))
48
 		rb.Add(nil, server.name, ERR_NICKNAMEINUSE, currentNick, nickname, client.t("Nickname is already in use"))
49
 	} else if err == errNicknameReserved {
49
 	} else if err == errNicknameReserved {
50
 		rb.Add(nil, server.name, ERR_NICKNAMEINUSE, currentNick, nickname, client.t("Nickname is reserved by a different account"))
50
 		rb.Add(nil, server.name, ERR_NICKNAMEINUSE, currentNick, nickname, client.t("Nickname is reserved by a different account"))
51
 	} else if err == errNicknameInvalid {
51
 	} else if err == errNicknameInvalid {
52
 		rb.Add(nil, server.name, ERR_ERRONEUSNICKNAME, currentNick, utils.SafeErrorParam(nickname), client.t("Erroneous nickname"))
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
 	} else if err != nil {
55
 	} else if err != nil {
54
 		rb.Add(nil, server.name, ERR_UNKNOWNERROR, currentNick, "NICK", fmt.Sprintf(client.t("Could not set or change nickname: %s"), err.Error()))
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
 		AccountName: details.accountName,
66
 		AccountName: details.accountName,
65
 		Message:     message,
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
 	if hadNick {
72
 	if hadNick {
71
 		if client == target {
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
 		} else {
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
 		target.server.whoWas.Append(details.WhoWas)
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
 		for session := range target.Friends() {
80
 		for session := range target.Friends() {
79
 			if session != rb.session {
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
 	for _, channel := range client.Channels() {
87
 	for _, channel := range client.Channels() {
86
-		channel.history.Add(histItem)
88
+		channel.AddHistoryItem(histItem)
87
 	}
89
 	}
88
 
90
 
89
 	if target.Registered() {
91
 	if target.Registered() {

+ 103
- 25
irc/nickserv.go View File

31
 }
31
 }
32
 
32
 
33
 func servCmdRequiresBouncerEnabled(config *Config) bool {
33
 func servCmdRequiresBouncerEnabled(config *Config) bool {
34
-	return config.Accounts.Bouncer.Enabled
34
+	return config.Accounts.Multiclient.Enabled
35
 }
35
 }
36
 
36
 
37
 const (
37
 const (
147
 			help: `Syntax: $bSESSIONS [nickname]$b
147
 			help: `Syntax: $bSESSIONS [nickname]$b
148
 
148
 
149
 SESSIONS lists information about the sessions currently attached, via
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
 can use this command to list another user's sessions.`,
151
 can use this command to list another user's sessions.`,
152
 			helpShort: `$bSESSIONS$b lists the sessions attached to a nickname.`,
152
 			helpShort: `$bSESSIONS$b lists the sessions attached to a nickname.`,
153
 			enabled:   servCmdRequiresBouncerEnabled,
153
 			enabled:   servCmdRequiresBouncerEnabled,
217
 			helpStrings: []string{
217
 			helpStrings: []string{
218
 				`Syntax $bSET <setting> <value>$b
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
 				`$bENFORCE$b
222
 				`$bENFORCE$b
223
 'enforce' lets you specify a custom enforcement mechanism for your registered
223
 'enforce' lets you specify a custom enforcement mechanism for your registered
228
 3. 'strict'  [you must already be authenticated to use the nick]
228
 3. 'strict'  [you must already be authenticated to use the nick]
229
 4. 'default' [use the server default]`,
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
 second client of yours that authenticates with SASL and requests the same nick
233
 second client of yours that authenticates with SASL and requests the same nick
234
 is allowed to attach to the nick as well (this is comparable to the behavior
234
 is allowed to attach to the nick as well (this is comparable to the behavior
235
 of IRC "bouncers" like ZNC). Your options are 'on' (allow this behavior),
235
 of IRC "bouncers" like ZNC). Your options are 'on' (allow this behavior),
247
 messages, but may be spammy. Your options are 'always', 'never', and the default
247
 messages, but may be spammy. Your options are 'always', 'never', and the default
248
 of 'commands-only' (the messages will be replayed in /HISTORY output, but not
248
 of 'commands-only' (the messages will be replayed in /HISTORY output, but not
249
 during autoreplay).`,
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
 			authRequired: true,
267
 			authRequired: true,
252
 			enabled:      servCmdRequiresAccreg,
268
 			enabled:      servCmdRequiresAccreg,
332
 		case ReplayJoinsNever:
348
 		case ReplayJoinsNever:
333
 			nsNotice(rb, client.t("You will not see JOINs and PARTs in /HISTORY output or in autoreplay"))
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
 			nsNotice(rb, client.t("This feature has been disabled by the server administrators"))
353
 			nsNotice(rb, client.t("This feature has been disabled by the server administrators"))
338
 		} else {
354
 		} else {
339
 			switch settings.AllowBouncer {
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
 				} else {
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
 	default:
393
 	default:
353
 		nsNotice(rb, client.t("No such setting"))
394
 		nsNotice(rb, client.t("No such setting"))
354
 	}
395
 	}
399
 			out.AutoreplayLines = newValue
440
 			out.AutoreplayLines = newValue
400
 			return
441
 			return
401
 		}
442
 		}
402
-	case "bouncer":
403
-		var newValue BouncerAllowedSetting
443
+	case "multiclient":
444
+		var newValue MulticlientAllowedSetting
404
 		if strings.ToLower(params[1]) == "default" {
445
 		if strings.ToLower(params[1]) == "default" {
405
-			newValue = BouncerAllowedServerDefault
446
+			newValue = MulticlientAllowedServerDefault
406
 		} else {
447
 		} else {
407
 			var enabled bool
448
 			var enabled bool
408
 			enabled, err = utils.StringToBool(params[1])
449
 			enabled, err = utils.StringToBool(params[1])
409
 			if enabled {
450
 			if enabled {
410
-				newValue = BouncerAllowedByUser
451
+				newValue = MulticlientAllowedByUser
411
 			} else {
452
 			} else {
412
-				newValue = BouncerDisallowedByUser
453
+				newValue = MulticlientDisallowedByUser
413
 			}
454
 			}
414
 		}
455
 		}
415
 		if err == nil {
456
 		if err == nil {
429
 				return
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
 	default:
504
 	default:
433
 		err = errInvalidParams
505
 		err = errInvalidParams
434
 	}
506
 	}
480
 	} else if ghost == client {
552
 	} else if ghost == client {
481
 		nsNotice(rb, client.t("You can't GHOST yourself (try /QUIT instead)"))
553
 		nsNotice(rb, client.t("You can't GHOST yourself (try /QUIT instead)"))
482
 		return
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
 	authorized := false
560
 	authorized := false
530
 
605
 
531
 	var username, passphrase string
606
 	var username, passphrase string
532
 	if len(params) == 1 {
607
 	if len(params) == 1 {
533
-		if client.certfp != "" {
608
+		if rb.session.certfp != "" {
534
 			username = params[0]
609
 			username = params[0]
535
 		} else {
610
 		} else {
536
 			// XXX undocumented compatibility mode with other nickservs, allowing
611
 			// XXX undocumented compatibility mode with other nickservs, allowing
553
 	}
628
 	}
554
 
629
 
555
 	// try certfp
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
 		loginSuccessful = (err == nil)
633
 		loginSuccessful = (err == nil)
559
 	}
634
 	}
560
 
635
 
618
 		email = params[1]
693
 		email = params[1]
619
 	}
694
 	}
620
 
695
 
621
-	certfp := client.certfp
696
+	certfp := rb.session.certfp
622
 	if passphrase == "*" {
697
 	if passphrase == "*" {
623
 		if certfp == "" {
698
 		if certfp == "" {
624
 			nsNotice(rb, client.t("You must be connected with TLS and a client certificate to do this"))
699
 			nsNotice(rb, client.t("You must be connected with TLS and a client certificate to do this"))
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
 	if err == nil {
737
 	if err == nil {
663
 		if callbackNamespace == "*" {
738
 		if callbackNamespace == "*" {
664
 			err = server.accounts.Verify(client, account, "")
739
 			err = server.accounts.Verify(client, account, "")
876
 		nsNotice(rb, fmt.Sprintf(client.t("Hostname:    %s"), session.hostname))
951
 		nsNotice(rb, fmt.Sprintf(client.t("Hostname:    %s"), session.hostname))
877
 		nsNotice(rb, fmt.Sprintf(client.t("Created at:  %s"), session.ctime.Format(time.RFC1123)))
952
 		nsNotice(rb, fmt.Sprintf(client.t("Created at:  %s"), session.ctime.Format(time.RFC1123)))
878
 		nsNotice(rb, fmt.Sprintf(client.t("Last active: %s"), session.atime.Format(time.RFC1123)))
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
 	if message.Is512() {
121
 	if message.Is512() {
122
 		rb.AddFromClient(message.Time, message.Msgid, fromNickMask, fromAccount, tags, command, target, message.Message)
122
 		rb.AddFromClient(message.Time, message.Msgid, fromNickMask, fromAccount, tags, command, target, message.Message)
123
 	} else {
123
 	} else {
124
-		if message.IsMultiline() && rb.session.capabilities.Has(caps.Multiline) {
124
+		if rb.session.capabilities.Has(caps.Multiline) {
125
 			batch := rb.session.composeMultilineBatch(fromNickMask, fromAccount, tags, command, target, message)
125
 			batch := rb.session.composeMultilineBatch(fromNickMask, fromAccount, tags, command, target, message)
126
 			rb.setNestedBatchTag(&batch[0])
126
 			rb.setNestedBatchTag(&batch[0])
127
 			rb.setNestedBatchTag(&batch[len(batch)-1])
127
 			rb.setNestedBatchTag(&batch[len(batch)-1])
292
 
292
 
293
 // Notice sends the client the given notice from the server.
293
 // Notice sends the client the given notice from the server.
294
 func (rb *ResponseBuffer) Notice(text string) {
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
 	"github.com/goshuirc/irc-go/ircfmt"
24
 	"github.com/goshuirc/irc-go/ircfmt"
25
 	"github.com/oragono/oragono/irc/caps"
25
 	"github.com/oragono/oragono/irc/caps"
26
 	"github.com/oragono/oragono/irc/connection_limits"
26
 	"github.com/oragono/oragono/irc/connection_limits"
27
+	"github.com/oragono/oragono/irc/history"
27
 	"github.com/oragono/oragono/irc/logger"
28
 	"github.com/oragono/oragono/irc/logger"
28
 	"github.com/oragono/oragono/irc/modes"
29
 	"github.com/oragono/oragono/irc/modes"
30
+	"github.com/oragono/oragono/irc/mysql"
29
 	"github.com/oragono/oragono/irc/sno"
31
 	"github.com/oragono/oragono/irc/sno"
30
 	"github.com/tidwall/buntdb"
32
 	"github.com/tidwall/buntdb"
31
 )
33
 )
84
 	signals           chan os.Signal
86
 	signals           chan os.Signal
85
 	snomasks          SnoManager
87
 	snomasks          SnoManager
86
 	store             *buntdb.DB
88
 	store             *buntdb.DB
89
+	historyDB         mysql.MySQL
87
 	torLimiter        connection_limits.TorLimiter
90
 	torLimiter        connection_limits.TorLimiter
88
 	whoWas            WhoWasList
91
 	whoWas            WhoWasList
89
 	stats             Stats
92
 	stats             Stats
122
 	server.monitorManager.Initialize()
125
 	server.monitorManager.Initialize()
123
 	server.snomasks.Initialize()
126
 	server.snomasks.Initialize()
124
 
127
 
125
-	if err := server.applyConfig(config, true); err != nil {
128
+	if err := server.applyConfig(config); err != nil {
126
 		return nil, err
129
 		return nil, err
127
 	}
130
 	}
128
 
131
 
143
 	if err := server.store.Close(); err != nil {
146
 	if err := server.store.Close(); err != nil {
144
 		server.logger.Error("shutdown", fmt.Sprintln("Could not close datastore:", err))
147
 		server.logger.Error("shutdown", fmt.Sprintln("Could not close datastore:", err))
145
 	}
148
 	}
149
+
150
+	server.historyDB.Close()
146
 }
151
 }
147
 
152
 
148
 // Run starts the server.
153
 // Run starts the server.
316
 
321
 
317
 	// client MUST send PASS if necessary, or authenticate with SASL if necessary,
322
 	// client MUST send PASS if necessary, or authenticate with SASL if necessary,
318
 	// before completing the other registration commands
323
 	// before completing the other registration commands
319
-	authOutcome := c.isAuthorized(server.Config())
324
+	authOutcome := c.isAuthorized(server.Config(), session)
320
 	var quitMessage string
325
 	var quitMessage string
321
 	switch authOutcome {
326
 	switch authOutcome {
322
 	case authFailPass:
327
 	case authFailPass:
376
 	// continue registration
381
 	// continue registration
377
 	d := c.Details()
382
 	d := c.Details()
378
 	server.logger.Info("localconnect", fmt.Sprintf("Client connected [%s] [u:%s] [r:%s]", d.nick, d.username, d.realname))
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
 	// send welcome text
386
 	// send welcome text
382
 	//NOTE(dan): we specifically use the NICK here instead of the nickmask
387
 	//NOTE(dan): we specifically use the NICK here instead of the nickmask
503
 		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)))
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
 	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"))
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
 		return fmt.Errorf("Error loading config file config: %s", err.Error())
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
 	if err != nil {
563
 	if err != nil {
555
 		return fmt.Errorf("Error applying config changes: %s", err.Error())
564
 		return fmt.Errorf("Error applying config changes: %s", err.Error())
556
 	}
565
 	}
558
 	return nil
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
 	if initial {
574
 	if initial {
563
 		server.configFilename = config.Filename
575
 		server.configFilename = config.Filename
564
 		server.name = config.Server.Name
576
 		server.name = config.Server.Name
568
 		// enforce configs that can't be changed after launch:
580
 		// enforce configs that can't be changed after launch:
569
 		if server.name != config.Server.Name {
581
 		if server.name != config.Server.Name {
570
 			return fmt.Errorf("Server name cannot be changed after launching the server, rehash aborted")
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
 			return fmt.Errorf("Datastore path cannot be changed after launching the server, rehash aborted")
584
 			return fmt.Errorf("Datastore path cannot be changed after launching the server, rehash aborted")
573
 		} else if globalCasemappingSetting != config.Server.Casemapping {
585
 		} else if globalCasemappingSetting != config.Server.Casemapping {
574
 			return fmt.Errorf("Casemapping cannot be changed after launching the server, rehash aborted")
586
 			return fmt.Errorf("Casemapping cannot be changed after launching the server, rehash aborted")
576
 	}
588
 	}
577
 
589
 
578
 	server.logger.Info("server", "Using config file", server.configFilename)
590
 	server.logger.Info("server", "Using config file", server.configFilename)
579
-	oldConfig := server.Config()
580
 
591
 
581
 	// first, reload config sections for functionality implemented in subpackages:
592
 	// first, reload config sections for functionality implemented in subpackages:
582
 	wasLoggingRawIO := !initial && server.logger.IsLoggingRawIO()
593
 	wasLoggingRawIO := !initial && server.logger.IsLoggingRawIO()
609
 		if !oldConfig.Channels.Registration.Enabled {
620
 		if !oldConfig.Channels.Registration.Enabled {
610
 			server.channels.loadRegisteredChannels(config)
621
 			server.channels.loadRegisteredChannels(config)
611
 		}
622
 		}
612
-
613
 		// resize history buffers as needed
623
 		// resize history buffers as needed
614
 		if oldConfig.History != config.History {
624
 		if oldConfig.History != config.History {
615
 			for _, channel := range server.channels.Channels() {
625
 			for _, channel := range server.channels.Channels() {
616
-				channel.history.Resize(config.History.ChannelLength, config.History.AutoresizeWindow)
626
+				channel.resizeHistory(config)
617
 			}
627
 			}
618
 			for _, client := range server.clients.AllClients() {
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
 		if err := server.loadDatastore(config); err != nil {
668
 		if err := server.loadDatastore(config); err != nil {
659
 			return err
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
 	server.setupPprofListener(config)
677
 	server.setupPprofListener(config)
778
 	server.channels.Initialize(server)
792
 	server.channels.Initialize(server)
779
 	server.accounts.Initialize(server)
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
 	return nil
804
 	return nil
782
 }
805
 }
783
 
806
 
835
 	return
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
 // elistMatcher takes and matches ELIST conditions
930
 // elistMatcher takes and matches ELIST conditions
839
 type elistMatcher struct {
931
 type elistMatcher struct {
840
 	MinClientsActive bool
932
 	MinClientsActive bool

+ 19
- 1
irc/stats.go View File

26
 	s.mutex.Unlock()
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
 // Transition a client from unregistered to registered
43
 // Transition a client from unregistered to registered
30
 func (s *Stats) Register() {
44
 func (s *Stats) Register() {
31
 	s.mutex.Lock()
45
 	s.mutex.Lock()
32
 	s.Unknown -= 1
46
 	s.Unknown -= 1
33
 	s.Total += 1
47
 	s.Total += 1
48
+	s.setMax()
49
+	s.mutex.Unlock()
50
+}
51
+
52
+func (s *Stats) setMax() {
34
 	if s.Max < s.Total {
53
 	if s.Max < s.Total {
35
 		s.Max = s.Total
54
 		s.Max = s.Total
36
 	}
55
 	}
37
-	s.mutex.Unlock()
38
 }
56
 }
39
 
57
 
40
 // Modify the Invisible count
58
 // Modify the Invisible count

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

5
 
5
 
6
 import (
6
 import (
7
 	"errors"
7
 	"errors"
8
+	"fmt"
8
 	"strings"
9
 	"strings"
10
+	"time"
11
+)
12
+
13
+const (
14
+	IRCv3TimestampFormat = "2006-01-02T15:04:05.000Z"
9
 )
15
 )
10
 
16
 
11
 var (
17
 var (
45
 
51
 
46
 func StringToBool(str string) (result bool, err error) {
52
 func StringToBool(str string) (result bool, err error) {
47
 	switch strings.ToLower(str) {
53
 	switch strings.ToLower(str) {
48
-	case "on", "true", "t", "yes", "y":
54
+	case "on", "true", "t", "yes", "y", "enabled":
49
 		result = true
55
 		result = true
50
-	case "off", "false", "f", "no", "n":
56
+	case "off", "false", "f", "no", "n", "disabled":
51
 		result = false
57
 		result = false
52
 	default:
58
 	default:
53
 		err = ErrInvalidParams
59
 		err = ErrInvalidParams
63
 	}
69
 	}
64
 	return param
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
 	validHostnameLabelRegexp = regexp.MustCompile(`^[0-9A-Za-z.\-]+$`)
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
 // AddrToIP returns the IP address for a net.Addr; unix domain sockets are treated as IPv4 loopback
21
 // AddrToIP returns the IP address for a net.Addr; unix domain sockets are treated as IPv4 loopback
30
 func AddrToIP(addr net.Addr) net.IP {
22
 func AddrToIP(addr net.Addr) net.IP {
31
 	if tcpaddr, ok := addr.(*net.TCPAddr); ok {
23
 	if tcpaddr, ok := addr.(*net.TCPAddr); ok {

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

23
 // SplitMessage represents a message that's been split for sending.
23
 // SplitMessage represents a message that's been split for sending.
24
 // Two possibilities:
24
 // Two possibilities:
25
 // (a) Standard message that can be relayed on a single 512-byte line
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
 // (b) multiline message that was split on the client side
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
 type SplitMessage struct {
29
 type SplitMessage struct {
30
 	Message string
30
 	Message string
31
 	Msgid   string
31
 	Msgid   string
36
 func MakeMessage(original string) (result SplitMessage) {
36
 func MakeMessage(original string) (result SplitMessage) {
37
 	result.Message = original
37
 	result.Message = original
38
 	result.Msgid = GenerateSecretToken()
38
 	result.Msgid = GenerateSecretToken()
39
-	result.Time = time.Now().UTC()
39
+	result.SetTime()
40
 
40
 
41
 	return
41
 	return
42
 }
42
 }
52
 }
52
 }
53
 
53
 
54
 func (sm *SplitMessage) SetTime() {
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
 func (sm *SplitMessage) LenLines() int {
59
 func (sm *SplitMessage) LenLines() int {
88
 	return false
89
 	return false
89
 }
90
 }
90
 
91
 
91
-func (sm *SplitMessage) IsMultiline() bool {
92
-	return sm.Message == "" && len(sm.Split) != 0
93
-}
94
-
95
 func (sm *SplitMessage) Is512() bool {
92
 func (sm *SplitMessage) Is512() bool {
96
 	return sm.Message != ""
93
 	return sm.Message != ""
97
 }
94
 }

+ 16
- 4
irc/znc.go View File

8
 	"strconv"
8
 	"strconv"
9
 	"strings"
9
 	"strings"
10
 	"time"
10
 	"time"
11
+
12
+	"github.com/oragono/oragono/irc/history"
11
 )
13
 )
12
 
14
 
13
 type zncCommandHandler func(client *Client, command string, params []string, rb *ResponseBuffer)
15
 type zncCommandHandler func(client *Client, command string, params []string, rb *ResponseBuffer)
43
 	}
45
 	}
44
 	seconds, _ := strconv.ParseInt(secondsPortion, 10, 64)
46
 	seconds, _ := strconv.ParseInt(secondsPortion, 10, 64)
45
 	fraction, _ := strconv.ParseFloat(fracPortion, 64)
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
 type zncPlaybackTimes struct {
51
 type zncPlaybackTimes struct {
89
 	//     3.3  When the client sends a subsequent redundant JOIN line for those
91
 	//     3.3  When the client sends a subsequent redundant JOIN line for those
90
 	//          channels; redundant JOIN is a complete no-op so we won't replay twice
92
 	//          channels; redundant JOIN is a complete no-op so we won't replay twice
91
 
93
 
92
-	config := client.server.Config()
93
 	if params[1] == "*" {
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
 	} else {
96
 	} else {
97
 		targets = make(StringSet)
97
 		targets = make(StringSet)
98
 		// TODO actually handle nickname targets
98
 		// TODO actually handle nickname targets
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
         # all users will receive simply `netname` as their cloaked hostname.
245
         # all users will receive simply `netname` as their cloaked hostname.
246
         num-bits: 80
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
 # account options
258
 # account options
250
 accounts:
259
 accounts:
337
         # rename-prefix - this is the prefix to use when renaming clients (e.g. Guest-AB54U31)
346
         # rename-prefix - this is the prefix to use when renaming clients (e.g. Guest-AB54U31)
338
         rename-prefix: Guest-
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
         # when disabled, each connection must use a separate nickname (as is the
353
         # when disabled, each connection must use a separate nickname (as is the
344
         # typical behavior of IRC servers). when enabled, a new connection that
354
         # typical behavior of IRC servers). when enabled, a new connection that
345
         # has authenticated with SASL can associate itself with an existing
355
         # has authenticated with SASL can associate itself with an existing
351
         # via nickserv
361
         # via nickserv
352
         allowed-by-default: true
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
     # vhosts controls the assignment of vhosts (strings displayed in place of the user's
369
     # vhosts controls the assignment of vhosts (strings displayed in place of the user's
355
     # hostname/IP) by the HostServ service
370
     # hostname/IP) by the HostServ service
356
     vhosts:
371
     vhosts:
585
     # up, and if the upgrade fails, the original database will be restored.
600
     # up, and if the upgrade fails, the original database will be restored.
586
     autoupgrade: true
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
 # languages config
614
 # languages config
589
 languages:
615
 languages:
590
     # whether to load languages
616
     # whether to load languages
657
 # message history tracking, for the RESUME extension and possibly other uses in future
683
 # message history tracking, for the RESUME extension and possibly other uses in future
658
 history:
684
 history:
659
     # should we store messages for later playback?
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
     # across server restarts. however, you should not enable this unless you understand
687
     # across server restarts. however, you should not enable this unless you understand
662
     # how it interacts with the GDPR and/or any data privacy laws that apply
688
     # how it interacts with the GDPR and/or any data privacy laws that apply
663
     # in your country and the countries of your users.
689
     # in your country and the countries of your users.
683
     # maximum number of CHATHISTORY messages that can be
709
     # maximum number of CHATHISTORY messages that can be
684
     # requested at once (0 disables support for CHATHISTORY)
710
     # requested at once (0 disables support for CHATHISTORY)
685
     chathistory-maxmessages: 100
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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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
 #include <sys/stropts.h>
44
 #include <sys/stropts.h>
45
 #include <sys/mman.h>
45
 #include <sys/mman.h>
46
 #include <sys/poll.h>
46
 #include <sys/poll.h>
47
+#include <sys/select.h>
47
 #include <sys/termio.h>
48
 #include <sys/termio.h>
48
 #include <termios.h>
49
 #include <termios.h>
49
 #include <fcntl.h>
50
 #include <fcntl.h>

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

459
 	MAP_SHARED                    = 0x1
459
 	MAP_SHARED                    = 0x1
460
 	MAP_TYPE                      = 0xf0
460
 	MAP_TYPE                      = 0xf0
461
 	MAP_VARIABLE                  = 0x0
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
 	MCL_CURRENT                   = 0x100
471
 	MCL_CURRENT                   = 0x100
463
 	MCL_FUTURE                    = 0x200
472
 	MCL_FUTURE                    = 0x200
464
 	MSG_ANY                       = 0x4
473
 	MSG_ANY                       = 0x4
483
 	MS_INVALIDATE                 = 0x40
492
 	MS_INVALIDATE                 = 0x40
484
 	MS_PER_SEC                    = 0x3e8
493
 	MS_PER_SEC                    = 0x3e8
485
 	MS_SYNC                       = 0x20
494
 	MS_SYNC                       = 0x20
495
+	NFDBITS                       = 0x20
486
 	NL0                           = 0x0
496
 	NL0                           = 0x0
487
 	NL1                           = 0x4000
497
 	NL1                           = 0x4000
488
 	NL2                           = 0x8000
498
 	NL2                           = 0x8000
688
 	SIOCGHIWAT                    = 0x40047301
698
 	SIOCGHIWAT                    = 0x40047301
689
 	SIOCGIFADDR                   = -0x3fd796df
699
 	SIOCGIFADDR                   = -0x3fd796df
690
 	SIOCGIFADDRS                  = 0x2000698c
700
 	SIOCGIFADDRS                  = 0x2000698c
691
-	SIOCGIFBAUDRATE               = -0x3fd79693
701
+	SIOCGIFBAUDRATE               = -0x3fdf9669
692
 	SIOCGIFBRDADDR                = -0x3fd796dd
702
 	SIOCGIFBRDADDR                = -0x3fd796dd
693
 	SIOCGIFCONF                   = -0x3ff796bb
703
 	SIOCGIFCONF                   = -0x3ff796bb
694
 	SIOCGIFCONFGLOB               = -0x3ff79670
704
 	SIOCGIFCONFGLOB               = -0x3ff79670

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

459
 	MAP_SHARED                    = 0x1
459
 	MAP_SHARED                    = 0x1
460
 	MAP_TYPE                      = 0xf0
460
 	MAP_TYPE                      = 0xf0
461
 	MAP_VARIABLE                  = 0x0
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
 	MCL_CURRENT                   = 0x100
471
 	MCL_CURRENT                   = 0x100
463
 	MCL_FUTURE                    = 0x200
472
 	MCL_FUTURE                    = 0x200
464
 	MSG_ANY                       = 0x4
473
 	MSG_ANY                       = 0x4
483
 	MS_INVALIDATE                 = 0x40
492
 	MS_INVALIDATE                 = 0x40
484
 	MS_PER_SEC                    = 0x3e8
493
 	MS_PER_SEC                    = 0x3e8
485
 	MS_SYNC                       = 0x20
494
 	MS_SYNC                       = 0x20
495
+	NFDBITS                       = 0x40
486
 	NL0                           = 0x0
496
 	NL0                           = 0x0
487
 	NL1                           = 0x4000
497
 	NL1                           = 0x4000
488
 	NL2                           = 0x8000
498
 	NL2                           = 0x8000
688
 	SIOCGHIWAT                    = 0x40047301
698
 	SIOCGHIWAT                    = 0x40047301
689
 	SIOCGIFADDR                   = -0x3fd796df
699
 	SIOCGIFADDR                   = -0x3fd796df
690
 	SIOCGIFADDRS                  = 0x2000698c
700
 	SIOCGIFADDRS                  = 0x2000698c
691
-	SIOCGIFBAUDRATE               = -0x3fd79693
701
+	SIOCGIFBAUDRATE               = -0x3fdf9669
692
 	SIOCGIFBRDADDR                = -0x3fd796dd
702
 	SIOCGIFBRDADDR                = -0x3fd796dd
693
 	SIOCGIFCONF                   = -0x3fef96bb
703
 	SIOCGIFCONF                   = -0x3fef96bb
694
 	SIOCGIFCONFGLOB               = -0x3fef9670
704
 	SIOCGIFCONFGLOB               = -0x3fef9670

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

229
 
229
 
230
 // String converts SID to a string format suitable for display, storage, or transmission.
230
 // String converts SID to a string format suitable for display, storage, or transmission.
231
 func (sid *SID) String() string {
231
 func (sid *SID) String() string {
232
-	// From https://docs.microsoft.com/en-us/windows/win32/secbiomet/general-constants
233
-	const SecurityMaxSidSize = 68
234
 	var s *uint16
232
 	var s *uint16
235
 	e := ConvertSidToStringSid(sid, &s)
233
 	e := ConvertSidToStringSid(sid, &s)
236
 	if e != nil {
234
 	if e != nil {
237
 		return ""
235
 		return ""
238
 	}
236
 	}
239
 	defer LocalFree((Handle)(unsafe.Pointer(s)))
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
 // Len returns the length, in bytes, of a valid security identifier SID.
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
 //sys	CoTaskMemFree(address unsafe.Pointer) = ole32.CoTaskMemFree
313
 //sys	CoTaskMemFree(address unsafe.Pointer) = ole32.CoTaskMemFree
314
 //sys	rtlGetVersion(info *OsVersionInfoEx) (ret error) = ntdll.RtlGetVersion
314
 //sys	rtlGetVersion(info *OsVersionInfoEx) (ret error) = ntdll.RtlGetVersion
315
 //sys	rtlGetNtVersionNumbers(majorVersion *uint32, minorVersion *uint32, buildNumber *uint32) = ntdll.RtlGetNtVersionNumbers
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
 // Process Status API (PSAPI)
321
 // Process Status API (PSAPI)
318
 //sys	EnumProcesses(processIds []uint32, bytesReturned *uint32) (err error) = psapi.EnumProcesses
322
 //sys	EnumProcesses(processIds []uint32, bytesReturned *uint32) (err error) = psapi.EnumProcesses
1378
 	buildNumber &= 0xffff
1382
 	buildNumber &= 0xffff
1379
 	return
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
 	GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT = 2
1742
 	GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT = 2
1743
 	GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS       = 4
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
 	procCoTaskMemFree                                        = modole32.NewProc("CoTaskMemFree")
248
 	procCoTaskMemFree                                        = modole32.NewProc("CoTaskMemFree")
249
 	procRtlGetVersion                                        = modntdll.NewProc("RtlGetVersion")
249
 	procRtlGetVersion                                        = modntdll.NewProc("RtlGetVersion")
250
 	procRtlGetNtVersionNumbers                               = modntdll.NewProc("RtlGetNtVersionNumbers")
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
 	procEnumProcesses                                        = modpsapi.NewProc("EnumProcesses")
255
 	procEnumProcesses                                        = modpsapi.NewProc("EnumProcesses")
252
 	procWSAStartup                                           = modws2_32.NewProc("WSAStartup")
256
 	procWSAStartup                                           = modws2_32.NewProc("WSAStartup")
253
 	procWSACleanup                                           = modws2_32.NewProc("WSACleanup")
257
 	procWSACleanup                                           = modws2_32.NewProc("WSACleanup")
2760
 	return
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
 func EnumProcesses(processIds []uint32, bytesReturned *uint32) (err error) {
2815
 func EnumProcesses(processIds []uint32, bytesReturned *uint32) (err error) {
2764
 	var _p0 *uint32
2816
 	var _p0 *uint32
2765
 	if len(processIds) > 0 {
2817
 	if len(processIds) > 0 {

+ 10
- 9
vendor/modules.txt View File

9
 # github.com/go-ldap/ldap/v3 v3.1.6
9
 # github.com/go-ldap/ldap/v3 v3.1.6
10
 ## explicit
10
 ## explicit
11
 github.com/go-ldap/ldap/v3
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
 # github.com/goshuirc/e-nfa v0.0.0-20160917075329-7071788e3940
15
 # github.com/goshuirc/e-nfa v0.0.0-20160917075329-7071788e3940
13
 ## explicit
16
 ## explicit
14
 github.com/goshuirc/e-nfa
17
 github.com/goshuirc/e-nfa
26
 # github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b
29
 # github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b
27
 ## explicit
30
 ## explicit
28
 github.com/mgutz/ansi
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
 # github.com/oragono/confusables v0.0.0-20190624102032-fe1cf31a24b0
36
 # github.com/oragono/confusables v0.0.0-20190624102032-fe1cf31a24b0
30
 ## explicit
37
 ## explicit
31
 github.com/oragono/confusables
38
 github.com/oragono/confusables
32
 # github.com/oragono/go-ident v0.0.0-20170110123031-337fed0fd21a
39
 # github.com/oragono/go-ident v0.0.0-20170110123031-337fed0fd21a
33
 ## explicit
40
 ## explicit
34
 github.com/oragono/go-ident
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
 ## explicit
43
 ## explicit
44
+# github.com/tidwall/btree v0.0.0-20191029221954-400434d76274
37
 github.com/tidwall/btree
45
 github.com/tidwall/btree
38
 # github.com/tidwall/buntdb v1.1.2
46
 # github.com/tidwall/buntdb v1.1.2
39
 ## explicit
47
 ## explicit
40
 github.com/tidwall/buntdb
48
 github.com/tidwall/buntdb
41
 # github.com/tidwall/gjson v1.3.4
49
 # github.com/tidwall/gjson v1.3.4
42
-## explicit
43
 github.com/tidwall/gjson
50
 github.com/tidwall/gjson
44
 # github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb
51
 # github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb
45
-## explicit
46
 github.com/tidwall/grect
52
 github.com/tidwall/grect
47
 # github.com/tidwall/match v1.0.1
53
 # github.com/tidwall/match v1.0.1
48
-## explicit
49
 github.com/tidwall/match
54
 github.com/tidwall/match
50
 # github.com/tidwall/pretty v1.0.0
55
 # github.com/tidwall/pretty v1.0.0
51
-## explicit
52
 github.com/tidwall/pretty
56
 github.com/tidwall/pretty
53
 # github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e
57
 # github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e
54
-## explicit
55
 github.com/tidwall/rtree
58
 github.com/tidwall/rtree
56
 github.com/tidwall/rtree/base
59
 github.com/tidwall/rtree/base
57
 # github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563
60
 # github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563
58
-## explicit
59
 github.com/tidwall/tinyqueue
61
 github.com/tidwall/tinyqueue
60
 # golang.org/x/crypto v0.0.0-20191112222119-e1110fd1c708
62
 # golang.org/x/crypto v0.0.0-20191112222119-e1110fd1c708
61
 ## explicit
63
 ## explicit
63
 golang.org/x/crypto/blowfish
65
 golang.org/x/crypto/blowfish
64
 golang.org/x/crypto/sha3
66
 golang.org/x/crypto/sha3
65
 golang.org/x/crypto/ssh/terminal
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
 golang.org/x/sys/cpu
69
 golang.org/x/sys/cpu
69
 golang.org/x/sys/unix
70
 golang.org/x/sys/unix
70
 golang.org/x/sys/windows
71
 golang.org/x/sys/windows

Loading…
Cancel
Save