diff --git a/README.md b/README.md index 96ffe620..f88d3754 100644 --- a/README.md +++ b/README.md @@ -6,12 +6,11 @@ and issues are welcome. ## Some Features - follows the RFC where possible -- JSON-based configuration -- server password -- channels with many standard modes -- IRC operators -- TLS support (but better to use stunnel with proxy protocol) -- haproxy PROXY protocol header for hostname setting +- gcfg gitconfig-style configuration +- server password (PASS command) +- channels with most standard modes +- IRC operators (OPER command) +- haproxy [PROXY protocol](http://haproxy.1wt.eu/download/1.5/doc/proxy-protocol.txt) header for hostname setting - passwords stored in bcrypt format - channels that persist between restarts (+P) @@ -23,27 +22,39 @@ I wanted to learn Go. "Ergonomadic" is an anagram of "Go IRC Daemon". +## What about SSL/TLS support? + +Go has a not-yet-verified-as-safe TLS 1.2 implementation. Sadly, many +popular IRC clients will negotiate nothing newer than SSLv2. If you +want to use SSL to protect traffic, I recommend using +[stunnel](https://www.stunnel.org/index.html) version 4.56 with +haproxy's +[PROXY protocol](http://haproxy.1wt.eu/download/1.5/doc/proxy-protocol.txt). This +will allow the server to get the client's original addresses for +hostname lookups. + ## Installation ```sh go get go install -ergonomadic -conf '/path/to/config.json' -initdb +ergonomadic -conf ergonomadic.conf -initdb ``` ## Configuration -See the example `config.json`. Passwords are base64-encoded bcrypted -byte strings. You can generate them with the `genpasswd` subcommand. +See the example `ergonomadic.conf`. Passwords are base64-encoded +bcrypted byte strings. You can generate them with the `genpasswd` +subcommand. ```sh -ergonomadic -genpasswd 'hunter21!' +ergonomadic -genpasswd 'hunter2!' ``` ## Running the Server ```sh -ergonomadic -conf '/path/to/config.json' +ergonomadic -conf ergonomadic.conf ``` ## Helpful Documentation diff --git a/config.json b/config.json deleted file mode 100644 index b607611a..00000000 --- a/config.json +++ /dev/null @@ -1,39 +0,0 @@ -// Ergonomadic IRC Server Config -// ----------------------------- -// Passwords are generated by `ergonomadic -genpasswd "$plaintext"`. -// Comments are not allowed in the actual config file. -{ - // `name` is usually a hostname. - "name": "irc.example.com", - - // The path to the MOTD is relative to this file's directory. - "motd": "motd.txt", - - // PASS command password - "password": "JDJhJDA0JHBBenUyV3Z5UU5iWUpiYmlNMlNLZC5VRDZDM21HUzFVbmxLUUI3NTVTLkZJOERLdUFaUWNt", - - // `listeners` are places to bind and listen for - // connections. http://golang.org/pkg/net/#Dial demonstrates valid - // values for `net` and `address`. `net` is optional and defaults - // to `tcp`. - "listeners": [ { - "address": "localhost:7777" - }, { - "net": "tcp6", - "address": "[::1]:7777" - } ], - - // Operators for the OPER command - "operators": [ { - "name": "root", - "password": "JDJhJDA0JHBBenUyV3Z5UU5iWUpiYmlNMlNLZC5VRDZDM21HUzFVbmxLUUI3NTVTLkZJOERLdUFaUWNt" - } ], - - // Global debug flags. `net` generates a lot of output. - "debug": { - "net": true, - "client": false, - "channel": false, - "server": false - } -} diff --git a/ergonomadic.conf b/ergonomadic.conf new file mode 100644 index 00000000..2f360024 --- /dev/null +++ b/ergonomadic.conf @@ -0,0 +1,16 @@ +[server] +name = "irc.example.com" ; required, usually a hostname +database = "ergonomadic.db" ; path relative to this file +listen = "localhost:6667" ; see `net.Listen` for examples +listen = "[::1]:6667" ; multiple `listen`s are allowed. +motd = "motd.txt" ; path relative to this file +password = "JDJhJDA0JHJzVFFlNXdOUXNhLmtkSGRUQVVEVHVYWXRKUmdNQ3FKVTRrczRSMTlSWGRPZHRSMVRzQmtt" ; 'test' + +[operator "root"] +password = "JDJhJDA0JEhkcm10UlNFRkRXb25iOHZuSDVLZXVBWlpyY0xyNkQ4dlBVc1VMWVk1LlFjWFpQbGxZNUtl" ; 'toor' + +[debug] +net = true +client = false +channel = false +server = false diff --git a/ergonomadic.go b/ergonomadic.go index 663d1838..525e38ac 100644 --- a/ergonomadic.go +++ b/ergonomadic.go @@ -5,6 +5,8 @@ import ( "fmt" "github.com/jlatt/ergonomadic/irc" "log" + "os" + "path/filepath" ) func main() { @@ -26,18 +28,22 @@ func main() { if err != nil { log.Fatal(err) } + err = os.Chdir(filepath.Dir(*conf)) + if err != nil { + log.Fatal(err) + } if *initdb { - irc.InitDB(config.Database()) - log.Println("database initialized: " + config.Database()) + irc.InitDB(config.Server.Database) + log.Println("database initialized: " + config.Server.Database) return } // TODO move to data structures - irc.DEBUG_NET = config.Debug["net"] - irc.DEBUG_CLIENT = config.Debug["client"] - irc.DEBUG_CHANNEL = config.Debug["channel"] - irc.DEBUG_SERVER = config.Debug["server"] + irc.DEBUG_NET = config.Debug.Net + irc.DEBUG_CLIENT = config.Debug.Client + irc.DEBUG_CHANNEL = config.Debug.Channel + irc.DEBUG_SERVER = config.Debug.Server irc.NewServer(config).Run() } diff --git a/irc/config.go b/irc/config.go index 9aabe12f..34181acd 100644 --- a/irc/config.go +++ b/irc/config.go @@ -1,14 +1,17 @@ package irc import ( - "encoding/json" + "code.google.com/p/gcfg" + "errors" "log" - "os" - "path/filepath" ) -func decodePassword(password string) []byte { - bytes, err := DecodePassword(password) +type PassConfig struct { + Password string +} + +func (conf *PassConfig) PasswordBytes() []byte { + bytes, err := DecodePassword(conf.Password) if err != nil { log.Fatal(err) } @@ -16,72 +19,49 @@ func decodePassword(password string) []byte { } type Config struct { - Debug map[string]bool - Listeners []ListenerConfig - MOTD string - Name string - Operators []OperatorConfig - Password string - directory string + Server struct { + PassConfig + Database string + Listen []string + MOTD string + Name string + } + + Operator map[string]*PassConfig + + Debug struct { + Net bool + Client bool + Channel bool + Server bool + } } -func (conf *Config) Database() string { - return filepath.Join(conf.directory, "ergonomadic.db") -} - -func (conf *Config) PasswordBytes() []byte { - return decodePassword(conf.Password) -} - -func (conf *Config) OperatorsMap() map[string][]byte { +func (conf *Config) Operators() map[string][]byte { operators := make(map[string][]byte) - for _, opConf := range conf.Operators { - operators[opConf.Name] = opConf.PasswordBytes() + for name, opConf := range conf.Operator { + operators[name] = opConf.PasswordBytes() } return operators } -type OperatorConfig struct { - Name string - Password string -} - -func (conf *OperatorConfig) PasswordBytes() []byte { - return decodePassword(conf.Password) -} - -type ListenerConfig struct { - Net string - Address string - Key string - Certificate string -} - -func (config *ListenerConfig) IsTLS() bool { - return (config.Key != "") && (config.Certificate != "") -} - func LoadConfig(filename string) (config *Config, err error) { config = &Config{} - - file, err := os.Open(filename) + err = gcfg.ReadFileInto(config, filename) if err != nil { return } - defer file.Close() - - decoder := json.NewDecoder(file) - err = decoder.Decode(config) - if err != nil { + if config.Server.Name == "" { + err = errors.New("server.name missing") return } - - config.directory = filepath.Dir(filename) - config.MOTD = filepath.Join(config.directory, config.MOTD) - for _, lconf := range config.Listeners { - if lconf.Net == "" { - lconf.Net = "tcp" - } + if config.Server.Database == "" { + err = errors.New("server.database missing") + return + } + if len(config.Server.Listen) == 0 { + err = errors.New("server.listen missing") + return } return } diff --git a/irc/server.go b/irc/server.go index 6b770be3..129a312b 100644 --- a/irc/server.go +++ b/irc/server.go @@ -2,10 +2,7 @@ package irc import ( "bufio" - "crypto/rand" - "crypto/tls" "database/sql" - "encoding/binary" "fmt" "log" "net" @@ -41,21 +38,21 @@ func NewServer(config *Config) *Server { clients: make(ClientNameMap), commands: make(chan Command, 16), ctime: time.Now(), - db: OpenDB(config.Database()), + db: OpenDB(config.Server.Database), idle: make(chan *Client, 16), - motdFile: config.MOTD, - name: config.Name, + motdFile: config.Server.MOTD, + name: config.Server.Name, newConns: make(chan net.Conn, 16), - operators: config.OperatorsMap(), - password: config.PasswordBytes(), + operators: config.Operators(), + password: config.Server.PasswordBytes(), signals: make(chan os.Signal, 1), timeout: make(chan *Client, 16), } server.loadChannels() - for _, listenerConf := range config.Listeners { - go server.listen(listenerConf) + for _, addr := range config.Server.Listen { + go server.listen(addr) } signal.Notify(server.signals, syscall.SIGINT, syscall.SIGHUP, @@ -171,33 +168,18 @@ func (server *Server) InitPhase() Phase { return Authorization } -func newListener(config ListenerConfig) (net.Listener, error) { - if config.IsTLS() { - certificate, err := tls.LoadX509KeyPair(config.Certificate, config.Key) - if err != nil { - return nil, err - } - return tls.Listen("tcp", config.Address, &tls.Config{ - Certificates: []tls.Certificate{certificate}, - PreferServerCipherSuites: true, - }) - } - - return net.Listen("tcp", config.Address) -} - // // listen goroutine // -func (s *Server) listen(config ListenerConfig) { - listener, err := newListener(config) +func (s *Server) listen(addr string) { + listener, err := net.Listen("tcp", addr) if err != nil { log.Fatal(s, "listen error: ", err) } if DEBUG_SERVER { - log.Printf("%s listening on %s", s, config.Address) + log.Printf("%s listening on %s", s, addr) } for { @@ -216,24 +198,6 @@ func (s *Server) listen(config ListenerConfig) { } } -func (s *Server) GenerateGuestNick() string { - bytes := make([]byte, 8) - for { - _, err := rand.Read(bytes) - if err != nil { - panic(err) - } - randInt, n := binary.Uvarint(bytes) - if n <= 0 { - continue // TODO handle error - } - nick := fmt.Sprintf("guest%d", randInt) - if s.clients.Get(nick) == nil { - return nick - } - } -} - // // server functionality //