Initial commit of utilities

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
This commit is contained in:
Jason A. Donenfeld
2021-05-27 12:17:12 +02:00
commit b52ec14d2d
20 changed files with 3764 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
/ircmirror
/irc-simple-responder
*.cache
.idea

339
LICENSE Normal file
View File

@@ -0,0 +1,339 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.

15
Makefile Normal file
View File

@@ -0,0 +1,15 @@
targets := $(patsubst cmd/%,%,$(wildcard cmd/*))
input := $(wildcard hbot/* go.mod go.sum)
all: $(targets)
define gobuild
$(1): $$(wildcard cmd/$(1)/*.go) $$(input)
CGO_ENABLED=0 go build -buildmode=pie -v -o $(1) ./cmd/$(1)
endef
$(foreach target,$(targets),$(eval $(call gobuild,$(target))))
clean:
rm -f $(targets)
fmt:
go fmt ./...
test:
go test -v ./...
.PHONY: all clean fmt test

30
README.md Normal file
View File

@@ -0,0 +1,30 @@
# IRC Utilities for Go
This is a small collection of utilities for interacting with IRC in Go.
### `hbot` - small bot and message parsing library
Based on [hellabot](https://github.com/whyrusleeping/hellabot), [kittybot](https://github.com/ugjka/kittybot), and [sorcix-irc](https://github.com/sorcix/irc).
This is a simple message parser and trigger-based IRC client library.
```go
import "golang.zx2c4.com/irc/hbot"
```
### `ircmirror` - mirrors one channel to another, one-way
To assist in channel migrations, this mirrors messages from one channel to another, by joining the source channel from several IP addresses via WireGuard.
```go
go get golang.zx2c4.com/irc/cmd/ircmirror
```
### `irc-simple-responder` - responds to all messages
This is best used with `+z` in a channel. It responds to all messages with a static string.
```go
go get golang.zx2c4.com/irc/cmd/irc-simple-responder
```

View File

@@ -0,0 +1,126 @@
/* SPDX-License-Identifier: GPL-2.0
*
* Copyright (C) 2021 Jason A. Donenfeld. All Rights Reserved.
*/
package main
import (
"bufio"
"flag"
"fmt"
"log"
"net"
"os"
"regexp"
"strings"
"sync"
"time"
"golang.zx2c4.com/irc/hbot"
)
func main() {
channelsArg := flag.String("channels", "", "channels to join, separated by commas")
serverArg := flag.String("server", "", "server and port")
nickArg := flag.String("nick", "", "nickname")
passwordArg := flag.String("password-file", "", "optional file with password")
messageArg := flag.String("message", "", "message with which to respond")
flag.Parse()
if matched, _ := regexp.MatchString(`^(#[a-zA-Z0-9_-]+,)*(#[a-zA-Z0-9_-]+)$`, *channelsArg); !matched {
fmt.Fprintln(os.Stderr, "Invalid channels")
flag.Usage()
os.Exit(1)
}
if _, _, err := net.SplitHostPort(*serverArg); err != nil {
fmt.Fprintln(os.Stderr, "Invalid server")
flag.Usage()
os.Exit(1)
}
if matched, _ := regexp.MatchString(`^[a-zA-Z0-9\[\]_-]{1,16}$`, *nickArg); !matched {
fmt.Fprintln(os.Stderr, "Invalid nick")
flag.Usage()
os.Exit(1)
}
password := ""
if len(*passwordArg) > 0 {
f, err := os.Open(*passwordArg)
if err != nil {
fmt.Fprintf(os.Stderr, "Unable to open password file: %v\n", err)
os.Exit(1)
}
password, err = bufio.NewReader(f).ReadString('\n')
f.Close()
if err != nil {
fmt.Fprintf(os.Stderr, "Unable to read password file: %v\n", err)
os.Exit(1)
}
if len(password) > 0 && password[len(password)-1] == '\n' {
password = password[:len(password)-1]
}
}
if len(*messageArg) == 0 {
fmt.Fprintln(os.Stderr, "Missing message")
flag.Usage()
os.Exit(1)
}
const messengerTimeout = time.Minute * 10
messengers := make(map[string]time.Time, 1024)
var messengersMu sync.Mutex
go func() {
for range time.Tick(messengerTimeout / 20) {
messengersMu.Lock()
for nick, last := range messengers {
if time.Since(last) >= messengerTimeout {
delete(messengers, nick)
}
}
messengersMu.Unlock()
}
}()
bot := hbot.NewBot(&hbot.Config{
Host: *serverArg,
Nick: *nickArg,
User: hbot.CommonBotUserPrefix + *nickArg,
Channels: strings.Split(*channelsArg, ","),
Logger: hbot.Logger{Verbosef: log.Printf, Errorf: log.Printf},
Password: password,
})
bot.AddTrigger(hbot.Trigger{
Condition: func(b *hbot.Bot, m *hbot.Message) bool {
if m.Command != "PRIVMSG" || strings.HasPrefix(m.Prefix.User, hbot.CommonBotUserPrefix) || strings.HasPrefix(m.Prefix.User, "~"+hbot.CommonBotUserPrefix) {
return false
}
nick := strings.ToLower(m.Prefix.Name)
messengersMu.Lock()
defer messengersMu.Unlock()
if last, ok := messengers[nick]; ok && time.Since(last) < messengerTimeout {
return false
}
if len(messengers) > 1024*1024*1024 {
return false
}
messengers[nick] = time.Now()
return true
},
Action: func(b *hbot.Bot, m *hbot.Message) {
message := *messageArg
target := m.Prefix.Name
if strings.Contains(m.Param(0), "#") {
target = m.Param(0)
if target[0] == '@' || target[0] == '+' {
target = target[1:]
}
message = m.Prefix.Name + ": " + message
}
log.Printf("Responding to %q in %q", m.Prefix.String(), target)
b.Msg(target, message)
},
})
for {
bot.Run()
time.Sleep(time.Second * 5)
}
}

177
cmd/ircmirror/ircwriters.go Normal file
View File

@@ -0,0 +1,177 @@
/* SPDX-License-Identifier: GPL-2.0
*
* Copyright (C) 2021 Jason A. Donenfeld. All Rights Reserved.
*/
package main
import (
"bufio"
"container/list"
"log"
"math/rand"
"net"
"os"
"regexp"
"strconv"
"strings"
"sync"
"time"
"golang.zx2c4.com/irc/hbot"
)
var words []string
func init() {
f, err := os.Open("/usr/share/dict/words")
if err != nil {
log.Fatalf("Unable to open dictionary: %v", err)
}
defer f.Close()
scanner := bufio.NewScanner(f)
matcher := regexp.MustCompile(`^[a-zA-Z0-9_-]{3,}$`)
for scanner.Scan() {
word := scanner.Text()
if !matcher.MatchString(word) {
continue
}
words = append(words, word)
}
if len(words) == 0 {
log.Fatalln("Did not find any words in dictionary")
}
}
func randomNick() string {
return words[rand.Intn(len(words))] + words[rand.Intn(len(words))] + strconv.Itoa(rand.Intn(10000))
}
type ircWriter struct {
mu sync.Mutex
bot *hbot.Bot
nick string
mungedNick string
usageElem *list.Element
name string
}
type ircWriters struct {
mu sync.Mutex
byNick map[string]*ircWriter
byUsage list.List
server string
channel string
}
func (writers *ircWriters) runWriter(name string, dialer func(network, address string) (net.Conn, error)) {
writer := &ircWriter{name: name}
startNick := randomNick()
logf := func(format string, args ...interface{}) {
log.Printf("[DST %s] "+format, append([]interface{}{name}, args...)...)
}
writer.bot = hbot.NewBot(&hbot.Config{
Host: writers.server,
Nick: startNick,
User: hbot.CommonBotUserPrefix + startNick,
Channels: []string{writers.channel},
Dial: dialer,
Logger: hbot.Logger{Verbosef: logf, Errorf: logf},
})
go func() {
<-writer.bot.Joined()
writers.mu.Lock()
defer writers.mu.Unlock()
writer.mu.Lock()
defer writer.mu.Unlock()
writer.usageElem = writers.byUsage.PushBack(writer)
}()
writer.bot.AddTrigger(hbot.Trigger{
Condition: func(bot *hbot.Bot, m *hbot.Message) bool {
return (m.Command == "436" || m.Command == "433") && len(writer.nick) > 0
},
Action: func(bot *hbot.Bot, m *hbot.Message) {
logf("Failed with nick %q, trying %q\n", writer.mungedNick, writer.mungedNick+"_")
writer.mungedNick += "_"
if len(writer.mungedNick) > 16 {
usidx := strings.IndexByte(writer.mungedNick, '_')
uslen := len(writer.mungedNick[usidx:])
if uslen >= 16 {
writer.mungedNick = writer.nick + "-"
if len(writer.mungedNick) > 16 {
writer.mungedNick = writer.nick[:15] + "-"
}
} else {
writer.mungedNick = writer.mungedNick[:16-uslen] + writer.mungedNick[usidx:]
}
}
bot.SetNick(writer.mungedNick)
},
})
writer.bot.Run()
writers.mu.Lock()
writer.mu.Lock()
if writer.usageElem != nil {
writers.byUsage.Remove(writer.usageElem)
delete(writers.byNick, writer.nick)
}
writer.mu.Unlock()
writers.mu.Unlock()
}
func (writers *ircWriters) getWriter(nick string) *ircWriter {
writers.mu.Lock()
if writer, ok := writers.byNick[nick]; ok {
writer.mu.Lock()
if writer.nick == nick {
writers.byUsage.MoveToFront(writer.usageElem)
writers.mu.Unlock()
if writer.bot.Nick() != writer.mungedNick {
log.Printf("[DST %s] Changing nick to %q\n", writer.name, nick)
writer.bot.SetNick(writer.mungedNick)
}
return writer
}
writer.mu.Unlock()
}
if writers.byUsage.Len() == 0 {
writers.mu.Unlock()
return nil
}
writer := writers.byUsage.Back().Value.(*ircWriter)
writer.mu.Lock()
delete(writers.byNick, writer.nick)
writer.nick = nick
writer.mungedNick = nick + "-"
if len(writer.mungedNick) > 16 {
writer.mungedNick = nick[:15] + "-"
}
writers.byNick[nick] = writer
writers.byUsage.MoveToFront(writer.usageElem)
writers.mu.Unlock()
log.Printf("[DST %s] Changing nick to %q\n", writer.name, nick)
writer.bot.SetNick(writer.mungedNick)
return writer
}
func (writers *ircWriters) queueMessage(from, message string) {
writer := writers.getWriter(from)
if writer == nil {
time.AfterFunc(time.Second*3, func() {
writers.queueMessage(from, message)
})
return
}
log.Printf("[DST %s] Queueing message from %q\n", writer.name, from)
writer.bot.Msg(writers.channel, message)
writer.mu.Unlock()
}
func newIrcWriterGroup(server, channel string) *ircWriters {
return &ircWriters{
mu: sync.Mutex{},
byNick: make(map[string]*ircWriter, 400),
server: server,
channel: channel,
}
}

61
cmd/ircmirror/keycache.go Normal file
View File

@@ -0,0 +1,61 @@
/* SPDX-License-Identifier: GPL-2.0
*
* Copyright (C) 2021 Jason A. Donenfeld. All Rights Reserved.
*/
package main
import (
"crypto/rand"
"encoding/base64"
"errors"
"log"
"os"
"strings"
)
const privateKeyCacheFile = "./private-key.cache"
func loadCachedPrivateKey() ([]byte, error) {
bytes, err := os.ReadFile(privateKeyCacheFile)
if err != nil {
return nil, err
}
log.Println("Loading cached private key from file")
privateKeyB64 := strings.TrimSpace(string(bytes))
privateKey, err := base64.StdEncoding.DecodeString(privateKeyB64)
if err != nil {
return nil, err
}
if len(privateKey) != 32 {
return nil, errors.New("invalid private key")
}
return privateKey, nil
}
func saveCachedPrivateKey(privateKey []byte) error {
if len(privateKey) != 32 {
return errors.New("invalid private key")
}
return os.WriteFile(privateKeyCacheFile, []byte(base64.StdEncoding.EncodeToString(privateKey)+"\n"), 0600)
}
func loadOrGeneratePrivateKey() ([]byte, error) {
privateKey, err := loadCachedPrivateKey()
if err == nil {
return privateKey, nil
}
log.Println("Generating new private key")
var k [32]byte
_, err = rand.Read(k[:])
if err != nil {
return nil, err
}
k[0] &= 248
k[31] = (k[31] & 127) | 64
err = saveCachedPrivateKey(k[:])
if err != nil {
return nil, err
}
return k[:], nil
}

158
cmd/ircmirror/main.go Normal file
View File

@@ -0,0 +1,158 @@
/* SPDX-License-Identifier: GPL-2.0
*
* Copyright (C) 2021 Jason A. Donenfeld. All Rights Reserved.
*/
package main
import (
"flag"
"fmt"
"log"
"math/rand"
"net"
"os"
"regexp"
"strings"
"sync"
"syscall"
"time"
"golang.org/x/sys/unix"
"golang.zx2c4.com/irc/hbot"
)
func raiseFileLimit() error {
var lim unix.Rlimit
err := unix.Getrlimit(syscall.RLIMIT_NOFILE, &lim)
if err != nil {
return err
}
if lim.Cur == lim.Max {
return nil
}
log.Printf("Raising file limit from %d to %d\n", lim.Cur, lim.Max)
lim.Cur = lim.Max
return unix.Setrlimit(syscall.RLIMIT_NOFILE, &lim)
}
func main() {
accountIdArg := flag.String("account-id", "", "account ID number")
srcChannelArg := flag.String("src-channel", "", "source channel")
dstChannelArg := flag.String("dst-channel", "", "destination channel")
srcServerArg := flag.String("src-server", "", "source server")
dstServerArg := flag.String("dst-server", "", "destination server")
flag.Parse()
if matched, _ := regexp.MatchString(`^[0-9]+$`, *accountIdArg); !matched {
fmt.Fprintln(os.Stderr, "Invalid account ID")
flag.Usage()
os.Exit(1)
}
if matched, _ := regexp.MatchString(`^#[a-zA-Z0-9_-]+$`, *srcChannelArg); !matched {
fmt.Fprintln(os.Stderr, "Invalid source channel")
flag.Usage()
os.Exit(1)
}
if matched, _ := regexp.MatchString(`^#[a-zA-Z0-9_-]+$`, *dstChannelArg); !matched {
fmt.Fprintln(os.Stderr, "Invalid destination channel")
flag.Usage()
os.Exit(1)
}
if _, _, err := net.SplitHostPort(*srcServerArg); err != nil {
fmt.Fprintln(os.Stderr, "Invalid source server")
flag.Usage()
os.Exit(1)
}
if _, _, err := net.SplitHostPort(*dstServerArg); err != nil {
fmt.Fprintln(os.Stderr, "Invalid destination server")
flag.Usage()
os.Exit(1)
}
privateKey, err := loadOrGeneratePrivateKey()
if err != nil {
log.Fatalln(err)
}
endpoints, err := getVpnEndpoints()
if err != nil {
log.Fatalln(err)
}
vpnConf, err := registerVpnConf(privateKey, *accountIdArg)
if err != nil {
log.Fatalln(err)
}
rand.Seed(time.Now().UnixNano())
err = raiseFileLimit()
if err != nil {
log.Fatalln(err)
}
dialers, err := makeDialers(vpnConf, endpoints)
if err != nil {
log.Fatalln(err)
}
if len(dialers) < 2 {
log.Fatalln("Not enough dialers returned")
}
writers := newIrcWriterGroup(*dstServerArg, *dstChannelArg)
const writersPerIP = 10
type nextDialer struct {
sync.Mutex
*dialer
}
dialNext := func(nd *nextDialer, v4 bool) {
var last *dialer
for {
nd.Lock()
if nd.dialer == last {
nd.dialer = &dialers[rand.Intn(len(dialers))]
}
last = nd.dialer
nd.Unlock()
name := last.name
dial := last.dial
if v4 {
name += "-v4"
dial, _ = last.splitByAf()
} else {
name += "-v6"
_, dial = last.splitByAf()
}
writers.runWriter(name, dial)
}
}
for i := 0; i < vpnProviderMaxEndpointsInParallel/2; i++ {
nd := new(nextDialer)
for i := 0; i < writersPerIP; i++ {
go dialNext(nd, true)
go dialNext(nd, false)
}
}
srcDial := dialers[rand.Intn(len(dialers))]
logf := func(format string, args ...interface{}) {
log.Printf("[SRC %s] "+format, append([]interface{}{srcDial.name}, args...)...)
}
bot := hbot.NewBot(&hbot.Config{
Host: *srcServerArg,
Nick: randomNick(),
Channels: []string{*srcChannelArg},
Dial: srcDial.dial,
Logger: hbot.Logger{Verbosef: logf, Errorf: logf},
})
bot.AddTrigger(hbot.Trigger{
Condition: func(b *hbot.Bot, m *hbot.Message) bool {
return m.Param(0) == *srcChannelArg && m.Command == "PRIVMSG" &&
!strings.HasPrefix(m.Prefix.User, hbot.CommonBotUserPrefix) &&
!strings.HasPrefix(m.Prefix.User, "~"+hbot.CommonBotUserPrefix)
},
Action: func(b *hbot.Bot, m *hbot.Message) {
writers.queueMessage(m.Prefix.Name, m.Trailing())
},
})
for {
bot.Run()
time.Sleep(time.Second * 5)
}
}

84
cmd/ircmirror/net.go Normal file
View File

@@ -0,0 +1,84 @@
/* SPDX-License-Identifier: GPL-2.0
*
* Copyright (C) 2021 Jason A. Donenfeld. All Rights Reserved.
*/
package main
import (
"encoding/hex"
"fmt"
"log"
"net"
"golang.zx2c4.com/wireguard/conn"
"golang.zx2c4.com/wireguard/device"
"golang.zx2c4.com/wireguard/tun/netstack"
)
func makeNet(conf *vpnConf, endpoint vpnEndpoint) (*device.Device, *netstack.Net, error) {
var localAddresses, dnsServers []net.IP
for _, ip := range conf.ips {
localAddresses = append(localAddresses, ip.IPAddr().IP)
}
for _, ip := range conf.dnses {
dnsServers = append(dnsServers, ip.IPAddr().IP)
}
tun, stack, err := netstack.CreateNetTUN(localAddresses, dnsServers, 1420)
if err != nil {
return nil, nil, err
}
logf := func(format string, args ...interface{}) {
log.Printf("[NET %s] "+format, append([]interface{}{endpoint.name}, args...)...)
}
dev := device.NewDevice(tun, conn.NewStdNetBind(), &device.Logger{logf, logf})
err = dev.IpcSet(fmt.Sprintf("private_key=%s\npublic_key=%s\nendpoint=%s\nallowed_ip=0.0.0.0/0\nallowed_ip=::/0\n",
hex.EncodeToString(conf.privateKey), hex.EncodeToString(endpoint.publicKey), endpoint.endpoint.String()))
if err != nil {
return nil, nil, err
}
err = dev.Up()
if err != nil {
return nil, nil, err
}
return dev, stack, nil
}
type dialer struct {
name string
dial func(network, address string) (net.Conn, error)
}
func makeDialers(conf *vpnConf, endpoints []vpnEndpoint) (dialers []dialer, err error) {
var devs []*device.Device
defer func() {
if err != nil {
for _, dev := range devs {
dev.Close()
}
}
}()
for _, endpoint := range endpoints {
dev, stack, err := makeNet(conf, endpoint)
if err != nil {
return nil, err
}
devs = append(devs, dev)
dialers = append(dialers, dialer{endpoint.name, stack.Dial})
}
return dialers, nil
}
func (d *dialer) splitByAf() (v4, v6 func(network, address string) (net.Conn, error)) {
return func(network, address string) (net.Conn, error) {
if len(network) == 3 {
network += "4"
}
return d.dial(network, address)
}, func(network, address string) (net.Conn, error) {
if len(network) == 3 {
network += "6"
}
return d.dial(network, address)
}
}

View File

@@ -0,0 +1,137 @@
/* SPDX-License-Identifier: GPL-2.0
*
* Copyright (C) 2021 Jason A. Donenfeld. All Rights Reserved.
*/
package main
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"net/url"
"regexp"
"strings"
"golang.org/x/crypto/curve25519"
"inet.af/netaddr"
)
const vpnProviderMaxEndpointsInParallel = 20
type vpnEndpoint struct {
publicKey []byte
endpoint netaddr.IPPort
name string
country string
}
type vpnConf struct {
privateKey []byte
ips []netaddr.IP
dnses []netaddr.IP
}
type apiRelaysWireGuardV1Key []byte
func (key *apiRelaysWireGuardV1Key) UnmarshalText(text []byte) error {
k, err := base64.StdEncoding.DecodeString(string(text))
if err != nil {
return err
}
if len(k) != 32 {
return errors.New("key must be 32 bytes")
}
*key = k
return nil
}
type apiRelaysWireGuardV1Relay struct {
Hostname string `json:"hostname"`
EndpointV4 netaddr.IP `json:"ipv4_addr_in"`
EndpointV6 netaddr.IP `json:"ipv6_addr_in"`
PublicKey apiRelaysWireGuardV1Key `json:"public_key"`
MultihopPort uint16 `json:"multihop_port"`
}
type apiRelaysWireGuardV1City struct {
Name string `json:"name"`
Code string `json:"code"`
Latitude float64 `json:"latitude"`
Longitude float64 `json:"Longitude"`
Relays []apiRelaysWireGuardV1Relay `json:"relays"`
}
type apiRelaysWireGuardV1Country struct {
Name string `json:"name"`
Code string `json:"code"`
Cities []apiRelaysWireGuardV1City `json:"cities"`
}
type apiRelaysWireGuardV1Root struct {
Countries []apiRelaysWireGuardV1Country `json:"countries"`
}
func getVpnEndpoints() ([]vpnEndpoint, error) {
log.Println("Getting VPN server list")
resp, err := http.Get("https://api.mullvad.net/public/relays/wireguard/v1/")
if err != nil {
return nil, err
}
bytes, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var relays apiRelaysWireGuardV1Root
err = json.Unmarshal(bytes, &relays)
if err != nil {
return nil, err
}
var endpoints []vpnEndpoint
for _, country := range relays.Countries {
for _, city := range country.Cities {
for _, relay := range city.Relays {
endpoints = append(endpoints, vpnEndpoint{
publicKey: relay.PublicKey,
endpoint: netaddr.IPPortFrom(relay.EndpointV4, 51820),
name: strings.TrimSuffix(relay.Hostname, "-wireguard"),
})
}
}
}
return endpoints, nil
}
func registerVpnConf(privateKey []byte, accountId string) (*vpnConf, error) {
log.Println("Registering VPN private key")
publicKey, err := curve25519.X25519(privateKey, curve25519.Basepoint)
if err != nil {
return nil, err
}
resp, err := http.PostForm("https://api.mullvad.net/wg/", url.Values{
"account": {accountId},
"pubkey": {base64.StdEncoding.EncodeToString(publicKey)},
})
if err != nil {
return nil, err
}
ipBytes, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if match, _ := regexp.Match(`^[0-9a-f:/.,]+$`, ipBytes); !match {
return nil, fmt.Errorf("registration rejected: %q", string(ipBytes))
}
conf := &vpnConf{privateKey: privateKey, dnses: []netaddr.IP{netaddr.MustParseIP("193.138.218.74")}}
for _, ipStr := range strings.Split(string(ipBytes), ",") {
ip, err := netaddr.ParseIPPrefix(strings.TrimSpace(ipStr))
if err != nil {
return nil, err
}
conf.ips = append(conf.ips, ip.IP())
}
return conf, nil
}

178
cmd/wurgurboo/cgit.go Normal file
View File

@@ -0,0 +1,178 @@
/* SPDX-License-Identifier: GPL-2.0
*
* Copyright (C) 2021 Jason A. Donenfeld. All Rights Reserved.
*/
package main
import (
"container/list"
"encoding/hex"
"encoding/xml"
"io"
"log"
"net/http"
"path"
"sync"
"time"
)
type CgitCommit struct {
Text string `xml:",chardata"`
Title string `xml:"title"`
Updated string `xml:"updated"`
Author struct {
Text string `xml:",chardata"`
Name string `xml:"name"`
Email string `xml:"email"`
} `xml:"author"`
Published string `xml:"published"`
Link struct {
Text string `xml:",chardata"`
Rel string `xml:"rel,attr"`
Type string `xml:"type,attr"`
Href string `xml:"href,attr"`
} `xml:"link"`
ID string `xml:"id"`
Content []struct {
Text string `xml:",chardata"`
Type string `xml:"type,attr"`
Div struct {
Text string `xml:",chardata"`
Xmlns string `xml:"xmlns,attr"`
Pre string `xml:"pre"`
} `xml:"div"`
} `xml:"content"`
}
type cgitFeed struct {
XMLName xml.Name `xml:"feed"`
Text string `xml:",chardata"`
Xmlns string `xml:"xmlns,attr"`
Title string `xml:"title"`
Subtitle string `xml:"subtitle"`
Link struct {
Text string `xml:",chardata"`
Rel string `xml:"rel,attr"`
Type string `xml:"type,attr"`
Href string `xml:"href,attr"`
} `xml:"link"`
Entry []CgitCommit `xml:"entry"`
}
type cgitFeedStatus struct {
repo string
title string
seenEntries map[[32]byte]bool
orderedEntries list.List
}
type CgitFeedMonitorer struct {
feeds []*cgitFeedStatus
updates chan CgitFeedUpdate
ticker *time.Ticker
wg sync.WaitGroup
mu sync.Mutex
}
type CgitFeedUpdate struct {
RepoTitle string
Commit *CgitCommit
}
func NewCgitFeedMonitorer(pollInterval time.Duration) *CgitFeedMonitorer {
fm := &CgitFeedMonitorer{
updates: make(chan CgitFeedUpdate, 32),
ticker: time.NewTicker(pollInterval),
}
fm.wg.Add(1)
go func() {
defer fm.wg.Done()
for range fm.ticker.C {
fm.mu.Lock()
for _, feed := range fm.feeds {
fm.wg.Add(1)
go func(feed *cgitFeedStatus) {
defer fm.wg.Done()
fm.updateFeed(feed, true)
}(feed)
}
fm.mu.Unlock()
}
}()
return fm
}
func (fm *CgitFeedMonitorer) Stop() {
fm.ticker.Stop()
fm.wg.Wait()
close(fm.updates)
}
func (fm *CgitFeedMonitorer) Updates() <-chan CgitFeedUpdate {
return fm.updates
}
func (fm *CgitFeedMonitorer) AddFeed(repo string) {
go func() {
status := &cgitFeedStatus{
repo: repo,
title: path.Base(repo),
seenEntries: make(map[[32]byte]bool, 1024),
}
fm.updateFeed(status, false)
fm.mu.Lock()
fm.feeds = append(fm.feeds, status)
fm.mu.Unlock()
}()
}
func (fm *CgitFeedMonitorer) updateFeed(fs *cgitFeedStatus, alert bool) {
feed, err := fs.fetchFeed()
if err != nil {
log.Printf("Unable to fetch commits for %q: %v", fs.title, err)
return
}
for i := len(feed.Entry) - 1; i >= 0; i-- {
commit := &feed.Entry[i]
var commitID [32]byte
if hex.DecodedLen(len(commit.ID)) > len(commitID) {
continue
}
n, err := hex.Decode(commitID[:], []byte(commit.ID))
if err != nil || n < 20 {
continue
}
if _, ok := fs.seenEntries[commitID]; ok {
continue
}
fs.seenEntries[commitID] = true
fs.orderedEntries.PushBack(commitID)
for len(fs.seenEntries) > 1024 {
first := fs.orderedEntries.Front()
delete(fs.seenEntries, first.Value.([32]byte))
fs.orderedEntries.Remove(first)
}
if alert {
fm.updates <- CgitFeedUpdate{fs.title, commit}
}
}
}
func (fs *cgitFeedStatus) fetchFeed() (*cgitFeed, error) {
resp, err := http.Get(fs.repo + "/atom/")
if err != nil {
return nil, err
}
bytes, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
return nil, err
}
var feed cgitFeed
err = xml.Unmarshal(bytes, &feed)
if err != nil {
return nil, err
}
return &feed, nil
}

