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 table
 20
 21import (
 22	"bufio"
 23	"context"
 24	"fmt"
 25	"os"
 26	"runtime/debug"
 27	"strings"
 28	"sync"
 29	"time"
 30
 31	"github.com/foxcpp/maddy/framework/config"
 32	"github.com/foxcpp/maddy/framework/hooks"
 33	"github.com/foxcpp/maddy/framework/log"
 34	"github.com/foxcpp/maddy/framework/module"
 35)
 36
 37const FileModName = "table.file"
 38
 39type File struct {
 40	instName string
 41	file     string
 42
 43	m      map[string][]string
 44	mLck   sync.RWMutex
 45	mStamp time.Time
 46
 47	stopReloader chan struct{}
 48	forceReload  chan struct{}
 49
 50	log log.Logger
 51}
 52
 53func NewFile(_, instName string, _, inlineArgs []string) (module.Module, error) {
 54	m := &File{
 55		instName:     instName,
 56		m:            make(map[string][]string),
 57		stopReloader: make(chan struct{}),
 58		forceReload:  make(chan struct{}),
 59		log:          log.Logger{Name: FileModName},
 60	}
 61
 62	switch len(inlineArgs) {
 63	case 1:
 64		m.file = inlineArgs[0]
 65	case 0:
 66	default:
 67		return nil, fmt.Errorf("%s: cannot use multiple files with single %s, use %s multiple times to do so", FileModName, FileModName, FileModName)
 68	}
 69
 70	return m, nil
 71}
 72
 73func (f *File) Name() string {
 74	return FileModName
 75}
 76
 77func (f *File) InstanceName() string {
 78	return f.instName
 79}
 80
 81func (f *File) Init(cfg *config.Map) error {
 82	var file string
 83	cfg.Bool("debug", true, false, &f.log.Debug)
 84	cfg.String("file", false, false, "", &file)
 85	if _, err := cfg.Process(); err != nil {
 86		return err
 87	}
 88
 89	if file != "" {
 90		if f.file != "" {
 91			return fmt.Errorf("%s: file path specified both in directive and in argument, do it once", FileModName)
 92		}
 93		f.file = file
 94	}
 95
 96	if err := readFile(f.file, f.m); err != nil {
 97		if !os.IsNotExist(err) {
 98			return err
 99		}
100		f.log.Printf("ignoring non-existent file: %s", f.file)
101	}
102
103	go f.reloader()
104	hooks.AddHook(hooks.EventReload, func() {
105		f.forceReload <- struct{}{}
106	})
107
108	return nil
109}
110
111var reloadInterval = 15 * time.Second
112
113func (f *File) reloader() {
114	defer func() {
115		if err := recover(); err != nil {
116			stack := debug.Stack()
117			log.Printf("panic during m reload: %v\n%s", err, stack)
118		}
119	}()
120
121	t := time.NewTicker(reloadInterval)
122	defer t.Stop()
123
124	for {
125		select {
126		case <-t.C:
127			f.reload()
128
129		case <-f.forceReload:
130			f.reload()
131
132		case <-f.stopReloader:
133			f.stopReloader <- struct{}{}
134			return
135		}
136	}
137}
138
139func (f *File) reload() {
140	info, err := os.Stat(f.file)
141	if err != nil {
142		if os.IsNotExist(err) {
143			f.mLck.Lock()
144			f.m = map[string][]string{}
145			f.mLck.Unlock()
146			return
147		}
148		f.log.Error("os stat", err)
149	}
150	if info.ModTime().Before(f.mStamp) || time.Since(info.ModTime()) < (reloadInterval/2) {
151		return // reload not necessary
152	}
153
154	f.log.Debugf("reloading")
155
156	newm := make(map[string][]string, len(f.m)+5)
157	if err := readFile(f.file, newm); err != nil {
158		if os.IsNotExist(err) {
159			f.log.Printf("ignoring non-existent file: %s", f.file)
160			return
161		}
162
163		f.log.Println(err)
164		return
165	}
166	// after reading we need to check whether file has changed in between
167	info2, err := os.Stat(f.file)
168	if err != nil {
169		f.log.Println(err)
170		return
171	}
172
173	if !info2.ModTime().Equal(info.ModTime()) {
174		// file has changed in the meantime
175		return
176	}
177
178	f.mLck.Lock()
179	f.m = newm
180	f.mStamp = info.ModTime()
181	f.mLck.Unlock()
182}
183
184func (f *File) Close() error {
185	f.stopReloader <- struct{}{}
186	<-f.stopReloader
187	return nil
188}
189
190func readFile(path string, out map[string][]string) error {
191	f, err := os.Open(path)
192	if err != nil {
193		return err
194	}
195
196	scnr := bufio.NewScanner(f)
197	lineCounter := 0
198
199	parseErr := func(text string) error {
200		return fmt.Errorf("%s:%d: %s", path, lineCounter, text)
201	}
202
203	for scnr.Scan() {
204		lineCounter++
205		if strings.HasPrefix(scnr.Text(), "#") {
206			continue
207		}
208
209		text := strings.TrimSpace(scnr.Text())
210		if text == "" {
211			continue
212		}
213
214		parts := strings.SplitN(text, ":", 2)
215		if len(parts) == 1 {
216			parts = append(parts, "")
217		}
218
219		from := strings.TrimSpace(parts[0])
220		if len(from) == 0 {
221			return parseErr("empty address before colon")
222		}
223
224		for _, to := range strings.Split(parts[1], ",") {
225			to := strings.TrimSpace(to)
226			out[from] = append(out[from], to)
227		}
228	}
229	return scnr.Err()
230}
231
232func (f *File) Lookup(_ context.Context, val string) (string, bool, error) {
233	// The existing map is never modified, instead it is replaced with a new
234	// one if reload is performed.
235	f.mLck.RLock()
236	usedFile := f.m
237	f.mLck.RUnlock()
238
239	newVal, ok := usedFile[val]
240
241	if len(newVal) == 0 {
242		return "", false, nil
243	}
244
245	return newVal[0], ok, nil
246}
247
248func (f *File) LookupMulti(_ context.Context, val string) ([]string, error) {
249	f.mLck.RLock()
250	usedFile := f.m
251	f.mLck.RUnlock()
252
253	return usedFile[val], nil
254}
255
256func init() {
257	module.Register(FileModName, NewFile)
258}