Index: /trunk/cmd/jounce/main.go
===================================================================
--- /trunk/cmd/jounce/main.go	(revision 61)
+++ /trunk/cmd/jounce/main.go	(revision 62)
@@ -2,4 +2,5 @@
 
 import (
+	"crypto/tls"
 	"flag"
 	"log"
@@ -7,20 +8,51 @@
 
 	"git.sr.ht/~emersion/jounce"
+	"git.sr.ht/~emersion/jounce/config"
 )
 
 func main() {
-	var addr, hostname string
-	flag.StringVar(&addr, "addr", ":6667", "listening address")
-	flag.StringVar(&hostname, "hostname", "localhost", "server hostname")
+	var addr, configPath string
+	flag.StringVar(&addr, "listen", "", "listening address")
+	flag.StringVar(&configPath, "config", "", "path to configuration file")
 	flag.Parse()
 
-	ln, err := net.Listen("tcp", addr)
-	if err != nil {
-		log.Fatalf("failed to start listener: %v", err)
+	var cfg *config.Server
+	if configPath != "" {
+		var err error
+		cfg, err = config.Load(configPath)
+		if err != nil {
+			log.Fatalf("failed to load config file: %v", err)
+		}
+	} else {
+		cfg = config.Defaults()
+	}
+
+	if addr != "" {
+		cfg.Addr = addr
+	}
+
+	var ln net.Listener
+	if cfg.TLS != nil {
+		cert, err := tls.LoadX509KeyPair(cfg.TLS.CertPath, cfg.TLS.KeyPath)
+		if err != nil {
+			log.Fatalf("failed to load TLS certificate and key: %v", err)
+		}
+
+		tlsCfg := &tls.Config{Certificates: []tls.Certificate{cert}}
+		ln, err = tls.Listen("tcp", cfg.Addr, tlsCfg)
+		if err != nil {
+			log.Fatalf("failed to start TLS listener: %v", err)
+		}
+	} else {
+		var err error
+		ln, err = net.Listen("tcp", addr)
+		if err != nil {
+			log.Fatalf("failed to start listener: %v", err)
+		}
 	}
 
 	srv := jounce.NewServer()
 	// TODO: load from config/DB
-	srv.Hostname = hostname
+	srv.Hostname = cfg.Hostname
 	srv.Upstreams = []jounce.Upstream{{
 		Addr:     "chat.freenode.net:6697",
@@ -31,5 +63,5 @@
 	}}
 
-	log.Printf("server listening on %v", addr)
+	log.Printf("server listening on %q", cfg.Addr)
 	go srv.Run()
 	log.Fatal(srv.Serve(ln))
Index: /trunk/config/config.go
===================================================================
--- /trunk/config/config.go	(revision 62)
+++ /trunk/config/config.go	(revision 62)
@@ -0,0 +1,176 @@
+package config
+
+import (
+	"bufio"
+	"fmt"
+	"io"
+	"os"
+	"strings"
+	"unicode"
+)
+
+type TLS struct {
+	CertPath, KeyPath string
+}
+
+type Server struct {
+	Addr     string
+	Hostname string
+	TLS      *TLS
+}
+
+func Defaults() *Server {
+	hostname, err := os.Hostname()
+	if err != nil {
+		hostname = "localhost"
+	}
+	return &Server{
+		Addr:     ":6667",
+		Hostname: hostname,
+	}
+}
+
+func Load(path string) (*Server, error) {
+	f, err := os.Open(path)
+	if err != nil {
+		return nil, err
+	}
+	defer f.Close()
+
+	return Parse(f)
+}
+
+func Parse(r io.Reader) (*Server, error) {
+	p := parser{br: bufio.NewReader(r)}
+	directives, err := p.file()
+	if err != nil {
+		return nil, fmt.Errorf("failed to parse config file: %v", err)
+	}
+
+	srv := Defaults()
+	for _, d := range directives {
+		switch d.Name {
+		case "listen":
+			if err := d.parseParams(&srv.Addr); err != nil {
+				return nil, err
+			}
+		case "hostname":
+			if err := d.parseParams(&srv.Hostname); err != nil {
+				return nil, err
+			}
+		case "tls":
+			tls := &TLS{}
+			if err := d.parseParams(&tls.CertPath, &tls.KeyPath); err != nil {
+				return nil, err
+			}
+			srv.TLS = tls
+		default:
+			return nil, fmt.Errorf("unknown directive %q", d.Name)
+		}
+	}
+
+	return srv, nil
+}
+
+type directive struct {
+	Name   string
+	Params []string
+}
+
+func (d *directive) parseParams(out ...*string) error {
+	if len(d.Params) != len(out) {
+		return fmt.Errorf("directive %q has wrong number of parameters: expected %v, got %v", d.Name, len(out), len(d.Params))
+	}
+	for i := range out {
+		*out[i] = d.Params[i]
+	}
+	return nil
+}
+
+type parser struct {
+	br *bufio.Reader
+}
+
+func (p *parser) skipSpace() error {
+	for {
+		r, _, err := p.br.ReadRune()
+		if err == io.EOF {
+			break
+		} else if err != nil {
+			return err
+		}
+		if !unicode.IsSpace(r) || r == '\n' {
+			p.br.UnreadRune()
+			break
+		}
+	}
+	return nil
+}
+
+func (p *parser) atom() (string, error) {
+	var sb strings.Builder
+	for {
+		r, _, err := p.br.ReadRune()
+		if err == io.EOF && sb.Len() > 0 {
+			break
+		} else if err != nil {
+			return "", err
+		}
+		if unicode.IsSpace(r) {
+			p.br.UnreadRune()
+			if err := p.skipSpace(); err != nil {
+				return "", err
+			}
+			break
+		}
+		sb.WriteRune(r)
+	}
+	return sb.String(), nil
+}
+
+func (p *parser) directive() (*directive, error) {
+	name, err := p.atom()
+	if err == io.EOF {
+		return nil, io.EOF
+	} else if err != nil {
+		return nil, fmt.Errorf("failed to read directive name: %v", err)
+	}
+
+	var params []string
+	for {
+		r, _, err := p.br.ReadRune()
+		if err == io.EOF {
+			break
+		} else if err != nil {
+			return nil, err
+		}
+		if r == '\n' {
+			break
+		}
+		p.br.UnreadRune()
+
+		param, err := p.atom()
+		if err == io.EOF {
+			break
+		} else if err != nil {
+			return nil, fmt.Errorf("failed to read directive param: %v", err)
+		}
+		params = append(params, param)
+	}
+
+	return &directive{name, params}, nil
+}
+
+func (p *parser) file() ([]directive, error) {
+	var l []directive
+	for {
+		d, err := p.directive()
+		if err == io.EOF {
+			break
+		} else if err != nil {
+			return nil, err
+		}
+		l = append(l, *d)
+	}
+	return l, nil
+}