101
cmd/wurgurboo/main.go Normal file
View File

@@ -0,0 +1,101 @@
/* SPDX-License-Identifier: GPL-2.0
*
* Copyright (C) 2021 Jason A. Donenfeld. All Rights Reserved.
*/
package main
import (
"bufio"
"flag"
"fmt"
"log"
"net"
"os"
"regexp"
"time"
"golang.zx2c4.com/irc/hbot"
)
func main() {
channelArg := flag.String("channel", "", "channel to join")
serverArg := flag.String("server", "", "server and port")
nickArg := flag.String("nick", "", "nickname")
passwordArg := flag.String("password-file", "", "optional file with password")
flag.Parse()
if matched, _ := regexp.MatchString(`^#[a-zA-Z0-9_-]+$`, *channelArg); !matched {
fmt.Fprintln(os.Stderr, "Invalid channel")
flag.Usage()
os.Exit(1)
}
if _, _, err := net.SplitHostPort(*serverArg); err != nil {
fmt.Fprintln(os.Stderr, "Invalid server")
flag.Usage()
os.Exit(1)
}
if matched, _ := regexp.MatchString(`^[a-zA-Z0-9\[\]_-]{1,16}$`, *nickArg); !matched {
fmt.Fprintln(os.Stderr, "Invalid nick")
flag.Usage()
os.Exit(1)
}
password := ""
if len(*passwordArg) > 0 {
f, err := os.Open(*passwordArg)
if err != nil {
fmt.Fprintf(os.Stderr, "Unable to open password file: %v\n", err)
os.Exit(1)
}
password, err = bufio.NewReader(f).ReadString('\n')
f.Close()
if err != nil {
fmt.Fprintf(os.Stderr, "Unable to read password file: %v\n", err)
os.Exit(1)
}
if len(password) > 0 && password[len(password)-1] == '\n' {
password = password[:len(password)-1]
}
}
feeds := NewCgitFeedMonitorer(time.Second * 10)
feeds.AddFeed("https://git.zx2c4.com/wireguard-linux/")
feeds.AddFeed("https://git.zx2c4.com/wireguard-tools/")
feeds.AddFeed("https://git.zx2c4.com/wireguard-linux-compat/")
feeds.AddFeed("https://git.zx2c4.com/wireguard-windows/")
feeds.AddFeed("https://git.zx2c4.com/wireguard-go/")
feeds.AddFeed("https://git.zx2c4.com/wireguard-freebsd/")
feeds.AddFeed("https://git.zx2c4.com/wireguard-openbsd/")
feeds.AddFeed("https://git.zx2c4.com/wireguard-android/")
feeds.AddFeed("https://git.zx2c4.com/android-wireguard-module-builder/")
feeds.AddFeed("https://git.zx2c4.com/wireguard-apple/")
feeds.AddFeed("https://git.zx2c4.com/wireguard-rs/")
feeds.AddFeed("https://git.zx2c4.com/wintun/")
feeds.AddFeed("https://git.zx2c4.com/wg-dynamic/")
bot := hbot.NewBot(&hbot.Config{
Host: *serverArg,
Nick: *nickArg,
Realname: "Your Friendly Neighborhood WurGur Bot",
Channels: []string{*channelArg},
Logger: hbot.Logger{Verbosef: log.Printf, Errorf: log.Printf},
Password: password,
})
urlShortener := regexp.MustCompile(`(.*\?id=[a-f0-9]{8})([a-f0-9]+)$`)
go func() {
for commit := range feeds.Updates() {
<-bot.Joined()
log.Printf("New commit %s in %s", commit.Commit.ID, commit.RepoTitle)
url := commit.Commit.Link.Href
if matches := urlShortener.FindStringSubmatch(url); len(matches) == 3 {
url = matches[1]
}
bot.Msg(*channelArg, fmt.Sprintf("\x01ACTION found a new commit in \x0303%s\x0f - \x0306%s\x0f - %s\x01", commit.RepoTitle, commit.Commit.Title, url))
}
}()
for {
bot.Run()
time.Sleep(time.Second * 5)
}
}

