diff --git a/irc/client_lookup_set.go b/irc/client_lookup_set.go index 6db2384d..b83990bf 100644 --- a/irc/client_lookup_set.go +++ b/irc/client_lookup_set.go @@ -7,14 +7,12 @@ package irc import ( "regexp" "strings" + "sync" "time" - "github.com/goshuirc/irc-go/ircmatch" - "github.com/oragono/oragono/irc/caps" "github.com/oragono/oragono/irc/modes" - - "sync" + "github.com/oragono/oragono/irc/utils" ) // ClientManager keeps track of clients by nick, enforcing uniqueness of casefolded nicks @@ -301,12 +299,16 @@ func (clients *ClientManager) FindAll(userhost string) (set ClientSet) { if err != nil { return set } - matcher := ircmatch.MakeMatch(userhost) + matcher, err := utils.CompileGlob(userhost, false) + if err != nil { + // not much we can do here + return + } clients.RLock() defer clients.RUnlock() for _, client := range clients.byNick { - if matcher.Match(client.NickMaskCasefolded()) { + if matcher.MatchString(client.NickMaskCasefolded()) { set.Add(client) } } @@ -330,8 +332,9 @@ type MaskInfo struct { // UserMaskSet holds a set of client masks and lets you match hostnames to them. type UserMaskSet struct { sync.RWMutex - masks map[string]MaskInfo - regexp *regexp.Regexp + serialCacheUpdateMutex sync.Mutex + masks map[string]MaskInfo + regexp *regexp.Regexp } func NewUserMaskSet() *UserMaskSet { @@ -345,6 +348,9 @@ func (set *UserMaskSet) Add(mask, creatorNickmask, creatorAccount string) (maskA return } + set.serialCacheUpdateMutex.Lock() + defer set.serialCacheUpdateMutex.Unlock() + set.Lock() if set.masks == nil { set.masks = make(map[string]MaskInfo) @@ -373,6 +379,9 @@ func (set *UserMaskSet) Remove(mask string) (maskRemoved string, err error) { return } + set.serialCacheUpdateMutex.Lock() + defer set.serialCacheUpdateMutex.Unlock() + set.Lock() _, removed := set.masks[mask] if removed { @@ -430,31 +439,14 @@ func (set *UserMaskSet) Length() int { // parts are re-joined and finally all masks are joined into a big // or-expression. func (set *UserMaskSet) setRegexp() { - var re *regexp.Regexp - set.RLock() maskExprs := make([]string, len(set.masks)) - index := 0 for mask := range set.masks { - manyParts := strings.Split(mask, "*") - manyExprs := make([]string, len(manyParts)) - for mindex, manyPart := range manyParts { - oneParts := strings.Split(manyPart, "?") - oneExprs := make([]string, len(oneParts)) - for oindex, onePart := range oneParts { - oneExprs[oindex] = regexp.QuoteMeta(onePart) - } - manyExprs[mindex] = strings.Join(oneExprs, ".") - } - maskExprs[index] = strings.Join(manyExprs, ".*") - index++ + maskExprs = append(maskExprs, mask) } set.RUnlock() - if index > 0 { - expr := "^(" + strings.Join(maskExprs, "|") + ")$" - re, _ = regexp.Compile(expr) - } + re, _ := utils.CompileMasks(maskExprs) set.Lock() set.regexp = re diff --git a/irc/config.go b/irc/config.go index 982afc3a..68d85c7c 100644 --- a/irc/config.go +++ b/irc/config.go @@ -853,7 +853,7 @@ func LoadConfig(filename string) (config *Config, err error) { } for _, glob := range config.Server.WebSockets.AllowedOrigins { - globre, err := utils.CompileGlob(glob) + globre, err := utils.CompileGlob(glob, false) if err != nil { return nil, fmt.Errorf("invalid websocket allowed-origin expression: %s", glob) } @@ -1219,7 +1219,7 @@ func compileGuestRegexp(guestFormat string, casemapping Casemapping) (standard, return } - standard, err = utils.CompileGlob(guestFormat) + standard, err = utils.CompileGlob(guestFormat, true) if err != nil { return } @@ -1235,6 +1235,6 @@ func compileGuestRegexp(guestFormat string, casemapping Casemapping) (standard, if err != nil { return } - folded, err = utils.CompileGlob(fmt.Sprintf("%s*%s", initialFolded, finalFolded)) + folded, err = utils.CompileGlob(fmt.Sprintf("%s*%s", initialFolded, finalFolded), false) return } diff --git a/irc/handlers.go b/irc/handlers.go index 64d16cde..350a9f89 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -21,7 +21,6 @@ import ( "time" "github.com/goshuirc/irc-go/ircfmt" - "github.com/goshuirc/irc-go/ircmatch" "github.com/goshuirc/irc-go/ircmsg" "github.com/oragono/oragono/irc/caps" "github.com/oragono/oragono/irc/custime" @@ -1280,10 +1279,14 @@ func klineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res return false } - matcher := ircmatch.MakeMatch(mask) + matcher, err := utils.CompileGlob(mask, false) + if err != nil { + rb.Add(nil, server.name, ERR_UNKNOWNERROR, details.nick, msg.Command, client.t("Erroneous nickname")) + return false + } for _, clientMask := range client.AllNickmasks() { - if !klineMyself && matcher.Match(clientMask) { + if !klineMyself && matcher.MatchString(clientMask) { rb.Add(nil, server.name, ERR_UNKNOWNERROR, details.nick, msg.Command, client.t("This ban matches you. To KLINE yourself, you must use the command: /KLINE MYSELF ")) return false } @@ -1327,7 +1330,7 @@ func klineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res for _, mcl := range server.clients.AllClients() { for _, clientMask := range mcl.AllNickmasks() { - if matcher.Match(clientMask) { + if matcher.MatchString(clientMask) { clientsToKill = append(clientsToKill, mcl) killedClientNicks = append(killedClientNicks, mcl.nick) } diff --git a/irc/kline.go b/irc/kline.go index 4099f482..ca669a5e 100644 --- a/irc/kline.go +++ b/irc/kline.go @@ -6,12 +6,14 @@ package irc import ( "encoding/json" "fmt" + "regexp" "strings" "sync" "time" - "github.com/goshuirc/irc-go/ircmatch" "github.com/tidwall/buntdb" + + "github.com/oragono/oragono/irc/utils" ) const ( @@ -23,7 +25,7 @@ type KLineInfo struct { // Mask that is blocked. Mask string // Matcher, to facilitate fast matching. - Matcher ircmatch.Matcher + Matcher *regexp.Regexp // Info contains information on the ban. Info IPBanInfo } @@ -80,9 +82,14 @@ func (km *KLineManager) AddMask(mask string, duration time.Duration, reason, ope } func (km *KLineManager) addMaskInternal(mask string, info IPBanInfo) { + re, err := utils.CompileGlob(mask, false) + // this is validated externally and shouldn't fail regardless + if err != nil { + return + } kln := KLineInfo{ Mask: mask, - Matcher: ircmatch.MakeMatch(mask), + Matcher: re, Info: info, } @@ -189,7 +196,7 @@ func (km *KLineManager) CheckMasks(masks ...string) (isBanned bool, info IPBanIn for _, entryInfo := range km.entries { for _, mask := range masks { - if entryInfo.Matcher.Match(mask) { + if entryInfo.Matcher.MatchString(mask) { return true, entryInfo.Info } } diff --git a/irc/utils/glob.go b/irc/utils/glob.go index 58fb7f34..891358b3 100644 --- a/irc/utils/glob.go +++ b/irc/utils/glob.go @@ -11,21 +11,53 @@ import ( // yet another glob implementation in Go -func CompileGlob(glob string) (result *regexp.Regexp, err error) { - var buf bytes.Buffer - buf.WriteByte('^') +func addRegexp(buf *bytes.Buffer, glob string, submatch bool) (err error) { for _, r := range glob { switch r { case '*': - buf.WriteString("(.*)") + if submatch { + buf.WriteString("(.*)") + } else { + buf.WriteString(".*") + } case '?': - buf.WriteString("(.)") + if submatch { + buf.WriteString("(.)") + } else { + buf.WriteString(".") + } case 0xFFFD: - return nil, &syntax.Error{Code: syntax.ErrInvalidUTF8, Expr: glob} + return &syntax.Error{Code: syntax.ErrInvalidUTF8, Expr: glob} default: buf.WriteString(regexp.QuoteMeta(string(r))) } } + return +} + +func CompileGlob(glob string, submatch bool) (result *regexp.Regexp, err error) { + var buf bytes.Buffer + buf.WriteByte('^') + err = addRegexp(&buf, glob, submatch) + if err != nil { + return + } buf.WriteByte('$') return regexp.Compile(buf.String()) } + +func CompileMasks(masks []string) (result *regexp.Regexp, err error) { + var buf bytes.Buffer + buf.WriteString("^(") + for i, mask := range masks { + err = addRegexp(&buf, mask, false) + if err != nil { + return + } + if i != len(masks)-1 { + buf.WriteByte('|') + } + } + buf.WriteString(")$") + return regexp.Compile(buf.String()) +} diff --git a/irc/utils/glob_test.go b/irc/utils/glob_test.go index 2c9dee35..5cd83af8 100644 --- a/irc/utils/glob_test.go +++ b/irc/utils/glob_test.go @@ -9,7 +9,7 @@ import ( ) func globMustCompile(glob string) *regexp.Regexp { - re, err := CompileGlob(glob) + re, err := CompileGlob(glob, false) if err != nil { panic(err) } @@ -46,3 +46,123 @@ func TestGlob(t *testing.T) { assertMatches("S*e", "Skåne", true, t) assertMatches("Sk?ne", "Skåne", true, t) } + +func BenchmarkGlob(b *testing.B) { + g := globMustCompile("https://*google.com") + b.ResetTimer() + for i := 0; i < b.N; i++ { + g.MatchString("https://www.google.com") + } +} + +func BenchmarkGlobCompilation(b *testing.B) { + for i := 0; i < b.N; i++ { + CompileGlob("https://*google.com", false) + } +} + +// these are actual bans from my production network :-/ +var bans = []string{ + "*!*@tor-network.onion", + "`!*@*", + "qanon!*@*", + "*!bibi@tor-network.onion", + "shivarm!*@*", + "8====d!*@*", + "shiviram!*@*", + "poop*!*@*", + "shivoram!*@*", + "shivvy!*@*", + "shavirim!*@*", + "shivarm_!*@*", + "_!*@*", +} + +func TestMasks(t *testing.T) { + matcher, err := CompileMasks(bans) + if err != nil { + panic(err) + } + + if !matcher.MatchString("evan!user@tor-network.onion") { + t.Errorf("match expected") + } + if !matcher.MatchString("`!evan@b9un4fv3he44q.example.com") { + t.Errorf("match expected") + } + if matcher.MatchString("horse!horse@t5dwi8vacg47y.example.com") { + t.Errorf("match not expected") + } + if matcher.MatchString("horse_!horse@t5dwi8vacg47y.example.com") { + t.Errorf("match not expected") + } + if matcher.MatchString("shivaram!shivaram@yrqgsrjy2p7my.example.com") { + t.Errorf("match not expected") + } +} + +func BenchmarkMasksCompile(b *testing.B) { + for i := 0; i < b.N; i++ { + CompileMasks(bans) + } +} + +func BenchmarkMasksMatch(b *testing.B) { + matcher, _ := CompileMasks(bans) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + matcher.MatchString("evan!user@tor-network.onion") + matcher.MatchString("horse_!horse@t5dwi8vacg47y.example.com") + matcher.MatchString("shivaram!shivaram@yrqgsrjy2p7my.example.com") + } +} + +// compare performance to compilation of the | clauses as separate regexes +// first for compilation, then for matching + +func compileAll(masks []string) (result []*regexp.Regexp, err error) { + a := make([]*regexp.Regexp, 0, len(masks)) + for _, mask := range masks { + m, err := CompileGlob(mask, false) + if err != nil { + return nil, err + } + a = append(a, m) + } + return a, nil +} + +func matchesAny(masks []*regexp.Regexp, str string) bool { + for _, r := range masks { + if r.MatchString(str) { + return true + } + } + return false +} + +func BenchmarkLinearCompile(b *testing.B) { + for i := 0; i < b.N; i++ { + compileAll(bans) + } +} + +func BenchmarkLinearMatch(b *testing.B) { + a, err := compileAll(bans) + if err != nil { + panic(err) + } + if matchesAny(a, "horse_!horse@t5dwi8vacg47y.example.com") { + panic("incorrect match") + } + if !matchesAny(a, "evan!user@tor-network.onion") { + panic("incorrect match") + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + matchesAny(a, "horse_!horse@t5dwi8vacg47y.example.com") + matchesAny(a, "evan!user@tor-network.onion") + matchesAny(a, "shivaram!shivaram@yrqgsrjy2p7my.example.com") + } +} diff --git a/vendor/github.com/goshuirc/e-nfa/.travis.yml b/vendor/github.com/goshuirc/e-nfa/.travis.yml deleted file mode 100644 index 9e4d19ec..00000000 --- a/vendor/github.com/goshuirc/e-nfa/.travis.yml +++ /dev/null @@ -1,19 +0,0 @@ -language: go - -go: - - 1.4 - - tip - -before_install: - - go get golang.org/x/tools/cmd/cover - - go get golang.org/x/tools/cmd/vet - - go get golang.org/x/tools/cmd/goimports - - go get github.com/golang/lint/golint - - go get github.com/mattn/goveralls - -script: - - go vet ./... -# - $HOME/gopath/bin/goveralls -coverprofile=coverage.cov -service=travis-ci -# - bash <(curl -s https://codecov.io/bash) - - go test -bench=. -benchmem ./... - #- sh ./install_all_cmd.sh \ No newline at end of file diff --git a/vendor/github.com/goshuirc/e-nfa/README.md b/vendor/github.com/goshuirc/e-nfa/README.md deleted file mode 100644 index cf13e416..00000000 --- a/vendor/github.com/goshuirc/e-nfa/README.md +++ /dev/null @@ -1,122 +0,0 @@ -ε-NFA: Epsilon-Nondeterministic finite automaton -============== - -[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/kkdai/e-nfa/master/LICENSE) [![GoDoc](https://godoc.org/github.com/kkdai/e-nfa?status.svg)](https://godoc.org/github.com/kkdai/e-nfa) [![Build Status](https://travis-ci.org/kkdai/e-nfa.svg?branch=master)](https://travis-ci.org/kkdai/e-nfa) - - - -![image](https://upload.wikimedia.org/wikipedia/commons/thumb/0/0e/NFAexample.svg/250px-NFAexample.svg.png) - - - -What is Epsilon-Nondeterministic finite automaton -============= - -`ε-NFA`: Epsilon-Nondeterministic finite automaton (so call:Nondeterministic finite automaton with ε-moves) - -In the automata theory, a nondeterministic finite automaton with ε-moves (NFA-ε)(also known as NFA-λ) is an extension of nondeterministic finite automaton(NFA), which allows a transformation to a new state without consuming any input symbols. The transitions without consuming an input symbol are called ε-transitions or λ-transitions. In the state diagrams, they are usually labeled with the Greek letter ε or λ. - -(sited from [here](https://en.wikipedia.org/wiki/Nondeterministic_finite_automaton)) - - -Looking for DFA implement? -============= - -I also write a DFA implenent in Go here. [https://github.com/kkdai/dfa](https://github.com/kkdai/dfa) - -Looking for NFA implement? -============= - -I also write a NFA implenent in Go here. [https://github.com/kkdai/nfa](https://github.com/kkdai/nfa) - - -Installation and Usage -============= - - -Install ---------------- - - go get github.com/kkdai/e-nfa - - - -Usage ---------------- - -Following is sample code to implement a epsilon-NFA automata diagram as follow: - -![image](image/eNFA.png) - - - -```go - -package main - -import ( - "github.com/kkdai/enfa" - "fmt" -) - -func main() { - - nfa := NewENFA(0, false) - nfa.AddState(1, false) - nfa.AddState(2, false) - nfa.AddState(3, true) - nfa.AddState(4, false) - nfa.AddState(5, false) - - nfa.AddTransition(0, "1", 1) - nfa.AddTransition(0, "0", 4) - - nfa.AddTransition(1, "1", 2) - nfa.AddTransition(1, "", 3) //epsilon - nfa.AddTransition(2, "1", 3) - nfa.AddTransition(4, "0", 5) - nfa.AddTransition(4, "", 1, 2) //E -> epsilon B C - nfa.AddTransition(5, "0", 3) - - nfa.PrintTransitionTable() - - if !nfa.VerifyInputs([]string{"1"}) { - fmt.Printf("Verify inputs is failed") - } - - nfa.Reset() - - if !nfa.VerifyInputs([]string{"1", "1", "1"}) { - fmt.Printf("Verify inputs is failed") - } - - nfa.Reset() - - if !nfa.VerifyInputs([]string{"0", "1"}) { - fmt.Printf"Verify inputs is failed") - } - - nfa.Reset() - if !nfa.VerifyInputs([]string{"0", "0", "0"}) { - fmt.Printf("Verify inputs is failed") - } -} - -``` - -Inspired By -============= - -- [ε-NFA: Wiki](https://en.wikipedia.org/wiki/Nondeterministic_finite_automaton_with_%CE%B5-moves) -- [Coursera: Automata](https://class.coursera.org/automata-004/) - -Project52 ---------------- - -It is one of my [project 52](https://github.com/kkdai/project52). - - -License ---------------- - -This package is licensed under MIT license. See LICENSE for details. diff --git a/vendor/github.com/goshuirc/e-nfa/enfa.go b/vendor/github.com/goshuirc/e-nfa/enfa.go deleted file mode 100644 index e1d92be4..00000000 --- a/vendor/github.com/goshuirc/e-nfa/enfa.go +++ /dev/null @@ -1,185 +0,0 @@ -package enfa - -import "fmt" - -type transitionInput struct { - srcState int - input string -} - -type destState map[int]bool - -type ENFA struct { - initState int - currentState map[int]bool - totalStates []int - finalStates []int - transition map[transitionInput]destState - inputMap map[string]bool -} - -//New a new NFA -func NewENFA(initState int, isFinal bool) *ENFA { - - retNFA := &ENFA{ - transition: make(map[transitionInput]destState), - inputMap: make(map[string]bool), - initState: initState} - - retNFA.currentState = make(map[int]bool) - retNFA.currentState[initState] = true - retNFA.AddState(initState, isFinal) - return retNFA -} - -//Add new state in this NFA -func (d *ENFA) AddState(state int, isFinal bool) { - if state == -1 { - fmt.Println("Cannot add state as -1, it is dead state") - return - } - - d.totalStates = append(d.totalStates, state) - if isFinal { - d.finalStates = append(d.finalStates, state) - } -} - -//Add new transition function into NFA -func (d *ENFA) AddTransition(srcState int, input string, dstStateList ...int) { - find := false - - //find input if exist in NFA input List - if _, ok := d.inputMap[input]; !ok { - //not exist, new input in this NFA - d.inputMap[input] = true - } - - for _, v := range d.totalStates { - if v == srcState { - find = true - } - } - - if !find { - fmt.Println("No such state:", srcState, " in current NFA") - return - } - - dstMap := make(map[int]bool) - for _, destState := range dstStateList { - dstMap[destState] = true - } - - targetTrans := transitionInput{srcState: srcState, input: input} - d.transition[targetTrans] = dstMap -} - -func (d *ENFA) CheckPathExist(src int, input string, dst int) bool { - retMap, _ := d.transition[transitionInput{srcState: src, input: input}] - if _, ok := retMap[dst]; ok { - return true - } - return false -} - -func (d *ENFA) Input(testInput string) []int { - updateCurrentState := make(map[int]bool) - for current, _ := range d.currentState { - for _, realTestInput := range []string{testInput, "*", "?"} { - intputTrans := transitionInput{srcState: current, input: realTestInput} - valMap, ok := d.transition[intputTrans] - if ok { - for dst, _ := range valMap { - updateCurrentState[dst] = true - - //Update epsilon input way... if exist - epsilonTrans := transitionInput{srcState: dst} - if eMap, ok := d.transition[epsilonTrans]; ok { - for eDst, _ := range eMap { - updateCurrentState[eDst] = true - } - } - } - } else { - //dead state, remove in current state - //do nothing. - } - } - } - - //update curret state - d.currentState = updateCurrentState - - //return result - var ret []int - for state, _ := range updateCurrentState { - ret = append(ret, state) - } - return ret -} - -//To verify current state if it is final state -func (d *ENFA) Verify() bool { - for _, val := range d.finalStates { - for cState, _ := range d.currentState { - if val == cState { - return true - } - } - } - return false -} - -//Reset NFA state to initilize state, but all state and transition function will remain -func (d *ENFA) Reset() { - initState := make(map[int]bool) - initState[d.initState] = true - d.currentState = initState -} - -//Verify if list of input could be accept by NFA or not -func (d *ENFA) VerifyInputs(inputs []string) bool { - for _, v := range inputs { - d.Input(v) - } - return d.Verify() -} - -//To print detail transition table contain of current NFA -func (d *ENFA) PrintTransitionTable() { - fmt.Println("===================================================") - //list all inputs - var inputList []string - for key, _ := range d.inputMap { - if key == "" { - fmt.Printf("\tε|") - } else { - fmt.Printf("\t%s|", key) - } - inputList = append(inputList, key) - } - - fmt.Printf("\n") - fmt.Println("---------------------------------------------------") - - for _, state := range d.totalStates { - fmt.Printf("%d |", state) - for _, key := range inputList { - checkInput := transitionInput{srcState: state, input: key} - if dstState, ok := d.transition[checkInput]; ok { - fmt.Printf("\t") - for val, _ := range dstState { - fmt.Printf("%d,", val) - } - fmt.Printf("|") - } else { - fmt.Printf("\tNA|") - } - } - fmt.Printf("\n") - } - - fmt.Println("---------------------------------------------------") - fmt.Println("===================================================") -} diff --git a/vendor/github.com/goshuirc/irc-go/ircmatch/doc.go b/vendor/github.com/goshuirc/irc-go/ircmatch/doc.go deleted file mode 100644 index 0561b4d0..00000000 --- a/vendor/github.com/goshuirc/irc-go/ircmatch/doc.go +++ /dev/null @@ -1,7 +0,0 @@ -// written by Daniel Oaks -// released under the ISC license - -/* -Package ircmatch handles matching IRC strings with the traditional glob-like syntax. -*/ -package ircmatch diff --git a/vendor/github.com/goshuirc/irc-go/ircmatch/ircmatch.go b/vendor/github.com/goshuirc/irc-go/ircmatch/ircmatch.go deleted file mode 100644 index 03e7cf54..00000000 --- a/vendor/github.com/goshuirc/irc-go/ircmatch/ircmatch.go +++ /dev/null @@ -1,57 +0,0 @@ -package ircmatch - -import enfa "github.com/goshuirc/e-nfa" - -// Matcher represents an object that can match IRC strings. -type Matcher struct { - internalENFA *enfa.ENFA -} - -// MakeMatch creates a Matcher. -func MakeMatch(globTemplate string) Matcher { - var newmatch Matcher - - // assemble internal enfa - newmatch.internalENFA = enfa.NewENFA(0, false) - - var currentState int - var lastWasStar bool - for _, char := range globTemplate { - if char == '*' { - if lastWasStar { - continue - } - newmatch.internalENFA.AddTransition(currentState, "*", currentState) - lastWasStar = true - continue - } else if char == '?' { - newmatch.internalENFA.AddState(currentState+1, false) - newmatch.internalENFA.AddTransition(currentState, "?", currentState+1) - currentState++ - } else { - newmatch.internalENFA.AddState(currentState+1, false) - newmatch.internalENFA.AddTransition(currentState, string(char), currentState+1) - currentState++ - } - - lastWasStar = false - } - - // create end state - newmatch.internalENFA.AddState(currentState+1, true) - newmatch.internalENFA.AddTransition(currentState, "", currentState+1) - - return newmatch -} - -// Match returns true if the given string matches this glob. -func (menfa *Matcher) Match(search string) bool { - var searchChars []string - for _, char := range search { - searchChars = append(searchChars, string(char)) - } - - isMatch := menfa.internalENFA.VerifyInputs(searchChars) - menfa.internalENFA.Reset() - return isMatch -} diff --git a/vendor/modules.txt b/vendor/modules.txt index d829c119..5a9312b3 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -17,11 +17,9 @@ github.com/go-sql-driver/mysql github.com/gorilla/websocket # github.com/goshuirc/e-nfa v0.0.0-20160917075329-7071788e3940 ## explicit -github.com/goshuirc/e-nfa # github.com/goshuirc/irc-go v0.0.0-20200311142257-57fd157327ac ## explicit github.com/goshuirc/irc-go/ircfmt -github.com/goshuirc/irc-go/ircmatch github.com/goshuirc/irc-go/ircmsg # github.com/onsi/ginkgo v1.12.0 ## explicit