mirror of
https://blitiri.com.ar/repos/chasquid
synced 2025-12-17 14:37:02 +00:00
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.
This commit is contained in:
91
chasquid.go
91
chasquid.go
@@ -12,46 +12,87 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/mail"
|
"net/mail"
|
||||||
"net/textproto"
|
"net/textproto"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"blitiri.com.ar/go/chasquid/internal/config"
|
||||||
|
|
||||||
_ "net/http/pprof"
|
_ "net/http/pprof"
|
||||||
|
|
||||||
"github.com/golang/glog"
|
"github.com/golang/glog"
|
||||||
"golang.org/x/net/trace"
|
"golang.org/x/net/trace"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
var (
|
||||||
// TODO: get this via config/dynamically. It's only used for show.
|
configDir = flag.String("config_dir", "/etc/chasquid",
|
||||||
hostname = "charqui.com.ar"
|
"configuration directory")
|
||||||
|
|
||||||
// Maximum data size, in bytes.
|
testCert = flag.String("test_cert", ".cert.pem",
|
||||||
maxDataSize = 52428800
|
"Certificate file, for testing purposes")
|
||||||
|
testKey = flag.String("test_key", ".key.pem",
|
||||||
|
"Key file, for testing purposes")
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
monAddr := ":1099"
|
conf, err := config.Load(*configDir + "/chasquid.conf")
|
||||||
glog.Infof("Monitoring HTTP server listening on %s", monAddr)
|
if err != nil {
|
||||||
go http.ListenAndServe(monAddr, 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()
|
s.ListenAndServe()
|
||||||
}
|
}
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
|
// Main hostname, used for display only.
|
||||||
|
Hostname string
|
||||||
|
|
||||||
|
// Maximum data size.
|
||||||
|
MaxDataSize int64
|
||||||
|
|
||||||
// Certificate and key pairs.
|
// Certificate and key pairs.
|
||||||
certs, keys []string
|
certs, keys []string
|
||||||
|
|
||||||
// Addresses.
|
// Addresses.
|
||||||
addrs []string
|
addrs []string
|
||||||
|
|
||||||
// Main hostname, used for display only.
|
|
||||||
hostname string
|
|
||||||
|
|
||||||
// TLS config.
|
// TLS config.
|
||||||
tlsConfig *tls.Config
|
tlsConfig *tls.Config
|
||||||
|
|
||||||
@@ -62,9 +103,8 @@ type Server struct {
|
|||||||
commandTimeout time.Duration
|
commandTimeout time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewServer(hostname string) *Server {
|
func NewServer() *Server {
|
||||||
return &Server{
|
return &Server{
|
||||||
hostname: hostname,
|
|
||||||
connTimeout: 20 * time.Minute,
|
connTimeout: 20 * time.Minute,
|
||||||
commandTimeout: 1 * time.Minute,
|
commandTimeout: 1 * time.Minute,
|
||||||
}
|
}
|
||||||
@@ -134,6 +174,8 @@ func (s *Server) serve(l net.Listener) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sc := &Conn{
|
sc := &Conn{
|
||||||
|
hostname: s.Hostname,
|
||||||
|
maxDataSize: s.MaxDataSize,
|
||||||
netconn: conn,
|
netconn: conn,
|
||||||
tc: textproto.NewConn(conn),
|
tc: textproto.NewConn(conn),
|
||||||
tlsConfig: s.tlsConfig,
|
tlsConfig: s.tlsConfig,
|
||||||
@@ -145,10 +187,19 @@ func (s *Server) serve(l net.Listener) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Conn struct {
|
type Conn struct {
|
||||||
|
// Main hostname, used for display only.
|
||||||
|
hostname string
|
||||||
|
|
||||||
|
// Maximum data size.
|
||||||
|
maxDataSize int64
|
||||||
|
|
||||||
// Connection information.
|
// Connection information.
|
||||||
netconn net.Conn
|
netconn net.Conn
|
||||||
tc *textproto.Conn
|
tc *textproto.Conn
|
||||||
|
|
||||||
|
// System configuration.
|
||||||
|
config *config.Config
|
||||||
|
|
||||||
// TLS configuration.
|
// TLS configuration.
|
||||||
tlsConfig *tls.Config
|
tlsConfig *tls.Config
|
||||||
|
|
||||||
@@ -174,7 +225,7 @@ func (c *Conn) Handle() {
|
|||||||
defer tr.Finish()
|
defer tr.Finish()
|
||||||
tr.LazyPrintf("RemoteAddr: %s", c.netconn.RemoteAddr())
|
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 cmd, params string
|
||||||
var err error
|
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) {
|
func (c *Conn) EHLO(params string) (code int, msg string) {
|
||||||
buf := bytes.NewBuffer(nil)
|
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, "8BITMIME\n")
|
||||||
fmt.Fprintf(buf, "PIPELINING\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, "STARTTLS\n")
|
||||||
fmt.Fprintf(buf, "HELP\n")
|
fmt.Fprintf(buf, "HELP\n")
|
||||||
return 250, buf.String()
|
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.
|
// one, we don't want the command timeout to interfere.
|
||||||
c.netconn.SetDeadline(c.deadline)
|
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)
|
c.data, err = ioutil.ReadAll(dotr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 554, fmt.Sprintf("error reading DATA: %v", err)
|
return 554, fmt.Sprintf("error reading DATA: %v", err)
|
||||||
|
|||||||
@@ -356,7 +356,9 @@ func realMain(m *testing.M) int {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
s := NewServer("localhost")
|
s := NewServer()
|
||||||
|
s.Hostname = "localhost"
|
||||||
|
s.MaxDataSize = 50 * 1024 * 1025
|
||||||
s.AddCerts(tmpDir+"/cert.pem", tmpDir+"/key.pem")
|
s.AddCerts(tmpDir+"/cert.pem", tmpDir+"/key.pem")
|
||||||
s.AddAddr(srvAddr)
|
s.AddAddr(srvAddr)
|
||||||
go s.ListenAndServe()
|
go s.ListenAndServe()
|
||||||
|
|||||||
60
internal/config/config.go
Normal file
60
internal/config/config.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
43
internal/config/config.pb.go
Normal file
43
internal/config/config.pb.go
Normal file
@@ -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() {}
|
||||||
22
internal/config/config.proto
Normal file
22
internal/config/config.proto
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
104
internal/config/config_test.go
Normal file
104
internal/config/config_test.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user