11
go.mod Normal file
View File

@@ -0,0 +1,11 @@
module golang.zx2c4.com/irc
go 1.16
require (
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a
golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea
golang.zx2c4.com/wireguard v0.0.0-20210525143454-64cb82f2b3f5
golang.zx2c4.com/wireguard/tun/netstack v0.0.0-20210525143454-64cb82f2b3f5
inet.af/netaddr v0.0.0-20210526175434-db50905a50be
)

619
go.sum Normal file
View File

@@ -0,0 +1,619 @@
bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI=
github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0=
github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA=
github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc=
github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/Microsoft/go-winio v0.4.16-0.20201130162521-d1ffc52c7331/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0=
github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0=
github.com/Microsoft/hcsshim v0.8.14/go.mod h1:NtVKoYxQuTLx6gEq0L96c9Ju4JbRJ4nY2ow3VK6a9Lg=
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/cenkalti/backoff v1.1.1-0.20190506075156-2146c9339422/go.mod h1:b6Nc7NRH5C4aCISLry0tLnTjcuTEvoiqcWDdsU0sOGM=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/cilium/ebpf v0.0.0-20200110133405-4032b1d8aae3/go.mod h1:MA5e5Lr8slmEg9bt0VpxxWqJlO4iwu3FBdHUzV7wQVg=
github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/containerd/cgroups v0.0.0-20200531161412-0dbf7f05ba59/go.mod h1:pA0z1pT8KYB3TCXK/ocprsh7MAkoW8bZVzPdih9snmM=
github.com/containerd/cgroups v0.0.0-20201119153540-4cbc285b3327/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE=
github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw=
github.com/containerd/console v0.0.0-20191206165004-02ecf6a7291e/go.mod h1:8Pf4gM6VEbTNRIT26AyyU7hxdQU3MvAvxVI0sc00XBE=
github.com/containerd/console v1.0.1/go.mod h1:XUsP6YE/mKtz6bxc+I8UiKKTP04qjQL4qcS3XoQ5xkw=
github.com/containerd/containerd v1.3.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
github.com/containerd/containerd v1.3.9/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
github.com/containerd/continuity v0.0.0-20210208174643-50096c924a4e/go.mod h1:EXlVlkqNba9rJe3j7w3Xa924itAMLgZH4UD/Q4PExuQ=
github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI=
github.com/containerd/fifo v0.0.0-20191213151349-ff969a566b00/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0=
github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0=
github.com/containerd/go-runc v0.0.0-20200220073739-7016d3ce2328/go.mod h1:PpyHrqVs8FTi9vpyHwPwiNEGaACDxT/N/pLcvMSRA9g=
github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o=
github.com/containerd/ttrpc v1.0.2/go.mod h1:UAxOpgT9ziI0gJrmKvgcZivgxOp8iFPSk8httJEt98Y=
github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc=
github.com/containerd/typeurl v0.0.0-20200205145503-b45ef1f1f737/go.mod h1:TB1hUtrpaiO88KEK56ijojHS1+NeF0izUACaJW2mdXg=
github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=
github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v1.4.2-0.20191028175130-9e7d5ac5ea55/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.3.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA=
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0=
github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg=
github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc=
github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I=
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/flock v0.8.0/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
github.com/gogo/googleapis v1.4.0/go.mod h1:5YRNX2z1oM5gXdAkurHa942MDgEJyk02w4OecKY87+c=
github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-github/v32 v32.1.0/go.mod h1:rIEpZD9CTDQwDK9GDrtMTycQNA4JU3qBsCizh3q2WCI=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210115211752-39141e76b647/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/subcommands v1.0.2-0.20190508160503-636abe8753b8/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.4-0.20190131011033-7dc38fb350b1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mattbaird/jsonpatch v0.0.0-20171005235357-81af80346b1a/go.mod h1:M1qoD/MqPgTZIk0EWKB38wE28ACRfVcn+cU08jyArI0=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mohae/deepcopy v0.0.0-20170308212314-bb9b5e7adda9/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
github.com/opencontainers/runtime-spec v1.0.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190522114515-bc1a522cf7b1/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/vishvananda/netlink v1.0.1-0.20190930145447-2ec5bdc52b86/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk=
github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go4.org/intern v0.0.0-20210108033219-3eb7198706b2 h1:VFTf+jjIgsldaz/Mr00VaCSswHJrI2hIjQygE/W4IMg=
go4.org/intern v0.0.0-20210108033219-3eb7198706b2/go.mod h1:vLqJ+12kCw61iCWsPto0EOHhBS+o4rO5VIucbc9g2Cc=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222175341-b30ae309168e/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063 h1:1tk03FUNpulq2cuWpXZWj649rwJpk0d20rxWiopKRmc=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a h1:kr2P4QFmQr29mSLA43kwrOcgcReGTfbE9N577tCTuBc=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210423184538-5f58ad60dda6 h1:0PC75Fz/kyMGhL0e1QnypqK2kQMqKt9csD1GnMJR+Zk=
golang.org/x/net v0.0.0-20210423184538-5f58ad60dda6/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200120151820-655fe14d7479/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200916030750-2334cc1a136f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210309040221-94ec62e08169/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea h1:+WiDlPBBaO+h9vPNZi8uJ3k4BkKQB7Iow3aqwHVA5hI=
golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba h1:O8mE0/t419eoIwhTFpKVkHiTs/Igowgfkj25AcZrtiE=
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.zx2c4.com/wireguard v0.0.0-20210424170727-c9db4b7aaa22/go.mod h1:a057zjmoc00UN7gVkaJt2sXVK523kMJcogDTEvPIasg=
golang.zx2c4.com/wireguard v0.0.0-20210525143454-64cb82f2b3f5 h1:5D3v3AKu7ktIhDlqZhZ4+YeNKsW+dnc2+zfFAdhwa8M=
golang.zx2c4.com/wireguard v0.0.0-20210525143454-64cb82f2b3f5/go.mod h1:laHzsbfMhGSobUmruXWAyMKKHSqvIcrqZJMyHD+/3O8=
golang.zx2c4.com/wireguard/tun/netstack v0.0.0-20210525143454-64cb82f2b3f5 h1:8ZX3V5aSM69ogmjzY4xNIeGqGrYADLXkdW291Qk6RgQ=
golang.zx2c4.com/wireguard/tun/netstack v0.0.0-20210525143454-64cb82f2b3f5/go.mod h1:ktZCwqIAoiVqur1XJH+0oApuKBKbjGUoNZT6vwkmWM0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200117163144-32f20d992d24/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.36.0-dev.0.20210208035533-9280052d3665/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.25.1-0.20201020201750-d3470999428b/go.mod h1:hFxJC2f0epmp1elRCiEGJTKAWbwxZ2nvqZdHl3FQXCY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
gvisor.dev/gvisor v0.0.0-20210506004418-fbfeba3024f0 h1:Ny53d6x76f7EgvYe7pi8SQcIzdRM69y1ckQ8rRtT6ww=
gvisor.dev/gvisor v0.0.0-20210506004418-fbfeba3024f0/go.mod h1:ucHEMlckp+S/YzKEpwwAyGBhAh807Wxq/8Erc6gFxCE=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.1.1/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las=
inet.af/netaddr v0.0.0-20210526175434-db50905a50be h1:HcS1f/Rn3H4SfKmtr8PO79fL8fRzmK+9GktL7Edhgzw=
inet.af/netaddr v0.0.0-20210526175434-db50905a50be/go.mod h1:z0nx+Dh+7N7CC8V5ayHtHGpZpxLQZZxkIaaz6HN65Ls=
k8s.io/api v0.16.13/go.mod h1:QWu8UWSTiuQZMMeYjwLs6ILu5O74qKSJ0c+4vrchDxs=
k8s.io/apimachinery v0.16.13/go.mod h1:4HMHS3mDHtVttspuuhrJ1GGr/0S9B6iWYWZ57KnnZqQ=
k8s.io/apimachinery v0.16.14-rc.0/go.mod h1:4HMHS3mDHtVttspuuhrJ1GGr/0S9B6iWYWZ57KnnZqQ=
k8s.io/client-go v0.16.13/go.mod h1:UKvVT4cajC2iN7DCjLgT0KVY/cbY6DGdUCyRiIfws5M=
k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=
k8s.io/kube-openapi v0.0.0-20200410163147-594e756bea31/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E=
k8s.io/utils v0.0.0-20190801114015-581e00157fb1/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI=
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=

