Working prefix
parent
7eaa92cb20
commit
e7f8fc93e4
|
@ -33,6 +33,8 @@ type Server struct {
|
||||||
config *Config
|
config *Config
|
||||||
httpServer *http.Server
|
httpServer *http.Server
|
||||||
httpsServer *http.Server
|
httpsServer *http.Server
|
||||||
|
smtpServer *smtp.Server
|
||||||
|
smtpBackend *smtpBackend
|
||||||
topics map[string]*topic
|
topics map[string]*topic
|
||||||
visitors map[string]*visitor
|
visitors map[string]*visitor
|
||||||
firebase subscriber
|
firebase subscriber
|
||||||
|
@ -85,11 +87,12 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
topicRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}$`) // Regex must match JS & Android app!
|
topicRegex = regexp.MustCompile(`^[-_A-Za-z0-9]{1,64}$`) // No /!
|
||||||
jsonRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/json$`)
|
topicPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}$`) // Regex must match JS & Android app!
|
||||||
sseRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/sse$`)
|
jsonPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/json$`)
|
||||||
rawRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/raw$`)
|
ssePathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/sse$`)
|
||||||
sendRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/(publish|send|trigger)$`)
|
rawPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/raw$`)
|
||||||
|
publishPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/(publish|send|trigger)$`)
|
||||||
|
|
||||||
staticRegex = regexp.MustCompile(`^/static/.+`)
|
staticRegex = regexp.MustCompile(`^/static/.+`)
|
||||||
docsRegex = regexp.MustCompile(`^/docs(|/.*)$`)
|
docsRegex = regexp.MustCompile(`^/docs(|/.*)$`)
|
||||||
|
@ -225,6 +228,9 @@ func (s *Server) Run() error {
|
||||||
if s.config.ListenHTTPS != "" {
|
if s.config.ListenHTTPS != "" {
|
||||||
listenStr += fmt.Sprintf(" %s/https", s.config.ListenHTTPS)
|
listenStr += fmt.Sprintf(" %s/https", s.config.ListenHTTPS)
|
||||||
}
|
}
|
||||||
|
if s.config.SMTPServerListen != "" {
|
||||||
|
listenStr += fmt.Sprintf(" %s/smtp", s.config.SMTPServerListen)
|
||||||
|
}
|
||||||
log.Printf("Listening on %s", listenStr)
|
log.Printf("Listening on %s", listenStr)
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.HandleFunc("/", s.handle)
|
mux.HandleFunc("/", s.handle)
|
||||||
|
@ -243,7 +249,7 @@ func (s *Server) Run() error {
|
||||||
}
|
}
|
||||||
if s.config.SMTPServerListen != "" {
|
if s.config.SMTPServerListen != "" {
|
||||||
go func() {
|
go func() {
|
||||||
errChan <- s.runMailserver()
|
errChan <- s.runSMTPServer()
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
|
@ -264,6 +270,9 @@ func (s *Server) Stop() {
|
||||||
if s.httpsServer != nil {
|
if s.httpsServer != nil {
|
||||||
s.httpsServer.Close()
|
s.httpsServer.Close()
|
||||||
}
|
}
|
||||||
|
if s.smtpServer != nil {
|
||||||
|
s.smtpServer.Close()
|
||||||
|
}
|
||||||
close(s.closeChan)
|
close(s.closeChan)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -295,17 +304,17 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request) error {
|
||||||
return s.handleDocs(w, r)
|
return s.handleDocs(w, r)
|
||||||
} else if r.Method == http.MethodOptions {
|
} else if r.Method == http.MethodOptions {
|
||||||
return s.handleOptions(w, r)
|
return s.handleOptions(w, r)
|
||||||
} else if r.Method == http.MethodGet && topicRegex.MatchString(r.URL.Path) {
|
} else if r.Method == http.MethodGet && topicPathRegex.MatchString(r.URL.Path) {
|
||||||
return s.handleTopic(w, r)
|
return s.handleTopic(w, r)
|
||||||
} else if (r.Method == http.MethodPut || r.Method == http.MethodPost) && topicRegex.MatchString(r.URL.Path) {
|
} else if (r.Method == http.MethodPut || r.Method == http.MethodPost) && topicPathRegex.MatchString(r.URL.Path) {
|
||||||
return s.withRateLimit(w, r, s.handlePublish)
|
return s.withRateLimit(w, r, s.handlePublish)
|
||||||
} else if r.Method == http.MethodGet && sendRegex.MatchString(r.URL.Path) {
|
} else if r.Method == http.MethodGet && publishPathRegex.MatchString(r.URL.Path) {
|
||||||
return s.withRateLimit(w, r, s.handlePublish)
|
return s.withRateLimit(w, r, s.handlePublish)
|
||||||
} else if r.Method == http.MethodGet && jsonRegex.MatchString(r.URL.Path) {
|
} else if r.Method == http.MethodGet && jsonPathRegex.MatchString(r.URL.Path) {
|
||||||
return s.withRateLimit(w, r, s.handleSubscribeJSON)
|
return s.withRateLimit(w, r, s.handleSubscribeJSON)
|
||||||
} else if r.Method == http.MethodGet && sseRegex.MatchString(r.URL.Path) {
|
} else if r.Method == http.MethodGet && ssePathRegex.MatchString(r.URL.Path) {
|
||||||
return s.withRateLimit(w, r, s.handleSubscribeSSE)
|
return s.withRateLimit(w, r, s.handleSubscribeSSE)
|
||||||
} else if r.Method == http.MethodGet && rawRegex.MatchString(r.URL.Path) {
|
} else if r.Method == http.MethodGet && rawPathRegex.MatchString(r.URL.Path) {
|
||||||
return s.withRateLimit(w, r, s.handleSubscribeRaw)
|
return s.withRateLimit(w, r, s.handleSubscribeRaw)
|
||||||
}
|
}
|
||||||
return errHTTPNotFound
|
return errHTTPNotFound
|
||||||
|
@ -726,12 +735,15 @@ func (s *Server) updateStatsAndPrune() {
|
||||||
messages += msgs
|
messages += msgs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mail
|
||||||
|
mailSuccess, mailFailure := s.smtpBackend.Counts()
|
||||||
|
|
||||||
// Print stats
|
// Print stats
|
||||||
log.Printf("Stats: %d message(s) published, %d topic(s) active, %d subscriber(s), %d message(s) buffered, %d visitor(s)",
|
log.Printf("Stats: %d message(s) published, %d in cache, %d successful mails, %d failed, %d topic(s) active, %d subscriber(s), %d visitor(s)",
|
||||||
s.messages, len(s.topics), subscribers, messages, len(s.visitors))
|
s.messages, messages, mailSuccess, mailFailure, len(s.topics), subscribers, len(s.visitors))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) runMailserver() error {
|
func (s *Server) runSMTPServer() error {
|
||||||
sub := func(m *message) error {
|
sub := func(m *message) error {
|
||||||
url := fmt.Sprintf("%s/%s", s.config.BaseURL, m.Topic)
|
url := fmt.Sprintf("%s/%s", s.config.BaseURL, m.Topic)
|
||||||
req, err := http.NewRequest("PUT", url, strings.NewReader(m.Message))
|
req, err := http.NewRequest("PUT", url, strings.NewReader(m.Message))
|
||||||
|
@ -748,18 +760,16 @@ func (s *Server) runMailserver() error {
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
ms := smtp.NewServer(newMailBackend(s.config, sub))
|
s.smtpBackend = newMailBackend(s.config, sub)
|
||||||
|
s.smtpServer = smtp.NewServer(s.smtpBackend)
|
||||||
ms.Addr = s.config.SMTPServerListen
|
s.smtpServer.Addr = s.config.SMTPServerListen
|
||||||
ms.Domain = s.config.SMTPServerDomain
|
s.smtpServer.Domain = s.config.SMTPServerDomain
|
||||||
ms.ReadTimeout = 10 * time.Second
|
s.smtpServer.ReadTimeout = 10 * time.Second
|
||||||
ms.WriteTimeout = 10 * time.Second
|
s.smtpServer.WriteTimeout = 10 * time.Second
|
||||||
ms.MaxMessageBytes = 2 * s.config.MessageLimit
|
s.smtpServer.MaxMessageBytes = 2 * s.config.MessageLimit
|
||||||
ms.MaxRecipients = 1
|
s.smtpServer.MaxRecipients = 1
|
||||||
ms.AllowInsecureAuth = true
|
s.smtpServer.AllowInsecureAuth = true
|
||||||
|
return s.smtpServer.ListenAndServe()
|
||||||
log.Println("Starting server at", ms.Addr)
|
|
||||||
return ms.ListenAndServe()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) runManager() {
|
func (s *Server) runManager() {
|
||||||
|
|
|
@ -5,17 +5,25 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"github.com/emersion/go-smtp"
|
"github.com/emersion/go-smtp"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
|
||||||
"net/mail"
|
"net/mail"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errInvalidDomain = errors.New("invalid domain")
|
||||||
|
errInvalidAddress = errors.New("invalid address")
|
||||||
|
errInvalidTopic = errors.New("invalid topic")
|
||||||
|
errTooManyRecipients = errors.New("too many recipients")
|
||||||
|
)
|
||||||
|
|
||||||
// smtpBackend implements SMTP server methods.
|
// smtpBackend implements SMTP server methods.
|
||||||
type smtpBackend struct {
|
type smtpBackend struct {
|
||||||
config *Config
|
config *Config
|
||||||
sub subscriber
|
sub subscriber
|
||||||
|
success int64
|
||||||
|
failure int64
|
||||||
|
mu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func newMailBackend(conf *Config, sub subscriber) *smtpBackend {
|
func newMailBackend(conf *Config, sub subscriber) *smtpBackend {
|
||||||
|
@ -26,18 +34,23 @@ func newMailBackend(conf *Config, sub subscriber) *smtpBackend {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *smtpBackend) Login(state *smtp.ConnectionState, username, password string) (smtp.Session, error) {
|
func (b *smtpBackend) Login(state *smtp.ConnectionState, username, password string) (smtp.Session, error) {
|
||||||
return &smtpSession{config: b.config, sub: b.sub}, nil
|
return &smtpSession{backend: b}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *smtpBackend) AnonymousLogin(state *smtp.ConnectionState) (smtp.Session, error) {
|
func (b *smtpBackend) AnonymousLogin(state *smtp.ConnectionState) (smtp.Session, error) {
|
||||||
return &smtpSession{config: b.config, sub: b.sub}, nil
|
return &smtpSession{backend: b}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *smtpBackend) Counts() (success int64, failure int64) {
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
return b.success, b.failure
|
||||||
}
|
}
|
||||||
|
|
||||||
// smtpSession is returned after EHLO.
|
// smtpSession is returned after EHLO.
|
||||||
type smtpSession struct {
|
type smtpSession struct {
|
||||||
config *Config
|
backend *smtpBackend
|
||||||
sub subscriber
|
topic string
|
||||||
from, to string
|
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,63 +59,84 @@ func (s *smtpSession) AuthPlain(username, password string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *smtpSession) Mail(from string, opts smtp.MailOptions) error {
|
func (s *smtpSession) Mail(from string, opts smtp.MailOptions) error {
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
s.from = from
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *smtpSession) Rcpt(to string) error {
|
func (s *smtpSession) Rcpt(to string) error {
|
||||||
s.mu.Lock()
|
return s.withFailCount(func() error {
|
||||||
defer s.mu.Unlock()
|
conf := s.backend.config
|
||||||
addressList, err := mail.ParseAddressList(to)
|
addressList, err := mail.ParseAddressList(to)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
} else if len(addressList) != 1 {
|
} else if len(addressList) != 1 {
|
||||||
return errors.New("only one recipient supported")
|
return errTooManyRecipients
|
||||||
} else if !strings.HasSuffix(addressList[0].Address, "@"+s.config.SMTPServerDomain) {
|
|
||||||
return errors.New("invalid domain")
|
|
||||||
} else if s.config.SMTPServerAddrPrefix != "" && !strings.HasPrefix(addressList[0].Address, s.config.SMTPServerAddrPrefix) {
|
|
||||||
return errors.New("invalid address")
|
|
||||||
}
|
}
|
||||||
// FIXME check topic format
|
to = addressList[0].Address
|
||||||
s.to = addressList[0].Address
|
if !strings.HasSuffix(to, "@"+conf.SMTPServerDomain) {
|
||||||
|
return errInvalidDomain
|
||||||
|
}
|
||||||
|
to = strings.TrimSuffix(to, "@"+conf.SMTPServerDomain)
|
||||||
|
if conf.SMTPServerAddrPrefix != "" {
|
||||||
|
if !strings.HasPrefix(to, conf.SMTPServerAddrPrefix) {
|
||||||
|
return errInvalidAddress
|
||||||
|
}
|
||||||
|
to = strings.TrimPrefix(to, conf.SMTPServerAddrPrefix)
|
||||||
|
}
|
||||||
|
if !topicRegex.MatchString(to) {
|
||||||
|
return errInvalidTopic
|
||||||
|
}
|
||||||
|
s.mu.Lock()
|
||||||
|
s.topic = to
|
||||||
|
s.mu.Unlock()
|
||||||
return nil
|
return nil
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *smtpSession) Data(r io.Reader) error {
|
func (s *smtpSession) Data(r io.Reader) error {
|
||||||
s.mu.Lock()
|
return s.withFailCount(func() error {
|
||||||
defer s.mu.Unlock()
|
b, err := io.ReadAll(r) // Protected by MaxMessageBytes
|
||||||
b, err := ioutil.ReadAll(r)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("Data:", string(b))
|
|
||||||
msg, err := mail.ReadMessage(bytes.NewReader(b))
|
msg, err := mail.ReadMessage(bytes.NewReader(b))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
body, err := io.ReadAll(msg.Body)
|
body, err := io.ReadAll(io.LimitReader(msg.Body, int64(s.backend.config.MessageLimit)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
topic := strings.TrimSuffix(s.to, "@"+s.config.SMTPServerDomain)
|
m := newDefaultMessage(s.topic, string(body))
|
||||||
m := newDefaultMessage(topic, string(body))
|
|
||||||
subject := msg.Header.Get("Subject")
|
subject := msg.Header.Get("Subject")
|
||||||
if subject != "" {
|
if subject != "" {
|
||||||
m.Title = subject
|
m.Title = subject
|
||||||
}
|
}
|
||||||
return s.sub(m)
|
if err := s.backend.sub(m); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.backend.mu.Lock()
|
||||||
|
s.backend.success++
|
||||||
|
s.backend.mu.Unlock()
|
||||||
|
return nil
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *smtpSession) Reset() {
|
func (s *smtpSession) Reset() {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
s.from = ""
|
s.topic = ""
|
||||||
s.to = ""
|
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *smtpSession) Logout() error {
|
func (s *smtpSession) Logout() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *smtpSession) withFailCount(fn func() error) error {
|
||||||
|
err := fn()
|
||||||
|
s.backend.mu.Lock()
|
||||||
|
defer s.backend.mu.Unlock()
|
||||||
|
if err != nil {
|
||||||
|
s.backend.failure++
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue