diff --git a/client/connection.go b/client/connection.go index 54b7435..e9c7708 100644 --- a/client/connection.go +++ b/client/connection.go @@ -31,6 +31,7 @@ type Conn struct { stRemovers []Remover // I/O stuff to server + dialer *net.Dialer sock net.Conn io *bufio.ReadWriter in chan *Line @@ -58,6 +59,9 @@ type Config struct { SSL bool SSLConfig *tls.Config + // Local address to connect to the server. + LocalAddr string + // Replaceable function to customise the 433 handler's new nick NewNick func(string) string @@ -91,7 +95,7 @@ func NewConfig(nick string, args ...string) *Config { NewNick: func(s string) string { return s + "_" }, Recover: (*Conn).LogPanic, // in dispatch.go SplitLen: 450, - Timeout: 60, + Timeout: 60 * time.Second, } cfg.Me.Ident = "goirc" if len(args) > 0 && args[0] != "" { @@ -122,8 +126,24 @@ func Client(cfg *Config) *Conn { cfg.Me.Ident = "goirc" cfg.Me.Name = "Powered by GoIRC" } + + dialer := new(net.Dialer) + if cfg.LocalAddr != "" { + if !hasPort(cfg.LocalAddr) { + cfg.LocalAddr += ":0" + } + + local, err := net.ResolveTCPAddr("tcp", cfg.LocalAddr) + if err == nil { + dialer.LocalAddr = local + } else { + logging.Error("irc.Client(): Cannot resolve local address %s: %s", cfg.LocalAddr, err) + } + } + conn := &Conn{ cfg: cfg, + dialer: dialer, in: make(chan *Line, 32), out: make(chan string, 32), intHandlers: handlerSet(), @@ -209,29 +229,27 @@ func (conn *Conn) Connect() error { } if conn.cfg.SSL { if !hasPort(conn.cfg.Server) { - conn.cfg.Server += ":6697" + conn.cfg.Server = net.JoinHostPort(conn.cfg.Server, "6697") } if &conn.cfg.Timeout != nil { conn.cfg.Timeout = (60 * time.Second) } logging.Info("irc.Connect(): Connecting to %s with SSL.", conn.cfg.Server) - dialer := &net.Dialer{ - Timeout: conn.cfg.Timeout, - } - if s, err := tls.DialWithDialer(dialer, "tcp", conn.cfg.Server, conn.cfg.SSLConfig); err == nil { + conn.dialer.Timeout = conn.cfg.Timeout + if s, err := tls.DialWithDialer(conn.dialer, "tcp", conn.cfg.Server, conn.cfg.SSLConfig); err == nil { conn.sock = s } else { return err } } else { if !hasPort(conn.cfg.Server) { - conn.cfg.Server += ":6667" + conn.cfg.Server = net.JoinHostPort(conn.cfg.Server, "6667") } if &conn.cfg.Timeout != nil { conn.cfg.Timeout = (60 * time.Second) } logging.Info("irc.Connect(): Connecting to %s without SSL.", conn.cfg.Server) - if s, err := net.DialTimeout("tcp", conn.cfg.Server, conn.cfg.Timeout); err == nil { + if s, err := conn.dialer.DialTimeout("tcp", conn.cfg.Server, conn.cfg.Timeout); err == nil { conn.sock = s } else { return err @@ -239,7 +257,7 @@ func (conn *Conn) Connect() error { } conn.connected = true conn.postConnect(true) - conn.dispatch(&Line{Cmd: REGISTER}) + conn.dispatch(&Line{Cmd: REGISTER, Time: time.Now()}) return nil } @@ -393,7 +411,7 @@ func (conn *Conn) shutdown() { conn.wg.Wait() // reinit datastructures ready for next connection conn.initialise() - conn.dispatch(&Line{Cmd: DISCONNECTED}) + conn.dispatch(&Line{Cmd: DISCONNECTED, Time: time.Now()}) } // Dumps a load of information about the current state of the connection to a diff --git a/client/handlers.go b/client/handlers.go index af6a6e2..13d1e2d 100644 --- a/client/handlers.go +++ b/client/handlers.go @@ -5,6 +5,7 @@ package client import ( "strings" + "time" ) // sets up the internal event handlers to do essential IRC protocol things @@ -42,7 +43,7 @@ func (conn *Conn) h_REGISTER(line *Line) { // Handler to trigger a CONNECTED event on receipt of numeric 001 func (conn *Conn) h_001(line *Line) { // we're connected! - conn.dispatch(&Line{Cmd: CONNECTED}) + conn.dispatch(&Line{Cmd: CONNECTED, Time: time.Now()}) // and we're being given our hostname (from the server's perspective) t := line.Args[len(line.Args)-1] if idx := strings.LastIndex(t, " "); idx != -1 { diff --git a/fix/README.md b/fix/README.md deleted file mode 100644 index 8b6a429..0000000 --- a/fix/README.md +++ /dev/null @@ -1,35 +0,0 @@ -## client api changes - -`goirc.go` is a fix compatible with `go tool fix` that attempts to -programaticallly change old client code to work with the newer, shinier -API. It might even work, depending on the complexity of your code. You'll -need to have a Go source tree in the path specified by GOROOT. GOOS and -GOARCH may not be set in your environment; use your head. - -### To install: - - cd $GOROOT/src/cmd/fix - ln -s $GOPATH/src/github.com/fluffle/goirc/fix/goirc.go - go build - mv fix $GOROOT/pkg/tool/$GOOS_$GOARCH - -### To fix: - - go tool fix -r goirc -diff /path/to/code - - go tool fix -r goirc /path/to/code - -### Things that aren't fixed by this - -This fix doesn't take care of some bad design decisions I made: - - - conn.State is left as-is. If you're using this you'll need to rewrite - things to get scope into your handlers in a different fashion. - - conn.ER and conn.ED are left as-is. These should be completely removed - (and probably shouldn't have been left accessible in the first place). - -It's also quite likely that this won't produce the nicest "fixed" code. In -particular, if you're seeing lots of lines like `conn.Config().XXX = "foo"`, -you probably want to consider creating a `Config` struct and then passing -it to `client.Client()`. - diff --git a/fix/goirc.go b/fix/goirc.go deleted file mode 100644 index 5931169..0000000 --- a/fix/goirc.go +++ /dev/null @@ -1,213 +0,0 @@ -package main - -import ( - "fmt" - "go/ast" - "go/token" - "strings" -) - -// DISCLAIMER: will probably not fix everything. Notable omissions: -// - conn.State will not be removed, if you were using it. -// - conn.ER/ED will not be removed; exposing them was a Bad Idea, and -// the required changes are quite a challenge to express programatically. - -func init() { - register(goircFix) -} - -var goircFix = fix{ - "goirc", - "2013-03-24", - goircNewApi, - `Update code that uses goirc/client to new API.`, -} - -const ( - clientPath = "github.com/fluffle/goirc/client" - statePath = "github.com/fluffle/goirc/state" -) - -var goircConstants = map[string]string{ - `"REGISTER"`: "REGISTER", - `"CONNECTED"`: "CONNECTED", - `"DISCONNECTED"`: "DISCONNECTED", - `"ACTION"`: "ACTION", - `"AWAY"`: "AWAY", - `"CTCP"`: "CTCP", - `"CTCPREPLY"`: "CTCPREPLY", - `"INVITE"`: "INVITE", - `"JOIN"`: "JOIN", - `"KICK"`: "KICK", - `"MODE"`: "MODE", - `"NICK"`: "NICK", - `"NOTICE"`: "NOTICE", - `"OPER"`: "OPER", - `"PART"`: "PART", - `"PASS"`: "PASS", - `"PING"`: "PING", - `"PONG"`: "PONG", - `"PRIVMSG"`: "PRIVMSG", - `"QUIT"`: "QUIT", - `"TOPIC"`: "TOPIC", - `"USER"`: "USER", - `"VERSION"`: "VERSION", - `"VHOST"`: "VHOST", - `"WHO"`: "WHO", - `"WHOIS"`: "WHOIS", -} - -var goircStructToConfig = map[string]string{ - "Host": "Server", - "Network": "Server", - "NewNick": "NewNick", - "SSL": "SSL", - "SSLConfig": "SSLConfig", - "PingFreq": "PingFreq", - "Flood": "Flood", -} - -var goircStructToMethod = map[string]string{ - "Me": "Me", - "ST": "StateTracker", - "Connected": "Connected", -} - -var goircMethodRename = map[string]string{ - "AddHandler": "HandleFunc", - "Connect": "ConnectTo", -} - -func addCall(t ast.Expr, method string) *ast.CallExpr { - return &ast.CallExpr{ - Fun: &ast.SelectorExpr{ - X: t, - Sel: ast.NewIdent(method), - }, - } -} - -func goircNewApi(f *ast.File) bool { - state := stateApi(f) - client := clientApi(f) - return client || state -} - -func stateApi(f *ast.File) bool { - spec := importSpec(f, statePath) - if spec == nil { - return false - } - stateImport := "state" - if spec.Name != nil { - stateImport = spec.Name.Name - } - return renameFixTab(f, []rename{ - {"github.com/fluffle/goirc/state", "", - stateImport + ".StateTracker", stateImport + ".Tracker"}, - {"github.com/fluffle/goirc/state", "", - stateImport + ".MockStateTracker", stateImport + ".MockTracker"}, - }) -} - -func clientApi(f *ast.File) bool { - spec := importSpec(f, clientPath) - if spec == nil { - return false - } - clientImport := "client" - if spec.Name != nil { - clientImport = spec.Name.Name - } - fixed := false - - maybeReplaceBasicLit := func (expr *ast.Expr) { - str, ok := (*expr).(*ast.BasicLit) - if !ok || str == nil || str.Kind != token.STRING { return } - if repl, ok := goircConstants[strings.ToUpper(str.Value)]; ok { - *expr = &ast.SelectorExpr{ - ast.NewIdent(clientImport), - ast.NewIdent(repl), - } - fixed = true - } - } - - maybeReplaceConnSelectors := func (expr *ast.Expr) { - sel, ok := (*expr).(*ast.SelectorExpr) - if !ok || !isClientConn(sel.X, clientImport) { return } - name := sel.Sel.String() - if rep, ok := goircStructToConfig[name]; ok { - sel.X = addCall(sel.X, "Config") - sel.Sel = ast.NewIdent(rep) - fixed = true - } else if meth, ok := goircStructToMethod[name]; ok { - *expr = addCall(sel.X, meth) - fixed = true - } else if meth, ok := goircMethodRename[name]; ok { - sel.Sel = ast.NewIdent(meth) - fixed = true - } - } - - walk(f, func(n interface{}) { - if expr, ok := n.(*ast.Expr); ok { - maybeReplaceBasicLit(expr) - maybeReplaceConnSelectors(expr) - } - if expr, ok := n.(*ast.CallExpr); ok { - if sel, ok := n.(*ast.SelectorExpr); ok && - isPkgDot(sel, clientImport, "Client") { - // s/Client/SimpleClient/ - sel.Sel = ast.NewIdent("SimpleClient") - // and delete the last arg from args - expr.Args = expr.Args[:3] - fixed = true - } - } - }) - return fixed -} - -func isClientConn(t ast.Expr, pkg string) bool { - // TODO(fluffle): when Conn is a struct member and we're looking for e.g. - // struct.Conn.AddHandler() - // we will pass in the *ast.SelectorExpr{X: struct, Sel: Conn} to this. - // Unfortunately the *ast.Ident{Conn} in this case often has no *ast.Object - // associated with it, so to divine it's type we need to recurse down until - // X is an *ast.Ident instead of another *ast.SelectorExpr, then look for - // the struct member types all the way back up until we get to "Conn". - // This is a massive pain-in-the-arse; I can see why type checking of this - // sort is often not done, s/AddHandler/HandleFunc/g is much simpler. - id, ok := t.(*ast.Ident) - if !ok || id.Obj == nil { return false } - switch dec := id.Obj.Decl.(type) { - case *ast.ValueSpec: - // Declared with var X Type - return dec.Type != nil && isPtrPkgDot(dec.Type, pkg, "Conn") - case *ast.AssignStmt: - // Declared with X := Expr producing Type - // NOTE: not taking care of multiple-assignment case atm! - switch rhs := dec.Rhs[0].(type) { - case *ast.CallExpr: - // X := client.Client() or client.SimpleClient() - return isPkgDot(rhs.Fun, pkg, "Client") || - isPkgDot(rhs.Fun, pkg, "SimpleClient") - case *ast.UnaryExpr: - // X := &client.Conn{} - lit, ok := rhs.X.(*ast.CompositeLit) - return ok && isPkgDot(lit.Type, pkg, "Conn") - case *ast.CompositeLit: - // X := client.Conn{} - return isPkgDot(rhs.Type, pkg, "Conn") - default: - fmt.Printf("rhs: %#v\n", rhs) - } - case *ast.Field: - // Declared with func f(X Type) - return isPkgDot(dec.Type, pkg, "Conn") - default: - fmt.Printf("dec: %#v\n", dec) - } - return false -}