151
hbot/commands.go Normal file
View File

@@ -0,0 +1,151 @@
package hbot
import (
"bufio"
"fmt"
"strings"
"unicode/utf8"
)
// Action sends an action to 'who' (user or channel)
func (bot *Bot) Action(who, text string) {
msg := fmt.Sprintf("\u0001ACTION %s\u0001", text)
bot.Msg(who, msg)
}
// ChMode is used to change users modes in a channel
// operator = "+o" deop = "-o"
// ban = "+b"
func (bot *Bot) ChMode(user, channel, mode string) {
bot.Send("MODE " + channel + " " + mode + " " + user)
}
// Join a channel
func (bot *Bot) Join(ch string) {
bot.Send("JOIN " + ch)
}
// Msg sends a message to 'who' (user or channel)
func (bot *Bot) Msg(who, text string) {
const command = "PRIVMSG"
for _, line := range bot.splitText(text, command, who) {
bot.Send(command + " " + who + " :" + line)
}
}
// MsgMaxSize returns maximum number of bytes that fit into one message.
// Useful, for example, if you want to generate a wall of emojis that fit into one message,
// or you want to cap some output to one message
func (bot *Bot) MsgMaxSize(who string) int {
const command = "PRIVMSG"
maxSize := bot.maxMsgSize(command, who)
return maxSize
}
// Notice sends a NOTICE message to 'who' (user or channel)
func (bot *Bot) Notice(who, text string) {
const command = "NOTICE"
for _, line := range bot.splitText(text, command, who) {
bot.Send(command + " " + who + " :" + line)
}
}
// NoticeMaxSize returns maximum number of bytes that fit into one message.
// Useful, for example, if you want to generate a wall of emojis that fit into one message,
// or you want to cap some output to one message
func (bot *Bot) NoticeMaxSize(who string) int {
const command = "NOTICE"
maxSize := bot.maxMsgSize(command, who)
return maxSize
}
// Part a channel
func (bot *Bot) Part(ch, msg string) {
bot.Send("PART " + ch + " " + msg)
}
// Reply sends a message to where the message came from (user or channel)
func (bot *Bot) Reply(m *Message, text string) {
bot.Msg(replyTarget(m), text)
}
// ReplyMaxSize is just like MsgMaxSize
// but calculates message size for the reply target
func (bot *Bot) ReplyMaxSize(m *Message) int {
const command = "PRIVMSG"
maxSize := bot.maxMsgSize(command, replyTarget(m))
return maxSize
}
// Send any command to the server
func (bot *Bot) Send(command string) {
bot.outgoing <- command
}
// SetNick sets the bots nick on the irc server.
// This does not alter *Bot.Nick, so be vary of that
func (bot *Bot) SetNick(nick string) {
bot.Send(fmt.Sprintf("NICK %s", nick))
}
// Topic sets the channel 'c' topic (requires bot has proper permissions)
func (bot *Bot) Topic(c, topic string) {
str := fmt.Sprintf("TOPIC %s :%s", c, topic)
bot.Send(str)
}
func (bot *Bot) maxMsgSize(command, who string) int {
// Maximum message size that fits into 512 bytes.
// Carriage return and linefeed are not counted here as they
// are added by handleOutgoingMessages()
maxSize := 510 - len(fmt.Sprintf(":%s %s %s :", bot.Prefix(), command, who))
if _, present := bot.CapStatus(CapIdentifyMsg); present {
maxSize--
}
// https://ircv3.net/specs/extensions/multiline
if bot.config.MsgSafetyBuffer {
maxSize -= 10
}
return maxSize
}
func replyTarget(m *Message) string {
if to := m.Param(0); strings.Contains(to, "#") {
return to
}
return m.Prefix.Name
}
// Splits a given string into a string slice, in chunks ending
// either with \n, or with \r\n, or splitting text to maximally allowed size.
func (bot *Bot) splitText(text, command, who string) []string {
var ret []string
// Sanitize input
text = strings.ToValidUTF8(text, "")
maxSize := bot.maxMsgSize(command, who)
scanner := bufio.NewScanner(strings.NewReader(text))
for scanner.Scan() {
line := scanner.Text()
for len(line) > maxSize {
totalSize := 0
runeSize := 0
// utf-8 aware splitting
for _, v := range line {
runeSize = utf8.RuneLen(v)
if totalSize+runeSize > maxSize {
ret = append(ret, line[:totalSize])
line = line[totalSize:]
totalSize = runeSize
continue
}
totalSize += runeSize
}
}
ret = append(ret, line)
}
return ret
}

