maddy

Fork https://github.com/foxcpp/maddy

git clone git://git.lin.moe/go/maddy.git

  1/*
  2Maddy Mail Server - Composable all-in-one email server.
  3Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors
  4
  5This program is free software: you can redistribute it and/or modify
  6it under the terms of the GNU General Public License as published by
  7the Free Software Foundation, either version 3 of the License, or
  8(at your option) any later version.
  9
 10This program is distributed in the hope that it will be useful,
 11but WITHOUT ANY WARRANTY; without even the implied warranty of
 12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 13GNU General Public License for more details.
 14
 15You should have received a copy of the GNU General Public License
 16along with this program.  If not, see <https://www.gnu.org/licenses/>.
 17*/
 18
 19package maddy
 20
 21import (
 22	"errors"
 23	"fmt"
 24	"io"
 25	"net/http"
 26	"os"
 27	"path/filepath"
 28	"runtime"
 29	"runtime/debug"
 30
 31	"github.com/caddyserver/certmagic"
 32	parser "github.com/foxcpp/maddy/framework/cfgparser"
 33	"github.com/foxcpp/maddy/framework/config"
 34	modconfig "github.com/foxcpp/maddy/framework/config/module"
 35	"github.com/foxcpp/maddy/framework/config/tls"
 36	"github.com/foxcpp/maddy/framework/hooks"
 37	"github.com/foxcpp/maddy/framework/log"
 38	"github.com/foxcpp/maddy/framework/module"
 39	"github.com/foxcpp/maddy/internal/authz"
 40	maddycli "github.com/foxcpp/maddy/internal/cli"
 41	"github.com/urfave/cli/v2"
 42
 43	// Import packages for side-effect of module registration.
 44	_ "github.com/foxcpp/maddy/internal/auth/dovecot_sasl"
 45	_ "github.com/foxcpp/maddy/internal/auth/external"
 46	_ "github.com/foxcpp/maddy/internal/auth/ldap"
 47	_ "github.com/foxcpp/maddy/internal/auth/netauth"
 48	_ "github.com/foxcpp/maddy/internal/auth/pam"
 49	_ "github.com/foxcpp/maddy/internal/auth/pass_table"
 50	_ "github.com/foxcpp/maddy/internal/auth/plain_separate"
 51	_ "github.com/foxcpp/maddy/internal/auth/shadow"
 52	_ "github.com/foxcpp/maddy/internal/check/authorize_sender"
 53	_ "github.com/foxcpp/maddy/internal/check/command"
 54	_ "github.com/foxcpp/maddy/internal/check/dkim"
 55	_ "github.com/foxcpp/maddy/internal/check/dns"
 56	_ "github.com/foxcpp/maddy/internal/check/dnsbl"
 57	_ "github.com/foxcpp/maddy/internal/check/milter"
 58	_ "github.com/foxcpp/maddy/internal/check/requiretls"
 59	_ "github.com/foxcpp/maddy/internal/check/rspamd"
 60	_ "github.com/foxcpp/maddy/internal/check/spf"
 61	_ "github.com/foxcpp/maddy/internal/endpoint/dovecot_sasld"
 62	_ "github.com/foxcpp/maddy/internal/endpoint/imap"
 63	_ "github.com/foxcpp/maddy/internal/endpoint/openmetrics"
 64	_ "github.com/foxcpp/maddy/internal/endpoint/smtp"
 65	_ "github.com/foxcpp/maddy/internal/imap_filter"
 66	_ "github.com/foxcpp/maddy/internal/imap_filter/command"
 67	_ "github.com/foxcpp/maddy/internal/libdns"
 68	_ "github.com/foxcpp/maddy/internal/modify"
 69	_ "github.com/foxcpp/maddy/internal/modify/dkim"
 70	_ "github.com/foxcpp/maddy/internal/storage/blob/fs"
 71	_ "github.com/foxcpp/maddy/internal/storage/blob/s3"
 72	_ "github.com/foxcpp/maddy/internal/storage/imapsql"
 73	_ "github.com/foxcpp/maddy/internal/table"
 74	_ "github.com/foxcpp/maddy/internal/target/queue"
 75	_ "github.com/foxcpp/maddy/internal/target/remote"
 76	_ "github.com/foxcpp/maddy/internal/target/smtp"
 77	_ "github.com/foxcpp/maddy/internal/tls"
 78	_ "github.com/foxcpp/maddy/internal/tls/acme"
 79)
 80
 81var (
 82	Version = "go-build"
 83
 84	enableDebugFlags = false
 85)
 86
 87func BuildInfo() string {
 88	version := Version
 89	if info, ok := debug.ReadBuildInfo(); ok && info.Main.Version != "(devel)" {
 90		version = info.Main.Version
 91	}
 92
 93	return fmt.Sprintf(`%s %s/%s %s
 94
 95default config: %s
 96default state_dir: %s
 97default runtime_dir: %s`,
 98		version, runtime.GOOS, runtime.GOARCH, runtime.Version(),
 99		filepath.Join(ConfigDirectory, "maddy.conf"),
100		DefaultStateDirectory,
101		DefaultRuntimeDirectory)
102}
103
104func init() {
105	maddycli.AddGlobalFlag(
106		&cli.PathFlag{
107			Name:    "config",
108			Usage:   "Configuration file to use",
109			EnvVars: []string{"MADDY_CONFIG"},
110			Value:   filepath.Join(ConfigDirectory, "maddy.conf"),
111		},
112	)
113	maddycli.AddGlobalFlag(&cli.BoolFlag{
114		Name:        "debug",
115		Usage:       "enable debug logging early",
116		Destination: &log.DefaultLogger.Debug,
117	})
118	maddycli.AddSubcommand(&cli.Command{
119		Name:  "run",
120		Usage: "Start the server",
121		Flags: []cli.Flag{
122			&cli.StringFlag{
123				Name:        "libexec",
124				Value:       DefaultLibexecDirectory,
125				Usage:       "path to the libexec directory",
126				Destination: &config.LibexecDirectory,
127			},
128			&cli.StringSliceFlag{
129				Name:  "log",
130				Usage: "default logging target(s)",
131				Value: cli.NewStringSlice("stderr"),
132			},
133			&cli.BoolFlag{
134				Name:   "v",
135				Usage:  "print version and build metadata, then exit",
136				Hidden: true,
137			},
138		},
139		Action: Run,
140	})
141	maddycli.AddSubcommand(&cli.Command{
142		Name:  "version",
143		Usage: "Print version and build metadata, then exit",
144		Action: func(c *cli.Context) error {
145			fmt.Println(BuildInfo())
146			return nil
147		},
148	})
149
150	if enableDebugFlags {
151		maddycli.AddGlobalFlag(&cli.StringFlag{
152			Name:  "debug.pprof",
153			Usage: "enable live profiler HTTP endpoint and listen on the specified address",
154		})
155		maddycli.AddGlobalFlag(&cli.IntFlag{
156			Name:  "debug.blockprofrate",
157			Usage: "set blocking profile rate",
158		})
159		maddycli.AddGlobalFlag(&cli.IntFlag{
160			Name:  "debug.mutexproffract",
161			Usage: "set mutex profile fraction",
162		})
163	}
164}
165
166// Run is the entry point for all server-running code. It takes care of command line arguments processing,
167// logging initialization, directives setup, configuration reading. After all that, it
168// calls moduleMain to initialize and run modules.
169func Run(c *cli.Context) error {
170	certmagic.UserAgent = "maddy/" + Version
171
172	if c.NArg() != 0 {
173		return cli.Exit(fmt.Sprintln("usage:", os.Args[0], "[options]"), 2)
174	}
175
176	if c.Bool("v") {
177		fmt.Println("maddy", BuildInfo())
178		return nil
179	}
180
181	var err error
182	log.DefaultLogger.Out, err = LogOutputOption(c.StringSlice("log"))
183	if err != nil {
184		systemdStatusErr(err)
185		return cli.Exit(err.Error(), 2)
186	}
187
188	initDebug(c)
189
190	os.Setenv("PATH", config.LibexecDirectory+string(filepath.ListSeparator)+os.Getenv("PATH"))
191
192	f, err := os.Open(c.Path("config"))
193	if err != nil {
194		systemdStatusErr(err)
195		return cli.Exit(err.Error(), 2)
196	}
197	defer f.Close()
198
199	cfg, err := parser.Read(f, c.Path("config"))
200	if err != nil {
201		systemdStatusErr(err)
202		return cli.Exit(err.Error(), 2)
203	}
204
205	defer log.DefaultLogger.Out.Close()
206
207	if err := moduleMain(cfg); err != nil {
208		systemdStatusErr(err)
209		return cli.Exit(err.Error(), 1)
210	}
211
212	return nil
213}
214
215func initDebug(c *cli.Context) {
216	if !enableDebugFlags {
217		return
218	}
219
220	if c.IsSet("debug.pprof") {
221		profileEndpoint := c.String("debug.pprof")
222		go func() {
223			log.Println("listening on", "http://"+profileEndpoint, "for profiler requests")
224			log.Println("failed to listen on profiler endpoint:", http.ListenAndServe(profileEndpoint, nil))
225		}()
226	}
227
228	// These values can also be affected by environment so set them
229	// only if argument is specified.
230	if c.IsSet("debug.mutexproffract") {
231		runtime.SetMutexProfileFraction(c.Int("debug.mutexproffract"))
232	}
233	if c.IsSet("debug.blockprofrate") {
234		runtime.SetBlockProfileRate(c.Int("debug.blockprofrate"))
235	}
236}
237
238func InitDirs() error {
239	if config.StateDirectory == "" {
240		config.StateDirectory = DefaultStateDirectory
241	}
242	if config.RuntimeDirectory == "" {
243		config.RuntimeDirectory = DefaultRuntimeDirectory
244	}
245	if config.LibexecDirectory == "" {
246		config.LibexecDirectory = DefaultLibexecDirectory
247	}
248
249	if err := ensureDirectoryWritable(config.StateDirectory); err != nil {
250		return err
251	}
252	if err := ensureDirectoryWritable(config.RuntimeDirectory); err != nil {
253		return err
254	}
255
256	// Make sure all paths we are going to use are absolute
257	// before we change the working directory.
258	if !filepath.IsAbs(config.StateDirectory) {
259		return errors.New("statedir should be absolute")
260	}
261	if !filepath.IsAbs(config.RuntimeDirectory) {
262		return errors.New("runtimedir should be absolute")
263	}
264	if !filepath.IsAbs(config.LibexecDirectory) {
265		return errors.New("-libexec should be absolute")
266	}
267
268	// Change the working directory to make all relative paths
269	// in configuration relative to state directory.
270	if err := os.Chdir(config.StateDirectory); err != nil {
271		log.Println(err)
272	}
273
274	return nil
275}
276
277func ensureDirectoryWritable(path string) error {
278	if err := os.MkdirAll(path, 0o700); err != nil {
279		return err
280	}
281
282	testFile, err := os.Create(filepath.Join(path, "writeable-test"))
283	if err != nil {
284		return err
285	}
286	testFile.Close()
287	return os.RemoveAll(testFile.Name())
288}
289
290func ReadGlobals(cfg []config.Node) (map[string]interface{}, []config.Node, error) {
291	globals := config.NewMap(nil, config.Node{Children: cfg})
292	globals.String("state_dir", false, false, DefaultStateDirectory, &config.StateDirectory)
293	globals.String("runtime_dir", false, false, DefaultRuntimeDirectory, &config.RuntimeDirectory)
294	globals.String("hostname", false, false, "", nil)
295	globals.String("autogenerated_msg_domain", false, false, "", nil)
296	globals.Custom("tls", false, false, nil, tls.TLSDirective, nil)
297	globals.Custom("tls_client", false, false, nil, tls.TLSClientBlock, nil)
298	globals.Bool("storage_perdomain", false, false, nil)
299	globals.Bool("auth_perdomain", false, false, nil)
300	globals.StringList("auth_domains", false, false, nil, nil)
301	globals.Custom("log", false, false, defaultLogOutput, logOutput, &log.DefaultLogger.Out)
302	globals.Bool("debug", false, log.DefaultLogger.Debug, &log.DefaultLogger.Debug)
303	config.EnumMapped(globals, "auth_map_normalize", true, false, authz.NormalizeFuncs, authz.NormalizeAuto, nil)
304	modconfig.Table(globals, "auth_map", true, false, nil, nil)
305	globals.AllowUnknown()
306	unknown, err := globals.Process()
307	return globals.Values, unknown, err
308}
309
310func moduleMain(cfg []config.Node) error {
311	globals, modBlocks, err := ReadGlobals(cfg)
312	if err != nil {
313		return err
314	}
315
316	if err := InitDirs(); err != nil {
317		return err
318	}
319
320	hooks.AddHook(hooks.EventLogRotate, reinitLogging)
321
322	endpoints, mods, err := RegisterModules(globals, modBlocks)
323	if err != nil {
324		return err
325	}
326
327	err = initModules(globals, endpoints, mods)
328	if err != nil {
329		return err
330	}
331
332	systemdStatus(SDReady, "Listening for incoming connections...")
333
334	handleSignals()
335
336	systemdStatus(SDStopping, "Waiting for running transactions to complete...")
337
338	hooks.RunHooks(hooks.EventShutdown)
339
340	return nil
341}
342
343type ModInfo struct {
344	Instance module.Module
345	Cfg      config.Node
346}
347
348func RegisterModules(globals map[string]interface{}, nodes []config.Node) (endpoints, mods []ModInfo, err error) {
349	mods = make([]ModInfo, 0, len(nodes))
350
351	for _, block := range nodes {
352		var instName string
353		var modAliases []string
354		if len(block.Args) == 0 {
355			instName = block.Name
356		} else {
357			instName = block.Args[0]
358			modAliases = block.Args[1:]
359		}
360
361		modName := block.Name
362
363		endpFactory := module.GetEndpoint(modName)
364		if endpFactory != nil {
365			inst, err := endpFactory(modName, block.Args)
366			if err != nil {
367				return nil, nil, err
368			}
369
370			endpoints = append(endpoints, ModInfo{Instance: inst, Cfg: block})
371			continue
372		}
373
374		factory := module.Get(modName)
375		if factory == nil {
376			return nil, nil, config.NodeErr(block, "unknown module or global directive: %s", modName)
377		}
378
379		if module.HasInstance(instName) {
380			return nil, nil, config.NodeErr(block, "config block named %s already exists", instName)
381		}
382
383		inst, err := factory(modName, instName, modAliases, nil)
384		if err != nil {
385			return nil, nil, err
386		}
387
388		module.RegisterInstance(inst, config.NewMap(globals, block))
389		for _, alias := range modAliases {
390			if module.HasInstance(alias) {
391				return nil, nil, config.NodeErr(block, "config block named %s already exists", alias)
392			}
393			module.RegisterAlias(alias, instName)
394		}
395
396		log.Debugf("%v:%v: register config block %v %v", block.File, block.Line, instName, modAliases)
397		mods = append(mods, ModInfo{Instance: inst, Cfg: block})
398	}
399
400	if len(endpoints) == 0 {
401		return nil, nil, fmt.Errorf("at least one endpoint should be configured")
402	}
403
404	return endpoints, mods, nil
405}
406
407func initModules(globals map[string]interface{}, endpoints, mods []ModInfo) error {
408	for _, endp := range endpoints {
409		if err := endp.Instance.Init(config.NewMap(globals, endp.Cfg)); err != nil {
410			return err
411		}
412
413		if closer, ok := endp.Instance.(io.Closer); ok {
414			endp := endp
415			hooks.AddHook(hooks.EventShutdown, func() {
416				log.Debugf("close %s (%s)", endp.Instance.Name(), endp.Instance.InstanceName())
417				if err := closer.Close(); err != nil {
418					log.Printf("module %s (%s) close failed: %v", endp.Instance.Name(), endp.Instance.InstanceName(), err)
419				}
420			})
421		}
422	}
423
424	for _, inst := range mods {
425		if module.Initialized[inst.Instance.InstanceName()] {
426			continue
427		}
428
429		return fmt.Errorf("Unused configuration block at %s:%d - %s (%s)",
430			inst.Cfg.File, inst.Cfg.Line, inst.Instance.InstanceName(), inst.Instance.Name())
431	}
432
433	return nil
434}