author | Alberto Bertogli
<albertito@blitiri.com.ar> 2015-10-31 19:37:30 UTC |
committer | Alberto Bertogli
<albertito@blitiri.com.ar> 2015-11-01 02:19:12 UTC |
parent | f055a3460ecd94a88246ccf2247172887832a63a |
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) + } +}