314
hbot/hbot.go Normal file
View File

@@ -0,0 +1,314 @@
// Package hbot is IRCv3 enabled framework for writing IRC bots
package hbot
import (
"bufio"
"crypto/tls"
"fmt"
"net"
"strconv"
"strings"
"sync"
"time"
)
// Bot implements an irc bot to be connected to a given server
type Bot struct {
config Config
con net.Conn
outgoing chan string
handlers []Handler
mu sync.Mutex
joinOnce sync.Once
closeOnce sync.Once
wg sync.WaitGroup
capHandler ircCaps
prefix Prefix
prefixMu sync.RWMutex
joined chan struct{}
}
type Logger struct {
Verbosef func(format string, args ...interface{})
Errorf func(format string, args ...interface{})
}
func DiscardLogf(format string, args ...interface{}) {}
type TLSKnob int
const (
AutoTLS TLSKnob = iota
YesTLS
NoTLS
)
type Config struct {
Host string
Nick string
Realname string
User string
Password string
Channels []string
SASL bool
MsgSafetyBuffer bool // Set it if long messages get truncated on the receiving end
Dial func(network, addr string) (net.Conn, error) // An optional custom function for connecting to the server
ThrottleDelay time.Duration // Duration to wait between sending of messages to avoid being kicked by the server for flooding (default 200ms)
PingTimeout time.Duration // Maximum time between incoming data
UseTLS TLSKnob
TLSConfig tls.Config
Logger Logger
}
// CommonBotUserPrefix may be optionally used as a user prefix or realname to identify as a bot.
// e.g. `/mode #CHANNEL +q $~x:*!~botsan-*@*` or `/mode #CHANNEL +q $~r:botsan-*`.
const CommonBotUserPrefix = "botsan-"
// NewBot creates a new instance of Bot
func NewBot(conf *Config) *Bot {
bot := Bot{config: *conf, joined: make(chan struct{})}
if bot.config.ThrottleDelay == 0 {
bot.config.ThrottleDelay = 200 * time.Millisecond
}
if bot.config.PingTimeout == 0 {
bot.config.PingTimeout = 300 * time.Second
}
if bot.config.Logger.Verbosef == nil {
bot.config.Logger.Verbosef = DiscardLogf
}
if bot.config.Logger.Errorf == nil {
bot.config.Logger.Errorf = DiscardLogf
}
if len(bot.config.Realname) == 0 {
bot.config.Realname = bot.config.Nick
}
if len(bot.config.User) == 0 {
bot.config.User = bot.config.Nick
}
if len(bot.config.Nick) > 16 {
bot.config.Nick = bot.config.Nick[:16]
}
bot.AddTrigger(pingPong)
bot.AddTrigger(joinChannels)
bot.AddTrigger(getPrefix)
bot.AddTrigger(setNick)
bot.AddTrigger(nickError)
bot.AddTrigger(&bot.capHandler)
bot.AddTrigger(saslFail)
bot.AddTrigger(saslSuccess)
bot.AddTrigger(errorLogger)
return &bot
}
// Joined returns a channel that closes after channels are joined
func (bot *Bot) Joined() <-chan struct{} {
bot.mu.Lock()
defer bot.mu.Unlock()
return bot.joined
}
// saslAuthenticate performs SASL authentication
// ref: https://github.com/atheme/charybdis/blob/master/doc/sasl.txt
func (bot *Bot) saslAuthenticate(user, pass string) {
bot.capHandler.saslEnable()
bot.capHandler.saslCreds(user, pass)
bot.config.Logger.Verbosef("Beginning sasl authentication")
bot.Send("CAP LS")
bot.sendUserCommand(bot.config.User, bot.config.Realname, "0")
bot.SetNick(bot.config.Nick)
}
// standardRegistration performs a basic set of registration commands
func (bot *Bot) standardRegistration() {
bot.Send("CAP LS")
// Server registration
if bot.config.Password != "" {
bot.Send("PASS " + bot.config.Password)
}
bot.config.Logger.Verbosef("Sending standard registration")
bot.sendUserCommand(bot.config.User, bot.config.Realname, "0")
bot.SetNick(bot.config.Nick)
}
// Set username, real name, and mode
func (bot *Bot) sendUserCommand(user, realname, mode string) {
bot.Send(fmt.Sprintf("USER %s %s * :%s", user, mode, realname))
}
func (bot *Bot) connect(host string) error {
bot.config.Logger.Verbosef("Connecting")
dial := bot.config.Dial
if dial == nil {
dial = net.Dial
}
var err error
bot.con, err = dial("tcp", host)
if err != nil {
return err
}
if bot.config.UseTLS == AutoTLS {
_, portStr, _ := net.SplitHostPort(host)
port, _ := strconv.Atoi(portStr)
switch port {
case 6667:
bot.config.UseTLS = NoTLS
case 6697:
bot.config.UseTLS = YesTLS
default:
bot.config.Logger.Errorf("Warning: port is neither the standard TCP port (6667) nor the standard TLS port (6697); falling back to TCP")
bot.config.UseTLS = NoTLS
}
}
if bot.config.UseTLS == YesTLS {
if len(bot.config.TLSConfig.ServerName) == 0 {
hostname, _, err := net.SplitHostPort(host)
if err != nil {
return err
}
bot.config.TLSConfig.ServerName = hostname
}
bot.con = tls.Client(bot.con, &bot.config.TLSConfig)
}
return nil
}
// Incoming message gathering routine
func (bot *Bot) handleIncomingMessages() {
defer bot.wg.Done()
scan := bufio.NewScanner(bot.con)
for scan.Scan() {
// Disconnect if we have seen absolutely nothing for 300 seconds
bot.con.SetDeadline(time.Now().Add(bot.config.PingTimeout))
text := scan.Text()
msg := ParseMessage(text)
go func() {
for _, h := range bot.handlers {
go h.Handle(bot, msg)
}
}()
}
bot.close("incoming", scan.Err())
}
// Handles message speed throtling
func (bot *Bot) handleOutgoingMessages() {
defer bot.wg.Done()
for s := range bot.outgoing {
_, err := fmt.Fprint(bot.con, s+"\r\n")
if err != nil {
bot.close("outgoing", err)
return
}
time.Sleep(bot.config.ThrottleDelay)
}
}
// Run starts the bot and connects to the server. Blocks until we disconnect from the server.
func (bot *Bot) Run() {
bot.mu.Lock()
bot.joinOnce = sync.Once{}
bot.closeOnce = sync.Once{}
bot.wg = sync.WaitGroup{}
bot.capHandler.reset()
bot.outgoing = make(chan string, 128)
bot.prefix = Prefix{
Name: bot.config.Nick,
User: bot.config.Nick,
Host: strings.Repeat("*", 510-353-len(bot.config.Nick)*2),
}
bot.mu.Unlock()
// Attempt reconnection
err := bot.connect(bot.config.Host)
if err != nil {
bot.config.Logger.Errorf("Connection error: %v", err)
return
}
bot.config.Logger.Verbosef("Connected")
bot.wg.Add(2)
go bot.handleIncomingMessages()
go bot.handleOutgoingMessages()
if bot.config.SASL {
bot.saslAuthenticate(bot.config.Nick, bot.config.Password)
} else {
bot.standardRegistration()
}
bot.wg.Wait()
bot.mu.Lock()
bot.joined = make(chan struct{})
bot.mu.Unlock()
bot.config.Logger.Verbosef("Disconnected")
}
// CapStatus returns whether the server capability is enabled and present
func (bot *Bot) CapStatus(cap string) (enabled, present bool) {
bot.capHandler.mu.Lock()
defer bot.capHandler.mu.Unlock()
if v, ok := bot.capHandler.capsEnabled[cap]; ok {
return v, true
}
return false, false
}
func (bot *Bot) close(fault string, err error) {
bot.closeOnce.Do(func() {
if err != nil {
bot.config.Logger.Errorf("Closing from %s: %v", fault, err)
}
bot.con.Close()
select {
case bot.outgoing <- "PING":
default:
}
})
}
// Close closes the bot
func (bot *Bot) Close() {
bot.close("", nil)
}
// Prefix returns the bot's prefix.
func (bot *Bot) Prefix() Prefix {
bot.mu.Lock()
defer bot.mu.Unlock()
return bot.prefix
}
// Nick returns the bot's nick.
func (bot *Bot) Nick() string {
bot.mu.Lock()
defer bot.mu.Unlock()
return bot.config.Nick
}
// Handler is used to subscribe and react to events on the bot Server
type Handler interface {
Handle(*Bot, *Message)
}
// Trigger is a Handler which is guarded by a condition.
// DO NOT alter *Message in your triggers or you'll have strange things happen.
type Trigger struct {
// Returns true if this trigger applies to the passed in message
Condition func(*Bot, *Message) bool
// The action to perform if Condition is true
Action func(*Bot, *Message)
}
// AddTrigger adds a trigger to the bot's handlers
func (bot *Bot) AddTrigger(h Handler) {
bot.handlers = append(bot.handlers, h)
}
// Handle executes the trigger action if the condition is satisfied
func (t Trigger) Handle(bot *Bot, m *Message) {
if t.Condition(bot, m) {
t.Action(bot, m)
}
}

