diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..1ae1e5c Binary files /dev/null and b/.DS_Store differ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..53a59f6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +doall.sh +all.txt \ No newline at end of file diff --git a/examples/simple-tor.go/simple-tor.go b/examples/simple-tor.go/simple-tor.go index baeeecc..e749135 100644 --- a/examples/simple-tor.go/simple-tor.go +++ b/examples/simple-tor.go/simple-tor.go @@ -1,10 +1,11 @@ package main import ( - "github.com/thoj/go-ircevent" "crypto/tls" "log" "os" + + irc "github.com/kofany/go-ircevent" ) const addr = "libera75jm6of4wxpxt4aynol3xjmbtxgfyjpu34ss4d7r7q2v5zrpyd.onion:6697" @@ -37,7 +38,7 @@ func main() { irccon.UseTLS = true irccon.TLSConfig = &tls.Config{ InsecureSkipVerify: true, - Certificates: []tls.Certificate{clientCert}, + Certificates: []tls.Certificate{clientCert}, } irccon.AddCallback("001", func(e *irc.Event) {}) irccon.AddCallback("376", func(e *irc.Event) { diff --git a/examples/simple/simple.go b/examples/simple/simple.go index f2c27c6..7386886 100644 --- a/examples/simple/simple.go +++ b/examples/simple/simple.go @@ -1,27 +1,28 @@ package main import ( - "github.com/thoj/go-ircevent" "crypto/tls" "fmt" + + irc "github.com/kofany/go-ircevent" ) -const channel = "#go-eventirc-test"; +const channel = "#go-eventirc-test" const serverssl = "irc.freenode.net:7000" func main() { - ircnick1 := "blatiblat" - irccon := irc.IRC(ircnick1, "IRCTestSSL") - irccon.VerboseCallbackHandler = true - irccon.Debug = true - irccon.UseTLS = true - irccon.TLSConfig = &tls.Config{InsecureSkipVerify: true} - irccon.AddCallback("001", func(e *irc.Event) { irccon.Join(channel) }) - irccon.AddCallback("366", func(e *irc.Event) { }) - err := irccon.Connect(serverssl) + ircnick1 := "blatiblat" + irccon := irc.IRC(ircnick1, "IRCTestSSL") + irccon.VerboseCallbackHandler = true + irccon.Debug = true + irccon.UseTLS = true + irccon.TLSConfig = &tls.Config{InsecureSkipVerify: true} + irccon.AddCallback("001", func(e *irc.Event) { irccon.Join(channel) }) + irccon.AddCallback("366", func(e *irc.Event) {}) + err := irccon.Connect(serverssl) if err != nil { - fmt.Printf("Err %s", err ) + fmt.Printf("Err %s", err) return } - irccon.Loop() + irccon.Loop() } diff --git a/go.mod b/go.mod index e1f2f41..01b0092 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ -module github.com/thoj/go-ircevent +module github.com/kofany/go-ircevent -go 1.12 +go 1.23.2 require ( golang.org/x/net v0.0.0-20210614182718-04defd469f4e diff --git a/go.sum b/go.sum index 2dc5506..e62a462 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,5 @@ golang.org/x/net v0.0.0-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q= golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/irc.go b/irc.go index a02b2b7..23ee7f0 100644 --- a/irc.go +++ b/irc.go @@ -1,12 +1,13 @@ +// irc.go - corrected version // Copyright 2009 Thomas Jager All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. /* -This package provides an event based IRC client library. It allows to +This package provides an event-based IRC client library. It allows you to register callbacks for the events you need to handle. Its features -include handling standard CTCP, reconnecting on errors and detecting -stones servers. +include handling standard CTCP, reconnecting on errors, and detecting +stone servers. Details of the IRC protocol can be found in the following RFCs: https://tools.ietf.org/html/rfc1459 https://tools.ietf.org/html/rfc2810 @@ -36,7 +37,7 @@ import ( ) const ( - VERSION = "go-ircevent v2.1" + VERSION = "go-ircevent v2.1+myip" ) const CAP_TIMEOUT = time.Second * 15 @@ -65,7 +66,7 @@ func (irc *Connection) readLoop() { msg, err := br.ReadString('\n') - // We got past our blocking read, so bin timeout + // We got past our blocking read, so clear timeout if irc.socket != nil { var zero time.Time irc.socket.SetReadDeadline(zero) @@ -103,13 +104,13 @@ func unescapeTagValue(value string) string { return value } -//Parse raw irc messages +// Parse raw IRC messages func parseToEvent(msg string) (*Event, error) { - msg = strings.TrimSuffix(msg, "\n") //Remove \r\n + msg = strings.TrimSuffix(msg, "\n") // Remove \r\n msg = strings.TrimSuffix(msg, "\r") event := &Event{Raw: msg} if len(msg) < 5 { - return nil, errors.New("Malformed msg from server") + return nil, errors.New("malformed msg from server") } if msg[0] == '@' { @@ -125,25 +126,24 @@ func parseToEvent(msg string) (*Event, error) { event.Tags[parts[0]] = unescapeTagValue(parts[1]) } } - msg = msg[i+1 : len(msg)] + msg = msg[i+1:] } else { - return nil, errors.New("Malformed msg from server") + return nil, errors.New("malformed msg from server") } } if msg[0] == ':' { if i := strings.Index(msg, " "); i > -1 { event.Source = msg[1:i] - msg = msg[i+1 : len(msg)] - + msg = msg[i+1:] } else { - return nil, errors.New("Malformed msg from server") + return nil, errors.New("malformed msg from server") } if i, j := strings.Index(event.Source, "!"), strings.Index(event.Source, "@"); i > -1 && j > -1 && i < j { event.Nick = event.Source[0:i] event.User = event.Source[i+1 : j] - event.Host = event.Source[j+1 : len(event.Source)] + event.Host = event.Source[j+1:] } } @@ -176,12 +176,12 @@ func (irc *Connection) writeLoop() { irc.Log.Printf("--> %s\n", strings.TrimSpace(b)) } - // Set a write deadline based on the time out + // Set a write deadline based on the timeout irc.socket.SetWriteDeadline(time.Now().Add(irc.Timeout)) _, err := w.Write([]byte(b)) - // Past blocking write, bin timeout + // Clear the write deadline var zero time.Time irc.socket.SetWriteDeadline(zero) @@ -202,16 +202,16 @@ func (irc *Connection) pingLoop() { for { select { case <-ticker.C: - //Ping if we haven't received anything from the server within the keep alive period + // Ping if we haven't received anything from the server within the keep-alive period irc.lastMessageMutex.Lock() if time.Since(irc.lastMessage) >= irc.KeepAlive { irc.SendRawf("PING %d", time.Now().UnixNano()) } irc.lastMessageMutex.Unlock() case <-ticker2.C: - //Ping at the ping frequency + // Ping at the ping frequency irc.SendRawf("PING %d", time.Now().UnixNano()) - //Try to recapture nickname if it's not as configured. + // Try to recapture nickname if it's not as configured. irc.Lock() if irc.nick != irc.nickcurrent { irc.nickcurrent = irc.nick @@ -288,7 +288,7 @@ func (irc *Connection) Notice(target, message string) { irc.pwrite <- fmt.Sprintf("NOTICE %s :%s\r\n", target, message) } -// Send a formated notification to a nickname. +// Send a formatted notification to a nickname. // RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.4.2 func (irc *Connection) Noticef(target, format string, a ...interface{}) { irc.Notice(target, fmt.Sprintf(format, a...)) @@ -311,7 +311,7 @@ func (irc *Connection) Privmsg(target, message string) { irc.pwrite <- fmt.Sprintf("PRIVMSG %s :%s\r\n", target, message) } -// Send formated string to specified target (channel or nickname). +// Send formatted string to specified target (channel or nickname). func (irc *Connection) Privmsgf(target, format string, a ...interface{}) { irc.Privmsg(target, fmt.Sprintf(format, a...)) } @@ -344,7 +344,7 @@ func (irc *Connection) SendRaw(message string) { irc.pwrite <- message + "\r\n" } -// Send raw formated string. +// Send raw formatted string. func (irc *Connection) SendRawf(format string, a ...interface{}) { irc.SendRaw(fmt.Sprintf(format, a...)) } @@ -397,6 +397,7 @@ func (irc *Connection) Connected() bool { // stops all goroutines and then closes the socket. func (irc *Connection) Disconnect() { irc.Lock() + irc.fullyConnected = false defer irc.Unlock() if irc.end != nil { @@ -419,6 +420,9 @@ func (irc *Connection) Disconnect() { // Reconnect to a server using the current connection. func (irc *Connection) Reconnect() error { + irc.Lock() + irc.fullyConnected = false + irc.Unlock() irc.end = make(chan struct{}) return irc.Connect(irc.Server) } @@ -428,10 +432,10 @@ func (irc *Connection) Reconnect() error { // RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.1 func (irc *Connection) Connect(server string) error { irc.Server = server - // mark Server as stopped since there can be an error during connect + // Mark Server as stopped since there can be an error during connect irc.stopped = true - // make sure everything is ready for connection + // Make sure everything is ready for connection if len(irc.Server) == 0 { return errors.New("empty 'server'") } @@ -462,7 +466,19 @@ func (irc *Connection) Connect(server string) error { return errors.New("empty 'user'") } - dialer := proxy.FromEnvironmentUsing(&net.Dialer{Timeout: irc.Timeout}) + var localAddr net.Addr + if irc.localIP != "" { + localAddr = &net.TCPAddr{ + IP: net.ParseIP(irc.localIP), + Port: 0, + } + } + + dialer := proxy.FromEnvironmentUsing(&net.Dialer{ + LocalAddr: localAddr, + Timeout: irc.Timeout, + }) + irc.socket, err = dialer.Dial("tcp", irc.Server) if err != nil { return err @@ -596,7 +612,7 @@ func (irc *Connection) negotiateCaps() error { remaining_caps-- } - irc.pwrite <- fmt.Sprintf("CAP END\r\n") + irc.pwrite <- "CAP END\r\n" return nil } @@ -605,7 +621,7 @@ func (irc *Connection) negotiateCaps() error { // The nickname is later used to address the user. Returns nil if nick // or user are empty. func IRC(nick, user string) *Connection { - // catch invalid values + // Catch invalid values if len(nick) == 0 { return nil } @@ -614,18 +630,28 @@ func IRC(nick, user string) *Connection { } irc := &Connection{ - nick: nick, - nickcurrent: nick, - user: user, - Log: log.New(os.Stdout, "", log.LstdFlags), - end: make(chan struct{}), - Version: VERSION, - KeepAlive: 4 * time.Minute, - Timeout: 1 * time.Minute, - PingFreq: 15 * time.Minute, - SASLMech: "PLAIN", - QuitMessage: "", + nick: nick, + nickcurrent: nick, + user: user, + Log: log.New(os.Stdout, "", log.LstdFlags), + end: make(chan struct{}), + Version: VERSION, + KeepAlive: 4 * time.Minute, + Timeout: 1 * time.Minute, + PingFreq: 15 * time.Minute, + SASLMech: "PLAIN", + QuitMessage: "", + fullyConnected: false, // Initialize to false + DCCManager: NewDCCManager(), // DCC chat support } irc.setupCallbacks() return irc } + +// SetLocalIP sets the local IP address to bind when connecting. +// This allows the client to specify which local interface/IP to use. +func (irc *Connection) SetLocalIP(ip string) { + irc.localIP = ip +} + +// DCC diff --git a/irc_callback.go b/irc_callback.go index c19c211..c7cd293 100644 --- a/irc_callback.go +++ b/irc_callback.go @@ -1,7 +1,14 @@ +// irc_callback.go - corrected version +// Copyright 2009 Thomas Jager +// All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + package irc import ( "context" + "net" "reflect" "runtime" "strconv" @@ -9,95 +16,97 @@ import ( "time" ) -// Tuple type for uniquely identifying callbacks +// CallbackID is a tuple type for uniquely identifying callbacks. type CallbackID struct { EventCode string ID int } -// Register a callback to a connection and event code. A callback is a function -// which takes only an Event pointer as parameter. Valid event codes are all -// IRC/CTCP commands and error/response codes. To register a callback for all -// events pass "*" as the event code. This function returns the ID of the -// registered callback for later management. +// AddCallback registers a callback to a connection and event code. +// A callback is a function which takes only an Event pointer as a parameter. +// Valid event codes are all IRC/CTCP commands and error/response codes. +// To register a callback for all events, pass "*" as the event code. +// This function returns the ID of the registered callback for later management. func (irc *Connection) AddCallback(eventcode string, callback func(*Event)) int { eventcode = strings.ToUpper(eventcode) irc.eventsMutex.Lock() - _, ok := irc.events[eventcode] - if !ok { + defer irc.eventsMutex.Unlock() + + if irc.events == nil { + irc.events = make(map[string]map[int]func(*Event)) + } + + if _, ok := irc.events[eventcode]; !ok { irc.events[eventcode] = make(map[int]func(*Event)) } id := irc.idCounter irc.idCounter++ irc.events[eventcode][id] = callback - irc.eventsMutex.Unlock() return id } -// Remove callback i (ID) from the given event code. This functions returns -// true upon success, false if any error occurs. +// RemoveCallback removes callback i (ID) from the given event code. +// This function returns true upon success, false if any error occurs. func (irc *Connection) RemoveCallback(eventcode string, i int) bool { eventcode = strings.ToUpper(eventcode) irc.eventsMutex.Lock() - event, ok := irc.events[eventcode] - if ok { + defer irc.eventsMutex.Unlock() + + if event, ok := irc.events[eventcode]; ok { if _, ok := event[i]; ok { - delete(irc.events[eventcode], i) - irc.eventsMutex.Unlock() + delete(event, i) return true } irc.Log.Printf("Event found, but no callback found at id %d\n", i) - irc.eventsMutex.Unlock() return false } - irc.eventsMutex.Unlock() irc.Log.Println("Event not found") return false } -// Remove all callbacks from a given event code. It returns true -// if given event code is found and cleared. +// ClearCallback removes all callbacks from a given event code. +// It returns true if the given event code is found and cleared. func (irc *Connection) ClearCallback(eventcode string) bool { eventcode = strings.ToUpper(eventcode) irc.eventsMutex.Lock() - _, ok := irc.events[eventcode] - if ok { + defer irc.eventsMutex.Unlock() + + if _, ok := irc.events[eventcode]; ok { irc.events[eventcode] = make(map[int]func(*Event)) - irc.eventsMutex.Unlock() return true } - irc.eventsMutex.Unlock() irc.Log.Println("Event not found") return false } -// Replace callback i (ID) associated with a given event code with a new callback function. +// ReplaceCallback replaces callback i (ID) associated with a given event code with a new callback function. func (irc *Connection) ReplaceCallback(eventcode string, i int, callback func(*Event)) { eventcode = strings.ToUpper(eventcode) irc.eventsMutex.Lock() - event, ok := irc.events[eventcode] - irc.eventsMutex.Unlock() - if ok { + defer irc.eventsMutex.Unlock() + + if event, ok := irc.events[eventcode]; ok { if _, ok := event[i]; ok { event[i] = callback return } irc.Log.Printf("Event found, but no callback found at id %d\n", i) + return } - irc.Log.Printf("Event not found. Use AddCallBack\n") + irc.Log.Printf("Event not found. Use AddCallback\n") } -// Execute all callbacks associated with a given event. +// RunCallbacks executes all callbacks associated with a given event. func (irc *Connection) RunCallbacks(event *Event) { msg := event.Message() if event.Code == "PRIVMSG" && len(msg) > 2 && msg[0] == '\x01' { - event.Code = "CTCP" //Unknown CTCP + event.Code = "CTCP" // Unknown CTCP if i := strings.LastIndex(msg, "\x01"); i > 0 { msg = msg[1:i] @@ -106,22 +115,18 @@ func (irc *Connection) RunCallbacks(event *Event) { return } - if msg == "VERSION" { + switch { + case msg == "VERSION": event.Code = "CTCP_VERSION" - - } else if msg == "TIME" { + case msg == "TIME": event.Code = "CTCP_TIME" - - } else if strings.HasPrefix(msg, "PING") { + case strings.HasPrefix(msg, "PING"): event.Code = "CTCP_PING" - - } else if msg == "USERINFO" { + case msg == "USERINFO": event.Code = "CTCP_USERINFO" - - } else if msg == "CLIENTINFO" { + case msg == "CLIENTINFO": event.Code = "CTCP_CLIENTINFO" - - } else if strings.HasPrefix(msg, "ACTION") { + case strings.HasPrefix(msg, "ACTION"): event.Code = "CTCP_ACTION" if len(msg) > 6 { msg = msg[7:] @@ -135,19 +140,14 @@ func (irc *Connection) RunCallbacks(event *Event) { irc.eventsMutex.Lock() callbacks := make(map[int]func(*Event)) - eventCallbacks, ok := irc.events[event.Code] - id := 0 - if ok { - for _, callback := range eventCallbacks { + if eventCallbacks, ok := irc.events[event.Code]; ok { + for id, callback := range eventCallbacks { callbacks[id] = callback - id++ } } - allCallbacks, ok := irc.events["*"] - if ok { - for _, callback := range allCallbacks { + if allCallbacks, ok := irc.events["*"]; ok { + for id, callback := range allCallbacks { callbacks[id] = callback - id++ } } irc.eventsMutex.Unlock() @@ -158,7 +158,9 @@ func (irc *Connection) RunCallbacks(event *Event) { event.Ctx = context.Background() if irc.CallbackTimeout != 0 { - event.Ctx, _ = context.WithTimeout(event.Ctx, irc.CallbackTimeout) + var cancel context.CancelFunc + event.Ctx, cancel = context.WithTimeout(event.Ctx, irc.CallbackTimeout) + defer cancel() } done := make(chan int) @@ -168,7 +170,7 @@ func (irc *Connection) RunCallbacks(event *Event) { cb(event) select { case done <- id: - case <-event.Ctx.Done(): // If we timed out, report how long until we eventually finished + case <-event.Ctx.Done(): irc.Log.Printf("Canceled callback %s finished in %s >> %#v\n", getFunctionName(cb), time.Since(start), @@ -182,9 +184,9 @@ func (irc *Connection) RunCallbacks(event *Event) { select { case jobID := <-done: delete(callbacks, jobID) - case <-event.Ctx.Done(): // context timed out! + case <-event.Ctx.Done(): timedOutCallbacks := []string{} - for _, cb := range callbacks { // Everything left here did not finish + for _, cb := range callbacks { timedOutCallbacks = append(timedOutCallbacks, getFunctionName(cb)) } irc.Log.Printf("Timeout while waiting for %d callback(s) to finish (%s)\n", @@ -200,67 +202,117 @@ func getFunctionName(f func(*Event)) string { return runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name() } -// Set up some initial callbacks to handle the IRC/CTCP protocol. +// setupCallbacks sets up some initial callbacks to handle the IRC/CTCP protocol. func (irc *Connection) setupCallbacks() { irc.events = make(map[string]map[int]func(*Event)) - //Handle ping events - irc.AddCallback("PING", func(e *Event) { irc.SendRaw("PONG :" + e.Message()) }) + // Handle PING events + irc.AddCallback("PING", func(e *Event) { + irc.SendRaw("PONG :" + e.Message()) + }) - //Version handler + // Version handler irc.AddCallback("CTCP_VERSION", func(e *Event) { irc.SendRawf("NOTICE %s :\x01VERSION %s\x01", e.Nick, irc.Version) }) + // Userinfo handler irc.AddCallback("CTCP_USERINFO", func(e *Event) { irc.SendRawf("NOTICE %s :\x01USERINFO %s\x01", e.Nick, irc.user) }) + // Clientinfo handler irc.AddCallback("CTCP_CLIENTINFO", func(e *Event) { irc.SendRawf("NOTICE %s :\x01CLIENTINFO PING VERSION TIME USERINFO CLIENTINFO\x01", e.Nick) }) + // Time handler irc.AddCallback("CTCP_TIME", func(e *Event) { ltime := time.Now() irc.SendRawf("NOTICE %s :\x01TIME %s\x01", e.Nick, ltime.String()) }) - irc.AddCallback("CTCP_PING", func(e *Event) { irc.SendRawf("NOTICE %s :\x01%s\x01", e.Nick, e.Message()) }) + // Ping handler + irc.AddCallback("CTCP_PING", func(e *Event) { + irc.SendRawf("NOTICE %s :\x01%s\x01", e.Nick, e.Message()) + }) - // 437: ERR_UNAVAILRESOURCE " :Nick/channel is temporarily unavailable" - // Add a _ to current nick. If irc.nickcurrent is empty this cannot - // work. It has to be set somewhere first in case the nick is already - // taken or unavailable from the beginning. + // Handle nickname in use (433) + irc.AddCallback("433", func(e *Event) { + irc.Lock() + defer irc.Unlock() + if !irc.fullyConnected { + if irc.nickcurrent == "" { + irc.nickcurrent = irc.nick + } + irc.modifyNick() + irc.SendRawf("NICK %s", irc.nickcurrent) + } + }) + + // Handle unavailable resource (437) irc.AddCallback("437", func(e *Event) { - // If irc.nickcurrent hasn't been set yet, set to irc.nick - if irc.nickcurrent == "" { - irc.nickcurrent = irc.nick + irc.Lock() + defer irc.Unlock() + if !irc.fullyConnected { + if irc.nickcurrent == "" { + irc.nickcurrent = irc.nick + } + irc.modifyNick() + irc.SendRawf("NICK %s", irc.nickcurrent) } + }) - if len(irc.nickcurrent) > 8 { - irc.nickcurrent = "_" + irc.nickcurrent - } else { - irc.nickcurrent = irc.nickcurrent + "_" + // Handle no nickname given (431) + irc.AddCallback("431", func(e *Event) { + irc.Lock() + defer irc.Unlock() + if !irc.fullyConnected { + if irc.nickcurrent == "" { + irc.nickcurrent = irc.nick + } + irc.modifyNick() + irc.SendRawf("NICK %s", irc.nickcurrent) } - irc.SendRawf("NICK %s", irc.nickcurrent) }) - // 433: ERR_NICKNAMEINUSE " :Nickname is already in use" - // Add a _ to current nick. - irc.AddCallback("433", func(e *Event) { - // If irc.nickcurrent hasn't been set yet, set to irc.nick - if irc.nickcurrent == "" { - irc.nickcurrent = irc.nick + // Handle erroneous nickname (432) + irc.AddCallback("432", func(e *Event) { + irc.Lock() + defer irc.Unlock() + if !irc.fullyConnected { + if irc.nickcurrent == "" { + irc.nickcurrent = irc.nick + } + // Add prefix 'Err' to try a different nickname + irc.nickcurrent = "Err" + irc.nickcurrent + irc.SendRawf("NICK %s", irc.nickcurrent) } + }) - if len(irc.nickcurrent) > 8 { - irc.nickcurrent = "_" + irc.nickcurrent - } else { - irc.nickcurrent = irc.nickcurrent + "_" + // Handle nickname collision (436) + irc.AddCallback("436", func(e *Event) { + irc.Lock() + defer irc.Unlock() + if !irc.fullyConnected { + if irc.nickcurrent == "" { + irc.nickcurrent = irc.nick + } + irc.modifyNick() + irc.SendRawf("NICK %s", irc.nickcurrent) } - irc.SendRawf("NICK %s", irc.nickcurrent) }) + // Handle restricted nickname (484) + irc.AddCallback("484", func(e *Event) { + irc.Lock() + defer irc.Unlock() + if !irc.fullyConnected { + // Keep the current nickname and do not attempt to change it further + } + }) + + // Handle PONG responses irc.AddCallback("PONG", func(e *Event) { ns, _ := strconv.ParseInt(e.Message(), 10, 64) delta := time.Duration(time.Now().UnixNano() - ns) @@ -269,19 +321,44 @@ func (irc *Connection) setupCallbacks() { } }) - // NICK Define a nickname. - // Set irc.nickcurrent to the new nick actually used in this connection. + // Handle NICK changes irc.AddCallback("NICK", func(e *Event) { if e.Nick == irc.nick { irc.nickcurrent = e.Message() } }) - // 1: RPL_WELCOME "Welcome to the Internet Relay Network !@" - // Set irc.nickcurrent to the actually used nick in this connection. + // Set fullyConnected to true on successful connection (001) irc.AddCallback("001", func(e *Event) { irc.Lock() irc.nickcurrent = e.Arguments[0] + irc.fullyConnected = true irc.Unlock() }) + // DCC Chat support + irc.addDCCChatCallback() + +} + +// modifyNick modifies the current nickname to try a different one. +func (irc *Connection) modifyNick() { + if len(irc.nickcurrent) > 8 { + irc.nickcurrent = "_" + irc.nickcurrent + } else { + irc.nickcurrent = irc.nickcurrent + "_" + } +} + +// DCC chat support +func (irc *Connection) addDCCChatCallback() { + irc.AddCallback("CTCP_DCC", func(e *Event) { + if len(e.Arguments) < 5 || e.Arguments[1] != "CHAT" { + return + } + nick := e.Nick + ip := net.ParseIP(e.Arguments[3]) + port, _ := strconv.Atoi(e.Arguments[4]) + + go irc.handleIncomingDCCChat(nick, ip, port) + }) } diff --git a/irc_dcc.go b/irc_dcc.go new file mode 100644 index 0000000..56140c0 --- /dev/null +++ b/irc_dcc.go @@ -0,0 +1,242 @@ +package irc + +import ( + "bufio" + "encoding/binary" + "fmt" + "net" + "sync" + "time" +) + +// DCCChat reprezentuje pojedyncze połączenie DCC CHAT +type DCCChat struct { + Nick string + Conn net.Conn + Incoming chan string + Outgoing chan string + mutex sync.Mutex +} + +// DCCManager zarządza wszystkimi połączeniami DCC +type DCCManager struct { + chats map[string]*DCCChat + mutex sync.Mutex +} + +// NewDCCManager tworzy nowy menedżer DCC +func NewDCCManager() *DCCManager { + return &DCCManager{ + chats: make(map[string]*DCCChat), + } +} + +func (irc *Connection) handleIncomingDCCChat(nick string, ip net.IP, port int) { + addr := fmt.Sprintf("%s:%d", ip.String(), port) + conn, err := net.Dial("tcp", addr) + if err != nil { + irc.Log.Printf("Error connecting to DCC CHAT from %s: %v", nick, err) + return + } + + chat := &DCCChat{ + Nick: nick, + Conn: conn, + Incoming: make(chan string, 100), + Outgoing: make(chan string, 100), + } + + irc.DCCManager.mutex.Lock() + irc.DCCManager.chats[nick] = chat + irc.DCCManager.mutex.Unlock() + + go irc.handleDCCChatConnection(chat) +} + +func (irc *Connection) handleDCCChatConnection(chat *DCCChat) { + defer chat.Conn.Close() + defer func() { + irc.DCCManager.mutex.Lock() + delete(irc.DCCManager.chats, chat.Nick) + irc.DCCManager.mutex.Unlock() + }() + + readDone := make(chan struct{}) + writeDone := make(chan struct{}) + + go func() { + irc.readDCCChat(chat) + close(readDone) + }() + + go func() { + irc.writeDCCChat(chat) + close(writeDone) + }() + + select { + case <-readDone: + irc.Log.Printf("DCC CHAT read routine finished for %s", chat.Nick) + case <-writeDone: + irc.Log.Printf("DCC CHAT write routine finished for %s", chat.Nick) + } + + irc.Log.Printf("DCC CHAT connection closed with %s", chat.Nick) +} + +func (irc *Connection) readDCCChat(chat *DCCChat) { + scanner := bufio.NewScanner(chat.Conn) + for scanner.Scan() { + chat.Incoming <- scanner.Text() + } + close(chat.Incoming) +} + +func (irc *Connection) writeDCCChat(chat *DCCChat) { + for msg := range chat.Outgoing { + _, err := fmt.Fprintf(chat.Conn, "%s\r\n", msg) + if err != nil { + irc.Log.Printf("Error writing to DCC CHAT with %s: %v", chat.Nick, err) + break + } + } + close(chat.Outgoing) +} +func (irc *Connection) InitiateDCCChat(target string) error { + listener, err := net.Listen("tcp", ":0") + if err != nil { + return fmt.Errorf("error creating listener for DCC CHAT: %v", err) + } + + port := listener.Addr().(*net.TCPAddr).Port + ip := irc.getLocalIP() + + irc.SendRawf("PRIVMSG %s :\001DCC CHAT chat %d %d\001", target, ip2int(ip), port) + + go func() { + conn, err := listener.Accept() + if err != nil { + irc.Log.Printf("Error accepting DCC CHAT connection: %v", err) + return + } + listener.Close() + + chat := &DCCChat{ + Nick: target, + Conn: conn, + Incoming: make(chan string, 100), + Outgoing: make(chan string, 100), + } + + irc.DCCManager.mutex.Lock() + irc.DCCManager.chats[target] = chat + irc.DCCManager.mutex.Unlock() + + go irc.handleDCCChatConnection(chat) + }() + + return nil +} + +func (irc *Connection) getLocalIP() net.IP { + conn, err := net.Dial("udp", "8.8.8.8:80") + if err != nil { + return net.ParseIP("127.0.0.1") + } + defer conn.Close() + + localAddr := conn.LocalAddr().(*net.UDPAddr) + return localAddr.IP +} + +func ip2int(ip net.IP) uint32 { + if len(ip) == 16 { + return binary.BigEndian.Uint32(ip[12:16]) + } + return binary.BigEndian.Uint32(ip) +} +func (irc *Connection) SendDCCMessage(nick, message string) error { + irc.DCCManager.mutex.Lock() + chat, exists := irc.DCCManager.chats[nick] + irc.DCCManager.mutex.Unlock() + + if !exists { + return fmt.Errorf("no active DCC chat with %s", nick) + } + + select { + case chat.Outgoing <- message: + return nil + default: + return fmt.Errorf("failed to send message to %s: channel full", nick) + } +} + +func (irc *Connection) GetDCCMessage(nick string) (string, error) { + irc.DCCManager.mutex.Lock() + chat, exists := irc.DCCManager.chats[nick] + irc.DCCManager.mutex.Unlock() + + if !exists { + return "", fmt.Errorf("no active DCC chat with %s", nick) + } + + select { + case msg, ok := <-chat.Incoming: + if !ok { + return "", fmt.Errorf("DCC chat with %s closed", nick) + } + return msg, nil + default: + return "", fmt.Errorf("no message available from %s", nick) + } +} + +// Dodaj te metody do pliku irc_dcc.go + +// CloseDCCChat zamyka połączenie DCC CHAT z określonym nickiem +func (irc *Connection) CloseDCCChat(nick string) error { + irc.DCCManager.mutex.Lock() + defer irc.DCCManager.mutex.Unlock() + + chat, exists := irc.DCCManager.chats[nick] + if !exists { + return fmt.Errorf("no active DCC chat with %s", nick) + } + + close(chat.Outgoing) + chat.Conn.Close() + delete(irc.DCCManager.chats, nick) + return nil +} + +// ListActiveDCCChats zwraca listę nicków, z którymi mamy aktywne połączenia DCC CHAT +func (irc *Connection) ListActiveDCCChats() []string { + irc.DCCManager.mutex.Lock() + defer irc.DCCManager.mutex.Unlock() + + var activeChats []string + for nick := range irc.DCCManager.chats { + activeChats = append(activeChats, nick) + } + return activeChats +} + +// IsDCCChatActive sprawdza, czy istnieje aktywne połączenie DCC CHAT z danym nickiem +func (irc *Connection) IsDCCChatActive(nick string) bool { + irc.DCCManager.mutex.Lock() + defer irc.DCCManager.mutex.Unlock() + + _, exists := irc.DCCManager.chats[nick] + return exists +} + +// SetDCCChatTimeout ustawia timeout dla połączeń DCC CHAT +func (irc *Connection) SetDCCChatTimeout(timeout time.Duration) { + irc.DCCManager.mutex.Lock() + defer irc.DCCManager.mutex.Unlock() + + for _, chat := range irc.DCCManager.chats { + chat.Conn.SetDeadline(time.Now().Add(timeout)) + } +} diff --git a/irc_sasl_test.go b/irc_sasl_test.go index 4020812..f5f7efa 100644 --- a/irc_sasl_test.go +++ b/irc_sasl_test.go @@ -42,11 +42,10 @@ func TestConnectionSASL(t *testing.T) { irccon.Loop() } - -// 1. Register fingerprint with IRC network -// 2. Add SASLKeyPem="-----BEGIN PRIVATE KEY-----..." -// and SASLCertPem="-----BEGIN CERTIFICATE-----..." -// to CI environment as masked variables +// 1. Register fingerprint with IRC network +// 2. Add SASLKeyPem="-----BEGIN PRIVATE KEY-----..." +// and SASLCertPem="-----BEGIN CERTIFICATE-----..." +// to CI environment as masked variables func TestConnectionSASLExternal(t *testing.T) { SASLServer := "irc.freenode.net:7000" keyPem := os.Getenv("SASLKeyPem") @@ -71,7 +70,7 @@ func TestConnectionSASLExternal(t *testing.T) { irccon.SASLMech = "EXTERNAL" irccon.TLSConfig = &tls.Config{ InsecureSkipVerify: true, - Certificates: []tls.Certificate{cert}, + Certificates: []tls.Certificate{cert}, } irccon.AddCallback("001", func(e *Event) { irccon.Join("#go-eventirc") }) diff --git a/irc_struct.go b/irc_struct.go index 9175537..a0a6172 100644 --- a/irc_struct.go +++ b/irc_struct.go @@ -1,4 +1,6 @@ -// Copyright 2009 Thomas Jager All rights reserved. +// irc_struct.go - corrected version +// Copyright 2009 Thomas Jager +// All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. @@ -16,6 +18,7 @@ import ( "golang.org/x/text/encoding" ) +// Connection represents an IRC connection. type Connection struct { sync.Mutex sync.WaitGroup @@ -46,8 +49,8 @@ type Connection struct { pwrite chan string end chan struct{} - nick string //The nickname we want. - nickcurrent string //The nickname we currently have. + nick string // The nickname we want. + nickcurrent string // The nickname we currently have. user string registered bool events map[string]map[int]func(*Event) @@ -61,12 +64,18 @@ type Connection struct { Log *log.Logger stopped bool - quit bool //User called Quit, do not reconnect. + quit bool // User called Quit, do not reconnect. + + idCounter int // Assign unique IDs to callbacks + + // New fields added for binding to a specific local IP and connection status + localIP string // Local IP to bind when connecting + fullyConnected bool // Indicates if the connection is fully established + DCCManager *DCCManager // DCC chat support - idCounter int // assign unique IDs to callbacks } -// A struct to represent an event. +// Event represents an IRC event. type Event struct { Code string Raw string @@ -80,7 +89,7 @@ type Event struct { Ctx context.Context } -// Retrieve the last message from Event arguments. +// Message retrieves the last message from Event arguments. // This function leaves the arguments untouched and // returns an empty string if there are none. func (e *Event) Message() string { @@ -90,11 +99,11 @@ func (e *Event) Message() string { return e.Arguments[len(e.Arguments)-1] } -// https://stackoverflow.com/a/10567935/6754440 -// Regex of IRC formatting. +// ircFormat is a regex for IRC formatting codes. var ircFormat = regexp.MustCompile(`[\x02\x1F\x0F\x16\x1D\x1E]|\x03(\d\d?(,\d\d?)?)?`) -// Retrieve the last message from Event arguments, but without IRC formatting (color. +// MessageWithoutFormat retrieves the last message from Event arguments, +// but without IRC formatting (e.g., colors). // This function leaves the arguments untouched and // returns an empty string if there are none. func (e *Event) MessageWithoutFormat() string { diff --git a/irc_test.go b/irc_test.go index b2d3065..87f1eac 100644 --- a/irc_test.go +++ b/irc_test.go @@ -13,7 +13,7 @@ const serverssl = "irc.freenode.net:7000" const channel = "#go-eventirc-test" const dict = "abcdefghijklmnopqrstuvwxyz" -//Spammy +// Spammy const verbose_tests = false const debug_tests = true @@ -172,10 +172,8 @@ func TestClearCallback(t *testing.T) { func TestIRCemptyNick(t *testing.T) { irccon := IRC("", "go-eventirc") - irccon = nil if irccon != nil { t.Error("empty nick didn't result in error") - t.Fail() } }