Skip to content

Commit

Permalink
add some validation for emails
Browse files Browse the repository at this point in the history
  • Loading branch information
pomdtr committed Jan 20, 2025
1 parent 1f2d0a3 commit 142ea3d
Show file tree
Hide file tree
Showing 8 changed files with 151 additions and 38 deletions.
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@ COPY --from=builder /build/smallweb /usr/local/bin/smallweb
ENV SMALLWEB_DIR=/smallweb
VOLUME ["$SMALLWEB_DIR"]

EXPOSE 7777
ENTRYPOINT ["/usr/local/bin/smallweb", "up", "--cron", "--addr", "0.0.0.0:7777"]
EXPOSE 7777 2222 2525
ENTRYPOINT ["/usr/local/bin/smallweb", "up", "--cron", "--http-addr", "0.0.0.0:7777", "--ssh-addr", "0.0.0.0:2222", "--smtp-addr", ":2525"]
161 changes: 135 additions & 26 deletions cmd/up.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cmd

import (
"bytes"
"context"
"crypto/tls"
"errors"
Expand All @@ -10,6 +11,7 @@ import (
"log/slog"
"net"
"net/http"
"net/mail"
"os"
"os/exec"
"os/signal"
Expand All @@ -26,7 +28,7 @@ import (
"github.com/charmbracelet/keygen"
"github.com/charmbracelet/ssh"
"github.com/creack/pty"
"github.com/mhale/smtpd"
"github.com/emersion/go-smtp"
"github.com/pkg/sftp"
"github.com/pomdtr/smallweb/app"
"github.com/pomdtr/smallweb/watcher"
Expand Down Expand Up @@ -303,36 +305,25 @@ func NewCmdUp() *cobra.Command {
if flags.smtpAddr != "" {
fmt.Fprintf(os.Stderr, "Starting SMTP server on %s...\n", flags.smtpAddr)

handler := func(remoteAddr net.Addr, from string, to []string, data []byte) error {
for _, recipient := range to {
parts := strings.SplitN(recipient, "@", 2)
appname, domain := parts[0], parts[1]
if domain != k.String("domain") {
continue
}
server := smtp.NewServer(smtp.BackendFunc(func(c *smtp.Conn) (smtp.Session, error) {
return &SmtpSession{
conn: c,
}, nil
}))

a, err := app.LoadApp(appname, k.String("dir"), k.String("domain"), slices.Contains(k.Strings("adminApps"), appname))
if err != nil {
if errors.Is(err, app.ErrAppNotFound) {
continue
}
server.Addr = flags.smtpAddr
server.Domain = k.String("domain")

return fmt.Errorf("failed to load app: %v", err)
}

wk := worker.NewWorker(a)
if err := wk.SendEmail(context.Background(), strings.NewReader(string(data))); err != nil {
return fmt.Errorf("failed to send email: %v", err)
}
if flags.cert != "" && flags.key != "" {
cert, err := tls.LoadX509KeyPair(flags.cert, flags.key)
if err != nil {
return fmt.Errorf("failed to load cert: %v", err)
}

return nil
}

if flags.cert != "" && flags.key != "" {
go smtpd.ListenAndServeTLS(flags.smtpAddr, flags.cert, flags.key, handler, "Smallweb", k.String("domain"))
server.TLSConfig = &tls.Config{Certificates: []tls.Certificate{cert}}
go server.ListenAndServeTLS()
} else {
go smtpd.ListenAndServe(flags.smtpAddr, handler, "Smallweb", k.String("domain"))
go server.ListenAndServe()
}
}

Expand Down Expand Up @@ -500,3 +491,121 @@ func (me *Handler) GetWorker(appname, rootDir, domain string) (*worker.Worker, e
me.workers[appname] = wk
return wk, nil
}

type SmtpSession struct {
conn *smtp.Conn
sender string
recipients []string
}

func (s *SmtpSession) Reset() {
s.sender = ""
s.recipients = nil
}

func (s *SmtpSession) Mail(from string, opts *smtp.MailOptions) error {
s.sender = from

parts := strings.Split(from, "@")
if len(parts) != 2 {
return fmt.Errorf("invalid email format: %s", from)
}
senderDomain := parts[1]

clientAddr := s.conn.Conn().RemoteAddr().String()
clientIp, _, err := net.SplitHostPort(clientAddr)
if err != nil {
return fmt.Errorf("failed to parse client IP: %w", err)
}

// if client is localhost, allow
if clientIp == "::1" {
return nil
}

// Check A/AAAA records for the sender domain
domainIps, err := net.LookupIP(senderDomain)
if err == nil {
for _, ip := range domainIps {
if ip.String() == clientIp {
return nil
}
}
}

// Check MX records and resolve their IPs
mxRecords, err := net.LookupMX(senderDomain)
if err == nil {
for _, mx := range mxRecords {
mxIps, err := net.LookupIP(mx.Host)
if err != nil {
continue
}
for _, ip := range mxIps {
if ip.String() == clientIp {
return nil
}
}
}
}

return fmt.Errorf("invalid sender domain: %s", senderDomain)
}
func (s *SmtpSession) Rcpt(to string, opts *smtp.RcptOptions) error {
parts := strings.Split(to, "@")
if parts[1] != k.String("domain") {
return fmt.Errorf("invalid sender domain: %s", parts[1])
}

s.recipients = append(s.recipients, to)
return nil
}

func (s *SmtpSession) Data(r io.Reader) error {
data, err := io.ReadAll(r)
if err != nil {
return fmt.Errorf("failed to read data: %w", err)
}

msg, err := mail.ReadMessage(bytes.NewReader(data))
if err != nil {
return fmt.Errorf("failed to read message: %w", err)
}

from := msg.Header.Get("From")
if fromAddr, err := mail.ParseAddress(from); err != nil || fromAddr.Address != s.sender {
return fmt.Errorf("invalid sender: %s", s.sender)
}

toAddrs, err := msg.Header.AddressList("To")
if err != nil {
return fmt.Errorf("failed to parse To header: %w", err)
}

for _, recipient := range s.recipients {
if !slices.ContainsFunc(toAddrs, func(toAddr *mail.Address) bool {
return recipient == toAddr.Address
}) {
return fmt.Errorf("invalid recipient: %s", recipient)
}

parts := strings.Split(recipient, "@")
if parts[1] != k.String("domain") {
return fmt.Errorf("invalid recipient domain: %s", parts[1])
}

a, err := app.LoadApp(parts[0], k.String("dir"), k.String("domain"), slices.Contains(k.Strings("adminApps"), parts[0]))
if err != nil {
return fmt.Errorf("failed to load app: %w", err)
}

wk := worker.NewWorker(a)
wk.SendEmail(context.Background(), bytes.NewReader(data))
}

return nil
}

func (s *SmtpSession) Logout() error {
return nil
}
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ services:
ports:
- 7777:7777
- 2222:2222
- 2525:2525
volumes:
- ./examples:/smallweb
2 changes: 1 addition & 1 deletion examples/inbox/email.eml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
Subject: Ceci est un test
From: [email protected]
From: "Myself" <me@localhost>
To: [email protected]
Content-Type: text/plain; charset=utf-8

Expand Down
4 changes: 2 additions & 2 deletions examples/inbox/run.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/bin/sh

curl smtp://localhost:2525 \
--mail-from '[email protected]' \
curl -v smtp://localhost:2525 \
--mail-from 'me@localhost' \
--mail-rcpt '[email protected]' \
--upload-file email.eml
7 changes: 4 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,15 @@ require (
require (
github.com/caddyserver/certmagic v0.21.4
github.com/carapace-sh/carapace v1.5.0
github.com/charmbracelet/keygen v0.5.1
github.com/charmbracelet/ssh v0.0.0-20241211182756-4fe22b0f1b7c
github.com/creack/pty v1.1.21
github.com/emersion/go-smtp v0.21.3
github.com/fsnotify/fsnotify v1.8.0
github.com/getsops/sops/v3 v3.9.1
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/google/uuid v1.6.0
github.com/knadh/koanf/providers/confmap v0.1.0
github.com/knadh/koanf/providers/posflag v0.1.0
github.com/mhale/smtpd v0.8.3
github.com/pkg/sftp v1.13.7
github.com/robfig/cron/v3 v3.0.1
golang.org/x/crypto v0.31.0
Expand Down Expand Up @@ -83,7 +83,6 @@ require (
github.com/caddyserver/zerossl v0.1.3 // indirect
github.com/carapace-sh/carapace-shlex v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/charmbracelet/keygen v0.5.1 // indirect
github.com/charmbracelet/lipgloss v0.13.0 // indirect
github.com/charmbracelet/x/ansi v0.2.3 // indirect
github.com/charmbracelet/x/conpty v0.1.0 // indirect
Expand All @@ -94,6 +93,7 @@ require (
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dlclark/regexp2 v1.11.0 // indirect
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 // indirect
github.com/fatih/color v1.17.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/getsops/gopgagent v0.0.0-20240527072608-0c14999532fe // indirect
Expand All @@ -106,6 +106,7 @@ require (
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/s2a-go v0.1.8 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
github.com/googleapis/gax-go/v2 v2.13.0 // indirect
github.com/gorilla/css v1.0.1 // indirect
Expand Down
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,10 @@ github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-smtp v0.21.3 h1:7uVwagE8iPYE48WhNsng3RRpCUpFvNl39JGNSIyGVMY=
github.com/emersion/go-smtp v0.21.3/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
Expand Down Expand Up @@ -300,8 +304,6 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mhale/smtpd v0.8.3 h1:8j8YNXajksoSLZja3HdwvYVZPuJSqAxFsib3adzRRt8=
github.com/mhale/smtpd v0.8.3/go.mod h1:MQl+y2hwIEQCXtNhe5+55n0GZOjSmeqORDIXbqUL3x4=
github.com/mholt/acmez/v2 v2.0.3 h1:CgDBlEwg3QBp6s45tPQmFIBrkRIkBT4rW4orMM6p4sw=
github.com/mholt/acmez/v2 v2.0.3/go.mod h1:pQ1ysaDeGrIMvJ9dfJMk5kJNkn7L2sb3UhyrX6Q91cw=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
Expand Down
4 changes: 2 additions & 2 deletions worker/worker.go
Original file line number Diff line number Diff line change
Expand Up @@ -508,7 +508,7 @@ func (me *Worker) Command(ctx context.Context, args ...string) (*exec.Cmd, error
return command, nil
}

func (me *Worker) SendEmail(ctx context.Context, message io.Reader) error {
func (me *Worker) SendEmail(ctx context.Context, data io.Reader) error {
deno, err := DenoExecutable()
if err != nil {
return fmt.Errorf("could not find deno executable")
Expand Down Expand Up @@ -541,7 +541,7 @@ func (me *Worker) SendEmail(ctx context.Context, message io.Reader) error {

go func() {
defer stdin.Close()
io.Copy(stdin, message)
io.Copy(stdin, data)
}()

return command.Run()
Expand Down

0 comments on commit 142ea3d

Please sign in to comment.