112
hbot/internaltriggers.go Normal file
View File

@@ -0,0 +1,112 @@
package hbot
import (
"fmt"
"strings"
)
// A trigger to respond to the servers ping pong messages.
// If PingPong messages are not responded to, the server assumes the
// client has timed out and will close the connection.
var pingPong = Trigger{
Condition: func(bot *Bot, m *Message) bool {
return m.Command == "PING"
},
Action: func(bot *Bot, m *Message) {
bot.Send("PONG :" + m.Trailing())
},
}
var joinChannels = Trigger{
Condition: func(bot *Bot, m *Message) bool {
return m.Command == "001" || m.Command == "372"
},
Action: func(bot *Bot, m *Message) {
bot.joinOnce.Do(func() {
for _, channel := range bot.config.Channels {
splitchan := strings.SplitN(channel, ":", 2)
bot.config.Logger.Verbosef("Joining %q", channel)
if len(splitchan) == 2 {
channel = splitchan[0]
password := splitchan[1]
bot.Send(fmt.Sprintf("JOIN %s %s", channel, password))
} else {
bot.Send(fmt.Sprintf("JOIN %s", channel))
}
}
// Fire Joined
select {
case <-bot.joined:
default:
close(bot.joined)
}
})
},
}
// Get bot's prefix by catching its own join
var getPrefix = Trigger{
Condition: func(bot *Bot, m *Message) bool {
return m.Command == "JOIN" && m.Prefix.Name == bot.Nick()
},
Action: func(bot *Bot, m *Message) {
bot.mu.Lock()
bot.config.Nick = m.Prefix.Name
bot.prefix = m.Prefix
bot.mu.Unlock()
},
}
// Track nick changes internally so we can adjust the bot's prefix
var setNick = Trigger{
Condition: func(bot *Bot, m *Message) bool {
return m.Command == "NICK" && m.Prefix.Name == bot.Nick()
},
Action: func(bot *Bot, m *Message) {
to := m.Param(0)
bot.mu.Lock()
bot.config.Nick = to
bot.prefix.Name = to
bot.mu.Unlock()
bot.config.Logger.Verbosef("Nick changed to %q", to)
},
}
// Throw errors on invalid nick changes
var nickError = Trigger{
Condition: func(bot *Bot, m *Message) bool {
return m.Command == "436" || m.Command == "433" ||
m.Command == "432" || m.Command == "431" || m.Command == "400"
},
Action: func(bot *Bot, m *Message) {
bot.config.Logger.Errorf("Nick change error %q: %q", m.Command, m.Params)
},
}
var saslFail = Trigger{
Condition: func(bot *Bot, m *Message) bool {
return m.Command == "904" || m.Command == "905" ||
m.Command == "906" || m.Command == "907"
},
Action: func(bot *Bot, m *Message) {
bot.config.Logger.Errorf("SASL error %q: %q", m.Command, m.Params)
},
}
var saslSuccess = Trigger{
Condition: func(bot *Bot, m *Message) bool {
return m.Command == "900" || m.Command == "903"
},
Action: func(bot *Bot, m *Message) {
bot.config.Logger.Verbosef("SASL success: %q", m.Trailing())
},
}
var errorLogger = Trigger{
Condition: func(bot *Bot, m *Message) bool {
return m.Command == "ERROR"
},
Action: func(bot *Bot, m *Message) {
bot.config.Logger.Errorf("Server error: %q", m.Trailing())
},
}

154
hbot/irc_caps.go Normal file
View File

@@ -0,0 +1,154 @@
package hbot
import (
"bytes"
"encoding/base64"
"strings"
"sync"
)
type ircCaps struct {
saslOn bool
saslUser string
saslPass string
caps []string
capsEnabled map[string]bool
mu sync.Mutex
done bool
}
func (c *ircCaps) saslEnable() {
c.mu.Lock()
c.saslOn = true
c.mu.Unlock()
}
func (c *ircCaps) saslCreds(user, pass string) {
c.mu.Lock()
c.saslUser = user
c.saslPass = pass
c.mu.Unlock()
}
func (c *ircCaps) reset() {
c.mu.Lock()
c.saslOn = false
c.done = false
c.caps = []string{}
c.capsEnabled = make(map[string]bool)
c.mu.Unlock()
}
func (c *ircCaps) saslAuth(m *Message) bool {
return m.Command == "AUTHENTICATE" && m.Param(0) == "+"
}
func (c *ircCaps) capLS(m *Message) bool {
return m.Command == "CAP" && m.Param(1) == "LS"
}
func (c *ircCaps) capACK(m *Message) bool {
return m.Command == "CAP" && m.Param(1) == "ACK"
}
func (c *ircCaps) Handle(bot *Bot, m *Message) {
c.mu.Lock()
defer c.mu.Unlock()
if c.done {
return
}
if c.capLS(m) {
for _, cap := range strings.Split(m.Trailing(), " ") {
if _, ok := allowedCAPs[cap]; ok {
c.capsEnabled[cap] = true
c.caps = append(c.caps, cap)
} else {
c.capsEnabled[cap] = false
}
}
bot.Send("CAP REQ :" + strings.Join(c.caps, " "))
}
if c.capACK(m) {
if c.saslOn && strings.Contains(m.Trailing(), "sasl") {
bot.Send("AUTHENTICATE PLAIN")
} else {
if c.saslOn {
bot.config.Logger.Errorf("SASL is not supported, despite request")
}
bot.Send("CAP END")
c.done = true
}
}
if c.saslAuth(m) {
out := bytes.Join([][]byte{[]byte(c.saslUser), []byte(c.saslUser), []byte(c.saslPass)}, []byte{0})
encpass := base64.StdEncoding.EncodeToString(out)
bot.Send("AUTHENTICATE " + encpass)
bot.Send("CAP END")
c.done = true
}
}
// Capabilities we can deal with
// without doing crazy things in the library
var allowedCAPs = map[string]struct{}{
CapAccountNotify: {},
CapAwayNotify: {},
CapExtendedJoin: {},
CapSASL: {},
CapChghost: {},
CapInviteNotify: {},
CapMultiPrefix: {},
CapCapNotify: {},
CapSetName: {},
CapServerTime: {},
CapAccountTag: {},
CapMessageTags: {},
}
// CapAccountNotify is account-notify CAP
const CapAccountNotify = "account-notify"
// CapAwayNotify is away-notify CAP
const CapAwayNotify = "away-notify"
// CapExtendedJoin is extended-join CAP
const CapExtendedJoin = "extended-join"
// CapSASL is SASL CAP
const CapSASL = "sasl"
// CapChghost is chghost CAP
const CapChghost = "chghost"
// CapInviteNotify is invite-notify CAP
const CapInviteNotify = "invite-notify"
// CapMultiPrefix is multi-prefix CAP
const CapMultiPrefix = "multi-prefix"
// CapUserhostInNames is userhost-in-names CAP
const CapUserhostInNames = "userhost-in-names"
// CapCapNotify is cap-notify CAP
const CapCapNotify = "cap-notify"
// CapIdentifyMsg is identify-msg CAP
const CapIdentifyMsg = "identify-msg"
// CapTLS is tls CAP
const CapTLS = "tls"
// CapSetName is setname CAP
const CapSetName = "setname"
// CapServerTime is server-time CAP
const CapServerTime = "server-time"
// CapAccountTag is account-tag CAP
const CapAccountTag = "account-tag"
// CapMessageTags is message-tags CAP
const CapMessageTags = "message-tags"

420
hbot/message.go Normal file
View File

