├── .version ├── lookup ├── relay.go ├── utils.go ├── structs.go └── auth.go ├── .deb └── control.tpl ├── handlers ├── index.go ├── structs.go ├── metrics.go └── auth.go ├── .gitignore ├── go.mod ├── nginx-mail-auth-http-server.conf.example ├── Dockerfile ├── configuration ├── read_configuration.go └── structs.go ├── .rpm └── nginx-mail-auth-http-server.spec.tpl ├── LICENSE ├── metrics ├── structs.go └── metrics.go ├── Makefile ├── localhost.crt ├── go.sum ├── localhost.key ├── main.go ├── README.md └── .github └── workflows └── release.yml /.version: -------------------------------------------------------------------------------- 1 | 2.1.3 2 | -------------------------------------------------------------------------------- /lookup/relay.go: -------------------------------------------------------------------------------- 1 | package lookup 2 | -------------------------------------------------------------------------------- /lookup/utils.go: -------------------------------------------------------------------------------- 1 | package lookup 2 | 3 | func WrapSecret(secret string) (result string) { 4 | if ShowSecretsInLog { 5 | return secret 6 | } else { 7 | return "***" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.deb/control.tpl: -------------------------------------------------------------------------------- 1 | Package: nginx-mail-auth-http-server 2 | Version: __VERSION__ 3 | Section: custom 4 | Priority: optional 5 | Architecture: all 6 | Essential: no 7 | Installed-Size: __SIZE__ 8 | Maintainer: reinvented-stuff.com 9 | Description: A reinvented server for Nginx mail_auth_http module 10 | -------------------------------------------------------------------------------- /handlers/index.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "fmt" 5 | "html" 6 | "net/http" 7 | 8 | "nginx_auth_server/server/metrics" 9 | ) 10 | 11 | func (env *Handlers) Index(rw http.ResponseWriter, req *http.Request) { 12 | metrics.Metrics.Inc("RequestIndex", 1) 13 | fmt.Fprintf(rw, "%s v%s\n", html.EscapeString(env.ApplicationDescription), html.EscapeString(env.BuildVersion)) 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | *.conf 18 | *.log 19 | *.bak 20 | .* 21 | 22 | nginx-mail-auth-http-server 23 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module nginx_auth_server/server 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/go-sql-driver/mysql v1.6.0 7 | github.com/jmoiron/sqlx v1.3.5 8 | github.com/lib/pq v1.10.7 9 | github.com/rs/zerolog v1.28.0 10 | github.com/sony/sonyflake v1.1.0 11 | golang.org/x/sync v0.1.0 12 | ) 13 | 14 | require ( 15 | github.com/mattn/go-colorable v0.1.12 // indirect 16 | github.com/mattn/go-isatty v0.0.14 // indirect 17 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /nginx-mail-auth-http-server.conf.example: -------------------------------------------------------------------------------- 1 | { 2 | "listen": "127.0.0.1:8080", 3 | "database": { 4 | "uri": "user:pass@tcp(127.0.0.1:3306)/postfix", 5 | "auth_lookup_queries": [ 6 | "SELECT '127.0.0.1' as address, 25 as port FROM virtual_mailbox_maps WHERE email_address = :user AND password = MD5(:pass) AND is_active = 1" 7 | ], 8 | "relay_lookup_queries": [ 9 | "SELECT '127.0.0.1' as address, 25 as port FROM virtual_mailbox_maps WHERE email_address = :mailTo AND is_active = 1" 10 | ] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /handlers/structs.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "github.com/jmoiron/sqlx" 5 | "github.com/sony/sonyflake" 6 | 7 | "nginx_auth_server/server/lookup" 8 | "nginx_auth_server/server/metrics" 9 | ) 10 | 11 | type Handlers struct { 12 | DB *sqlx.DB `` 13 | MetricsCounter *metrics.MetricsStruct `` 14 | Lookup *lookup.LookupStruct `` 15 | ApplicationDescription string `` 16 | BuildVersion string `` 17 | Flake *sonyflake.Sonyflake `` 18 | } 19 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.18.1-alpine AS builder 2 | 3 | ARG BUILD_VERSION=0.0.0 4 | ARG TARGETOS=linux 5 | ARG TARGETARCH=amd64 6 | ARG PROGNAME=nginx-mail-auth-http-server 7 | ARG LISTEN_ADDRESS="127.0.0.1" 8 | ARG LISTEN_PORT="8080" 9 | 10 | RUN mkdir -p -v /src 11 | WORKDIR /src 12 | COPY . /src 13 | 14 | RUN apk add git 15 | RUN GOOS="${TARGETOS}" GOARCH="${TARGETARCH}" go build -ldflags="-X 'main.BuildVersion=${BUILD_VERSION}'" -v -o nginx-mail-auth-http-server . 16 | 17 | 18 | FROM alpine:3.16 19 | 20 | COPY --from=builder /src/nginx-mail-auth-http-server nginx-mail-auth-http-server 21 | 22 | ENTRYPOINT ["./nginx-mail-auth-http-server"] 23 | -------------------------------------------------------------------------------- /lookup/structs.go: -------------------------------------------------------------------------------- 1 | package lookup 2 | 3 | var Debug bool = false 4 | var ShowSecretsInLog bool = false 5 | 6 | type LookupStruct struct { 7 | ShowSecretsInLog bool 8 | } 9 | 10 | type authResultStruct struct { 11 | AuthStatus string 12 | AuthServer string 13 | AuthPort int 14 | AuthWait string 15 | AuthErrorCode string 16 | AuthViaRelay bool 17 | AuthViaLogin bool 18 | } 19 | 20 | type UpstreamStruct struct { 21 | Address string `json:"address"` 22 | Port int `json:"port"` 23 | } 24 | 25 | type QueryParamsStruct struct { 26 | User string `db:"User"` 27 | Pass string `db:"Pass"` 28 | RcptTo string `db:"RcptTo"` 29 | MailFrom string `db:"MailFrom"` 30 | ClientIP string `db:"ClientIP"` 31 | } 32 | -------------------------------------------------------------------------------- /configuration/read_configuration.go: -------------------------------------------------------------------------------- 1 | package configuration 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | 7 | "github.com/rs/zerolog/log" 8 | ) 9 | 10 | func ReadConfigurationFile(configPtr string, configuration *ConfigurationStruct) { 11 | 12 | log.Debug().Msgf("Loading configuration file '%s'", configPtr) 13 | 14 | configFile, _ := os.Open(configPtr) 15 | defer configFile.Close() 16 | 17 | JSONDecoder := json.NewDecoder(configFile) 18 | 19 | err := JSONDecoder.Decode(&configuration) 20 | if err != nil { 21 | log.Fatal(). 22 | Err(err). 23 | Str("stage", "init"). 24 | Msgf("Error while loading configuration file '%s'", configPtr) 25 | } 26 | 27 | configuration.ConfigFile = configPtr 28 | 29 | log.Debug().Msg("Finished loading configuration file") 30 | 31 | } 32 | -------------------------------------------------------------------------------- /.rpm/nginx-mail-auth-http-server.spec.tpl: -------------------------------------------------------------------------------- 1 | Name: nginx-mail-auth-http-server 2 | Version: __VERSION__ 3 | Release: 1%{?dist} 4 | Summary: A reinvented server for Nginx mail_auth_http module 5 | 6 | License: MIT 7 | URL: https://reinvented-stuff.com/nginx-mail-auth-http-server 8 | Source0: __SOURCE_TARGZ_FILENAME__ 9 | 10 | 11 | %description 12 | nginx-mail-auth-http-server provides a simple way to authorise 13 | your mail server clients and direct the connections to a correct 14 | mail backend using Nginx as a reverse proxy. 15 | 16 | %prep 17 | %setup -q 18 | 19 | 20 | %build 21 | make %{?_smp_mflags} build 22 | 23 | 24 | %install 25 | rm -rf $RPM_BUILD_ROOT 26 | %make_install 27 | 28 | 29 | %files 30 | %attr(755, root, root) /usr/bin/nginx-mail-auth-http-server 31 | %attr(644, root, root) /usr/share/doc/nginx-mail-auth-http-server-__VERSION__/README.md 32 | %doc 33 | 34 | 35 | 36 | %changelog 37 | -------------------------------------------------------------------------------- /configuration/structs.go: -------------------------------------------------------------------------------- 1 | package configuration 2 | 3 | import ( 4 | "net" 5 | 6 | "nginx_auth_server/server/metrics" 7 | 8 | "github.com/jmoiron/sqlx" 9 | "github.com/sony/sonyflake" 10 | ) 11 | 12 | type DatabaseStruct struct { 13 | URI string `json:"uri"` 14 | Driver string `json:"driver"` 15 | AuthLookupQueries []string `json:"auth_lookup_queries"` 16 | RelayLookupQueries []string `json:"relay_lookup_queries"` 17 | } 18 | 19 | type ConfigurationStruct struct { 20 | ListenAddress string `json:"listen"` 21 | ListenNetTCPAddr *net.TCPAddr `` 22 | Logfile string `json:"logfile"` 23 | Database DatabaseStruct `json:"database"` 24 | DB *sqlx.DB `` 25 | Metrics metrics.MetricsStruct `` 26 | Flake sonyflake.Sonyflake `` 27 | ApplicationDescription string `` 28 | BuildVersion string `` 29 | ConfigFile string `` 30 | } 31 | 32 | var Configuration = ConfigurationStruct{} 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Reinvented Stuff 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /metrics/structs.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | type MetricEntity struct { 9 | Tag string 10 | Value int32 11 | } 12 | 13 | type MetricsStruct struct { 14 | Warnings int32 15 | Errors int32 16 | 17 | AuthRequests int32 18 | AuthRequestsFailed int32 19 | AuthRequestsFailedRelay int32 20 | AuthRequestsFailedLogin int32 21 | AuthRequestsSuccess int32 22 | AuthRequestsSuccessRelay int32 23 | AuthRequestsSuccessLogin int32 24 | AuthRequestsRelay int32 25 | AuthRequestsLogin int32 26 | InternalErrors int32 27 | 28 | Entities map[string]int32 29 | 30 | IncrementMutex sync.Mutex 31 | 32 | Started time.Time 33 | } 34 | 35 | var Metrics = MetricsStruct{ 36 | AuthRequests: 0, 37 | AuthRequestsFailed: 0, 38 | AuthRequestsFailedRelay: 0, 39 | AuthRequestsFailedLogin: 0, 40 | AuthRequestsSuccess: 0, 41 | AuthRequestsSuccessRelay: 0, 42 | AuthRequestsSuccessLogin: 0, 43 | AuthRequestsRelay: 0, 44 | AuthRequestsLogin: 0, 45 | InternalErrors: 0, 46 | 47 | Entities: make(map[string]int32), 48 | } 49 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION := $(shell cat .version ) 2 | PLATFORM := $(shell uname -s | tr [A-Z] [a-z]) 3 | GO = go 4 | 5 | PROGNAME = nginx-mail-auth-http-server 6 | PROGNAME_VERSION = $(PROGNAME)-$(VERSION) 7 | TARGZ_FILENAME = $(PROGNAME)-$(VERSION).tar.gz 8 | TARGZ_CONTENTS = nginx-mail-auth-http-server README.md Makefile .version 9 | 10 | PREFIX = /tmp 11 | PWD = $(shell pwd) 12 | 13 | export PROGROOT=$(PWD)/$(PROGNAME_VERSION) 14 | 15 | .PHONY: all version build clean install test 16 | 17 | $(TARGZ_FILENAME): 18 | mkdir -vp "$(PROGNAME_VERSION)" 19 | cp -v $(TARGZ_CONTENTS) "$(PROGNAME_VERSION)/" 20 | tar -zvcf "$(TARGZ_FILENAME)" "$(PROGNAME_VERSION)" 21 | 22 | $(PROGNAME): 23 | env GOOS="$(PLATFORM)" $(GO) build -ldflags="-X 'main.BuildVersion=$(VERSION)'" -v -o "$(PROGNAME)" . 24 | 25 | test: 26 | @echo "Not implemented yet" 27 | 28 | install: 29 | install -d $(DESTDIR)/usr/share/doc/$(PROGNAME_VERSION) 30 | install -d $(DESTDIR)/usr/bin 31 | install -m 755 $(PROGNAME) $(DESTDIR)/usr/bin 32 | install -m 644 README.md $(DESTDIR)/usr/share/doc/$(PROGNAME_VERSION) 33 | 34 | clean: 35 | rm -vf "$(PROGNAME)" 36 | 37 | build: $(PROGNAME) 38 | 39 | compress: $(TARGZ_FILENAME) 40 | -------------------------------------------------------------------------------- /handlers/metrics.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "nginx_auth_server/server/metrics" 8 | ) 9 | 10 | func (env *Handlers) Metrics(rw http.ResponseWriter, req *http.Request) { 11 | 12 | fmt.Fprintf(rw, "# TYPE AuthRequests counter\n") 13 | fmt.Fprintf(rw, "# HELP Number of events happened in Nginx Mail Auth Server\n") 14 | 15 | for item := range metrics.Metrics.Entities { 16 | fmt.Fprintf(rw, "AuthRequests{kind=\"%v\"} %v\n", item, metrics.Metrics.Entities[item]) 17 | } 18 | 19 | // fmt.Fprintf(rw, "# TYPE AuthRequests counter\n") 20 | // fmt.Fprintf(rw, "AuthRequests{result=\"started\"} %v\n", metrics.Metrics.AuthRequests) 21 | // fmt.Fprintf(rw, "AuthRequests{result=\"fail\"} %v\n", metrics.Metrics.AuthRequestsFailed) 22 | // fmt.Fprintf(rw, "AuthRequests{result=\"fail_relay\"} %v\n", metrics.Metrics.AuthRequestsFailedRelay) 23 | // fmt.Fprintf(rw, "AuthRequests{result=\"fail_login\"} %v\n", metrics.Metrics.AuthRequestsFailedLogin) 24 | // fmt.Fprintf(rw, "AuthRequests{result=\"success\"} %v\n", metrics.Metrics.AuthRequestsSuccess) 25 | // fmt.Fprintf(rw, "AuthRequests{result=\"success_relay\"} %v\n", metrics.Metrics.AuthRequestsSuccessRelay) 26 | // fmt.Fprintf(rw, "AuthRequests{result=\"success_login\"} %v\n", metrics.Metrics.AuthRequestsSuccessLogin) 27 | // fmt.Fprintf(rw, "AuthRequests{kind=\"relay\"} %v\n", metrics.Metrics.AuthRequestsRelay) 28 | // fmt.Fprintf(rw, "AuthRequests{kind=\"login\"} %v\n", metrics.Metrics.AuthRequestsLogin) 29 | 30 | fmt.Fprintf(rw, "# TYPE InternalErrors counter\n") 31 | fmt.Fprintf(rw, "InternalErrors %v\n", metrics.Metrics.InternalErrors) 32 | 33 | } 34 | -------------------------------------------------------------------------------- /metrics/metrics.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "github.com/rs/zerolog/log" 5 | ) 6 | 7 | func (env *MetricsStruct) Inc(metricName string, increment int32) bool { 8 | 9 | log.Debug(). 10 | Str("metricName", metricName). 11 | Int32("increment", increment). 12 | Interface("metricCurrentValue", env.Entities[metricName]). 13 | Msgf("Incrementing metric") 14 | 15 | env.IncrementMutex.Lock() 16 | env.Entities[metricName] += increment 17 | env.IncrementMutex.Unlock() 18 | 19 | log.Debug(). 20 | Str("metricName", metricName). 21 | Int32("increment", increment). 22 | Interface("metricCurrentValue", env.Entities[metricName]). 23 | Msgf("Metric has been incremented") 24 | 25 | return true 26 | } 27 | 28 | func (env *MetricsStruct) Error(errorSource string, increment int32) bool { 29 | 30 | log.Debug(). 31 | Str("errorSource", errorSource). 32 | Int32("increment", increment). 33 | Int32("Errors", env.Errors). 34 | Msgf("Incrementing the errors counter") 35 | 36 | env.IncrementMutex.Lock() 37 | env.Errors += increment 38 | env.IncrementMutex.Unlock() 39 | 40 | log.Debug(). 41 | Str("errorSource", errorSource). 42 | Int32("increment", increment). 43 | Int32("Errors", env.Errors). 44 | Msgf("Errors counter has been incremented") 45 | 46 | return true 47 | } 48 | 49 | func (env *MetricsStruct) Warning(warningSource string, increment int32) bool { 50 | 51 | log.Debug(). 52 | Str("warningSource", warningSource). 53 | Int32("increment", increment). 54 | Int32("Warnings", env.Warnings). 55 | Msgf("Incrementing the errors counter") 56 | 57 | env.IncrementMutex.Lock() 58 | env.Warnings += increment 59 | env.IncrementMutex.Unlock() 60 | 61 | log.Debug(). 62 | Str("warningSource", warningSource). 63 | Int32("increment", increment). 64 | Int32("Warnings", env.Warnings). 65 | Msgf("Warnings counter has been incremented") 66 | 67 | return true 68 | } 69 | -------------------------------------------------------------------------------- /localhost.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFCTCCAvGgAwIBAgIUZpmrSjKNOBui4gBINdUlkkYMkAIwDQYJKoZIhvcNAQEL 3 | BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIyMDYxMTE5MzI0NloXDTQ4MDEz 4 | MTE5MzI0NlowFDESMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEF 5 | AAOCAg8AMIICCgKCAgEAwdkPEqz4X9zdWEZ6kYYsh8eotVF20cqF4er5ydpGycso 6 | yRMkR01Gi/qENEd5VCn3Lh8CO/biYNYKsTmjFWP/HeM0OTNZTCN4bl1NgZ2GP27X 7 | dp+qyNEvHnIvFuNYCc+tts/GarRFn8Zr5yUdQqFlPcgLjkuJ3xZraxEdDxQAVdGM 8 | 8a2BcQPSPU8VIZtzXpoHKiD2RcH+wGYHrzD7A9H6QnBb22TnXyR2KMgxuxXD8HoL 9 | 9SFVMfXjt6X2KPN1V0jKAjY4KMw00C2NWiIURLJo1Ofi8fzxiGmU+4dtPyzpAojL 10 | YyQ/H/I9PztCryEeBvklDbY7LIjAN8jF5LicAXdrpq6O/vdHd1XlGwXJ4ChXfvae 11 | HdfYVqjHJ1muxu+2hnpC6wSxRfxBxxZw+IbEUSWVNTayViyGECUwPi63S6saQxZz 12 | 44Er5Q9bOpAxLHN8DSBRljxLCXyt/r+EKCRJWQi68bBMrEMl2t2KpN0ZtEEBmJ8W 13 | YaPwtWppjbff7whhSgw9eGbwRWuAd0Tg6kWhRYHCmB7ZceuROmxObd4fmxlIY3zh 14 | ndh4GHk5wB0t9CDpqNYws1BVoGkPBNaqQ/tBeouLcvT//SbemvlDxIVx4NBisi+x 15 | 8efvWwrruQ8nzwndyUdaGyPzqfEfHeVcCHYUaPPG3WzHAwzDAcM1GFJh3YhJJy0C 16 | AwEAAaNTMFEwHQYDVR0OBBYEFKcP8lAp0vDsSiz4hlWkqG3409qVMB8GA1UdIwQY 17 | MBaAFKcP8lAp0vDsSiz4hlWkqG3409qVMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI 18 | hvcNAQELBQADggIBALXac5cIoLE+b98CcqOIJRuUvZKyQAykJTQQ23dD8sRBhB6t 19 | ZJqberttUl7kbDWaxJNR3VcdrSF8N1fLY7njHPdFVjGCeKAy4h4FheSc/IuBImEE 20 | g/gLlrUEG7rKn/U6WABzHeig/FwR+kIKIWTNKXGdXrNcAPW8tVx5SfXjszCW33di 21 | dTIv8NrkVn58v+/rj5k0xUnFU2cqzxyK5dXmPfn8KgNlujDhoLInsVtSCdcdlFN5 22 | uHBRm+B2E9ynWY1jxNuGtB4FxGRxNIAr8SJErDJxlcr3+ysfPYNcp421i37Fk3JY 23 | uGCe0qnFo/bQovhxMOnjLr8zrio4zomcMI0GYrXye3ZX8l/p0kUaf8MKLaMdkEpX 24 | 1ugHeM8tqaI/Zmn7BWe4ofno1+nfdbFgjOQEVaS5MxK417J+Vybr5TDUm6q4pR+3 25 | o//XzOSYI9lryqbHuBbAywubCv5Zb2Igx5UzE/52U8A5U5mQl5a7qxs11hdNnM7Q 26 | KDSsKakfPPCf4x43kCpNC2nOyfveK0vb5wHdXOOlkjkO3sZSgSQ+XUDGHl53EMBm 27 | Nba5BDHUxVJoGrtF7+Jkyav0QAuzcUTdMdVSuaEaioIRYT8rFWSy5lN+jPjYmOZn 28 | u1xRO5z76y5x8gnaORDkXgP2SZ63xYQ/geVpX8034bPAdUbkWJAZ/KTJ1S02 29 | -----END CERTIFICATE----- 30 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 2 | github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= 3 | github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 4 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 5 | github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= 6 | github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= 7 | github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 8 | github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= 9 | github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 10 | github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= 11 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 12 | github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= 13 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 14 | github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg= 15 | github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= 16 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 17 | github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= 18 | github.com/rs/zerolog v1.28.0 h1:MirSo27VyNi7RJYP3078AA1+Cyzd2GB66qy3aUHvsWY= 19 | github.com/rs/zerolog v1.28.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0= 20 | github.com/sony/sonyflake v1.1.0 h1:wnrEcL3aOkWmPlhScLEGAXKkLAIslnBteNUq4Bw6MM4= 21 | github.com/sony/sonyflake v1.1.0/go.mod h1:LORtCywH/cq10ZbyfhKrHYgAUGH7mOBa76enV9txy/Y= 22 | golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= 23 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 24 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 25 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 h1:foEbQz/B0Oz6YIqu/69kfXPYeFQAuuMYFkjaqXzl5Wo= 26 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 27 | -------------------------------------------------------------------------------- /localhost.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQDB2Q8SrPhf3N1Y 3 | RnqRhiyHx6i1UXbRyoXh6vnJ2kbJyyjJEyRHTUaL+oQ0R3lUKfcuHwI79uJg1gqx 4 | OaMVY/8d4zQ5M1lMI3huXU2BnYY/btd2n6rI0S8eci8W41gJz622z8ZqtEWfxmvn 5 | JR1CoWU9yAuOS4nfFmtrER0PFABV0YzxrYFxA9I9TxUhm3NemgcqIPZFwf7AZgev 6 | MPsD0fpCcFvbZOdfJHYoyDG7FcPwegv1IVUx9eO3pfYo83VXSMoCNjgozDTQLY1a 7 | IhREsmjU5+Lx/PGIaZT7h20/LOkCiMtjJD8f8j0/O0KvIR4G+SUNtjssiMA3yMXk 8 | uJwBd2umro7+90d3VeUbBcngKFd+9p4d19hWqMcnWa7G77aGekLrBLFF/EHHFnD4 9 | hsRRJZU1NrJWLIYQJTA+LrdLqxpDFnPjgSvlD1s6kDEsc3wNIFGWPEsJfK3+v4Qo 10 | JElZCLrxsEysQyXa3Yqk3Rm0QQGYnxZho/C1ammNt9/vCGFKDD14ZvBFa4B3RODq 11 | RaFFgcKYHtlx65E6bE5t3h+bGUhjfOGd2HgYeTnAHS30IOmo1jCzUFWgaQ8E1qpD 12 | +0F6i4ty9P/9Jt6a+UPEhXHg0GKyL7Hx5+9bCuu5DyfPCd3JR1obI/Op8R8d5VwI 13 | dhRo88bdbMcDDMMBwzUYUmHdiEknLQIDAQABAoICAAmsUYfmtmo0351qaZleGatu 14 | N36ndwdsVMFQ9BqyLWV1IYQ50/ie5Dioop+SkCe2rw1PzNETNnlglPNXuJ9LoyJp 15 | q7nNqdTBJwmoLELzNQzF9N3dhSyKNmYNip4DcD7lFyUP5Isp6G4wUkLG466R1ox+ 16 | XAfuFbjafze+MzYFEyiKfBRfSnNKSTEirh+ZiJNIRQ1BdyiZ+qK9NKr1sGugtx1I 17 | 9WcoTNy+5AqQRYhcGK/6VyMUq7ys1CqI5JS8jrbGp2tJgK9AfsODUmdqBfA7pZGM 18 | 6hKV7bOXQ9udXBfn9M9Z5imnWGMOt9GyR6yXjs5dm7oTGuF+XXaAf/ze3XdmmdTc 19 | DWYtJ8lazo4s6XM6zOiIs6eb0SjV1Noq49TYOs8kiz2Mi1IeX0qWBa3fqfXSb9J+ 20 | mLyfXtHj0gR1w4PWkZD7PQkuwfARnuCK1vdAcfy0uMmw8f2j/HWmnxbaOFR6uFnV 21 | 3yCBvhPy14EGwZZ3HjQGrdGEqSWLVj77FUHa7FpMqdmENLmEbA/LizNP8HKPFSve 22 | t4+EqwTQuXFYfBjBNU6ubu+6QrXN17RorMepX5Rmyh8Wud0JlOULZZevtA4oTdLh 23 | c0bAbmUITHAy+SWhtCIuwGYWqHTGEOABkZmO/bSI7d4SXJ4y+jdxzRQ5vq874ZqZ 24 | vAARks/zSa25VSn26TwBAoIBAQD+zCmTsb/agLYfjompmuTHP2VSQxPT0LnDJ8mM 25 | HVxqhk3VJ0GXHx2phUZrqvKXkymXS69UkjNnMUI30+RbWaeHFmJ5dh4T6BTmkgoh 26 | /+I08IAPaYlz+npZcpi2XysbPNrhOdqK9NVPR3d7iWDumjkLQI5yHRP33hpbV2M7 27 | KIsBMylkVIbGxgY/4Efaq7kxOe3ZzV8obJDLvjnyPLFYseK4Ue3zRIJ6CTHgQT2I 28 | m6fr0gWPaXChbf7CTuj1FugCcTbDY0+DvLxizY1attG68ct5BwhrswmeNgPjzbh3 29 | M9UBsDG7cz6SuYyRJR/bD2J+iMvNyrVlUX4aacJx4aK9x04tAoIBAQDCw0Jc4b0E 30 | /QpfJpUtZL8cfWh7tj5y7dFCCai2Qy8sl4dCGf/g/l6XgpMUkA4hb6RdhhOrdIiK 31 | puXawwbqts5GOYpwmFuXXFQ1d58xr1MS0c2G2VAuUdpRMDVvmLbITAZnq1SI5Bje 32 | 9Y94lQFBfssx5Ml6ACEpZVsYXAKJmRdDSRMfPWMs6lTerAaNa8NobiHPrCmaG9uC 33 | 5rjl2a73dPtfsF6Fe86GBn/2Hn+u1I66oW8/D4kJJ3wngvVeOY+PFEwq3iFUw1mQ 34 | DuZZq9n4QtsPE/MVSBQQmjMqIVoHl8CO2YFvY3As6uxDInm/3sY5ecjSG3KpVNle 35 | F67hPYMkOd0BAoIBAQCrDKwstnsxWI7rCGlqLs2+5NREMTuySsEqPh+TrHJKIPdH 36 | TR8/MGhdUVdBGHXbQ7P8GZcOk0dm0y69A8adfMZ2bZSWLbFrc7X0Q74BrSJSn9GY 37 | hyT7cv+H5OKYK7NoiaX25TvNZSd+HWAc0tD73RTGdQQrn1G0aKoQ/81h/YmzuIDl 38 | FcmUz4OKhio2pmmgnl8KhFCJdriNzppgAFaeUGz/iCDR2wAspNeS0LlaaASGz2tk 39 | J0ixVoJcN1U0k7gVS6OT76uFqMJfIdbvOyP4+DG8gfgpNPwT9fcOiyB3BZjNlVcN 40 | nAQ5w86V8fI8wUtB3tvSv26fIeIiITWj9fGmrZHRAoIBAHvFvTnr18UAzoOTsIAt 41 | o2qGpEzij6NYUYEnREm4PpWXIsU2Yq/o19Jvj+skdWZ4Xbt1xrBSmaeL002IXa/y 42 | RvrH/Jv7p1F0wqtL/yaDJkcyf+vv1Q3qxNSNz5fBNH/sGLHvZwSr+MZQxkG6aBbo 43 | bleh7wySYoC9QfwkFRS+7tK68OUMjSdxMEhmiK99SaznOKOS5MlkZMc2u1CPW6s1 44 | c77nBdrMyH3SSaXu0fQYbzBaAanQxKTFrBgsGKUt7XbfTlx72DDXCOcNIQThut8U 45 | FcTqR7RBn0bByDxA/8cNgLkHr0NLrXFORqGPmlH+UHkcVSx5dw/3tUAfyuqnvdza 46 | zQECggEAVjsBSiVxWeBFWIi4izX4+yGEj1XmOqr77MUYczEGuujIYz1U6Ya9VwM+ 47 | v2e0BFhKrUGJBhoLeKZPdF2rocQKZV8cs4hgaDaGrsMsic1ojezJYrYeBlxVX1BC 48 | mAGTwcLymvZCmcL8kHcCnfG8a5qsOQrIWkQBwZSyjCuKOG7erlLiwyK6OSEED4ZR 49 | G/6zR6upmGsMT2rpq+GGL3J5Mhnpru4DDxGKIeZR5ngbmPo8/2Qt0k4fCbOlFH/I 50 | jg6QydKU3U/OtpZ9fKEo6iTVfzxkuEw0OlFz2fURmjZc7LDRc79TNw9PuMNmYlTM 51 | /7xTwl6r95coKS3upy+uIe8sH0LyYw== 52 | -----END PRIVATE KEY----- 53 | -------------------------------------------------------------------------------- /handlers/auth.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | 7 | "github.com/rs/zerolog/log" 8 | 9 | "nginx_auth_server/server/lookup" 10 | "nginx_auth_server/server/metrics" 11 | ) 12 | 13 | func (env *Handlers) Auth(rw http.ResponseWriter, req *http.Request) { 14 | 15 | metrics.Metrics.Inc("AuthRequests", 1) 16 | 17 | authMethod := req.Header.Get("Auth-Method") 18 | authUser := req.Header.Get("Auth-User") 19 | authPass := req.Header.Get("Auth-Pass") 20 | authProtocol := req.Header.Get("Auth-Protocol") 21 | authLoginAttempt := req.Header.Get("Auth-Login-Attempt") 22 | clientIP := req.Header.Get("Client-IP") 23 | clientHost := req.Header.Get("Client-Host") 24 | authSMTPHelo := req.Header.Get("Auth-SMTP-Helo") 25 | authSMTPFrom := req.Header.Get("Auth-SMTP-From") 26 | authSMTPTo := req.Header.Get("Auth-SMTP-To") 27 | 28 | log.Info(). 29 | Str("authMethod", authMethod). 30 | Str("authUser", authUser). 31 | Str("authPass", lookup.WrapSecret(authPass)). 32 | Str("authProtocol", authProtocol). 33 | Str("authLoginAttempt", authLoginAttempt). 34 | Str("clientIP", clientIP). 35 | Str("clientHost", clientHost). 36 | Str("authSMTPHelo", authSMTPHelo). 37 | Str("authSMTPFrom", authSMTPFrom). 38 | Str("authSMTPTo", authSMTPTo). 39 | Str("event", "auth"). 40 | Msgf("Incoming auth request") 41 | 42 | success, result, err := lookup.Authenticate(authUser, authPass, authProtocol, authSMTPFrom, authSMTPTo, clientIP) 43 | if err != nil { 44 | 45 | metrics.Metrics.Inc("InternalErrors", 1) 46 | 47 | log.Error(). 48 | Err(err). 49 | Str("event", "auth"). 50 | Msgf("Can't authenticate %s:%s", authUser, lookup.WrapSecret(authPass)) 51 | } 52 | 53 | rw.Header().Set("Auth-Status", result.AuthStatus) 54 | 55 | log.Debug(). 56 | Str("AuthStatus", result.AuthStatus). 57 | Str("AuthServer", result.AuthServer). 58 | Int("AuthPort", result.AuthPort). 59 | Str("AuthWait", result.AuthWait). 60 | Str("AuthErrorCode", result.AuthErrorCode). 61 | Bool("AuthViaRelay", result.AuthViaRelay). 62 | Bool("AuthViaLogin", result.AuthViaLogin). 63 | Bool("success", success). 64 | Msgf("Got result from authentication function") 65 | 66 | if success { 67 | 68 | log.Debug(). 69 | Msgf("Registering successful authentication in metrics") 70 | 71 | metrics.Metrics.Inc("AuthRequestsSuccess", 1) 72 | 73 | if result.AuthViaRelay { 74 | metrics.Metrics.Inc("AuthRequestsSuccessRelay", 1) 75 | 76 | } else if result.AuthViaLogin { 77 | metrics.Metrics.Inc("AuthRequestsSuccessLogin", 1) 78 | } 79 | 80 | log.Info(). 81 | Str("authMethod", authMethod). 82 | Str("authUser", authUser). 83 | Str("authPass", lookup.WrapSecret(authPass)). 84 | Str("authProtocol", authProtocol). 85 | Str("authLoginAttempt", authLoginAttempt). 86 | Str("clientIP", clientIP). 87 | Str("clientHost", clientHost). 88 | Str("authSMTPHelo", authSMTPHelo). 89 | Str("authSMTPFrom", authSMTPFrom). 90 | Str("authSMTPTo", authSMTPTo). 91 | Str("event", "auth"). 92 | Str("AuthStatus", result.AuthStatus). 93 | Str("AuthServer", result.AuthServer). 94 | Int("AuthPort", result.AuthPort). 95 | Str("AuthWait", result.AuthWait). 96 | Str("AuthErrorCode", result.AuthErrorCode). 97 | Str("event", "auth_ok"). 98 | Msgf("Successful authentication") 99 | 100 | rw.Header().Set("Auth-Server", result.AuthServer) 101 | rw.Header().Set("Auth-Port", strconv.Itoa(result.AuthPort)) 102 | 103 | } else { 104 | 105 | metrics.Metrics.Inc("AuthRequestsFailed", 1) 106 | 107 | if result.AuthViaRelay { 108 | metrics.Metrics.Inc("AuthRequestsFailedRelay", 1) 109 | 110 | } else if result.AuthViaLogin { 111 | metrics.Metrics.Inc("AuthRequestsFailedLogin", 1) 112 | } 113 | 114 | log.Info(). 115 | Str("authMethod", authMethod). 116 | Str("authUser", authUser). 117 | Str("authPass", lookup.WrapSecret(authPass)). 118 | Str("authProtocol", authProtocol). 119 | Str("authLoginAttempt", authLoginAttempt). 120 | Str("clientIP", clientIP). 121 | Str("clientHost", clientHost). 122 | Str("authSMTPHelo", authSMTPHelo). 123 | Str("authSMTPFrom", authSMTPFrom). 124 | Str("authSMTPTo", authSMTPTo). 125 | Str("event", "auth"). 126 | Str("AuthStatus", result.AuthStatus). 127 | Str("AuthServer", result.AuthServer). 128 | Int("AuthPort", result.AuthPort). 129 | Str("AuthWait", result.AuthWait). 130 | Str("AuthErrorCode", result.AuthErrorCode). 131 | Str("event", "auth_failed"). 132 | Msgf("Failed to authenticate") 133 | 134 | rw.Header().Set("Auth-Wait", result.AuthWait) 135 | 136 | if result.AuthErrorCode != "" { 137 | rw.Header().Set("Auth-Error-Code", result.AuthErrorCode) 138 | } 139 | } 140 | 141 | } 142 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "net" 8 | "net/http" 9 | "os" 10 | "os/signal" 11 | "syscall" 12 | "time" 13 | 14 | _ "github.com/go-sql-driver/mysql" 15 | _ "github.com/lib/pq" 16 | 17 | "github.com/jmoiron/sqlx" 18 | 19 | "github.com/rs/zerolog" 20 | "github.com/rs/zerolog/log" 21 | 22 | "github.com/sony/sonyflake" 23 | 24 | "nginx_auth_server/server/configuration" 25 | "nginx_auth_server/server/handlers" 26 | "nginx_auth_server/server/lookup" 27 | "nginx_auth_server/server/metrics" 28 | ) 29 | 30 | var ApplicationDescription = "Nginx Mail Auth HTTP Server" 31 | var BuildVersion = "0.0.0" 32 | var ShowSecretsInLog = false 33 | 34 | var Debug bool = false 35 | var DebugMetricsNotifierPeriod time.Duration = 60 36 | var ListLengthWatcherPeriod time.Duration = 60 37 | 38 | type handleSignalParamsStruct struct { 39 | httpServer http.Server 40 | db *sqlx.DB 41 | } 42 | 43 | var handleSignalParams = handleSignalParamsStruct{} 44 | 45 | var MetricsNotifierPeriod int = 60 46 | 47 | var ctx = context.Background() 48 | var flake = sonyflake.NewSonyflake(sonyflake.Settings{}) 49 | var DB *sqlx.DB 50 | 51 | var httpHandlers = handlers.Handlers{ 52 | ApplicationDescription: ApplicationDescription, 53 | BuildVersion: BuildVersion, 54 | DB: DB, 55 | Flake: flake, 56 | } 57 | 58 | func MetricsNotifier() { 59 | go func() { 60 | for { 61 | time.Sleep(DebugMetricsNotifierPeriod * time.Second) 62 | log.Debug(). 63 | Interface("Entities", metrics.Metrics.Entities). 64 | Int32("InternalErrors", metrics.Metrics.InternalErrors). 65 | Msg("Metrics") 66 | } 67 | }() 68 | } 69 | 70 | func handleSignal() { 71 | 72 | log.Debug().Msg("Initialising signal handling function") 73 | 74 | signalChannelSIGTERM := make(chan os.Signal) 75 | signal.Notify(signalChannelSIGTERM, os.Interrupt, syscall.SIGTERM) 76 | 77 | signalChannelSIGUSR1 := make(chan os.Signal) 78 | signal.Notify(signalChannelSIGUSR1, syscall.SIGUSR1) 79 | 80 | go func() { 81 | 82 | <-signalChannelSIGTERM 83 | 84 | err := handleSignalParams.httpServer.Shutdown(context.Background()) 85 | defer handleSignalParams.db.Close() 86 | 87 | if err != nil { 88 | log.Error().Err(err).Msgf("HTTP server Shutdown: %v", err) 89 | 90 | } else { 91 | log.Info().Msgf("HTTP server Shutdown complete") 92 | } 93 | 94 | log.Warn().Msg("SIGINT") 95 | os.Exit(0) 96 | 97 | }() 98 | 99 | go func() { 100 | for { 101 | <-signalChannelSIGUSR1 102 | 103 | log.Warn().Msg("SIGUSR1") 104 | 105 | configuration.ReadConfigurationFile(configuration.Configuration.ConfigFile, &configuration.Configuration) 106 | log.Info().Msgf("Configuration file reload complete") 107 | 108 | log.Info().Msg("Re-initialising database connection") 109 | DB = initDBConnection(configuration.Configuration.Database.URI, configuration.Configuration.Database.Driver) 110 | 111 | handleSignalParams.db = DB 112 | httpHandlers.DB = DB 113 | configuration.Configuration.DB = DB 114 | } 115 | }() 116 | } 117 | 118 | func initDBConnection(uri string, driver string) (db *sqlx.DB) { 119 | 120 | log.Debug().Msg("Initialising database connection") 121 | 122 | db, err := sqlx.Open(driver, uri) 123 | if err != nil { 124 | log.Fatal().Msgf("Error while initialising db: %v", err) 125 | } 126 | 127 | return db 128 | 129 | } 130 | 131 | func init() { 132 | 133 | configPtr := flag.String("config", "nginx-mail-auth-http-server.conf", "Path to configuration file") 134 | verbosePtr := flag.Bool("verbose", false, "Verbose output") 135 | logSecretsPtr := flag.Bool("log-secrets", false, "Show plaintext passwords in logs") 136 | showVersionPtr := flag.Bool("version", false, "Show version") 137 | 138 | flag.Parse() 139 | 140 | if *showVersionPtr { 141 | fmt.Printf("%s\n", ApplicationDescription) 142 | fmt.Printf("Version: %s\n", BuildVersion) 143 | os.Exit(0) 144 | } 145 | 146 | if *logSecretsPtr { 147 | lookup.ShowSecretsInLog = true 148 | } 149 | 150 | if *verbosePtr { 151 | zerolog.SetGlobalLevel(zerolog.DebugLevel) 152 | MetricsNotifier() 153 | } else { 154 | zerolog.SetGlobalLevel(zerolog.InfoLevel) 155 | } 156 | 157 | log.Debug().Msg("Logger initialised") 158 | 159 | configuration.ReadConfigurationFile(*configPtr, &configuration.Configuration) 160 | 161 | listenAddress, err := net.ResolveTCPAddr("tcp4", configuration.Configuration.ListenAddress) 162 | if err != nil { 163 | log.Fatal().Err(err).Msgf("Error while resolving listen address") 164 | } 165 | 166 | configuration.Configuration.ListenNetTCPAddr = listenAddress 167 | 168 | configuration.Configuration.ApplicationDescription = ApplicationDescription 169 | configuration.Configuration.BuildVersion = BuildVersion 170 | 171 | handleSignal() 172 | 173 | log.Debug().Msg("Initialising database connection") 174 | DB = initDBConnection(configuration.Configuration.Database.URI, configuration.Configuration.Database.Driver) 175 | 176 | handleSignalParams.db = DB 177 | httpHandlers.DB = DB 178 | configuration.Configuration.DB = DB 179 | 180 | if err := DB.Ping(); err != nil { 181 | log.Fatal(). 182 | Err(err). 183 | Str("stage", "init"). 184 | Msgf("Error while pinging db: %v", err) 185 | } 186 | 187 | log.Debug().Msg("Finished initialising database connection") 188 | 189 | } 190 | 191 | func main() { 192 | 193 | log.Info().Msgf("Listening on %s", configuration.Configuration.ListenNetTCPAddr.String()) 194 | 195 | srv := &http.Server{ 196 | Addr: configuration.Configuration.ListenNetTCPAddr.String(), 197 | ReadTimeout: 10 * time.Second, 198 | WriteTimeout: 10 * time.Second, 199 | } 200 | 201 | handleSignalParams.httpServer = *srv 202 | 203 | http.HandleFunc("/", httpHandlers.Index) 204 | http.HandleFunc("/auth", httpHandlers.Auth) 205 | http.HandleFunc("/metrics", httpHandlers.Metrics) 206 | 207 | if err := srv.ListenAndServe(); err != http.ErrServerClosed { 208 | log.Fatal().Err(err).Msgf("HTTP server ListenAndServe error") 209 | } 210 | 211 | } 212 | -------------------------------------------------------------------------------- /lookup/auth.go: -------------------------------------------------------------------------------- 1 | package lookup 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | // "encoding/json" 7 | // "flag" 8 | // "fmt" 9 | // "html" 10 | // "net/http" 11 | // "os" 12 | // "os/signal" 13 | "regexp" 14 | // "strconv" 15 | // "sync/atomic" 16 | // "syscall" 17 | // "time" 18 | 19 | "github.com/rs/zerolog/log" 20 | 21 | "nginx_auth_server/server/configuration" 22 | "nginx_auth_server/server/metrics" 23 | ) 24 | 25 | var mailHeaderRegex = regexp.MustCompile(`<(.*?)(\+.*?)?@(.*?)>`) 26 | 27 | func parseEmailAddress(email string) (emailParsed string, emailParts []string, err error) { 28 | 29 | var nonVERPAddress bytes.Buffer 30 | 31 | emailMatch := mailHeaderRegex.FindStringSubmatch(email) 32 | 33 | log.Debug(). 34 | Str("email", email). 35 | Msgf("Parsing email address") 36 | 37 | if len(emailMatch) == 4 { 38 | nonVERPAddress.WriteString(emailMatch[1]) 39 | nonVERPAddress.WriteString("@") 40 | nonVERPAddress.WriteString(emailMatch[3]) 41 | 42 | // nonVERPAddress.Reset() 43 | 44 | log.Debug(). 45 | Str("email", email). 46 | Str("emailParsed", nonVERPAddress.String()). 47 | Strs("emailMatch", emailMatch). 48 | Msgf("Parsed email address") 49 | 50 | return nonVERPAddress.String(), emailMatch, nil 51 | 52 | } else { 53 | 54 | log.Warn(). 55 | Str("nonVERPAddress", nonVERPAddress.String()). 56 | Str("email", email). 57 | Msgf("Email address is empty (could be incoming bounce)") 58 | 59 | return "", emailMatch, nil 60 | 61 | } 62 | 63 | } 64 | 65 | func canRelay(mailFrom string, rcptTo string) bool { 66 | if rcptTo != "" { 67 | return true 68 | } else { 69 | return false 70 | } 71 | } 72 | 73 | func canAuthenticate(user string, pass string) bool { 74 | if user != "" { 75 | return true 76 | } else { 77 | return false 78 | } 79 | } 80 | 81 | func Authenticate(user string, pass string, protocol string, mailFrom string, rcptTo string, clientIP string) (success bool, result authResultStruct, err error) { 82 | 83 | log.Info(). 84 | Str("user", user). 85 | Str("pass", WrapSecret(pass)). 86 | Str("protocol", protocol). 87 | Str("mailFrom", mailFrom). 88 | Str("rcptTo", rcptTo). 89 | Msgf("Processing authentication request") 90 | 91 | var queries []string 92 | var queryParams = QueryParamsStruct{ 93 | User: user, 94 | Pass: pass, 95 | RcptTo: "", 96 | MailFrom: "", 97 | ClientIP: clientIP, 98 | } 99 | 100 | if canRelay(mailFrom, rcptTo) && !canAuthenticate(user, pass) { 101 | 102 | metrics.Metrics.Inc("AuthRequestsRelay", 1) 103 | result.AuthViaRelay = true 104 | 105 | log.Info(). 106 | Str("protocol", protocol). 107 | Str("mailFrom", mailFrom). 108 | Str("rcptTo", rcptTo). 109 | Msg("Authenticating by relay access ('user' and 'pass' are empty)") 110 | 111 | queryParams.MailFrom, _, err = parseEmailAddress(mailFrom) 112 | if err != nil { 113 | metrics.Metrics.Inc("InternalErrors", 1) 114 | log.Error(). 115 | Err(err). 116 | Str("queryParams.MailFrom", queryParams.MailFrom). 117 | Msgf("Error while parsing MailFrom address") 118 | 119 | result.AuthStatus = "Temporary server problem, try again later" 120 | result.AuthErrorCode = "451 4.3.0" 121 | result.AuthWait = "5" 122 | 123 | return false, result, errors.New("Error while parsing MailFrom address") 124 | } 125 | 126 | queryParams.RcptTo, _, err = parseEmailAddress(rcptTo) 127 | if err != nil { 128 | metrics.Metrics.Inc("InternalErrors", 1) 129 | log.Error(). 130 | Err(err). 131 | Str("queryParams.RcptTo", queryParams.RcptTo). 132 | Msgf("Error while parsing RcptTo address") 133 | 134 | result.AuthStatus = "Temporary server problem, try again later" 135 | result.AuthErrorCode = "451 4.3.0" 136 | result.AuthWait = "5" 137 | 138 | return false, result, errors.New("Error while parsing RcptTo address") 139 | } 140 | 141 | if queryParams.RcptTo == "" { 142 | metrics.Metrics.Inc("InternalErrors", 1) 143 | log.Error(). 144 | Str("rcptTo", rcptTo). 145 | Str("queryParams.RcptTo", queryParams.RcptTo). 146 | Msg("Can't parse RCPT TO command for relay") 147 | 148 | result.AuthStatus = "Temporary server problem, try again later" 149 | result.AuthErrorCode = "451 4.3.0" 150 | result.AuthWait = "5" 151 | 152 | return false, result, errors.New("Can't parse RCPT TO command for relay") 153 | } 154 | 155 | log.Debug(). 156 | Str("MailFrom", mailFrom). 157 | Str("RcptTo", rcptTo). 158 | Str("queryParams.MailFrom", queryParams.MailFrom). 159 | Str("queryParams.RcptTo", queryParams.RcptTo). 160 | Msg("Relay lookup query parameters prepared") 161 | 162 | queries = configuration.Configuration.Database.RelayLookupQueries 163 | 164 | } else if canAuthenticate(user, pass) { 165 | 166 | metrics.Metrics.Inc("AuthRequestsLogin", 1) 167 | result.AuthViaLogin = true 168 | 169 | log.Info(). 170 | Str("protocol", protocol). 171 | Str("user", user). 172 | Str("pass", WrapSecret(pass)). 173 | Msg("Authenticating by credentials") 174 | 175 | queries = configuration.Configuration.Database.AuthLookupQueries 176 | 177 | } else { 178 | 179 | metrics.Metrics.Inc("InternalErrors", 1) 180 | 181 | log.Error(). 182 | Str("User", queryParams.User). 183 | Str("Pass", WrapSecret(queryParams.Pass)). 184 | Str("MailFrom", queryParams.MailFrom). 185 | Str("RcptTo", queryParams.RcptTo). 186 | Msg("Can't authenticate via relay nor login") 187 | 188 | result.AuthStatus = "Temporary server problem, try again later" 189 | result.AuthErrorCode = "451 4.3.0" 190 | result.AuthWait = "5" 191 | 192 | return false, result, err 193 | 194 | } 195 | 196 | log.Debug(). 197 | Strs("queries", queries). 198 | Msg("Lookup query prepared") 199 | 200 | for idx, query := range queries { 201 | 202 | log.Debug(). 203 | Int("queryIdx", idx). 204 | Str("query", query). 205 | Msg("Submitting lookup query") 206 | 207 | queryResult, err := configuration.Configuration.DB.NamedQuery(query, queryParams) 208 | 209 | if err != nil { 210 | 211 | metrics.Metrics.Inc("InternalErrors", 1) 212 | 213 | log.Error().Err(err).Msgf("Error while executing query: %v", err) 214 | 215 | result.AuthStatus = "Temporary server problem, try again later" 216 | result.AuthErrorCode = "451 4.3.0" 217 | result.AuthWait = "5" 218 | 219 | return false, result, err 220 | } 221 | 222 | defer queryResult.Close() 223 | 224 | for queryResult.Next() { 225 | log.Debug(). 226 | Str("protocol", protocol). 227 | Str("user", WrapSecret(user)). 228 | Str("pass", pass). 229 | Str("mailFrom", mailFrom). 230 | Str("rcptTo", rcptTo). 231 | Int("queryIdx", idx). 232 | Msgf("Found results after lookup query execution") 233 | 234 | var upstream = UpstreamStruct{} 235 | 236 | if err = queryResult.StructScan(&upstream); err != nil { 237 | 238 | metrics.Metrics.Inc("InternalErrors", 1) 239 | log.Error().Err(err).Msgf("Error while parsing lookup query result") 240 | 241 | result.AuthStatus = "Temporary server problem, try again later" 242 | result.AuthErrorCode = "451 4.3.0" 243 | result.AuthWait = "5" 244 | 245 | return false, result, err 246 | 247 | } else { 248 | log.Debug().Msgf("Lookup query results parsed successfully") 249 | } 250 | 251 | log.Info(). 252 | Str("protocol", protocol). 253 | Str("user", user). 254 | Str("pass", WrapSecret(pass)). 255 | Str("mailFrom", mailFrom). 256 | Str("rcptTo", rcptTo). 257 | Str("upstreamAddress", upstream.Address). 258 | Int("upstreamPort", upstream.Port). 259 | Int("queryIdx", idx). 260 | Msgf("Found upstream") 261 | 262 | result.AuthStatus = "OK" 263 | result.AuthServer = upstream.Address 264 | result.AuthPort = upstream.Port 265 | 266 | return true, result, nil 267 | 268 | } 269 | 270 | } 271 | 272 | log.Info(). 273 | Str("protocol", protocol). 274 | Str("user", user). 275 | Str("pass", WrapSecret(pass)). 276 | Str("mailFrom", mailFrom). 277 | Str("rcptTo", rcptTo). 278 | Msgf("No results after lookup") 279 | 280 | result.AuthStatus = "Error: authentication failed." 281 | result.AuthErrorCode = "535 5.7.8" 282 | result.AuthWait = "5" 283 | 284 | return false, result, nil 285 | } 286 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=reinvented-stuff_nginx-mail-auth-http-server&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=reinvented-stuff_nginx-mail-auth-http-server) 2 | [![Bugs](https://sonarcloud.io/api/project_badges/measure?project=reinvented-stuff_nginx-mail-auth-http-server&metric=bugs)](https://sonarcloud.io/summary/new_code?id=reinvented-stuff_nginx-mail-auth-http-server) 3 | [![Codacy Badge](https://app.codacy.com/project/badge/Grade/ce53b5588f004254be32426b09162eb3)](https://www.codacy.com/gh/reinvented-stuff/nginx-mail-auth-http-server/dashboard) 4 | 5 | 6 | # Nginx Mail Auth Server 7 | 8 | Nginx Mail Auth HTTP Server provides an auth service for [Nginx Mail](https://nginx.org/en/docs/mail/ngx_mail_core_module.html) module. 9 | 10 | Benifits of using nginx as a mail proxy: 11 | 12 | 1. Nginx is fast and thin 13 | 1. You can do load balancing 14 | 1. You can use multiple upstream servers 15 | 1. Configuration is dynamic 16 | 17 | ## Workflow Diagram 18 | 19 | ```text 20 | 21 | +-------------+ +---------------+ +--------------+ 22 | | | | | | | 23 | | MTA <----7------+ Nginx <----2-----+ Gmail | 24 | | | SMTP | | SMTP | | 25 | +------+------+ +-----^---+-----+ +------^-------+ 26 | | | | | 27 | | | | | 28 | 8 6 3 HTTP(S) 1 SMTP 29 | | | | | 30 | | | | | 31 | +------v------+ +-----+---v-----+ +------+-------+ 32 | | +-----5-----> | | | 33 | | MySQL | | Auth Server | | Client | 34 | | <-----4-----+ | | | 35 | +-------------+ MySQL +---------------+ +--------------+ 36 | 37 | 38 | ``` 39 | 40 | ## Run as binary 41 | 42 | ```text 43 | ./nginx-mail-auth-http-server -h 44 | Usage of ./nginx-mail-auth-http-server: 45 | -config string 46 | Path to configuration file (default "nginx-mail-auth-http-server.conf") 47 | -log-secrets 48 | Show plaintext passwords in logs 49 | -verbose 50 | Verbose output 51 | -version 52 | Show version 53 | ``` 54 | 55 | ## Run in Docker/Podman 56 | 57 | We currently publish docker images on [github](https://github.com/reinvented-stuff/nginx-mail-auth-http-server/packages/586191) and [quay.io](https://quay.io/repository/reinventedstuff/nginx-mail-auth-http-server). 58 | 59 | In order to pull any images from there you need to have a personal github token. 60 | Please, refer to the official documantation: 61 | [https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token#creating-a-token](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token#creating-a-token) 62 | 63 | ```bash 64 | docker run \ 65 | --log-driver=journald \ 66 | --log-opt=tag="nginx-auth" \ 67 | --network host \ 68 | --interactive \ 69 | --tty \ 70 | --name nginx-mail-auth-http-server \ 71 | -v /opt/nginx-mail-auth-http-server.conf:/nginx-mail-auth-http-server.conf:ro \ 72 | "docker.pkg.github.com/reinvented-stuff/nginx-mail-auth-http-server/nginx-mail-auth-http-server:1.4.2" 73 | ``` 74 | 75 | ```bash 76 | podman run \ 77 | --log-driver=journald \ 78 | --log-opt=tag="nginx-auth" \ 79 | --network host \ 80 | --interactive \ 81 | --tty \ 82 | --name nginx-mail-auth-http-server \ 83 | -v /opt/nginx-mail-auth-http-server.conf:/nginx-mail-auth-http-server.conf:ro \ 84 | "quay.io/reinventedstuff/nginx-mail-auth-http-server:1.4.2" 85 | ``` 86 | 87 | # Nginx 88 | 89 | nginx should be listening on 25/tcp port of your mail server. 90 | 91 | ## nginx.conf 92 | 93 | ```nginx 94 | user nginx; 95 | worker_processes auto; 96 | 97 | ... 98 | 99 | http { 100 | ... 101 | } 102 | 103 | mail { 104 | server_name mx.example.com; 105 | 106 | auth_http http://localhost:8080/auth; 107 | auth_http_header X-Origin-Mail-Key 9TlBLGKoOa; 108 | 109 | starttls on; 110 | ssl_certificate /etc/pki/tls/certs/mx.example.com.crt; 111 | ssl_certificate_key /etc/pki/tls/private/mx.example.com.key; 112 | ssl_protocols TLSv1.2 TLSv1.3; 113 | ssl_ciphers HIGH:!aNULL:!MD5; 114 | ssl_session_cache shared:SSL:10m; 115 | ssl_session_timeout 10m; 116 | 117 | 118 | server { 119 | listen 25; 120 | protocol smtp; 121 | smtp_auth login plain none; 122 | auth_http_header X-Origin-Server-Key zb4xKm9XmD; 123 | 124 | error_log /var/log/nginx/mx.example.com-mail-error.log; 125 | proxy_pass_error_message on; 126 | } 127 | } 128 | 129 | ``` 130 | 131 | # MTA 132 | 133 | ## Postfix configuration 134 | 135 | postfix is supposed to be listening a different port from the one nginx does listen. 136 | 137 | ### main.cf 138 | 139 | `mynetworks` should contain your nginx host. This will let postfix accept all mail from nginx. 140 | `smtpd_authorized_xclient_hosts` should contain your nginx host. This allows Nginx to pass XCLIENT command. 141 | 142 | ```bash 143 | inet_interfaces = localhost 144 | mynetworks = 127.0.0.0/8 145 | smtpd_authorized_xclient_hosts = 127.0.0.0/8 146 | smtpd_recipient_restrictions = 147 | permit_mynetworks, 148 | ... 149 | ``` 150 | 151 | ### master.cf 152 | 153 | To make postfix listen on a custom port you can comment out the default `smtp ...` line and add a new one as proposed below. 154 | 155 | ```text 156 | # ========================================================================== 157 | # service type private unpriv chroot wakeup maxproc command + args 158 | # (yes) (yes) (no) (never) (100) 159 | # ========================================================================== 160 | # smtp inet n - n - - smtpd 161 | 31025 inet n - n - - smtpd -o smtpd_tls_auth_only=no 162 | 163 | ... 164 | 165 | ``` 166 | 167 | # Application configuration 168 | 169 | The Auth Server shold be reachable by nginx. 170 | 171 | ## nginx-mail-auth-http-server.conf 172 | 173 | ```json 174 | { 175 | "listen": "127.0.0.1:8080", 176 | "database": { 177 | "uri": "mysqluser:mysqlpass@tcp(127.0.0.1:3306)/postfix", 178 | "driver": "mysql", 179 | 180 | "auth_lookup_queries": [ 181 | "SELECT '127.0.0.1' as address, 25 as port WHERE :User = 'root';", 182 | "SELECT '127.0.0.1' as address, 10025 as port;", 183 | ], 184 | 185 | "relay_lookup_queries": [ 186 | "SELECT '127.0.0.1' as address, 25 as port WHERE :RcptTo = 'nobody';" 187 | "SELECT '127.0.0.1' as address, 10025 as port;" 188 | ] 189 | } 190 | } 191 | ``` 192 | 193 | ## On-fly configuration reload 194 | 195 | The application supports hot configuration file reload. Use `SIGUSR1` signal for that. 196 | 197 | ```bash 198 | kill -s SIGUSR1 $(pgrep nginx-mail-auth-http-server) 199 | ``` 200 | 201 | ## Lookup queries 202 | 203 | It is required for queries to return two named values: `address` and `port` (of the upstream mail server). 204 | 205 | You can use the following named parameters in your lookup queries: 206 | 207 | * `:User` – Username part of the authentication request (only on AUTH command) 208 | * `:Pass` – Password part of the authentication request (only on AUTH command) 209 | * `:RcptTo` – RCPT TO command content (if no AUTH command passed) 210 | * `:MailFrom` – MAIL FROM command content (if no AUTH command passed) 211 | * `:ClientIP` – Client IP address passed by nginx 212 | 213 | Example: 214 | 215 | ```sql 216 | SELECT address, port 217 | FROM transport 218 | JOIN account ON account.transport_id = transport.id 219 | WHERE account.username = :User AND account.password = MD5(:Pass); 220 | ``` 221 | 222 | ## VERP (Variable envelope return path) 223 | 224 | Currently the server strips everything from the first found "+" symbol until the first "@" symbol. 225 | 226 | # Prometheus exporter 227 | 228 | Grafana dashboard: [https://grafana.com/grafana/dashboards/16427](https://grafana.com/grafana/dashboards/16427) 229 | 230 | There is a `/metrics` endpoint with a few things: 231 | 232 | ```prometheus 233 | # TYPE AuthRequests counter 234 | # HELP Number of events happened in Nginx Mail Auth Server 235 | AuthRequests{kind="AuthRequestsSuccessLogin"} 3 236 | AuthRequests{kind="RequestIndex"} 3 237 | AuthRequests{kind="AuthRequests"} 8 238 | AuthRequests{kind="AuthRequestsRelay"} 5 239 | AuthRequests{kind="AuthRequestsSuccess"} 8 240 | AuthRequests{kind="AuthRequestsSuccessRelay"} 5 241 | AuthRequests{kind="AuthRequestsLogin"} 3 242 | 243 | # TYPE InternalErrors counter 244 | InternalErrors 32 245 | ``` 246 | 247 | # IPv6 support 248 | 249 | To be done. 250 | 251 | # Test 252 | 253 | Request authentication with login and password: 254 | 255 | ```bash 256 | curl -v -k \ 257 | -H "Auth-Method: none" \ 258 | -H "Auth-User: pepe_likes" \ 259 | -H "Auth-Pass: koalas" \ 260 | -H "Auth-Protocol: smtp" \ 261 | -H "Auth-Login-Attempt: 1" \ 262 | -H "Client-IP: 10.13.199.8" \ 263 | -H "Client-Host: [UNAVAILABLE]" \ 264 | -H "Auth-SMTP-Helo: pepes_workstation" \ 265 | -H "Auth-SMTP-From: MAIL FROM:" \ 266 | -H "Auth-SMTP-To: RCPT TO:" \ 267 | http://127.0.0.1:8080/auth 268 | ``` 269 | 270 | Request authentication via relay: 271 | 272 | ```bash 273 | curl -v -k \ 274 | -H "Auth-Method: none" \ 275 | -H "Auth-Protocol: smtp" \ 276 | -H "Auth-Login-Attempt: 1" \ 277 | -H "Client-IP: 10.13.199.8" \ 278 | -H "Client-Host: [UNAVAILABLE]" \ 279 | -H "Auth-SMTP-Helo: pepes_workstation" \ 280 | -H "Auth-SMTP-From: MAIL FROM:" \ 281 | -H "Auth-SMTP-To: RCPT TO:" \ 282 | http://127.0.0.1:8080/auth 283 | ``` 284 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Everything 2 | 3 | on: 4 | push: 5 | branches: [ '*' ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | validate_new_version: 12 | name: Validate new version 13 | runs-on: ubuntu-latest 14 | outputs: 15 | planned_version: ${{ steps.validate_new_version.outputs.planned_version }} 16 | version_file_exists: ${{ steps.validate_new_version.outputs.version_file_exists }} 17 | tag_hash: ${{ steps.validate_new_version.outputs.tag_hash }} 18 | can_create: ${{ steps.validate_new_version.outputs.can_create }} 19 | tag_exists: ${{ steps.validate_new_version.outputs.tag_exists }} 20 | branch_name: ${{ steps.validate_new_version.outputs.branch_name }} 21 | 22 | steps: 23 | 24 | - name: Check out code 25 | uses: actions/checkout@v2 26 | with: 27 | fetch-depth: 0 28 | 29 | - name: Use latest released action 30 | id: validate_new_version 31 | uses: reinvented-stuff/validate-version-action@master 32 | with: 33 | version_filename: ".version" 34 | github_token: "${{ secrets.GITHUB_TOKEN }}" 35 | 36 | - name: Fail if version already exists 37 | id: fail_on_duplicate_version 38 | if: steps.validate_new_version.outputs.can_create != 'true' 39 | run: exit 2 40 | 41 | 42 | build_and_test: 43 | name: Build, Test 44 | runs-on: ubuntu-latest 45 | outputs: 46 | job_status: ${{ job.status }} 47 | filename: ${{ steps.artifact_meta.outputs.filename }} 48 | 49 | needs: 50 | - validate_new_version 51 | 52 | env: 53 | PLANNED_VERSION: ${{ needs.validate_new_version.outputs.planned_version }} 54 | TAG_HASH: ${{ needs.validate_new_version.outputs.tag_hash }} 55 | CAN_CREATE_RELEASE: ${{ needs.validate_new_version.outputs.can_create }} 56 | TAG_EXISTS: ${{ needs.validate_new_version.outputs.tag_exists }} 57 | BRANCH_NAME: ${{ needs.validate_new_version.outputs.branch_name }} 58 | APP_NAME: "nginx-mail-auth-http-server" 59 | TARGETOS: "linux" 60 | TARGETARCH: "amd64" 61 | 62 | if: > 63 | github.event_name == 'push' 64 | 65 | steps: 66 | 67 | - name: Validate envionment variables 68 | id: validate_envvars 69 | shell: bash 70 | run: | 71 | [[ ! -z "PLANNED_VERSION" ]] || exit 2 72 | [[ ! -z "TAG_HASH" ]] || exit 2 73 | [[ ! -z "CAN_CREATE_RELEASE" ]] || exit 2 74 | [[ ! -z "TAG_EXISTS" ]] || exit 2 75 | [[ ! -z "BRANCH_NAME" ]] || exit 2 76 | 77 | - name: Check out code 78 | uses: actions/checkout@v2 79 | with: 80 | fetch-depth: 0 81 | 82 | - name: Set up Go 83 | uses: actions/setup-go@v2 84 | with: 85 | go-version: 1.18 86 | 87 | - name: Define expected artifact name 88 | id: artifact_meta 89 | shell: bash 90 | run: | 91 | echo "::set-output name=filename::${APP_NAME}-${PLANNED_VERSION}-linux.x86_64.bin" 92 | 93 | - name: Build 94 | run: > 95 | GOOS="${TARGETOS}" 96 | GOARCH="${TARGETARCH}" 97 | go build 98 | -ldflags="-X 'main.BuildVersion=${PLANNED_VERSION}'" 99 | -v 100 | -o "${APP_NAME}-${PLANNED_VERSION}-linux.x86_64.bin" 101 | . 102 | 103 | - name: Validate build artifact exists 104 | id: validate_artifact_exists 105 | shell: bash 106 | run: | 107 | ls -laht "${{ steps.artifact_meta.outputs.filename }}" 108 | 109 | - name: Upload a Build Artifact 110 | id: upload_artifact 111 | uses: actions/upload-artifact@v2.2.2 112 | with: 113 | name: "${{ steps.artifact_meta.outputs.filename }}" 114 | path: "${{ steps.artifact_meta.outputs.filename }}" 115 | retention-days: 30 116 | 117 | - name: Login to Docker registry 118 | id: docker_registry_login 119 | run: | 120 | echo "${{ secrets.GITHUB_TOKEN }}" | docker login docker.pkg.github.com -u "${{ github.actor }}" --password-stdin 121 | echo "${{ secrets.QUAYIO_PASSWORD }}" | docker login quay.io -u "${{ secrets.QUAYIO_USERNAME }}" --password-stdin 122 | 123 | - name: Define version suffix 124 | id: version_suffix 125 | run: | 126 | if [[ "${BRANCH_NAME}" == "master" ]]; then 127 | short_suffix="" 128 | long_suffix="" 129 | else 130 | short_suffix="${BRANCH_NAME:0:1}" 131 | long_suffix="${BRANCH_NAME}" 132 | fi 133 | 134 | echo "::set-output name=short::${short_suffix}" 135 | echo "::set-output name=long::${long_suffix}" 136 | 137 | - name: Build Docker image 138 | id: build_docker_image 139 | shell: bash 140 | run: > 141 | GITHUB_IMAGE_ID=$(echo "${{ github.repository }}/${APP_NAME}${IMAGE_ID}" | tr '[A-Z]' '[a-z]') 142 | QUAYIO_IMAGE_ID=$(echo "reinventedstuff/${APP_NAME}${IMAGE_ID}" | tr '[A-Z]' '[a-z]') 143 | 144 | docker build 145 | --tag "docker.pkg.github.com/${GITHUB_IMAGE_ID}:${PLANNED_VERSION}${{ steps.version_suffix.outputs.long }}" 146 | --tag "quay.io/${QUAYIO_IMAGE_ID}:${PLANNED_VERSION}${{ steps.version_suffix.outputs.long }}" 147 | --build-arg BUILD_VERSION="${PLANNED_VERSION}" 148 | --file Dockerfile 149 | . 150 | 151 | - name: Push Docker image to registry 152 | run: | 153 | GITHUB_IMAGE_ID=$(echo "${{ github.repository }}/${APP_NAME}${IMAGE_ID}" | tr '[A-Z]' '[a-z]') 154 | QUAYIO_IMAGE_ID=$(echo "reinventedstuff/${APP_NAME}${IMAGE_ID}" | tr '[A-Z]' '[a-z]') 155 | 156 | docker push "docker.pkg.github.com/${GITHUB_IMAGE_ID}:${PLANNED_VERSION}${{ steps.version_suffix.outputs.long }}" 157 | docker push "quay.io/${QUAYIO_IMAGE_ID}:${PLANNED_VERSION}${{ steps.version_suffix.outputs.long }}" 158 | 159 | 160 | - name: Notify about build 161 | uses: rest-gateway/notification-action@master 162 | with: 163 | message: | 164 | Build ${{env.APP_NAME}}: ${{env.PLANNED_VERSION}}${{ steps.version_suffix.outputs.long }} 165 | 166 | Docker image: 167 | docker.pkg.github.com/${GITHUB_IMAGE_ID}:${PLANNED_VERSION}${{ steps.version_suffix.outputs.long }} 168 | quay.io/${QUAYIO_IMAGE_ID}:${PLANNED_VERSION}${{ steps.version_suffix.outputs.long }} 169 | 170 | recipient: "${{ secrets.NOTIFICATIONS_DEFAULT_RECIPIENT }}" 171 | rest_gateway_url: "${{ secrets.REST_GATEWAY_API_URL }}" 172 | rest_gateway_token: "${{ secrets.REST_GATEWAY_TOKEN }}" 173 | rest_gateway_bot_name: "${{ secrets.REST_GATEWAY_BOT_NAME }}" 174 | 175 | 176 | release: 177 | name: Release 178 | runs-on: ubuntu-latest 179 | 180 | outputs: 181 | job_status: ${{ job.status }} 182 | 183 | needs: 184 | - validate_new_version 185 | - build_and_test 186 | 187 | env: 188 | PLANNED_VERSION: ${{ needs.validate_new_version.outputs.planned_version }} 189 | TAG_HASH: ${{ needs.validate_new_version.outputs.tag_hash }} 190 | CAN_CREATE_RELEASE: ${{ needs.validate_new_version.outputs.can_create }} 191 | TAG_EXISTS: ${{ needs.validate_new_version.outputs.tag_exists }} 192 | BRANCH_NAME: ${{ needs.validate_new_version.outputs.branch_name }} 193 | ARTIFACT_NAME: ${{ needs.build_and_test.outputs.filename }} 194 | APP_NAME: "nginx-mail-auth-http-server" 195 | 196 | if: > 197 | needs.validate_new_version.outputs.can_create == 'true' && 198 | needs.validate_new_version.outputs.branch_name == 'master' && 199 | github.event_name == 'push' 200 | 201 | steps: 202 | 203 | - name: Validate envionment variables 204 | id: validate_envvars 205 | shell: bash 206 | run: | 207 | [[ ! -z "${{ env.PLANNED_VERSION }}" ]] || exit 2 208 | [[ ! -z "${{ env.TAG_HASH }}" ]] || exit 2 209 | [[ ! -z "${{ env.CAN_CREATE_RELEASE }}" ]] || exit 2 210 | [[ ! -z "${{ env.TAG_EXISTS }}" ]] || exit 2 211 | [[ ! -z "${{ env.BRANCH_NAME }}" ]] || exit 2 212 | [[ ! -z "${{ env.ARTIFACT_NAME }}" ]] || exit 2 213 | 214 | - name: Check out code 215 | uses: actions/checkout@v2 216 | with: 217 | fetch-depth: 0 218 | 219 | - name: Download artifact 220 | id: download_artifact 221 | uses: actions/download-artifact@v2 222 | with: 223 | name: "${{ env.ARTIFACT_NAME }}" 224 | 225 | - name: Define full path to downloaded artifact 226 | id: artifact_meta 227 | shell: bash 228 | run: | 229 | echo "::set-output name=full_path::${{ steps.download_artifact.outputs.download-path }}/${{ env.ARTIFACT_NAME }}" 230 | 231 | - name: Validate downloaded artifact 232 | id: validate_artifact_exists 233 | shell: bash 234 | run: | 235 | ls -la "${{ steps.download_artifact.outputs.download-path }}/${{ env.ARTIFACT_NAME }}" 236 | 237 | - name: Generate changelog 238 | id: generate_changelog 239 | shell: bash 240 | run: | 241 | described=$(git describe --tags || git rev-list --max-parents=0 HEAD) 242 | described_parts=( ${described//-/ } ) 243 | current_tag=${described_parts[0]} 244 | 245 | changelog_filename=".changelog" 246 | release_changelog_filename=".release_changelog" 247 | echo "current_tag=${current_tag}" 248 | 249 | echo "Listing current changes..." 250 | git log --pretty=oneline --format='%w(1000)* %cd %an <%ae>%n%w(60,0,2)- %s%n' --date="format:%a %b %d %Y" "$current_tag"..HEAD | tee "${changelog_filename}" 251 | git log --pretty=oneline --format='%w(200,0,2)- %s (%an <%ae>)' --date="format:%a %b %d %Y" "$current_tag"..HEAD | sort | tee "${release_changelog_filename}" 252 | 253 | echo "Changelog file..." 254 | cat .changelog 255 | 256 | echo "Preparing a GitHub Release Changelog" 257 | cat << EOF > "${release_changelog_filename}" 258 | Automatically generated release with assets. 259 | 260 | Changelog: 261 | $(cat "${release_changelog_filename}") 262 | EOF 263 | 264 | echo "::set-output name=changelog_filename::${changelog_filename}" 265 | echo "::set-output name=release_changelog_filename::${release_changelog_filename}" 266 | 267 | - name: Display changelog 268 | run: echo "${{ steps.generate_changelog.outputs.changelog }}" 269 | 270 | - name: Login to Docker registry 271 | id: docker_registry_login 272 | run: | 273 | echo "${{ secrets.GITHUB_TOKEN }}" | docker login docker.pkg.github.com -u "${{ github.actor }}" --password-stdin 274 | echo "${{ secrets.QUAYIO_PASSWORD }}" | docker login quay.io -u "${{ secrets.QUAYIO_USERNAME }}" --password-stdin 275 | 276 | - name: Define version suffix 277 | id: version_suffix 278 | run: | 279 | if [[ "${BRANCH_NAME}" == "master" ]]; then 280 | short_suffix="" 281 | long_suffix="" 282 | else 283 | short_suffix="${BRANCH_NAME:0:1}" 284 | long_suffix="${BRANCH_NAME}" 285 | fi 286 | 287 | echo "::set-output name=short::${short_suffix}" 288 | echo "::set-output name=long::${long_suffix}" 289 | 290 | - name: Build Docker image 291 | id: build_docker_image 292 | shell: bash 293 | run: > 294 | GITHUB_IMAGE_ID=$(echo "${{ github.repository }}/${APP_NAME}${IMAGE_ID}" | tr '[A-Z]' '[a-z]') 295 | QUAYIO_IMAGE_ID=$(echo "reinventedstuff/${APP_NAME}${IMAGE_ID}" | tr '[A-Z]' '[a-z]') 296 | 297 | docker build 298 | --tag "docker.pkg.github.com/${GITHUB_IMAGE_ID}:${PLANNED_VERSION}${{ steps.version_suffix.outputs.long }}" 299 | --tag "quay.io/${QUAYIO_IMAGE_ID}:${PLANNED_VERSION}${{ steps.version_suffix.outputs.long }}" 300 | --build-arg BUILD_VERSION="${PLANNED_VERSION}" 301 | --file Dockerfile 302 | . 303 | 304 | - name: Push Docker image to registry 305 | run: | 306 | GITHUB_IMAGE_ID=$(echo "${{ github.repository }}/${APP_NAME}${IMAGE_ID}" | tr '[A-Z]' '[a-z]') 307 | QUAYIO_IMAGE_ID=$(echo "reinventedstuff/${APP_NAME}${IMAGE_ID}" | tr '[A-Z]' '[a-z]') 308 | 309 | docker push "docker.pkg.github.com/${GITHUB_IMAGE_ID}:${PLANNED_VERSION}${{ steps.version_suffix.outputs.long }}" 310 | docker push "quay.io/${QUAYIO_IMAGE_ID}:${PLANNED_VERSION}${{ steps.version_suffix.outputs.long }}" 311 | 312 | - name: Setup RPM Build environment 313 | id: setup_rpmbuild_env 314 | shell: bash 315 | run: | 316 | sudo apt-get update 317 | sudo apt-get install rpm 318 | 319 | make build 320 | make compress 321 | 322 | # rpmdev-setuptree 323 | mkdir /home/runner/rpmbuild 324 | mkdir -pv /home/runner/rpmbuild/BUILD 325 | mkdir -pv /home/runner/rpmbuild/BUILDROOT 326 | mkdir -pv /home/runner/rpmbuild/RPMS 327 | mkdir -pv /home/runner/rpmbuild/SOURCES 328 | mkdir -pv /home/runner/rpmbuild/SPECS 329 | mkdir -pv /home/runner/rpmbuild/SRPMS 330 | 331 | cp -v ".rpm/nginx-mail-auth-http-server.spec.tpl" /home/runner/rpmbuild/SPECS 332 | mv -v /home/runner/rpmbuild/SPECS/nginx-mail-auth-http-server.spec.tpl /home/runner/rpmbuild/SPECS/nginx-mail-auth-http-server.spec 333 | 334 | sed -i"" \ 335 | -e "s/__VERSION__/${PLANNED_VERSION}/" \ 336 | -e "s/__SOURCE_TARGZ_FILENAME__/nginx-mail-auth-http-server-${PLANNED_VERSION}.tar.gz/" \ 337 | /home/runner/rpmbuild/SPECS/nginx-mail-auth-http-server.spec 338 | 339 | cat "${{steps.generate_changelog.outputs.changelog_filename}}" >> /home/runner/rpmbuild/SPECS/nginx-mail-auth-http-server.spec 340 | cat -n /home/runner/rpmbuild/SPECS/nginx-mail-auth-http-server.spec 341 | 342 | cp -v "nginx-mail-auth-http-server-${PLANNED_VERSION}.tar.gz" /home/runner/rpmbuild/SOURCES 343 | 344 | - name: Build RPM package 345 | id: build_rpm_package 346 | shell: bash 347 | run: | 348 | cd /home/runner 349 | rpmbuild -v -ba "rpmbuild/SPECS/nginx-mail-auth-http-server.spec" 350 | 351 | - name: Verify RPM package 352 | id: verify_rpm_package 353 | run: | 354 | ls -la /home/runner/rpmbuild/RPMS/x86_64/nginx-mail-auth-http-server-${PLANNED_VERSION}-1.x86_64.rpm 355 | echo "::set-output name=path_to_rpm_file::/home/runner/rpmbuild/RPMS/x86_64/nginx-mail-auth-http-server-${PLANNED_VERSION}-1.x86_64.rpm" 356 | echo "::set-output name=rpm_filename::nginx-mail-auth-http-server-${PLANNED_VERSION}-1.x86_64.rpm" 357 | 358 | - name: Upload a Build Artifact (RPM package) 359 | id: upload_artifact_rpm 360 | uses: actions/upload-artifact@v2.2.2 361 | with: 362 | name: "${{ steps.verify_rpm_package.outputs.rpm_filename }}" 363 | path: "${{ steps.verify_rpm_package.outputs.path_to_rpm_file }}" 364 | retention-days: 30 365 | 366 | - name: Setup DEB Build environment 367 | id: setup_debbuild_env 368 | shell: bash 369 | run: | 370 | size="$(stat --printf="%s" nginx-mail-auth-http-server)" 371 | 372 | mkdir -v /home/runner/debbuild 373 | mkdir -v /home/runner/debbuild/DEBIAN 374 | mkdir -vp /home/runner/debbuild/usr/bin 375 | mkdir -vp "/home/runner/debbuild/usr/share/doc/nginx-mail-auth-http-server-${PLANNED_VERSION}" 376 | 377 | cp -v ".deb/control.tpl" /home/runner/debbuild/DEBIAN 378 | mv -v /home/runner/debbuild/DEBIAN/control.tpl /home/runner/debbuild/DEBIAN/control 379 | 380 | cp -v "nginx-mail-auth-http-server-${PLANNED_VERSION}/nginx-mail-auth-http-server" /home/runner/debbuild/usr/bin/ 381 | cp -v "nginx-mail-auth-http-server-${PLANNED_VERSION}/README.md" "/home/runner/debbuild/usr/share/doc/nginx-mail-auth-http-server-${PLANNED_VERSION}" 382 | 383 | sed -i"" \ 384 | -e "s/__VERSION__/${PLANNED_VERSION}/" \ 385 | -e "s/__SIZE__/${size}/" \ 386 | /home/runner/debbuild/DEBIAN/control 387 | 388 | cat -n /home/runner/debbuild/DEBIAN/control 389 | 390 | - name: Build DEB package 391 | id: build_deb_package 392 | shell: bash 393 | run: | 394 | cd /home/runner 395 | dpkg-deb --build debbuild 396 | 397 | mv debbuild.deb nginx-mail-auth-http-server-${PLANNED_VERSION}_amd64.deb 398 | 399 | - name: Verify DEB package 400 | id: verify_deb_package 401 | run: | 402 | ls -la "/home/runner/nginx-mail-auth-http-server-${PLANNED_VERSION}_amd64.deb" 403 | echo "::set-output name=path_to_deb_file::/home/runner/nginx-mail-auth-http-server-${PLANNED_VERSION}_amd64.deb" 404 | echo "::set-output name=deb_filename::nginx-mail-auth-http-server-${PLANNED_VERSION}_amd64.deb" 405 | 406 | - name: Upload a Build Artifact (DEB package) 407 | id: upload_artifact_deb 408 | uses: actions/upload-artifact@v2.2.2 409 | with: 410 | name: "${{ steps.verify_deb_package.outputs.deb_filename }}" 411 | path: "${{ steps.verify_deb_package.outputs.path_to_deb_file }}" 412 | retention-days: 30 413 | 414 | - name: Install DEB package 415 | id: install_deb_package 416 | run: | 417 | sudo dpkg -i "${{steps.verify_deb_package.outputs.path_to_deb_file}}" 418 | ls -la /usr/bin/nginx-mail-auth-http-server 419 | ls -la /usr/share/doc/nginx-mail-auth-http-server* 420 | 421 | - name: Create a new tag 422 | if: > 423 | env.CAN_CREATE_RELEASE == 'true' && 424 | env.BRANCH_NAME == 'master' && 425 | github.event_name == 'push' 426 | run: | 427 | curl --request POST --url https://api.github.com/repos/${{ github.repository }}/git/tags \ 428 | -H 'authorization: Bearer ${{ secrets.GITHUB_TOKEN }}' \ 429 | -H 'content-type: application/json' \ 430 | --data '{"tag": "${env.PLANNED_VERSION}", 431 | "message": "Pipeline build tag", 432 | "object": "${{ github.sha }}", 433 | "type": "commit", 434 | "tagger": { 435 | "name": "Alice from Wonderland", 436 | "email": "noreply@localhost.localdomain", 437 | "date": "${{ steps.timestamp.outputs.timestamp }}" 438 | }' 439 | 440 | - name: Create a Release 441 | id: create_release 442 | if: > 443 | env.CAN_CREATE_RELEASE == 'true' && 444 | env.BRANCH_NAME == 'master' && 445 | github.event_name == 'push' 446 | uses: actions/create-release@v1 447 | env: 448 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 449 | with: 450 | tag_name: ${{ env.PLANNED_VERSION }} 451 | release_name: v${{ env.PLANNED_VERSION }} 452 | body_path: ${{ steps.generate_changelog.outputs.release_changelog_filename }} 453 | draft: false 454 | prerelease: false 455 | 456 | - name: Upload a Release Asset 457 | if: > 458 | env.CAN_CREATE_RELEASE == 'true' && 459 | env.BRANCH_NAME == 'master' && 460 | github.event_name == 'push' 461 | uses: actions/upload-release-asset@v1.0.2 462 | env: 463 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 464 | with: 465 | upload_url: ${{ steps.create_release.outputs.upload_url }} 466 | asset_path: ${{ steps.artifact_meta.outputs.full_path }} 467 | asset_name: ${{ env.ARTIFACT_NAME }} 468 | asset_content_type: application/octet-stream 469 | 470 | - name: Upload a Release Asset (RPM package) 471 | if: > 472 | needs.validate_new_version.outputs.can_create == 'true' && 473 | needs.validate_new_version.outputs.branch_name == 'master' && 474 | github.event_name == 'push' 475 | uses: actions/upload-release-asset@v1.0.2 476 | env: 477 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 478 | with: 479 | upload_url: ${{ steps.create_release.outputs.upload_url }} 480 | asset_path: ${{ steps.verify_rpm_package.outputs.path_to_rpm_file }} 481 | asset_name: ${{ steps.verify_rpm_package.outputs.rpm_filename }} 482 | asset_content_type: application/octet-stream 483 | 484 | - name: Upload a Release Asset (DEB package) 485 | if: > 486 | needs.validate_new_version.outputs.can_create == 'true' && 487 | needs.validate_new_version.outputs.branch_name == 'master' && 488 | github.event_name == 'push' 489 | uses: actions/upload-release-asset@v1.0.2 490 | env: 491 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 492 | with: 493 | upload_url: ${{ steps.create_release.outputs.upload_url }} 494 | asset_path: ${{ steps.verify_deb_package.outputs.path_to_deb_file }} 495 | asset_name: ${{ steps.verify_deb_package.outputs.deb_filename }} 496 | asset_content_type: application/octet-stream 497 | 498 | # - name: Upload RPM package to repository 499 | # if: > 500 | # needs.validate_new_version.outputs.can_create == 'true' && 501 | # needs.validate_new_version.outputs.branch_name == 'master' && 502 | # github.event_name == 'push' 503 | # id: upload_rpm_to_repository 504 | # run: | 505 | # export RSYNC_PASSWORD="${{ secrets.RSYNC_PASSWORD }}" 506 | # rsync \ 507 | # -raz -vv \ 508 | # --port "${{ secrets.RSYNC_PORT }}" \ 509 | # "${{steps.verify_rpm_package.outputs.path_to_rpm_file}}" \ 510 | # ${{ secrets.RSYNC_USERNAME }}@${{ secrets.RSYNC_HOSTNAME }}::${{ secrets.RSYNC_PATH_RPM_EL7 }} 511 | 512 | # - name: Upload DEB package to repository 513 | # if: > 514 | # needs.validate_new_version.outputs.can_create == 'true' && 515 | # needs.validate_new_version.outputs.branch_name == 'master' && 516 | # github.event_name == 'push' 517 | # id: upload_deb_to_repository 518 | # run: | 519 | # export RSYNC_PASSWORD="${{ secrets.RSYNC_PASSWORD }}" 520 | # rsync \ 521 | # -raz -vv \ 522 | # --port "${{ secrets.RSYNC_PORT }}" \ 523 | # "${{steps.verify_deb_package.outputs.path_to_deb_file}}" \ 524 | # ${{ secrets.RSYNC_USERNAME }}@${{ secrets.RSYNC_HOSTNAME }}::${{ secrets.RSYNC_PATH_DEB }} 525 | 526 | - name: Send out notification about release 527 | uses: rest-gateway/notification-action@master 528 | with: 529 | message: "Release ${{env.PLANNED_VERSION}} happened for ${{env.APP_NAME}}. Yay." 530 | recipient: "${{ secrets.NOTIFICATIONS_DEFAULT_RECIPIENT }}" 531 | rest_gateway_url: "${{ secrets.REST_GATEWAY_API_URL }}" 532 | rest_gateway_token: "${{ secrets.REST_GATEWAY_TOKEN }}" 533 | rest_gateway_bot_name: "${{ secrets.REST_GATEWAY_BOT_NAME }}" 534 | --------------------------------------------------------------------------------