git » chasquid » commit a809a3c

Basic configuration

author Alberto Bertogli
2015-10-31 19:37:30 UTC
committer Alberto Bertogli
2015-11-01 02:19:12 UTC
parent f055a3460ecd94a88246ccf2247172887832a63a

Basic configuration

This patch introduces a basic on disk configuration, comprised of a main
configuration file and per-domain directories.

It's still not complete, but will be extended in subsequent patches.

chasquid.go +71 -20
chasquid_test.go +3 -1
internal/config/config.go +60 -0
internal/config/config.pb.go +43 -0
internal/config/config.proto +22 -0
internal/config/config_test.go +104 -0

diff --git a/chasquid.go b/chasquid.go
index b3da182..c737d1b 100644
--- a/chasquid.go
+++ b/chasquid.go
@@ -12,46 +12,87 @@ import (
 	"net/http"
 	"net/mail"
 	"net/textproto"
+	"path/filepath"
 	"strings"
 	"time"
 
+	"blitiri.com.ar/go/chasquid/internal/config"
+
 	_ "net/http/pprof"
 
 	"github.com/golang/glog"
 	"golang.org/x/net/trace"
 )
 
-const (
-	// TODO: get this via config/dynamically. It's only used for show.
-	hostname = "charqui.com.ar"
+var (
+	configDir = flag.String("config_dir", "/etc/chasquid",
+		"configuration directory")
 
-	// Maximum data size, in bytes.
-	maxDataSize = 52428800
+	testCert = flag.String("test_cert", ".cert.pem",
+		"Certificate file, for testing purposes")
+	testKey = flag.String("test_key", ".key.pem",
+		"Key file, for testing purposes")
 )
 
 func main() {
 	flag.Parse()
 
-	monAddr := ":1099"
-	glog.Infof("Monitoring HTTP server listening on %s", monAddr)
-	go http.ListenAndServe(monAddr, nil)
+	conf, err := config.Load(*configDir + "/chasquid.conf")
+	if err != nil {
+		glog.Fatalf("Error reading config")
+	}
+
+	if conf.MonitoringAddress != "" {
+		glog.Infof("Monitoring HTTP server listening on %s",
+			conf.MonitoringAddress)
+		go http.ListenAndServe(conf.MonitoringAddress, nil)
+	}
+
+	s := NewServer()
+	s.Hostname = conf.Hostname
+	s.MaxDataSize = conf.MaxDataSizeMb * 1024 * 1024
+
+	// Load domains.
+	domains, err := filepath.Glob(*configDir + "/domains/*")
+	if err != nil {
+		glog.Fatalf("Error in glob: %v", err)
+	}
+	if len(domains) == 0 {
+		glog.Warningf("No domains found in config, using test certs")
+		s.AddCerts(*testCert, *testKey)
+	} else {
+		glog.Infof("Domain config paths:")
+		for _, d := range domains {
+			glog.Infof("  %s", d)
+			s.AddCerts(d+"/cert.pem", d+"/key.pem")
+		}
+	}
+
+	// Load addresses.
+	for _, addr := range conf.Address {
+		if addr == "systemd" {
+			// TODO
+		} else {
+			s.AddAddr(addr)
+		}
+	}
 
-	s := NewServer(hostname)
-	s.AddCerts(".cert.pem", ".key.pem")
-	s.AddAddr(":1025")
 	s.ListenAndServe()
 }
 
 type Server struct {
+	// Main hostname, used for display only.
+	Hostname string
+
+	// Maximum data size.
+	MaxDataSize int64
+
 	// Certificate and key pairs.
 	certs, keys []string
 
 	// Addresses.
 	addrs []string
 
-	// Main hostname, used for display only.
-	hostname string
-
 	// TLS config.
 	tlsConfig *tls.Config
 
@@ -62,9 +103,8 @@ type Server struct {
 	commandTimeout time.Duration
 }
 
-func NewServer(hostname string) *Server {
+func NewServer() *Server {
 	return &Server{
-		hostname:       hostname,
 		connTimeout:    20 * time.Minute,
 		commandTimeout: 1 * time.Minute,
 	}
@@ -134,6 +174,8 @@ func (s *Server) serve(l net.Listener) {
 		}
 
 		sc := &Conn{
+			hostname:       s.Hostname,
+			maxDataSize:    s.MaxDataSize,
 			netconn:        conn,
 			tc:             textproto.NewConn(conn),
 			tlsConfig:      s.tlsConfig,
@@ -145,10 +187,19 @@ func (s *Server) serve(l net.Listener) {
 }
 
 type Conn struct {
+	// Main hostname, used for display only.
+	hostname string
+
+	// Maximum data size.
+	maxDataSize int64
+
 	// Connection information.
 	netconn net.Conn
 	tc      *textproto.Conn
 
+	// System configuration.
+	config *config.Config
+
 	// TLS configuration.
 	tlsConfig *tls.Config
 
@@ -174,7 +225,7 @@ func (c *Conn) Handle() {
 	defer tr.Finish()
 	tr.LazyPrintf("RemoteAddr: %s", c.netconn.RemoteAddr())
 
-	c.tc.PrintfLine("220 %s ESMTP charquid", hostname)
+	c.tc.PrintfLine("220 %s ESMTP chasquid", c.hostname)
 
 	var cmd, params string
 	var err error
@@ -257,10 +308,10 @@ func (c *Conn) HELO(params string) (code int, msg string) {
 
 func (c *Conn) EHLO(params string) (code int, msg string) {
 	buf := bytes.NewBuffer(nil)
-	fmt.Fprintf(buf, hostname+" - Your hour of destiny has come.\n")
+	fmt.Fprintf(buf, c.hostname+" - Your hour of destiny has come.\n")
 	fmt.Fprintf(buf, "8BITMIME\n")
 	fmt.Fprintf(buf, "PIPELINING\n")
-	fmt.Fprintf(buf, "SIZE %d\n", maxDataSize)
+	fmt.Fprintf(buf, "SIZE %d\n", c.maxDataSize)
 	fmt.Fprintf(buf, "STARTTLS\n")
 	fmt.Fprintf(buf, "HELP\n")
 	return 250, buf.String()
@@ -373,7 +424,7 @@ func (c *Conn) DATA(params string, tr trace.Trace) (code int, msg string) {
 	// one, we don't want the command timeout to interfere.
 	c.netconn.SetDeadline(c.deadline)
 
-	dotr := io.LimitReader(c.tc.DotReader(), maxDataSize)
+	dotr := io.LimitReader(c.tc.DotReader(), c.maxDataSize)
 	c.data, err = ioutil.ReadAll(dotr)
 	if err != nil {
 		return 554, fmt.Sprintf("error reading DATA: %v", err)
diff --git a/chasquid_test.go b/chasquid_test.go
index 80dc18c..160024f 100644
--- a/chasquid_test.go
+++ b/chasquid_test.go
@@ -356,7 +356,9 @@ func realMain(m *testing.M) int {
 			return 1
 		}
 
-		s := NewServer("localhost")
+		s := NewServer()
+		s.Hostname = "localhost"
+		s.MaxDataSize = 50 * 1024 * 1025
 		s.AddCerts(tmpDir+"/cert.pem", tmpDir+"/key.pem")
 		s.AddAddr(srvAddr)
 		go s.ListenAndServe()
diff --git a/internal/config/config.go b/internal/config/config.go
new file mode 100644
index 0000000..3f8c3e2
--- /dev/null
+++ b/internal/config/config.go
@@ -0,0 +1,60 @@
+// Package config implements the chasquid configuration.
+package config
+
+// Generate the config protobuf.
+//go:generate protoc --go_out=. config.proto
+
+import (
+	"io/ioutil"
+	"os"
+
+	"github.com/golang/glog"
+	"github.com/golang/protobuf/proto"
+)
+
+// Load the config from the given file.
+func Load(path string) (*Config, error) {
+	c := &Config{}
+
+	buf, err := ioutil.ReadFile(path)
+	if err != nil {
+		glog.Errorf("Failed to read config at %q", path)
+		glog.Errorf("  (%v)", err)
+		return nil, err
+	}
+
+	err = proto.UnmarshalText(string(buf), c)
+	if err != nil {
+		glog.Errorf("Error parsing config: %v", err)
+		return nil, err
+	}
+
+	// Fill in defaults for anything that's missing.
+
+	if c.Hostname == "" {
+		c.Hostname, err = os.Hostname()
+		if err != nil {
+			glog.Errorf("Could not get hostname: %v", err)
+			return nil, err
+		}
+	}
+
+	if c.MaxDataSizeMb == 0 {
+		c.MaxDataSizeMb = 50
+	}
+
+	if len(c.Address) == 0 {
+		c.Address = append(c.Address, "systemd")
+	}
+
+	logConfig(c)
+	return c, nil
+}
+
+func logConfig(c *Config) {
+	glog.Infof("Configuration:")
+	glog.Infof("  Hostname: %q", c.Hostname)
+	glog.Infof("  Max data size (MB): %d", c.MaxDataSizeMb)
+	glog.Infof("  Addresses: %v", c.Address)
+	glog.Infof("  Monitoring address: %s", c.MonitoringAddress)
+}
diff --git a/internal/config/config.pb.go b/internal/config/config.pb.go
new file mode 100644
index 0000000..ddad207
--- /dev/null
+++ b/internal/config/config.pb.go
@@ -0,0 +1,43 @@
+// Code generated by protoc-gen-go.
+// source: config.proto
+// DO NOT EDIT!
+
+/*
+Package config is a generated protocol buffer package.
+
+It is generated from these files:
+	config.proto
+
+It has these top-level messages:
+	Config
+*/
+package config
+
+import proto "github.com/golang/protobuf/proto"
+import fmt "fmt"
+import math "math"
+
+// Reference imports to suppress errors if they are not otherwise used.
+var _ = proto.Marshal
+var _ = fmt.Errorf
+var _ = math.Inf
+
+type Config struct {
+	// Hostname to use when we say hello.
+	// For aesthetic purposes, but may help if our ip address resolves to it.
+	// Default: machine hostname.
+	Hostname string `protobuf:"bytes,1,opt,name=hostname" json:"hostname,omitempty"`
+	// Maximum email size, in megabytes.
+	// Default: 50.
+	MaxDataSizeMb int64 `protobuf:"varint,2,opt,name=max_data_size_mb" json:"max_data_size_mb,omitempty"`
+	// Addresses to listen on.
+	// Default: "systemd", which means systemd passes sockets to us.
+	Address []string `protobuf:"bytes,3,rep,name=address" json:"address,omitempty"`
+	// Address for the monitoring http server.
+	// Default: no monitoring http server.
+	MonitoringAddress string `protobuf:"bytes,4,opt,name=monitoring_address" json:"monitoring_address,omitempty"`
+}
+
+func (m *Config) Reset()         { *m = Config{} }
+func (m *Config) String() string { return proto.CompactTextString(m) }
+func (*Config) ProtoMessage()    {}
diff --git a/internal/config/config.proto b/internal/config/config.proto
new file mode 100644
index 0000000..d87682c
--- /dev/null
+++ b/internal/config/config.proto
@@ -0,0 +1,22 @@
+
+syntax = "proto3";
+
+message Config {
+	// Hostname to use when we say hello.
+	// For aesthetic purposes, but may help if our ip address resolves to it.
+	// Default: machine hostname.
+	string hostname = 1;
+
+	// Maximum email size, in megabytes.
+	// Default: 50.
+	int64 max_data_size_mb = 2;
+
+	// Addresses to listen on.
+	// Default: "systemd", which means systemd passes sockets to us.
+	repeated string address = 3;
+
+	// Address for the monitoring http server.
+	// Default: no monitoring http server.
+	string monitoring_address = 4;
+}
+
diff --git a/internal/config/config_test.go b/internal/config/config_test.go
new file mode 100644
index 0000000..6abdc47
--- /dev/null
+++ b/internal/config/config_test.go
@@ -0,0 +1,104 @@
+package config
+
+import (
+	"io/ioutil"
+	"os"
+	"testing"
+)
+
+func mustCreateConfig(t *testing.T, contents string) (string, string) {
+	tmpDir, err := ioutil.TempDir("", "chasquid_config_test:")
+	if err != nil {
+		t.Fatalf("Failed to create temp dir: %v\n", tmpDir)
+	}
+
+	confStr := []byte(contents)
+	err = ioutil.WriteFile(tmpDir+"/chasquid.conf", confStr, 0600)
+	if err != nil {
+		t.Fatalf("Failed to write tmp config: %v", err)
+	}
+
+	return tmpDir, tmpDir + "/chasquid.conf"
+}
+
+func TestEmptyConfig(t *testing.T) {
+	tmpDir, path := mustCreateConfig(t, "")
+	defer os.RemoveAll(tmpDir)
+	c, err := Load(path)
+	if err != nil {
+		t.Fatalf("error loading empty config: %v", err)
+	}
+
+	// Test the default values are set.
+
+	hostname, _ := os.Hostname()
+	if c.Hostname == "" || c.Hostname != hostname {
+		t.Errorf("invalid hostname %q, should be: %q", c.Hostname, hostname)
+	}
+
+	if c.MaxDataSizeMb != 50 {
+		t.Errorf("max data size != 50: %d", c.MaxDataSizeMb)
+	}
+
+	if len(c.Address) != 1 || c.Address[0] != "systemd" {
+		t.Errorf("unexpected address default: %v", c.Address)
+	}
+
+	if c.MonitoringAddress != "" {
+		t.Errorf("monitoring address is set: %v", c.MonitoringAddress)
+	}
+
+}
+
+func TestFullConfig(t *testing.T) {
+	confStr := `
+		hostname: "joust"
+		address: ":1234"
+		address: ":5678"
+		monitoring_address: ":1111"
+		max_data_size_mb: 26
+	`
+
+	tmpDir, path := mustCreateConfig(t, confStr)
+	defer os.RemoveAll(tmpDir)
+
+	c, err := Load(path)
+	if err != nil {
+		t.Fatalf("error loading non-existent config: %v", err)
+	}
+
+	if c.Hostname != "joust" {
+		t.Errorf("hostname %q != 'joust'", c.Hostname)
+	}
+
+	if c.MaxDataSizeMb != 26 {
+		t.Errorf("max data size != 26: %d", c.MaxDataSizeMb)
+	}
+
+	if len(c.Address) != 2 ||
+		c.Address[0] != ":1234" || c.Address[1] != ":5678" {
+		t.Errorf("different address: %v", c.Address)
+	}
+
+	if c.MonitoringAddress != ":1111" {
+		t.Errorf("monitoring address %q != ':1111;", c.MonitoringAddress)
+	}
+}
+
+func TestErrorLoading(t *testing.T) {
+	c, err := Load("/does/not/exist")
+	if err == nil {
+		t.Fatalf("loaded a non-existent config: %v", c)
+	}
+}
+
+func TestBrokenConfig(t *testing.T) {
+	tmpDir, path := mustCreateConfig(
+		t, "<invalid> this is not a valid protobuf")
+	defer os.RemoveAll(tmpDir)
+
+	c, err := Load(path)
+	if err == nil {
+		t.Fatalf("loaded an invalid config: %v", c)
+	}
+}