@@ -0,0 +1,420 @@
package hbot
import (
"bytes"
"strings"
)
// Various constants used for formatting IRC messages.
const (
tags byte = 0x40 // Tag start indicator
tagsEquals byte = 0x3D // Keys and values are separated using equal signs
tagsSeparator byte = 0x3B // Separator between multiple tags
prefix byte = 0x3A // Prefix or last argument
prefixUser byte = 0x21 // Username
prefixHost byte = 0x40 // Hostname
space byte = 0x20 // Separator
maxLength = 510 // Maximum length is 512 - 2 for the line endings.
)
var (
tagEscapeReplacer = strings.NewReplacer("\\:", ";", "\\s", " ", "\\r", "\r", "\\n", "\n")
)
func cutsetFunc(r rune) bool {
// Characters to trim from prefixes/messages.
return r == '\r' || r == '\n'
}
// Tags represents (optional) tags added to the start of each message
// See IRCv3.2 Message Tags (http://ircv3.net/specs/core/message-tags-3.2.html)
//
// <message> ::= ['@' <tags> <SPACE>] [':' <prefix> <SPACE> ] <command> <params> <crlf>
// <tags> ::= <tag> [';' <tag>]*
// <tag> ::= <key> ['=' <escaped value>]
// <key> ::= [ <vendor> '/' ] <sequence of letters, digits, hyphens (`-`)>
// <escaped value> ::= <sequence of any characters except NUL, CR, LF, semicolon (`;`) and SPACE>
// <vendor> ::= <host>
type Tags map[string]string
// ParseTags takes a string and attempts to create a Tags struct
func ParseTags(raw string) (t Tags) {
t = make(Tags)
tags := strings.Split(raw, string(tagsSeparator))
for _, val := range tags {
replacedVal := tagEscapeReplacer.Replace(val)
tagParts := strings.SplitN(replacedVal, string(tagsEquals), 2)
// Tag must at least contain a key
if len(tagParts) < 1 {
continue
}
// Tag only contains key, set empty value
if len(tagParts) == 1 {
t[tagParts[0]] = ""
continue
}
t[tagParts[0]] = tagParts[1]
}
return t
}
// GetTag checks whether a tag with the given key exists. The boolean return value indicates whether a value was found
func (t Tags) GetTag(key string) (string, bool) {
if t != nil {
if val, ok := t[key]; ok {
return val, true
}
}
return "", false
}
// writeTo is an utility function to write the tags list to a bytes.Buffer.
func (t Tags) writeTo(buffer *bytes.Buffer) {
buffer.WriteByte(tags)
i := 0
mapLen := len(t)
for k, v := range t {
buffer.WriteString(k)
if v != "" {
buffer.WriteByte(tagsEquals)
buffer.WriteString(v)
}
if i != mapLen-1 {
buffer.WriteByte(tagsSeparator)
}
i++
}
}
// Bytes returns the []byte representation of this collection of message tags
func (t Tags) Bytes() []byte {
if t == nil {
return nil
}
buffer := new(bytes.Buffer)
t.writeTo(buffer)
return buffer.Bytes()
}
// String returns the string representation of all set message tags
func (t Tags) String() (s string) {
return string(t.Bytes())
}
// Prefix represents the prefix (sender) of an IRC message.
// See RFC1459 section 2.3.1.
//
// <servername> | <nick> [ '!' <user> ] [ '@' <host> ]
//
type Prefix struct {
Name string // Nick- or servername
User string // Username
Host string // Hostname
}
func indexByte(s string, c byte) int {
return strings.IndexByte(s, c)
}
// ParsePrefix takes a string and attempts to create a Prefix struct.
func ParsePrefix(raw string) (p Prefix) {
user := indexByte(raw, prefixUser)
host := indexByte(raw, prefixHost)
switch {
case user > 0 && host > user:
p.Name = raw[:user]
p.User = raw[user+1 : host]
p.Host = raw[host+1:]
case user > 0:
p.Name = raw[:user]
p.User = raw[user+1:]
case host > 0:
p.Name = raw[:host]
p.Host = raw[host+1:]
default:
p.Name = raw
}
return
}
// Empty determines whether the prefix is empty.
func (p *Prefix) Empty() bool {
return len(p.User)+len(p.Host)+len(p.Name) == 0
}
// Len calculates the length of the string representation of this prefix.
func (p *Prefix) Len() (length int) {
length = len(p.Name)
if len(p.User) > 0 {
length = length + len(p.User) + 1
}
if len(p.Host) > 0 {
length = length + len(p.Host) + 1
}
return
}
// Bytes returns a []byte representation of this prefix.
func (p *Prefix) Bytes() []byte {
buffer := new(bytes.Buffer)
p.writeTo(buffer)
return buffer.Bytes()
}
// String returns a string representation of this prefix.
func (p *Prefix) String() (s string) {
// Benchmarks revealed that in this case simple string concatenation
// is actually faster than using a ByteBuffer as in (*Message).String()
s = p.Name
if len(p.User) > 0 {
s = s + string(prefixUser) + p.User
}
if len(p.Host) > 0 {
s = s + string(prefixHost) + p.Host
}
return
}
// IsHostmask returns true if this prefix looks like a user hostmask.
func (p *Prefix) IsHostmask() bool {
return len(p.User) > 0 && len(p.Host) > 0
}
// IsServer returns true if this prefix looks like a server name.
func (p *Prefix) IsServer() bool {
return len(p.User) <= 0 && len(p.Host) <= 0 // && indexByte(p.Name, '.') > 0
}
// writeTo is an utility function to write the prefix to the bytes.Buffer in Message.String().
func (p *Prefix) writeTo(buffer *bytes.Buffer) {
buffer.WriteString(p.Name)
if len(p.User) > 0 {
buffer.WriteByte(prefixUser)
buffer.WriteString(p.User)
}
if len(p.Host) > 0 {
buffer.WriteByte(prefixHost)
buffer.WriteString(p.Host)
}
return
}
// Message represents an IRC protocol message.
// See RFC1459 section 2.3.1.
//
// <message> ::= [':' <prefix> <SPACE> ] <command> <params> <crlf>
// <prefix> ::= <servername> | <nick> [ '!' <user> ] [ '@' <host> ]
// <command> ::= <letter> { <letter> } | <number> <number> <number>
// <SPACE> ::= ' ' { ' ' }
// <params> ::= <SPACE> [ ':' <trailing> | <middle> <params> ]
//
// <middle> ::= <Any *non-empty* sequence of octets not including SPACE
// or NUL or CR or LF, the first of which may not be ':'>
// <trailing> ::= <Any, possibly *empty*, sequence of octets not including
// NUL or CR or LF>
//
// <crlf> ::= CR LF
type Message struct {
Tags
Prefix
Command string
Params []string
}
// Param returns the i'th parameter.
// Returns the empty string if the requested parameter does not exist.
func (m *Message) Param(i int) string {
if i < 0 || i >= len(m.Params) {
return ""
}
return m.Params[i]
}
// Trailing returns the last parameter.
// Returns the empty string if there are no parameters.
func (m *Message) Trailing() string {
if len(m.Params) > 0 {
return m.Params[len(m.Params)-1]
}
return ""
}
// ParseMessage takes a string and attempts to create a Message struct.
// Returns nil if the Message is invalid.
func ParseMessage(raw string) (m *Message) {
// Ignore empty messages.
if raw = strings.TrimFunc(raw, cutsetFunc); len(raw) < 2 {
return nil
}
i, j, k := 0, 0, 0
m = new(Message)
if raw[k] == tags {
// Tags end with a space
k = indexByte(raw, space)
// Tags must not be empty if the indicator is present
if k < 2 {
return nil
}
m.Tags = ParseTags(raw[1:k])
// Skip space at the end of the tags
k++
}
if raw[k] == prefix {
// Prefix ends with a space.
i = k + indexByte(raw[k:], space)
// Prefix string must not be empty if the indicator is present.
if i < 2 {
return nil
}
m.Prefix = ParsePrefix(raw[k+1 : i])
// Skip space at the end of the prefix
i++
} else {
i = k
}
// Find end of command
j = i + indexByte(raw[i:], space)
// Extract command
if j > i {
m.Command = strings.ToUpper(raw[i:j])
} else {
m.Command = strings.ToUpper(raw[i:])
// We're done here!
return m
}
// Find prefix for trailer. Note that because we need to match the trailing
// argument even if it's the only one, we can't skip the space until we've
// searched for it.
i = strings.Index(raw[j:], " :")
// Skip the space
j++
if i < 0 {
// There is no trailing argument!
m.Params = strings.Split(raw[j:], string(space))
// We're done here!
return m
}
// Compensate for index on substring. Note that we skipped the space after
// looking for i, so we need to subtract 1 to account for that.
i = i + j - 1
// Check if we need to parse arguments.
if i > j {
m.Params = strings.Split(raw[j:i], string(space))
}
m.Params = append(m.Params, raw[i+2:])
return m
}
// Len calculates the length of the string representation of this message.
func (m *Message) Len() (length int) {
if !m.Prefix.Empty() {
length = m.Prefix.Len() + 2 // Include prefix and trailing space
}
length = length + len(m.Command)
if len(m.Params) > 0 {
length = length + len(m.Params)
for _, param := range m.Params {
length = length + len(param)
}
if trailing := m.Trailing(); len(trailing) < 1 || strings.Contains(trailing, " ") || trailing[0] == ':' {
// Add one for the colon in the trailing parameter
length++
}
}
return
}
// Bytes returns a []byte representation of this message.
//
// As noted in rfc2812 section 2.3, messages should not exceed 512 characters
// in length. This method forces that limit by discarding any characters
// exceeding the length limit.
func (m *Message) Bytes() []byte {
buffer := new(bytes.Buffer)
// Message tags
if m.Tags != nil {
buffer.WriteByte(tags)
m.Tags.writeTo(buffer)
buffer.WriteByte(space)
}
// Message prefix
if !m.Prefix.Empty() {
buffer.WriteByte(prefix)
m.Prefix.writeTo(buffer)
buffer.WriteByte(space)
}
// Command is required
buffer.WriteString(m.Command)
// Space separated list of arguments
if len(m.Params) > 1 {
buffer.WriteByte(space)
buffer.WriteString(strings.Join(m.Params[:len(m.Params)-1], string(space)))
}
if len(m.Params) > 0 {
buffer.WriteByte(space)
trailing := m.Trailing()
if len(trailing) < 1 || strings.Contains(trailing, " ") || trailing[0] == ':' {
buffer.WriteByte(prefix)
}
buffer.WriteString(trailing)
}
// We need the limit the buffer length.
if buffer.Len() > (maxLength) {
buffer.Truncate(maxLength)
}
return buffer.Bytes()
}
// String returns a string representation of this message.
//
// As noted in rfc2812 section 2.3, messages should not exceed 512 characters
// in length. This method forces that limit by discarding any characters
// exceeding the length limit.
func (m *Message) String() string {
return string(m.Bytes())
}

573
hbot/message_test.go Normal file
View File

