├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── VERSION ├── build.sh ├── init.go ├── main.go └── types ├── config.go ├── protocol_config.go └── proxy_config.go /.gitignore: -------------------------------------------------------------------------------- 1 | bin* 2 | config.json 3 | conf.d 4 | nginx-mail-auth-http 5 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:jessie 2 | 3 | ARG VERSION 4 | 5 | COPY bin/$VERSION/nginx-mail-auth-http-linux-amd64 /usr/local/bin/nginx-mail-auth-http 6 | 7 | RUN chmod +x /usr/local/bin/nginx-mail-auth-http && \ 8 | mkdir -p /etc/nginx-mail-auth-http/conf.d/ && \ 9 | echo '{}' > /etc/nginx-mail-auth-http/config.json 10 | 11 | EXPOSE 8278 12 | 13 | CMD ["nginx-mail-auth-http"] 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Henrik Urlund 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nginx-mail-auth-http 2 | A [Go](https://golang.org/) HTTP authentication server for `ngx_mail_auth_http_module`. Auth lookups will be cached to minimize workload. 3 | 4 | ### command options 5 | 6 | ``` 7 | $ nginx-mail-auth-http -help 8 | Usage of nginx-mail-auth-http: 9 | -auth-header string 10 | Checks the specified header in requests sent to the authentication server (default "Auth-Key") 11 | -auth-key string 12 | This header can be used as the shared secret to verify that the request comes from nginx 13 | -cache-cleanup string 14 | Interval between cache cleanups (see: https://golang.org/pkg/time/#ParseDuration) (default "1m") 15 | -cache-ttl string 16 | Time to keep proxy configs in cache since last usage (see: https://golang.org/pkg/time/#ParseDuration) (default "24h") 17 | -config-file string 18 | Name of config file (default "config.json") 19 | -config-path string 20 | Path where '-config-file' (and conf.d) can be found (default "/etc/nginx-mail-auth-http") 21 | -listen string 22 | Address to handle requests on incoming connections (default ":8278") 23 | -version 24 | Show version 25 | ``` 26 | 27 | ## configuration 28 | A configuration is based on (up to) 3 parts, *default*, *templates* and *proxy config* (only *default* is **required**) 29 | 30 | 31 | ### config.json 32 | 33 | ``` 34 | { 35 | "default": { 36 | PROXY_CONFIG 37 | }, 38 | "templates": { 39 | "your-template-name": { 40 | PROXY_CONFIG 41 | }, 42 | "your-template-name-N": { 43 | PROXY_CONFIG 44 | } 45 | } 46 | } 47 | ``` 48 | 49 | #### proxy config 50 | 51 | ``` 52 | { 53 | "pop3": { 54 | "ip": "YOUR_POP3_IP", 55 | "port": YOUR_POP3_PORT 56 | }, 57 | "imap": { 58 | "ip": "YOUR_IMAP_IP", 59 | "port": YOUR_IMAP_PORT 60 | }, 61 | "smtp": { 62 | "ip": "YOUR_SMTP_IP", 63 | "port": YOUR_SMTP_PORT 64 | } 65 | } 66 | ``` 67 | 68 | #### domain config 69 | almost the same as *proxy config* but it supports *template*: 70 | 71 | ``` 72 | { 73 | "template": TEMPLATE_NAME, 74 | ... 75 | } 76 | ``` 77 | 78 | If *template* is used *default* and *template* configuration can be overridden using *pop3*, *imap* and *smtp* settings as in proxy config (see configuration example 3). 79 | 80 | ## configuration examples 81 | 82 | ### example 1 83 | A basic `config.json` example that will auth all domains to a single server. 84 | 85 | ``` 86 | { 87 | "default": { 88 | "pop3": { 89 | "ip": "YOUR_POP3_IP", 90 | "port": YOUR_POP3_PORT 91 | }, 92 | "imap": { 93 | "ip": "YOUR_IMAP_IP", 94 | "port": YOUR_IMAP_PORT 95 | }, 96 | "smtp": { 97 | "ip": "YOUR_SMTP_IP", 98 | "port": YOUR_SMTP_PORT 99 | } 100 | } 101 | } 102 | ``` 103 | 104 | ### example 2 105 | Based on example 1 you can specify seperate domains to be auth'ed to another server by creating a seperate configuration in your `conf.d` folder (eg. `conf.d/example.com`): 106 | 107 | ``` 108 | { 109 | "pop3": { 110 | "ip": "ANOTHER_POP3_IP", 111 | }, 112 | "imap": { 113 | "ip": "ANOTHER_IMAP_IP", 114 | }, 115 | "smtp": { 116 | "ip": "ANOTHER_SMTP_IP", 117 | } 118 | } 119 | ``` 120 | 121 | (Please note that this configuration does not contain any port configuration. They will be applied from "default" defined in `config.json`). 122 | 123 | ### example 3 124 | If you have a lot of domains using the server from example 2, it might be a good idea to define the example 2 configuration as a `serverX` template in `config.json` 125 | 126 | ``` 127 | { 128 | "default": { 129 | ... 130 | }, 131 | "templates": { 132 | "serverX": { 133 | ... 134 | } 135 | } 136 | } 137 | ``` 138 | 139 | Instead of the complete configuration in your `conf.d/example.com` you can now define a reference to your template instead: 140 | 141 | ``` 142 | { 143 | "template": "serverX" 144 | } 145 | ``` 146 | 147 | And even template references can be overridden in your domain configuration: 148 | 149 | ``` 150 | { 151 | "template": "serverX", 152 | "smtp": { 153 | "ip": "YET_ANOTHER_SMTP_IP" 154 | } 155 | } 156 | ``` 157 | 158 | ## NGINX configuration 159 | 160 | `ngx_mail_auth_http_module` configuration: 161 | 162 | ``` 163 | mail { 164 | auth_http http://SERVER_IP:8278; 165 | 166 | # you are encuraged to configure auth_http_header (not required) 167 | # if you do so - remember to configure the '-auth-key' flag 168 | auth_http_header X-Auth-Key "YOUR_SECRET_STRING"; 169 | } 170 | ``` 171 | 172 | See [`ngx_mail_auth_http_module`](http://nginx.org/en/docs/mail/ngx_mail_auth_http_module.html) for more detailed configuration description. 173 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 1.0.1 2 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | VERSION=`cat VERSION` 3 | GOOS=linux 4 | GOARCH=amd64 5 | BIN_NAME="nginx-mail-auth-http" 6 | BIN_PATH="bin/$VERSION/$BIN_NAME-$GOOS-$GOARCH" 7 | GIT_REPO="github.com/urlund/$BIN_NAME" 8 | 9 | rm -rf $BIN_NAME 10 | 11 | sed -i '' -e "s/\"version: .*\"/\"version: $VERSION\"/g" init.go 12 | 13 | # check if docker is installed 14 | if [ $(which docker > /dev/null 2>&1; echo $?) -ne 0 ]; then 15 | echo "you must have docker installed to use this script" 16 | exit 1; 17 | fi 18 | 19 | # check if $GOPATH isset 20 | if [ -z $GOPATH ]; then 21 | echo "you must set \$GOPATH to use this script" 22 | exit 1; 23 | fi 24 | 25 | # run docker with build cmd 26 | docker run --rm -it -v "$GOPATH":/work -e "GOPATH=/work" -w /work/src/$GIT_REPO -e GOOS=$GOOS -e GOARCH=$GOARCH golang:latest go build -o $BIN_PATH 27 | 28 | docker build -t urlund/nginx-mail-auth-http -t urlund/nginx-mail-auth-http:$VERSION --build-arg VERSION=$VERSION . 29 | -------------------------------------------------------------------------------- /init.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "fmt" 7 | "io/ioutil" 8 | "os" 9 | "path/filepath" 10 | "sync" 11 | "time" 12 | 13 | "github.com/urlund/nginx-mail-auth-http/types" 14 | ) 15 | 16 | var ( 17 | cacheTTL string 18 | cacheCleanup string 19 | configFile string 20 | configPath string 21 | listen string 22 | authKey string 23 | authHeader string 24 | version bool 25 | config types.Config 26 | proxyConfigCache map[string]types.ProxyConfig 27 | timeout time.Duration 28 | cleanup time.Duration 29 | mu sync.Mutex 30 | ) 31 | 32 | func init() { 33 | proxyConfigCache = map[string]types.ProxyConfig{} 34 | 35 | // why 8278? because is an unsigned port @ iana.org 36 | // (and, 25 + 110 + 143 = 278 and starting with 8 because 8080 is starting with 8 and typically used for a personally hosted web server) 37 | flag.StringVar(&listen, "listen", ":8278", "Address to handle requests on incoming connections") 38 | flag.StringVar(&cacheTTL, "cache-ttl", "24h", "Time to keep proxy configs in cache since last usage (see: https://golang.org/pkg/time/#ParseDuration)") 39 | flag.StringVar(&cacheCleanup, "cache-cleanup", "1m", "Interval between cache cleanups (see: https://golang.org/pkg/time/#ParseDuration)") 40 | flag.StringVar(&configFile, "config-file", "config.json", "Name of config file") 41 | flag.StringVar(&configPath, "config-path", "/etc/nginx-mail-auth-http", "Path where '-config-file' (and conf.d) can be found") 42 | flag.StringVar(&authKey, "auth-key", "", "This header can be used as the shared secret to verify that the request comes from nginx") 43 | flag.StringVar(&authHeader, "auth-header", "Auth-Key", "Checks the specified header in requests sent to the authentication server") 44 | flag.BoolVar(&version, "version", version, "Show version") 45 | flag.Parse() 46 | 47 | if version { 48 | fmt.Println("version: 1.0.1") 49 | os.Exit(0) 50 | } 51 | 52 | var err error 53 | timeout, err = time.ParseDuration(cacheTTL) 54 | if err != nil { 55 | panic(err) 56 | } 57 | 58 | cleanup, err = time.ParseDuration(cacheCleanup) 59 | if err != nil { 60 | panic(err) 61 | } 62 | 63 | jsonBlob, err := ioutil.ReadFile(filepath.Join(configPath, configFile)) 64 | if err != nil { 65 | panic(err) 66 | } 67 | 68 | err = json.Unmarshal(jsonBlob, &config) 69 | if err != nil { 70 | panic(err) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "path/filepath" 9 | "regexp" 10 | "strconv" 11 | "time" 12 | 13 | "github.com/urlund/nginx-mail-auth-http/types" 14 | ) 15 | 16 | func debugConfig(p interface{}) { 17 | x, _ := json.Marshal(p) 18 | fmt.Println(string(x)) 19 | } 20 | 21 | func handleResponse(w http.ResponseWriter, r *http.Request, message string) { 22 | w.Header().Add("Auth-Status", message) 23 | 24 | if message == "OK" { 25 | if r.Header.Get("Auth-Method") == "cram-md5" { 26 | w.Header().Add("Auth-Pass", "plain-text-pass") 27 | } 28 | } 29 | } 30 | 31 | func getProxyConfig(domain string) (types.ProxyConfig, string) { 32 | proxyConfig := config.Default 33 | var domainConfig types.ProxyConfig 34 | 35 | jsonBlob, err := ioutil.ReadFile(filepath.Join(configPath, "conf.d", domain)) 36 | if err != nil { 37 | return proxyConfig, "" 38 | } 39 | 40 | err = json.Unmarshal(jsonBlob, &domainConfig) 41 | if err != nil { 42 | return proxyConfig, "unable to load proxy config" 43 | } 44 | 45 | // check if we need to apply a template 46 | if domainConfig.Template != "" { 47 | if templateConfig, templateFound := config.Templates[domainConfig.Template]; templateFound == true { 48 | proxyConfig.Apply(&templateConfig) 49 | } 50 | } 51 | 52 | // ... 53 | proxyConfig.Apply(&domainConfig) 54 | 55 | return proxyConfig, "" 56 | } 57 | 58 | func getAuthServerAndPort(w http.ResponseWriter, r *http.Request, domain string) (err string) { 59 | proxyConfig, cacheFound := proxyConfigCache[domain] 60 | 61 | if cacheFound == false || (cacheFound == true && time.Now().After(proxyConfig.Timeout)) { 62 | // get proxy config 63 | proxyConfig, err = getProxyConfig(domain) 64 | if err != "" { 65 | return err 66 | } 67 | 68 | proxyConfigCache[domain] = proxyConfig 69 | w.Header().Add("X-Cache", "MISS") 70 | } else { 71 | w.Header().Add("X-Cache", "HIT") 72 | } 73 | 74 | // get auth ip and port 75 | protocol := r.Header.Get("Auth-Protocol") 76 | ip := proxyConfig.IP(protocol) 77 | port := proxyConfig.Port(protocol) 78 | 79 | // check if ip and port was found 80 | if ip == "" || port == 0 { 81 | return fmt.Sprintf("unable to find proxy server or port for protocol: '%s'", protocol) 82 | } 83 | 84 | mu.Lock() 85 | defer mu.Unlock() 86 | 87 | // extend cache timeout 88 | proxyConfig.Timeout = time.Now().Add(timeout) 89 | proxyConfigCache[domain] = proxyConfig 90 | 91 | // set auth headers 92 | w.Header().Add("Auth-Server", ip) 93 | w.Header().Add("Auth-Port", strconv.Itoa(port)) 94 | 95 | // no error occured 96 | return "" 97 | } 98 | 99 | func main() { 100 | // cleanup expired cache entries 101 | go func() { 102 | for { 103 | for domain, proxyConfig := range proxyConfigCache { 104 | if time.Now().After(proxyConfig.Timeout) { 105 | delete(proxyConfigCache, domain) 106 | } 107 | } 108 | 109 | time.Sleep(cleanup) 110 | } 111 | }() 112 | 113 | // handle mail proxy auth 114 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 115 | if authKey != "" && r.Header.Get(authHeader) != authKey { 116 | handleResponse(w, r, "invalid auth key, check your configuration") 117 | return 118 | } 119 | 120 | user := r.Header.Get("Auth-User") 121 | if user == "" || r.Header.Get("Auth-Pass") == "" { 122 | handleResponse(w, r, "username and password are required") 123 | return 124 | } 125 | 126 | // validate user as email address 127 | re := regexp.MustCompile("(.+)@(.+\\..+)") 128 | if re.Match([]byte(user)) == false { 129 | handleResponse(w, r, "please use a valid email address") 130 | return 131 | } 132 | 133 | // user parts consist of [email, name, domain] 134 | userParts := re.FindStringSubmatch(user) 135 | if len(userParts) != 3 { 136 | handleResponse(w, r, "invalid email address") 137 | return 138 | } 139 | 140 | // get domain proxy server and port 141 | err := getAuthServerAndPort(w, r, userParts[2]) 142 | if err != "" { 143 | handleResponse(w, r, err) 144 | return 145 | } 146 | 147 | // ... 148 | handleResponse(w, r, "OK") 149 | }) 150 | 151 | // start (keep things running) 152 | http.ListenAndServe(listen, nil) 153 | } 154 | -------------------------------------------------------------------------------- /types/config.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | type Config struct { 4 | Default ProxyConfig `json:"default"` 5 | Templates map[string]ProxyConfig `json:"templates"` 6 | } 7 | -------------------------------------------------------------------------------- /types/protocol_config.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | type ProtocolConfig struct { 4 | IP string `json:"ip,omitempty"` 5 | Port int `json:"port,omitempty"` 6 | } 7 | -------------------------------------------------------------------------------- /types/proxy_config.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type ProxyConfig struct { 8 | POP3 ProtocolConfig `json:"pop3,omitempty"` 9 | IMAP ProtocolConfig `json:"imap,omitempty"` 10 | SMTP ProtocolConfig `json:"smtp,omitempty"` 11 | Template string `json:"template,omitempty"` 12 | Timeout time.Time 13 | } 14 | 15 | func (this *ProxyConfig) Port(protocol string) int { 16 | switch protocol { 17 | case "pop3": 18 | if this.POP3.Port != 0 { 19 | return this.POP3.Port 20 | } 21 | case "imap": 22 | if this.IMAP.Port != 0 { 23 | return this.IMAP.Port 24 | } 25 | case "smtp": 26 | if this.SMTP.Port != 0 { 27 | return this.SMTP.Port 28 | } 29 | } 30 | 31 | return 0 32 | } 33 | 34 | func (this *ProxyConfig) IP(protocol string) string { 35 | switch protocol { 36 | case "pop3": 37 | if this.POP3.IP != "" { 38 | return this.POP3.IP 39 | } 40 | case "imap": 41 | if this.IMAP.IP != "" { 42 | return this.IMAP.IP 43 | } 44 | case "smtp": 45 | if this.SMTP.IP != "" { 46 | return this.SMTP.IP 47 | } 48 | } 49 | 50 | return "" 51 | } 52 | 53 | func (this *ProxyConfig) Apply(config *ProxyConfig) { 54 | if config.POP3.IP != "" { 55 | this.POP3.IP = config.POP3.IP 56 | } 57 | 58 | if config.POP3.Port > 0 { 59 | this.POP3.Port = config.POP3.Port 60 | } 61 | 62 | if config.IMAP.IP != "" { 63 | this.IMAP.IP = config.IMAP.IP 64 | } 65 | 66 | if config.IMAP.Port > 0 { 67 | this.IMAP.Port = config.IMAP.Port 68 | } 69 | 70 | if config.SMTP.IP != "" { 71 | this.SMTP.IP = config.SMTP.IP 72 | } 73 | 74 | if config.SMTP.Port > 0 { 75 | this.SMTP.Port = config.SMTP.Port 76 | } 77 | 78 | if config.Template != "" { 79 | if config.Template != "" { 80 | this.Template = config.Template 81 | } 82 | 83 | if config.Template != "" { 84 | this.Template = config.Template 85 | } 86 | } 87 | } 88 | --------------------------------------------------------------------------------