@@ -0,0 +1,573 @@
package hbot
import (
"fmt"
"reflect"
"testing"
)
func ExampleParseMessage() {
message := ParseMessage("JOIN #help")
fmt.Println(message.Params[0])
// Output: #help
}
func ExampleMessage_String() {
message := &Message{
Prefix: Prefix{
Name: "sorcix",
User: "sorcix",
Host: "myhostname",
},
Command: "PRIVMSG",
Params: []string{"This is an example!"},
}
fmt.Println(message.String())
// Output: :sorcix!sorcix@myhostname PRIVMSG :This is an example!
}
var messageTests = [...]*struct {
parsed *Message
rawMessage string
rawPrefix string
hostmask bool // Is it very clear that the prefix is a hostname?
server bool // Is the prefix a servername?
}{
{
parsed: &Message{
Prefix: Prefix{
Name: "syrk",
User: "kalt",
Host: "millennium.stealth.net",
},
Command: "QUIT",
Params: []string{"Gone to have lunch"},
},
rawMessage: ":syrk!kalt@millennium.stealth.net QUIT :Gone to have lunch",
rawPrefix: "syrk!kalt@millennium.stealth.net",
hostmask: true,
},
{
parsed: &Message{
Prefix: Prefix{
Name: "Trillian",
},
Command: "SQUIT",
Params: []string{"cm22.eng.umd.edu", "Server out of control"},
},
rawMessage: ":Trillian SQUIT cm22.eng.umd.edu :Server out of control",
rawPrefix: "Trillian",
server: true,
},
{
parsed: &Message{
Prefix: Prefix{
Name: "WiZ",
User: "jto",
Host: "tolsun.oulu.fi",
},
Command: "JOIN",
Params: []string{"#Twilight_zone"},
},
rawMessage: ":WiZ!jto@tolsun.oulu.fi JOIN #Twilight_zone",
rawPrefix: "WiZ!jto@tolsun.oulu.fi",
hostmask: true,
},
{
parsed: &Message{
Prefix: Prefix{
Name: "WiZ",
User: "jto",
Host: "tolsun.oulu.fi",
},
Command: "PART",
Params: []string{"#playzone", "I lost"},
},
rawMessage: ":WiZ!jto@tolsun.oulu.fi PART #playzone :I lost",
rawPrefix: "WiZ!jto@tolsun.oulu.fi",
hostmask: true,
},
{
parsed: &Message{
Prefix: Prefix{
Name: "WiZ",
User: "jto",
Host: "tolsun.oulu.fi",
},
Command: "MODE",
Params: []string{"#eu-opers", "-l"},
},
rawMessage: ":WiZ!jto@tolsun.oulu.fi MODE #eu-opers -l",
rawPrefix: "WiZ!jto@tolsun.oulu.fi",
hostmask: true,
},
{
parsed: &Message{
Command: "MODE",
Params: []string{"&oulu", "+b", "*!*@*.edu", "+e", "*!*@*.bu.edu"},
},
rawMessage: "MODE &oulu +b *!*@*.edu +e *!*@*.bu.edu",
},
{
parsed: &Message{
Command: "PRIVMSG",
Params: []string{"#channel", "Message with :colons!"},
},
rawMessage: "PRIVMSG #channel :Message with :colons!",
},
{
parsed: &Message{
Prefix: Prefix{
Name: "irc.vives.lan",
},
Command: "251",
Params: []string{"test", "There are 2 users and 0 services on 1 servers"},
},
rawMessage: ":irc.vives.lan 251 test :There are 2 users and 0 services on 1 servers",
rawPrefix: "irc.vives.lan",
server: true,
},
{
parsed: &Message{
Prefix: Prefix{
Name: "irc.vives.lan",
},
Command: "376",
Params: []string{"test", "End of MOTD command"},
},
rawMessage: ":irc.vives.lan 376 test :End of MOTD command",
rawPrefix: "irc.vives.lan",
server: true,
},
{
parsed: &Message{
Prefix: Prefix{
Name: "irc.vives.lan",
},
Command: "250",
Params: []string{"test", "Highest connection count: 1 (1 connections received)"},
},
rawMessage: ":irc.vives.lan 250 test :Highest connection count: 1 (1 connections received)",
rawPrefix: "irc.vives.lan",
server: true,
},
{
parsed: &Message{
Prefix: Prefix{
Name: "sorcix",
User: "~sorcix",
Host: "sorcix.users.quakenet.org",
},
Command: "PRIVMSG",
Params: []string{"#viveslan", "\001ACTION is testing CTCP messages!\001"},
},
rawMessage: ":sorcix!~sorcix@sorcix.users.quakenet.org PRIVMSG #viveslan :\001ACTION is testing CTCP messages!\001",
rawPrefix: "sorcix!~sorcix@sorcix.users.quakenet.org",
hostmask: true,
},
{
parsed: &Message{
Prefix: Prefix{
Name: "sorcix",
User: "~sorcix",
Host: "sorcix.users.quakenet.org",
},
Command: "NOTICE",
Params: []string{"midnightfox", "\001PONG 1234567890\001"},
},
rawMessage: ":sorcix!~sorcix@sorcix.users.quakenet.org NOTICE midnightfox :\001PONG 1234567890\001",
rawPrefix: "sorcix!~sorcix@sorcix.users.quakenet.org",
hostmask: true,
},
{
parsed: &Message{
Prefix: Prefix{
Name: "a",
User: "b",
Host: "c",
},
Command: "QUIT",
},
rawMessage: ":a!b@c QUIT",
rawPrefix: "a!b@c",
hostmask: true,
},
{
parsed: &Message{
Prefix: Prefix{
Name: "a",
User: "b",
},
Command: "PRIVMSG",
Params: []string{"message"},
},
rawMessage: ":a!b PRIVMSG message",
rawPrefix: "a!b",
},
{
parsed: &Message{
Prefix: Prefix{
Name: "a",
Host: "c",
},
Command: "NOTICE",
Params: []string{":::Hey!"},
},
rawMessage: ":a@c NOTICE ::::Hey!",
rawPrefix: "a@c",
},
{
parsed: &Message{
Prefix: Prefix{
Name: "nick",
},
Command: "PRIVMSG",
Params: []string{"$@", "This message contains a\ttab!"},
},
rawMessage: ":nick PRIVMSG $@ :This message contains a\ttab!",
rawPrefix: "nick",
},
{
parsed: &Message{
Command: "TEST",
Params: []string{"$@", "", "param", "Trailing"},
},
rawMessage: "TEST $@ param Trailing",
},
{
rawMessage: ": PRIVMSG test :Invalid message with empty prefix.",
rawPrefix: "",
},
{
rawMessage: ": PRIVMSG test :Invalid message with space prefix",
rawPrefix: " ",
},
{
parsed: &Message{
Command: "TOPIC",
Params: []string{"#foo", ""},
},
rawMessage: "TOPIC #foo :",
rawPrefix: "",
},
{
parsed: &Message{
Prefix: Prefix{
Name: "name",
User: "user",
Host: "example.org",
},
Command: "PRIVMSG",
Params: []string{"#test", "Message with spaces at the end! "},
},
rawMessage: ":name!user@example.org PRIVMSG #test :Message with spaces at the end! ",
rawPrefix: "name!user@example.org",
hostmask: true,
},
{
parsed: &Message{
Command: "PASS",
Params: []string{"oauth:token_goes_here"},
},
rawMessage: "PASS oauth:token_goes_here",
rawPrefix: "",
},
{
parsed: &Message{
Command: "PRIVMSG",
Params: []string{"#some:channel", "http://example.com"},
},
rawMessage: "PRIVMSG #some:channel http://example.com",
rawPrefix: "",
},
}
// -----
// PREFIX
// -----
func TestPrefix_IsHostmask(t *testing.T) {
for i, test := range messageTests {
// Skip tests that have no prefix
if test.parsed == nil || test.parsed.Prefix.Empty() {
continue
}
if test.hostmask && !test.parsed.Prefix.IsHostmask() {
t.Errorf("Prefix %d should be recognized as a hostmask!", i)
}
}
}
func TestPrefix_IsServer(t *testing.T) {
for i, test := range messageTests {
// Skip tests that have no prefix
if test.parsed == nil || test.parsed.Prefix.Empty() {
continue
}
if test.server && !test.parsed.Prefix.IsServer() {
t.Errorf("Prefix %d should be recognized as a server!", i)
}
}
}
func TestPrefix_String(t *testing.T) {
var s string
for i, test := range messageTests {
// Skip tests that have no prefix
if test.parsed == nil || test.parsed.Prefix.Empty() {
continue
}
// Convert the prefix
s = test.parsed.Prefix.String()
// Result should be the same as the value in rawMessage.
if s != test.rawPrefix {
t.Errorf("Failed to stringify prefix %d:", i)
t.Logf("Output: %s", s)
t.Logf("Expected: %s", test.rawPrefix)
}
}
}
func TestPrefix_Len(t *testing.T) {
var l int
for i, test := range messageTests {
// Skip tests that have no prefix
if test.parsed == nil || test.parsed.Prefix.Empty() {
continue
}
l = test.parsed.Prefix.Len()
// Result should be the same as the value in rawMessage.
if l != len(test.rawPrefix) {
t.Errorf("Failed to calculate prefix length %d:", i)
t.Logf("Output: %d", l)
t.Logf("Expected: %d", len(test.rawPrefix))
}
}
}
func TestParsePrefix(t *testing.T) {
for i, test := range messageTests {
// Skip tests that have no prefix
if test.parsed == nil || test.parsed.Prefix.Empty() {
continue
}
// Parse the prefix
p := ParsePrefix(test.rawPrefix)
// Result struct should be the same as the value in parsed.
if p != test.parsed.Prefix {
t.Errorf("Failed to parse prefix %d:", i)
t.Logf("Output: %#v", p)
t.Logf("Expected: %#v", test.parsed.Prefix)
}
}
}
// -----
// MESSAGE
// -----
func TestMessage_String(t *testing.T) {
var s string
for i, test := range messageTests {
// Skip tests that have no valid struct
if test.parsed == nil {
continue
}
// Convert the prefix
s = test.parsed.String()
// Result should be the same as the value in rawMessage.
if s != test.rawMessage {
t.Errorf("Failed to stringify message %d:", i)
t.Logf("Output: %s", s)
t.Logf("Expected: %s", test.rawMessage)
}
}
}
func TestMessage_Len(t *testing.T) {
var l int
for i, test := range messageTests {
// Skip tests that have no valid struct
if test.parsed == nil {
continue
}
l = test.parsed.Len()
// Result should be the same as the value in rawMessage.
if l != len(test.rawMessage) {
t.Errorf("Failed to calculate message length %d:", i)
t.Logf("Output: %d", l)
t.Logf("Expected: %d", len(test.rawMessage))
}
}
}
func TestMessage_Param(t *testing.T) {
msg := &Message{
Params: []string{"foo", "bar", "baz"},
}
tests := [...]struct {
i int
value string
}{
{i: 0, value: "foo"},
{i: 2, value: "baz"},
{i: 99, value: ""},
{i: -1, value: ""},
}
for i, test := range tests {
p := msg.Param(test.i)
if p != test.value {
t.Errorf("Failed to get parameter: %d", i)
t.Logf("Output: %s", p)
t.Logf("Expected: %s", test.value)
}
}
}
func TestParseMessage(t *testing.T) {
var p *Message
for i, test := range messageTests {
// Parse the prefix
p = ParseMessage(test.rawMessage)
// Result struct should be the same as the value in parsed.
if !reflect.DeepEqual(p, test.parsed) {
t.Errorf("Failed to parse message %d:", i)
t.Logf("Output: %#v", p)
t.Logf("Expected: %#v", test.parsed)
}
}
}
// -----
// MESSAGE DECODE -> ENCODE
// -----
func TestMessageDecodeEncode(t *testing.T) {
var (
p *Message
s string
)
for i, test := range messageTests {
// Skip invalid messages
if test.parsed == nil {
continue
}
// Decode the message, then encode it again.
p = ParseMessage(test.rawMessage)
s = p.String()
// Result struct should be the same as the original.
if s != test.rawMessage {
t.Errorf("Message %d failed decode-encode sequence!", i)
}
}
}
// -----
// BENCHMARK
// -----
func BenchmarkPrefix_String_short(b *testing.B) {
b.ReportAllocs()
prefix := new(Prefix)
prefix.Name = "Namename"
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = prefix.String()
}
}
func BenchmarkPrefix_String_long(b *testing.B) {
b.ReportAllocs()
prefix := new(Prefix)
prefix.Name = "Namename"
prefix.User = "Username"
prefix.Host = "Hostname"
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = prefix.String()
}
}
func BenchmarkParsePrefix_short(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
ParsePrefix("Namename")
}
}
func BenchmarkParsePrefix_long(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
ParsePrefix("Namename!Username@Hostname")
}
}
func BenchmarkMessage_String(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = messageTests[0].parsed.String()
}
}
func BenchmarkParseMessage_short(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
ParseMessage("COMMAND arg1 :Message\r\n")
}
}
func BenchmarkParseMessage_medium(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
ParseMessage(":Namename COMMAND arg6 arg7 :Message message message\r\n")
}
}
func BenchmarkParseMessage_long(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
ParseMessage(":Namename!username@hostname COMMAND arg1 arg2 arg3 arg4 arg5 arg6 arg7 :Message message message message message\r\n")
}
}