Sfoglia il codice sorgente

Merge branch 'master' into dashboard

flashmob 7 anni fa
parent
commit
96340cd2c9

+ 3 - 2
.travis.yml

@@ -1,9 +1,10 @@
 language: go
 sudo: false
 go:
-  - 1.5
-  - 1.6
   - 1.7
+  - 1.8
+  - 1.9
+  - 1.10.x
   - master
 
 before_install:

+ 4 - 0
Makefile

@@ -31,6 +31,10 @@ dashboard: dashboard/*.go */*/*/*.js */*/*/*/*.js
 guerrillad: *.go */*.go */*/*.go
 	$(GO_VARS) $(GO) build -o="guerrillad" -ldflags="$(LD_FLAGS)" $(ROOT)/cmd/guerrillad
 
+guerrilladrace: *.go */*.go */*/*.go
+	$(GO_VARS) $(GO) build -o="guerrillad" -race -ldflags="$(LD_FLAGS)" $(ROOT)/cmd/guerrillad
+
+
 test: *.go */*.go */*/*.go
 	$(GO_VARS) $(GO) test -v .
 	$(GO_VARS) $(GO) test -v ./tests

+ 6 - 2
README.md

@@ -1,6 +1,8 @@
 
 [![Build Status](https://travis-ci.org/flashmob/go-guerrilla.svg?branch=master)](https://travis-ci.org/flashmob/go-guerrilla)
 
+Breaking change: The structure of the config has recently changed to accommodate more advanced TLS settings.
+
 Go-Guerrilla SMTP Daemon
 ====================
 
@@ -92,6 +94,7 @@ $ make guerrillad
 ```
 
 This will create a executable file named `guerrillad` that's ready to run.
+See the [build notes](https://github.com/flashmob/go-guerrilla/wiki/Build-Notes) for more details.
 
 Next, copy the `goguerrilla.conf.sample` file to `goguerrilla.conf.json`. 
 You may need to customize the `pid_file` setting to somewhere local, 
@@ -132,7 +135,7 @@ Go-Guerrilla can be imported and used as a package in your Go project.
 #### 1. Import the guerrilla package
 ```go
 import (
-    "github.com/flashmob/go-guerrilla/guerrilla"
+    "github.com/flashmob/go-guerrilla"
 )
 
 
@@ -245,7 +248,8 @@ The following processors can be imported to your project, then use the
 | Processor | Description |
 |-----------|-------------|
 |[MailDir](https://github.com/flashmob/maildir-processor)|Save emails to a maildir. [MailDiranasaurus](https://github.com/flashmob/maildiranasaurus) is an example project|
-|[FastCGI](https://github.com/flashmob/fastcgi-processor)|Deliver email directly to PHP-FPM or a similar FastCGI backend.
+|[FastCGI](https://github.com/flashmob/fastcgi-processor)|Deliver email directly to PHP-FPM or a similar FastCGI backend.|
+|[WildcardProcessor](https://github.com/DevelHell/wildcard-processor)|Use wildcards for recipients host validation.|
 
 Have a processor that you would like to share? Submit a PR to add it to the list!
 

+ 1 - 1
api.go

@@ -93,7 +93,7 @@ func (d *Daemon) LoadConfig(path string) (AppConfig, error) {
 	var ac AppConfig
 	data, err := ioutil.ReadFile(path)
 	if err != nil {
-		return ac, fmt.Errorf("Could not read config file: %s", err.Error())
+		return ac, fmt.Errorf("could not read config file: %s", err.Error())
 	}
 	err = ac.Load(data)
 	if err != nil {

+ 49 - 27
api_test.go

@@ -16,16 +16,14 @@ import (
 
 // Test Starting smtp without setting up logger / backend
 func TestSMTP(t *testing.T) {
+	done := make(chan bool)
 	go func() {
 		select {
 		case <-time.After(time.Second * 40):
-			//buf := make([]byte, 1<<16)
-			//stackSize := runtime.Stack(buf, true)
-			//fmt.Printf("%s\n", string(buf[0:stackSize]))
-			//panic("timeout")
 			t.Error("timeout")
 			return
-
+		case <-done:
+			return
 		}
 	}()
 
@@ -52,6 +50,7 @@ func TestSMTP(t *testing.T) {
 	}
 	time.Sleep(time.Second * 2)
 	d.Shutdown()
+	done <- true
 
 }
 
@@ -134,13 +133,15 @@ func TestSMTPLoadFile(t *testing.T) {
             "is_enabled" : true,
             "host_name":"mail.guerrillamail.com",
             "max_size": 100017,
-            "private_key_file":"config_test.go",
-            "public_key_file":"config_test.go",
             "timeout":160,
             "listen_interface":"127.0.0.1:2526",
-            "start_tls_on":false,
-            "tls_always_on":false,
-            "max_clients": 2
+            "max_clients": 2,
+			"tls" : {
+				"private_key_file":"config_test.go",
+            	"public_key_file":"config_test.go",
+				"start_tls_on":false,
+            	"tls_always_on":false
+			}
         }
     ]
 }
@@ -162,13 +163,15 @@ func TestSMTPLoadFile(t *testing.T) {
             "is_enabled" : true,
             "host_name":"mail.guerrillamail.com",
             "max_size": 100017,
-            "private_key_file":"config_test.go",
-            "public_key_file":"config_test.go",
             "timeout":160,
             "listen_interface":"127.0.0.1:2526",
-            "start_tls_on":false,
-            "tls_always_on":false,
-            "max_clients": 2
+            "max_clients": 2,
+			"tls" : {
+ 				"private_key_file":"config_test.go",
+				"public_key_file":"config_test.go",
+				"start_tls_on":false,
+            	"tls_always_on":false
+			}
         }
     ]
 }
@@ -311,9 +314,9 @@ func TestSetConfigError(t *testing.T) {
 
 	// lets add a new server with bad TLS
 	sc.ListenInterface = "127.0.0.1:2527"
-	sc.StartTLSOn = true
-	sc.PublicKeyFile = "tests/testlog" // totally wrong :->
-	sc.PublicKeyFile = "tests/testlog" // totally wrong :->
+	sc.TLS.StartTLSOn = true
+	sc.TLS.PublicKeyFile = "tests/testlog"  // totally wrong :->
+	sc.TLS.PrivateKeyFile = "tests/testlog" // totally wrong :->
 
 	cfg.Servers = append(cfg.Servers, sc)
 
@@ -414,25 +417,19 @@ func talkToServer(address string) {
 	}
 	in := bufio.NewReader(conn)
 	str, err := in.ReadString('\n')
-	//	fmt.Println(str)
 	fmt.Fprint(conn, "HELO maildiranasaurustester\r\n")
 	str, err = in.ReadString('\n')
-	//	fmt.Println(str)
 	fmt.Fprint(conn, "MAIL FROM:<[email protected]>r\r\n")
 	str, err = in.ReadString('\n')
-	//	fmt.Println(str)
 	fmt.Fprint(conn, "RCPT TO:[email protected]\r\n")
 	str, err = in.ReadString('\n')
-	//	fmt.Println(str)
 	fmt.Fprint(conn, "DATA\r\n")
 	str, err = in.ReadString('\n')
-	//	fmt.Println(str)
 	fmt.Fprint(conn, "Subject: Test subject\r\n")
 	fmt.Fprint(conn, "\r\n")
 	fmt.Fprint(conn, "A an email body\r\n")
 	fmt.Fprint(conn, ".\r\n")
 	str, err = in.ReadString('\n')
-	//	fmt.Println(str)
 	_ = str
 }
 
@@ -443,7 +440,7 @@ func TestReloadConfig(t *testing.T) {
 	os.Truncate("tests/testlog", 0)
 	d := Daemon{}
 	d.Start()
-
+	defer d.Shutdown()
 	cfg := AppConfig{
 		LogFile:      "tests/testlog",
 		AllowedHosts: []string{"grr.la"},
@@ -455,7 +452,6 @@ func TestReloadConfig(t *testing.T) {
 	// Look mom, reloading the config without shutting down!
 	d.ReloadConfig(cfg)
 
-	d.Shutdown()
 }
 
 func TestPubSubAPI(t *testing.T) {
@@ -464,7 +460,7 @@ func TestPubSubAPI(t *testing.T) {
 
 	d := Daemon{Config: &AppConfig{LogFile: "tests/testlog"}}
 	d.Start()
-
+	defer d.Shutdown()
 	// new config
 	cfg := AppConfig{
 		PidFile:      "tests/pidfilex.pid",
@@ -534,3 +530,29 @@ func TestAPILog(t *testing.T) {
 		t.Error("hai was not found in the log, it should have been in tests/testlog")
 	}
 }
+
+// Test the allowed_hosts config option with a single entry of ".", which will allow all hosts.
+func TestSkipAllowsHost(t *testing.T) {
+
+	d := Daemon{}
+	defer d.Shutdown()
+	// setting the allowed hosts to a single entry with a dot will let any host through
+	d.Config = &AppConfig{AllowedHosts: []string{"."}, LogFile: "off"}
+	d.Start()
+
+	conn, err := net.Dial("tcp", d.Config.Servers[0].ListenInterface)
+	if err != nil {
+		t.Error(t)
+		return
+	}
+	in := bufio.NewReader(conn)
+	fmt.Fprint(conn, "HELO test\r\n")
+	fmt.Fprint(conn, "RCPT TO: [email protected]\r\n")
+	in.ReadString('\n')
+	in.ReadString('\n')
+	str, _ := in.ReadString('\n')
+	if strings.Index(str, "250") != 0 {
+		t.Error("expected 250 reply, got:", str)
+	}
+
+}

+ 13 - 10
backends/gateway.go

@@ -140,12 +140,11 @@ func (gw *BackendGateway) Process(e *mail.Envelope) Result {
 	// or timeout
 	select {
 	case status := <-workerMsg.notifyMe:
-		defer workerMsgPool.Put(workerMsg) // can be recycled since we used the notifyMe channel
+		workerMsgPool.Put(workerMsg) // can be recycled since we used the notifyMe channel
 		if status.err != nil {
 			return NewResult(response.Canned.FailBackendTransaction + status.err.Error())
 		}
 		return NewResult(response.Canned.SuccessMessageQueued + status.queuedID)
-
 	case <-time.After(gw.saveTimeout()):
 		Log().Error("Backend has timed out while saving email")
 		return NewResult(response.Canned.FailBackendTimeout)
@@ -170,13 +169,20 @@ func (gw *BackendGateway) ValidateRcpt(e *mail.Envelope) RcptError {
 	// or timeout
 	select {
 	case status := <-workerMsg.notifyMe:
+		workerMsgPool.Put(workerMsg)
 		if status.err != nil {
 			return status.err
 		}
 		return nil
 
 	case <-time.After(gw.validateRcptTimeout()):
-		Log().Error("Backend has timed out while validating rcpt")
+		e.Lock()
+		go func() {
+			<-workerMsg.notifyMe
+			e.Unlock()
+			workerMsgPool.Put(workerMsg)
+			Log().Error("Backend has timed out while validating rcpt")
+		}()
 		return StorageTimeout
 	}
 }
@@ -261,7 +267,7 @@ func (gw *BackendGateway) Initialize(cfg BackendConfig) error {
 	gw.Lock()
 	defer gw.Unlock()
 	if gw.State != BackendStateNew && gw.State != BackendStateShuttered {
-		return errors.New("Can only Initialize in BackendStateNew or BackendStateShuttered state")
+		return errors.New("can only Initialize in BackendStateNew or BackendStateShuttered state")
 	}
 	err := gw.loadConfig(cfg)
 	if err != nil {
@@ -271,7 +277,7 @@ func (gw *BackendGateway) Initialize(cfg BackendConfig) error {
 	workersSize := gw.workersSize()
 	if workersSize < 1 {
 		gw.State = BackendStateError
-		return errors.New("Must have at least 1 worker")
+		return errors.New("must have at least 1 worker")
 	}
 	gw.processors = make([]Processor, 0)
 	gw.validators = make([]Processor, 0)
@@ -404,7 +410,6 @@ func (gw *BackendGateway) workDispatcher(
 
 			if state == dispatcherStateWorking {
 				msg.notifyMe <- &notifyMsg{err: errors.New("storage failed")}
-				msg.e.Unlock()
 			}
 			state = dispatcherStatePanic
 			return
@@ -422,14 +427,13 @@ func (gw *BackendGateway) workDispatcher(
 			Log().Infof("stop signal for worker (#%d)", workerId)
 			return
 		case msg = <-workIn:
-			msg.e.Lock()
-			state = dispatcherStateWorking
+			state = dispatcherStateWorking // recovers from panic if in this state
 			if msg.task == TaskSaveMail {
 				// process the email here
 				result, _ := save.Process(msg.e, TaskSaveMail)
 				state = dispatcherStateNotify
 				if result.Code() < 300 {
-					// if all good, let the gateway know that it was queued
+					// if all good, let the gateway know that it was saved
 					msg.notifyMe <- &notifyMsg{nil, msg.e.QueuedId}
 				} else {
 					// notify the gateway about the error
@@ -446,7 +450,6 @@ func (gw *BackendGateway) workDispatcher(
 					msg.notifyMe <- &notifyMsg{err: nil}
 				}
 			}
-			msg.e.Unlock()
 		}
 		state = dispatcherStateIdle
 	}

+ 14 - 0
backends/p_debugger.go

@@ -3,6 +3,7 @@ package backends
 import (
 	"github.com/flashmob/go-guerrilla/mail"
 	"strings"
+	"time"
 )
 
 // ----------------------------------------------------------------------------------
@@ -24,6 +25,7 @@ func init() {
 
 type debuggerConfig struct {
 	LogReceivedMails bool `json:"log_received_mails"`
+	SleepSec         int  `json:"sleep_seconds,omitempty"`
 }
 
 func Debugger() Decorator {
@@ -45,6 +47,18 @@ func Debugger() Decorator {
 					Log().Infof("Mail from: %s / to: %v", e.MailFrom.String(), e.RcptTo)
 					Log().Info("Headers are:", e.Header)
 				}
+
+				if config.SleepSec > 0 {
+					Log().Infof("sleeping for %d", config.SleepSec)
+					time.Sleep(time.Second * time.Duration(config.SleepSec))
+					Log().Infof("woke up")
+
+					if config.SleepSec == 1 {
+						panic("panic on purpose")
+					}
+
+				}
+
 				// continue to the next Processor in the decorator stack
 				return p.Process(e, task)
 			} else {

+ 20 - 32
backends/p_guerrilla_db_redis.go

@@ -5,21 +5,21 @@ import (
 	"compress/zlib"
 	"database/sql"
 	"fmt"
-	"github.com/flashmob/go-guerrilla/mail"
-	"github.com/garyburd/redigo/redis"
-	"github.com/go-sql-driver/mysql"
 	"io"
 	"math/rand"
 	"runtime/debug"
 	"strings"
 	"sync"
 	"time"
+
+	"github.com/flashmob/go-guerrilla/mail"
+	"github.com/garyburd/redigo/redis"
 )
 
 // ----------------------------------------------------------------------------------
-// Processor Name: GuerrillaRedsDB
+// Processor Name: GuerrillaRedisDB
 // ----------------------------------------------------------------------------------
-// Description   : Saves the body to redis, meta data to mysql. Example only.
+// Description   : Saves the body to redis, meta data to SQL. Example only.
 //               : Limitation: it doesn't save multiple recipients or validate them
 // ----------------------------------------------------------------------------------
 // Config Options: ...
@@ -30,7 +30,7 @@ import (
 // ----------------------------------------------------------------------------------
 func init() {
 	processors["guerrillaredisdb"] = func() Decorator {
-		return GuerrillaDbReddis()
+		return GuerrillaDbRedis()
 	}
 }
 
@@ -56,15 +56,13 @@ type stmtCache [GuerrillaDBAndRedisBatchMax]*sql.Stmt
 
 type guerrillaDBAndRedisConfig struct {
 	NumberOfWorkers    int    `json:"save_workers_size"`
-	MysqlTable         string `json:"mail_table"`
-	MysqlDB            string `json:"mysql_db"`
-	MysqlHost          string `json:"mysql_host"`
-	MysqlPass          string `json:"mysql_pass"`
-	MysqlUser          string `json:"mysql_user"`
+	Table              string `json:"mail_table"`
+	Driver             string `json:"sql_driver"`
+	DSN                string `json:"sql_dsn"`
 	RedisExpireSeconds int    `json:"redis_expire_seconds"`
 	RedisInterface     string `json:"redis_interface"`
 	PrimaryHost        string `json:"primary_mail_host"`
-	BatchTimeout       int    `json:"redis_mysql_batch_timeout,omitempty"`
+	BatchTimeout       int    `json:"redis_sql_batch_timeout,omitempty"`
 }
 
 // Load the backend config for the backend. It has already been unmarshalled
@@ -153,7 +151,7 @@ func (g *GuerrillaDBAndRedisBackend) prepareInsertQuery(rows int, db *sql.DB) *s
 	if g.cache[rows-1] != nil {
 		return g.cache[rows-1]
 	}
-	sqlstr := "INSERT INTO " + g.config.MysqlTable + " "
+	sqlstr := "INSERT INTO " + g.config.Table + " "
 	sqlstr += "(`date`, `to`, `from`, `subject`, `body`, `charset`, `mail`, `spam_score`, `hash`, `content_type`, `recipient`, `has_attach`, `ip_addr`, `return_path`, `is_tls`)"
 	sqlstr += " values "
 	values := "(NOW(), ?, ?, ?, ? , 'UTF-8' , ?, 0, ?, '', ?, 0, ?, ?, ?)"
@@ -304,7 +302,7 @@ func trimToLimit(str string, limit int) string {
 	return ret
 }
 
-func (g *GuerrillaDBAndRedisBackend) mysqlConnect() (*sql.DB, error) {
+func (g *GuerrillaDBAndRedisBackend) sqlConnect() (*sql.DB, error) {
 	tOut := GuerrillaDBAndRedisBatchTimeout
 	if g.config.BatchTimeout > 0 {
 		tOut = time.Duration(g.config.BatchTimeout)
@@ -314,24 +312,14 @@ func (g *GuerrillaDBAndRedisBackend) mysqlConnect() (*sql.DB, error) {
 	if tOut >= 30 {
 		tOut = 29
 	}
-	conf := mysql.Config{
-		User:         g.config.MysqlUser,
-		Passwd:       g.config.MysqlPass,
-		DBName:       g.config.MysqlDB,
-		Net:          "tcp",
-		Addr:         g.config.MysqlHost,
-		ReadTimeout:  tOut * time.Second,
-		WriteTimeout: tOut * time.Second,
-		Params:       map[string]string{"collation": "utf8_general_ci"},
-	}
-	if db, err := sql.Open("mysql", conf.FormatDSN()); err != nil {
-		Log().Error("cannot open mysql", err, "]")
+	if db, err := sql.Open(g.config.Driver, g.config.DSN); err != nil {
+		Log().Error("cannot open database", err, "]")
 		return nil, err
 	} else {
 		// do we have access?
-		_, err = db.Query("SELECT mail_id FROM " + g.config.MysqlTable + " LIMIT 1")
+		_, err = db.Query("SELECT mail_id FROM " + g.config.Table + " LIMIT 1")
 		if err != nil {
-			Log().Error("cannot select table", err)
+			Log().Error("cannot select table:", err)
 			return nil, err
 		}
 		return db, nil
@@ -352,9 +340,9 @@ func (c *redisClient) redisConnection(redisInterface string) (err error) {
 
 type feedChan chan []interface{}
 
-// GuerrillaDbReddis is a specialized processor for Guerrilla mail. It is here as an example.
+// GuerrillaDbRedis is a specialized processor for Guerrilla mail. It is here as an example.
 // It's an example of a 'monolithic' processor.
-func GuerrillaDbReddis() Decorator {
+func GuerrillaDbRedis() Decorator {
 
 	g := GuerrillaDBAndRedisBackend{}
 	redisClient := &redisClient{}
@@ -376,7 +364,7 @@ func GuerrillaDbReddis() Decorator {
 			return err
 		}
 		g.config = bcfg.(*guerrillaDBAndRedisConfig)
-		db, err = g.mysqlConnect()
+		db, err = g.sqlConnect()
 		if err != nil {
 			return err
 		}
@@ -401,7 +389,7 @@ func GuerrillaDbReddis() Decorator {
 
 	Svc.AddShutdowner(ShutdownWith(func() error {
 		db.Close()
-		Log().Infof("closed mysql")
+		Log().Infof("closed sql")
 		if redisClient.conn != nil {
 			Log().Infof("closed redis")
 			redisClient.conn.Close()

+ 48 - 66
backends/p_mysql.go → backends/p_sql.go

@@ -4,29 +4,26 @@ import (
 	"database/sql"
 	"fmt"
 	"strings"
-	"time"
 
 	"github.com/flashmob/go-guerrilla/mail"
-	"github.com/go-sql-driver/mysql"
 
-	"github.com/flashmob/go-guerrilla/response"
 	"math/big"
 	"net"
 	"runtime/debug"
+
+	"github.com/flashmob/go-guerrilla/response"
 )
 
 // ----------------------------------------------------------------------------------
-// Processor Name: mysql
+// Processor Name: sql
 // ----------------------------------------------------------------------------------
-// Description   : Saves the e.Data (email data) and e.DeliveryHeader together in mysql
+// Description   : Saves the e.Data (email data) and e.DeliveryHeader together in sql
 //               : using the hash generated by the "hash" processor and stored in
 //               : e.Hashes
 // ----------------------------------------------------------------------------------
-// Config Options: mail_table string - mysql table name
-//               : mysql_db string - mysql database name
-//               : mysql_host string - mysql host name, eg. 127.0.0.1
-//               : mysql_pass string - mysql password
-//               : mysql_user string - mysql username
+// Config Options: mail_table string - name of table for storing emails
+//               : sql_driver string - database driver name, eg. mysql
+//               : sql_dsn string - driver-specific data source name
 //               : primary_mail_host string - primary host name
 // --------------:-------------------------------------------------------------------
 // Input         : e.Data
@@ -37,64 +34,47 @@ import (
 // Output        : Sets e.QueuedId with the first item fromHashes[0]
 // ----------------------------------------------------------------------------------
 func init() {
-	processors["mysql"] = func() Decorator {
-		return MySql()
+	processors["sql"] = func() Decorator {
+		return SQL()
 	}
 }
 
-const procMySQLReadTimeout = time.Second * 10
-const procMySQLWriteTimeout = time.Second * 10
-
-type MysqlProcessorConfig struct {
-	MysqlTable  string `json:"mysql_mail_table"`
-	MysqlDB     string `json:"mysql_db"`
-	MysqlHost   string `json:"mysql_host"`
-	MysqlPass   string `json:"mysql_pass"`
-	MysqlUser   string `json:"mysql_user"`
+type SQLProcessorConfig struct {
+	Table       string `json:"mail_table"`
+	Driver      string `json:"sql_driver"`
+	DSN         string `json:"sql_dsn"`
 	PrimaryHost string `json:"primary_mail_host"`
 }
 
-type MysqlProcessor struct {
+type SQLProcessor struct {
 	cache  stmtCache
-	config *MysqlProcessorConfig
+	config *SQLProcessorConfig
 }
 
-func (m *MysqlProcessor) connect(config *MysqlProcessorConfig) (*sql.DB, error) {
+func (s *SQLProcessor) connect() (*sql.DB, error) {
 	var db *sql.DB
 	var err error
-	conf := mysql.Config{
-		User:         config.MysqlUser,
-		Passwd:       config.MysqlPass,
-		DBName:       config.MysqlDB,
-		Net:          "tcp",
-		Addr:         config.MysqlHost,
-		ReadTimeout:  procMySQLReadTimeout,
-		WriteTimeout: procMySQLWriteTimeout,
-		Params:       map[string]string{"collation": "utf8_general_ci"},
-	}
-	if db, err = sql.Open("mysql", conf.FormatDSN()); err != nil {
-		Log().Error("cannot open mysql", err)
+	if db, err = sql.Open(s.config.Driver, s.config.DSN); err != nil {
+		Log().Error("cannot open database: ", err)
 		return nil, err
 	}
 	// do we have permission to access the table?
-	_, err = db.Query("SELECT mail_id FROM " + m.config.MysqlTable + " LIMIT 1")
+	_, err = db.Query("SELECT mail_id FROM " + s.config.Table + " LIMIT 1")
 	if err != nil {
-		//Log().Error("cannot select table", err)
 		return nil, err
 	}
-	Log().Info("connected to mysql on tcp ", config.MysqlHost)
 	return db, err
 }
 
 // prepares the sql query with the number of rows that can be batched with it
-func (g *MysqlProcessor) prepareInsertQuery(rows int, db *sql.DB) *sql.Stmt {
+func (s *SQLProcessor) prepareInsertQuery(rows int, db *sql.DB) *sql.Stmt {
 	if rows == 0 {
 		panic("rows argument cannot be 0")
 	}
-	if g.cache[rows-1] != nil {
-		return g.cache[rows-1]
+	if s.cache[rows-1] != nil {
+		return s.cache[rows-1]
 	}
-	sqlstr := "INSERT INTO " + g.config.MysqlTable + " "
+	sqlstr := "INSERT INTO " + s.config.Table + " "
 	sqlstr += "(`date`, `to`, `from`, `subject`, `body`,  `mail`, `spam_score`, "
 	sqlstr += "`hash`, `content_type`, `recipient`, `has_attach`, `ip_addr`, "
 	sqlstr += "`return_path`, `is_tls`, `message_id`, `reply_to`, `sender`)"
@@ -113,11 +93,11 @@ func (g *MysqlProcessor) prepareInsertQuery(rows int, db *sql.DB) *sql.Stmt {
 		Log().WithError(sqlErr).Panic("failed while db.Prepare(INSERT...)")
 	}
 	// cache it
-	g.cache[rows-1] = stmt
+	s.cache[rows-1] = stmt
 	return stmt
 }
 
-func (g *MysqlProcessor) doQuery(c int, db *sql.DB, insertStmt *sql.Stmt, vals *[]interface{}) (execErr error) {
+func (s *SQLProcessor) doQuery(c int, db *sql.DB, insertStmt *sql.Stmt, vals *[]interface{}) (execErr error) {
 	defer func() {
 		if r := recover(); r != nil {
 			Log().Error("Recovered form panic:", r, string(debug.Stack()))
@@ -132,7 +112,7 @@ func (g *MysqlProcessor) doQuery(c int, db *sql.DB, insertStmt *sql.Stmt, vals *
 		}
 	}()
 	// prepare the query used to insert when rows reaches batchMax
-	insertStmt = g.prepareInsertQuery(c, db)
+	insertStmt = s.prepareInsertQuery(c, db)
 	_, execErr = insertStmt.Exec(*vals...)
 	if execErr != nil {
 		Log().WithError(execErr).Error("There was a problem the insert")
@@ -141,7 +121,7 @@ func (g *MysqlProcessor) doQuery(c int, db *sql.DB, insertStmt *sql.Stmt, vals *
 }
 
 // for storing ip addresses in the ip_addr column
-func (g *MysqlProcessor) ip2bint(ip string) *big.Int {
+func (s *SQLProcessor) ip2bint(ip string) *big.Int {
 	bint := big.NewInt(0)
 	addr := net.ParseIP(ip)
 	if strings.Index(ip, "::") > 0 {
@@ -152,7 +132,7 @@ func (g *MysqlProcessor) ip2bint(ip string) *big.Int {
 	return bint
 }
 
-func (g *MysqlProcessor) fillAddressFromHeader(e *mail.Envelope, headerKey string) string {
+func (s *SQLProcessor) fillAddressFromHeader(e *mail.Envelope, headerKey string) string {
 	if v, ok := e.Header[headerKey]; ok {
 		addr, err := mail.NewAddress(v[0])
 		if err != nil {
@@ -163,23 +143,22 @@ func (g *MysqlProcessor) fillAddressFromHeader(e *mail.Envelope, headerKey strin
 	return ""
 }
 
-func MySql() Decorator {
-
-	var config *MysqlProcessorConfig
+func SQL() Decorator {
+	var config *SQLProcessorConfig
 	var vals []interface{}
 	var db *sql.DB
-	m := &MysqlProcessor{}
+	s := &SQLProcessor{}
 
 	// open the database connection (it will also check if we can select the table)
 	Svc.AddInitializer(InitializeWith(func(backendConfig BackendConfig) error {
-		configType := BaseConfig(&MysqlProcessorConfig{})
+		configType := BaseConfig(&SQLProcessorConfig{})
 		bcfg, err := Svc.ExtractConfig(backendConfig, configType)
 		if err != nil {
 			return err
 		}
-		config = bcfg.(*MysqlProcessorConfig)
-		m.config = config
-		db, err = m.connect(config)
+		config = bcfg.(*SQLProcessorConfig)
+		s.config = config
+		db, err = s.connect()
 		if err != nil {
 			return err
 		}
@@ -221,19 +200,19 @@ func MySql() Decorator {
 				for i := range e.RcptTo {
 
 					// use the To header, otherwise rcpt to
-					to = trimToLimit(m.fillAddressFromHeader(e, "To"), 255)
+					to = trimToLimit(s.fillAddressFromHeader(e, "To"), 255)
 					if to == "" {
 						// trimToLimit(strings.TrimSpace(e.RcptTo[i].User)+"@"+config.PrimaryHost, 255)
 						to = trimToLimit(strings.TrimSpace(e.RcptTo[i].String()), 255)
 					}
-					mid := trimToLimit(m.fillAddressFromHeader(e, "Message-Id"), 255)
+					mid := trimToLimit(s.fillAddressFromHeader(e, "Message-Id"), 255)
 					if mid == "" {
 						mid = fmt.Sprintf("%s.%s@%s", hash, e.RcptTo[i].User, config.PrimaryHost)
 					}
 					// replyTo is the 'Reply-to' header, it may be blank
-					replyTo := trimToLimit(m.fillAddressFromHeader(e, "Reply-To"), 255)
+					replyTo := trimToLimit(s.fillAddressFromHeader(e, "Reply-To"), 255)
 					// sender is the 'Sender' header, it may be blank
-					sender := trimToLimit(m.fillAddressFromHeader(e, "Sender"), 255)
+					sender := trimToLimit(s.fillAddressFromHeader(e, "Sender"), 255)
 
 					recipient := trimToLimit(strings.TrimSpace(e.RcptTo[i].String()), 255)
 					contentType := ""
@@ -265,16 +244,19 @@ func MySql() Decorator {
 						hash, // hash (redis hash if saved in redis)
 						contentType,
 						recipient,
-						m.ip2bint(e.RemoteIP).Bytes(),         // ip_addr store as varbinary(16)
+						s.ip2bint(e.RemoteIP).Bytes(),         // ip_addr store as varbinary(16)
 						trimToLimit(e.MailFrom.String(), 255), // return_path
-						e.TLS,   // is_tls
-						mid,     // message_id
-						replyTo, // reply_to
+						// is_tls
+						e.TLS,
+						// message_id
+						mid,
+						// reply_to
+						replyTo,
 						sender,
 					)
 
-					stmt := m.prepareInsertQuery(1, db)
-					err := m.doQuery(1, db, stmt, &vals)
+					stmt := s.prepareInsertQuery(1, db)
+					err := s.doQuery(1, db, stmt, &vals)
 					if err != nil {
 						return NewResult(fmt.Sprint("554 Error: could not save email")), StorageError
 					}

+ 93 - 0
backends/p_sql_test.go

@@ -0,0 +1,93 @@
+package backends
+
+import (
+	"database/sql"
+	"flag"
+	"fmt"
+	"strconv"
+	"strings"
+	"testing"
+	"time"
+
+	"github.com/flashmob/go-guerrilla/log"
+	"github.com/flashmob/go-guerrilla/mail"
+
+	_ "github.com/go-sql-driver/mysql"
+)
+
+var (
+	mailTableFlag = flag.String("mail-table", "test", "Table to use for testing the SQL backend")
+	sqlDSNFlag    = flag.String("sql-dsn", "", "DSN to use for testing the SQL backend")
+	sqlDriverFlag = flag.String("sql-driver", "mysql", "Driver to use for testing the SQL backend")
+)
+
+func TestSQL(t *testing.T) {
+	if *sqlDSNFlag == "" {
+		t.Skip("requires -sql-dsn to run")
+	}
+
+	logger, err := log.GetLogger(log.OutputOff.String(), log.DebugLevel.String())
+	if err != nil {
+		t.Fatal("get logger:", err)
+	}
+
+	cfg := BackendConfig{
+		"save_process":      "sql",
+		"mail_table":        *mailTableFlag,
+		"primary_mail_host": "example.com",
+		"sql_driver":        *sqlDriverFlag,
+		"sql_dsn":           *sqlDSNFlag,
+	}
+	backend, err := New(cfg, logger)
+	if err != nil {
+		t.Fatal("new backend:", err)
+	}
+	if err := backend.Start(); err != nil {
+		t.Fatal("start backend: ", err)
+	}
+
+	hash := strconv.FormatInt(time.Now().UnixNano(), 10)
+	envelope := &mail.Envelope{
+		RcptTo: []mail.Address{{User: "user", Host: "example.com"}},
+		Hashes: []string{hash},
+	}
+
+	// The SQL processor is expected to use the hash to queue the mail.
+	result := backend.Process(envelope)
+	if !strings.Contains(result.String(), hash) {
+		t.Errorf("expected message to be queued with hash, got %q", result)
+	}
+
+	// Ensure that a record actually exists.
+	results, err := findRows(hash)
+	if err != nil {
+		t.Fatal("find rows: ", err)
+	}
+	if len(results) != 1 {
+		t.Fatalf("expected one row, got %d", len(results))
+	}
+}
+
+func findRows(hash string) ([]string, error) {
+	db, err := sql.Open(*sqlDriverFlag, *sqlDSNFlag)
+	if err != nil {
+		return nil, err
+	}
+	defer db.Close()
+
+	stmt := fmt.Sprintf(`SELECT hash FROM %s WHERE hash = ?`, *mailTableFlag)
+	rows, err := db.Query(stmt, hash)
+	if err != nil {
+		return nil, err
+	}
+
+	var results []string
+	for rows.Next() {
+		var result string
+		if err := rows.Scan(&result); err != nil {
+			return nil, err
+		}
+		results = append(results, result)
+	}
+	return results, nil
+}

+ 1 - 1
client.go

@@ -179,7 +179,7 @@ func (c *client) getID() uint64 {
 // UpgradeToTLS upgrades a client connection to TLS
 func (client *client) upgradeToTLS(tlsConfig *tls.Config) error {
 	var tlsConn *tls.Conn
-	// load the config thread-safely
+	// wrap client.conn in a new TLS server side connection
 	tlsConn = tls.Server(client.conn, tlsConfig)
 	// Call handshake here to get any handshake error before reading starts
 	err := tlsConn.Handshake()

+ 1 - 1
cmd/guerrillad/root.go

@@ -1,7 +1,7 @@
 package main
 
 import (
-	"github.com/Sirupsen/logrus"
+	"github.com/sirupsen/logrus"
 	"github.com/spf13/cobra"
 )
 

+ 8 - 3
cmd/guerrillad/serve.go

@@ -2,9 +2,6 @@ package main
 
 import (
 	"fmt"
-	"github.com/flashmob/go-guerrilla"
-	"github.com/flashmob/go-guerrilla/log"
-	"github.com/spf13/cobra"
 	"os"
 	"os/exec"
 	"os/signal"
@@ -12,6 +9,14 @@ import (
 	"strings"
 	"syscall"
 	"time"
+
+	"github.com/flashmob/go-guerrilla"
+	"github.com/flashmob/go-guerrilla/log"
+	//_ "github.com/flashmob/go-guerrilla/mail/iconv"
+	_ "github.com/flashmob/go-guerrilla/mail/encoding"
+	"github.com/spf13/cobra"
+
+	_ "github.com/go-sql-driver/mysql"
 )
 
 const (

+ 104 - 64
cmd/guerrillad/serve_test.go

@@ -3,12 +3,6 @@ package main
 import (
 	"crypto/tls"
 	"encoding/json"
-	"github.com/flashmob/go-guerrilla"
-	"github.com/flashmob/go-guerrilla/backends"
-	"github.com/flashmob/go-guerrilla/log"
-	test "github.com/flashmob/go-guerrilla/tests"
-	"github.com/flashmob/go-guerrilla/tests/testcert"
-	"github.com/spf13/cobra"
 	"io/ioutil"
 	"os"
 	"os/exec"
@@ -18,6 +12,13 @@ import (
 	"sync"
 	"testing"
 	"time"
+
+	"github.com/flashmob/go-guerrilla"
+	"github.com/flashmob/go-guerrilla/backends"
+	"github.com/flashmob/go-guerrilla/log"
+	test "github.com/flashmob/go-guerrilla/tests"
+	"github.com/flashmob/go-guerrilla/tests/testcert"
+	"github.com/spf13/cobra"
 )
 
 var configJsonA = `
@@ -42,27 +43,31 @@ var configJsonA = `
             "is_enabled" : true,
             "host_name":"mail.test.com",
             "max_size": 1000000,
-            "private_key_file":"../..//tests/mail2.guerrillamail.com.key.pem",
-            "public_key_file":"../../tests/mail2.guerrillamail.com.cert.pem",
             "timeout":180,
             "listen_interface":"127.0.0.1:3536",
-            "start_tls_on":true,
-            "tls_always_on":false,
             "max_clients": 1000,
-            "log_file" : "../../tests/testlog"
+            "log_file" : "../../tests/testlog",
+			"tls" : {
+				"private_key_file":"../../tests/mail2.guerrillamail.com.key.pem",
+            	"public_key_file":"../../tests/mail2.guerrillamail.com.cert.pem",
+				"start_tls_on":true,
+            	"tls_always_on":false
+			}
         },
         {
             "is_enabled" : false,
             "host_name":"enable.test.com",
             "max_size": 1000000,
-            "private_key_file":"../..//tests/mail2.guerrillamail.com.key.pem",
-            "public_key_file":"../../tests/mail2.guerrillamail.com.cert.pem",
             "timeout":180,
             "listen_interface":"127.0.0.1:2228",
-            "start_tls_on":true,
-            "tls_always_on":false,
             "max_clients": 1000,
-            "log_file" : "../../tests/testlog"
+            "log_file" : "../../tests/testlog",
+			"tls" : {
+				"private_key_file":"../../tests/mail2.guerrillamail.com.key.pem",
+				"public_key_file":"../../tests/mail2.guerrillamail.com.cert.pem",
+				"start_tls_on":true,
+            	"tls_always_on":false
+			}
         }
     ]
 }
@@ -91,20 +96,22 @@ var configJsonB = `
             "is_enabled" : true,
             "host_name":"mail.test.com",
             "max_size": 1000000,
-            "private_key_file":"../..//tests/mail2.guerrillamail.com.key.pem",
-            "public_key_file":"../../tests/mail2.guerrillamail.com.cert.pem",
             "timeout":180,
             "listen_interface":"127.0.0.1:3536",
-            "start_tls_on":true,
-            "tls_always_on":false,
             "max_clients": 1000,
-            "log_file" : "../../tests/testlog"
+            "log_file" : "../../tests/testlog",
+			"tls" : {
+				"private_key_file":"../../tests/mail2.guerrillamail.com.key.pem",
+            	"public_key_file":"../../tests/mail2.guerrillamail.com.cert.pem",
+            	"start_tls_on":true,
+            	"tls_always_on":false
+			}
         }
     ]
 }
 `
 
-// backend_name changed, is guerrilla-redis-db + added a server
+// added a server
 var configJsonC = `
 {
     "log_file" : "../../tests/testlog",
@@ -117,48 +124,49 @@ var configJsonC = `
       "guerrillamail.net",
       "guerrillamail.org"
     ],
-    "backend_name": "guerrilla-redis-db",
     "backend_config" :
         {
-            "mysql_db":"gmail_mail",
-            "mysql_host":"127.0.0.1:3306",
-            "mysql_pass":"ok",
-            "mysql_user":"root",
+            "sql_driver": "mysql",
+            "sql_dsn": "root:ok@tcp(127.0.0.1:3306)/gmail_mail?readTimeout=10s&writeTimeout=10s",
             "mail_table":"new_mail",
             "redis_interface" : "127.0.0.1:6379",
             "redis_expire_seconds" : 7200,
             "save_workers_size" : 3,
             "primary_mail_host":"sharklasers.com",
             "save_workers_size" : 1,
-	    "save_process": "HeadersParser|Debugger",
-	    "log_received_mails": true
+	    	"save_process": "HeadersParser|Debugger",
+	    	"log_received_mails": true
         },
     "servers" : [
         {
             "is_enabled" : true,
             "host_name":"mail.test.com",
             "max_size": 1000000,
-            "private_key_file":"../..//tests/mail2.guerrillamail.com.key.pem",
-            "public_key_file":"../../tests/mail2.guerrillamail.com.cert.pem",
             "timeout":180,
             "listen_interface":"127.0.0.1:25",
-            "start_tls_on":true,
-            "tls_always_on":false,
             "max_clients": 1000,
-            "log_file" : "../../tests/testlog"
+            "log_file" : "../../tests/testlog",
+			"tls" : {
+				"private_key_file":"../../tests/mail2.guerrillamail.com.key.pem",
+            	"public_key_file":"../../tests/mail2.guerrillamail.com.cert.pem",
+				"start_tls_on":true,
+            	"tls_always_on":false
+			}
         },
         {
             "is_enabled" : true,
             "host_name":"mail.test.com",
             "max_size":1000000,
-            "private_key_file":"../..//tests/mail2.guerrillamail.com.key.pem",
-            "public_key_file":"../../tests/mail2.guerrillamail.com.cert.pem",
             "timeout":180,
             "listen_interface":"127.0.0.1:465",
-            "start_tls_on":false,
-            "tls_always_on":true,
             "max_clients":500,
-            "log_file" : "../../tests/testlog"
+            "log_file" : "../../tests/testlog",
+			"tls" : {
+				"private_key_file":"../../tests/mail2.guerrillamail.com.key.pem",
+            	"public_key_file":"../../tests/mail2.guerrillamail.com.cert.pem",
+				"start_tls_on":false,
+            	"tls_always_on":true
+			}
         }
     ]
 }
@@ -187,27 +195,31 @@ var configJsonD = `
             "is_enabled" : true,
             "host_name":"mail.test.com",
             "max_size": 1000000,
-            "private_key_file":"../..//tests/mail2.guerrillamail.com.key.pem",
-            "public_key_file":"../../tests/mail2.guerrillamail.com.cert.pem",
             "timeout":180,
             "listen_interface":"127.0.0.1:2552",
-            "start_tls_on":true,
-            "tls_always_on":false,
             "max_clients": 1000,
-            "log_file" : "../../tests/testlog"
+            "log_file" : "../../tests/testlog",
+			"tls" : {
+				"private_key_file":"../../tests/mail2.guerrillamail.com.key.pem",
+            	"public_key_file":"../../tests/mail2.guerrillamail.com.cert.pem",
+				"start_tls_on":true,
+            	"tls_always_on":false
+			}
         },
         {
             "is_enabled" : true,
             "host_name":"secure.test.com",
             "max_size":1000000,
-            "private_key_file":"../..//tests/mail2.guerrillamail.com.key.pem",
-            "public_key_file":"../../tests/mail2.guerrillamail.com.cert.pem",
             "timeout":180,
             "listen_interface":"127.0.0.1:4655",
-            "start_tls_on":false,
-            "tls_always_on":true,
             "max_clients":500,
-            "log_file" : "../../tests/testlog"
+            "log_file" : "../../tests/testlog",
+			"tls" : {
+				"private_key_file":"../../tests/mail2.guerrillamail.com.key.pem",
+				"public_key_file":"../../tests/mail2.guerrillamail.com.cert.pem",
+				"start_tls_on":false,
+            	"tls_always_on":true
+			}
         }
     ]
 }
@@ -231,13 +243,11 @@ var configJsonE = `
             "save_process_old": "HeadersParser|Debugger|Hasher|Header|Compressor|Redis|MySql",
             "save_process": "GuerrillaRedisDB",
             "log_received_mails" : true,
-            "mysql_db":"gmail_mail",
-            "mysql_host":"127.0.0.1:3306",
-            "mysql_pass":"secret",
-            "mysql_user":"root",
+            "sql_driver": "mysql",
+            "sql_dsn": "root:secret@tcp(127.0.0.1:3306)/gmail_mail?readTimeout=10s&writeTimeout=10s",
             "mail_table":"new_mail",
             "redis_interface" : "127.0.0.1:6379",
-             "redis_expire_seconds" : 7200,
+            "redis_expire_seconds" : 7200,
             "save_workers_size" : 3,
             "primary_mail_host":"sharklasers.com"
         },
@@ -246,27 +256,31 @@ var configJsonE = `
             "is_enabled" : true,
             "host_name":"mail.test.com",
             "max_size": 1000000,
-            "private_key_file":"../..//tests/mail2.guerrillamail.com.key.pem",
-            "public_key_file":"../../tests/mail2.guerrillamail.com.cert.pem",
             "timeout":180,
             "listen_interface":"127.0.0.1:2552",
-            "start_tls_on":true,
-            "tls_always_on":false,
             "max_clients": 1000,
-            "log_file" : "../../tests/testlog"
+            "log_file" : "../../tests/testlog",
+			"tls" : {
+				"private_key_file":"../../tests/mail2.guerrillamail.com.key.pem",
+            	"public_key_file":"../../tests/mail2.guerrillamail.com.cert.pem",
+				"start_tls_on":true,
+            	"tls_always_on":false
+			}
         },
         {
             "is_enabled" : true,
             "host_name":"secure.test.com",
             "max_size":1000000,
-            "private_key_file":"../..//tests/mail2.guerrillamail.com.key.pem",
-            "public_key_file":"../../tests/mail2.guerrillamail.com.cert.pem",
             "timeout":180,
             "listen_interface":"127.0.0.1:4655",
-            "start_tls_on":false,
-            "tls_always_on":true,
             "max_clients":500,
-            "log_file" : "../../tests/testlog"
+            "log_file" : "../../tests/testlog",
+			"tls" : {
+				"private_key_file":"../../tests/mail2.guerrillamail.com.key.pem",
+            	"public_key_file":"../../tests/mail2.guerrillamail.com.cert.pem",
+				"start_tls_on":false,
+            	"tls_always_on":true
+			}
         }
     ]
 }
@@ -670,6 +684,32 @@ func TestServerStopEvent(t *testing.T) {
 
 }
 
+// just a utility for debugging when using the debugger, skipped by default
+func TestDebug(t *testing.T) {
+
+	t.SkipNow()
+	conf := guerrilla.ServerConfig{ListenInterface: "127.0.0.1:2526"}
+	if conn, buffin, err := test.Connect(conf, 20); err != nil {
+		t.Error("Could not connect to new server", conf.ListenInterface, err)
+	} else {
+		if result, err := test.Command(conn, buffin, "HELO"); err == nil {
+			expect := "250 mai1.guerrillamail.com Hello"
+			if strings.Index(result, expect) != 0 {
+				t.Error("Expected", expect, "but got", result)
+			} else {
+				if result, err = test.Command(conn, buffin, "RCPT TO:[email protected]"); err == nil {
+					expect := "250 2.1.5 OK"
+					if strings.Index(result, expect) != 0 {
+						t.Error("Expected:", expect, "but got:", result)
+					}
+				}
+			}
+		}
+		conn.Close()
+
+	}
+}
+
 // Start with configJsonD.json,
 // then connect to 127.0.0.1:4655 & HELO & try RCPT TO with an invalid host [grr.la]
 // then change the config to enable add new host [grr.la] to allowed_hosts

+ 137 - 56
config.go

@@ -45,20 +45,13 @@ type ServerConfig struct {
 	// MaxSize is the maximum size of an email that will be accepted for delivery.
 	// Defaults to 10 Mebibytes
 	MaxSize int64 `json:"max_size"`
-	// PrivateKeyFile path to cert private key in PEM format. Will be ignored if blank
-	PrivateKeyFile string `json:"private_key_file"`
-	// PublicKeyFile path to cert (public key) chain in PEM format.
-	// Will be ignored if blank
-	PublicKeyFile string `json:"public_key_file"`
+	// TLS Configuration
+	TLS ServerTLSConfig `json:"tls,omitempty"`
 	// Timeout specifies the connection timeout in seconds. Defaults to 30
 	Timeout int `json:"timeout"`
 	// Listen interface specified in <ip>:<port> - defaults to 127.0.0.1:2525
 	ListenInterface string `json:"listen_interface"`
-	// StartTLSOn should we offer STARTTLS command. Cert must be valid.
-	// False by default
-	StartTLSOn bool `json:"start_tls_on,omitempty"`
-	// TLSAlwaysOn run this server as a pure TLS server, i.e. SMTPS
-	TLSAlwaysOn bool `json:"tls_always_on,omitempty"`
+
 	// MaxClients controls how many maxiumum clients we can handle at once.
 	// Defaults to 100
 	MaxClients int `json:"max_clients"`
@@ -68,10 +61,95 @@ type ServerConfig struct {
 	// XClientOn when using a proxy such as Nginx, XCLIENT command is used to pass the
 	// original client's IP address & client's HELO
 	XClientOn bool `json:"xclient_on,omitempty"`
+}
+
+type ServerTLSConfig struct {
+
+	// StartTLSOn should we offer STARTTLS command. Cert must be valid.
+	// False by default
+	StartTLSOn bool `json:"start_tls_on,omitempty"`
+	// AlwaysOn run this server as a pure TLS server, i.e. SMTPS
+	AlwaysOn bool `json:"tls_always_on,omitempty"`
+	// PrivateKeyFile path to cert private key in PEM format.
+	PrivateKeyFile string `json:"private_key_file"`
+	// PublicKeyFile path to cert (public key) chain in PEM format.
+	PublicKeyFile string `json:"public_key_file"`
+
+	// TLS Protocols to use. [0] = min, [1]max
+	// Use Go's default if empty
+	Protocols []string `json:"protocols,omitempty"`
+	// TLS Ciphers to use.
+	// Use Go's default if empty
+	Ciphers []string `json:"ciphers,omitempty"`
+	// TLS Curves to use.
+	// Use Go's default if empty
+	Curves []string `json:"curves,omitempty"`
+	// TLS Root cert authorities to use. "A PEM encoded CA's certificate file.
+	// Defaults to system's root CA file if empty
+	RootCAs string `json:"root_cas_file,omitempty"`
+	// declares the policy the server will follow for TLS Client Authentication.
+	// Use Go's default if empty
+	ClientAuthType string `json:"client_auth_type,omitempty"`
+	// controls whether the server selects the
+	// client's most preferred ciphersuite
+	PreferServerCipherSuites bool `json:"prefer_server_cipher_suites,omitempty"`
 
 	// The following used to watch certificate changes so that the TLS can be reloaded
-	_privateKeyFile_mtime int
-	_publicKeyFile_mtime  int
+	_privateKeyFile_mtime int64
+	_publicKeyFile_mtime  int64
+}
+
+// https://golang.org/pkg/crypto/tls/#pkg-constants
+// Ciphers introduced before Go 1.7 are listed here,
+// ciphers since Go 1.8, see tls_go1.8.go
+var TLSCiphers = map[string]uint16{
+
+	// // Note: Generally avoid using CBC unless for compatibility
+	"TLS_RSA_WITH_3DES_EDE_CBC_SHA":        tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
+	"TLS_RSA_WITH_AES_128_CBC_SHA":         tls.TLS_RSA_WITH_AES_128_CBC_SHA,
+	"TLS_RSA_WITH_AES_256_CBC_SHA":         tls.TLS_RSA_WITH_AES_256_CBC_SHA,
+	"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
+	"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
+	"TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA":  tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
+	"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA":   tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
+	"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA":   tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
+
+	"TLS_RSA_WITH_RC4_128_SHA":        tls.TLS_RSA_WITH_RC4_128_SHA,
+	"TLS_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
+	"TLS_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
+
+	"TLS_ECDHE_ECDSA_WITH_RC4_128_SHA":        tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA,
+	"TLS_ECDHE_RSA_WITH_RC4_128_SHA":          tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA,
+	"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
+	"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384":   tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
+	"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
+
+	// Include to prevent downgrade attacks
+	"TLS_FALLBACK_SCSV": tls.TLS_FALLBACK_SCSV,
+}
+
+// https://golang.org/pkg/crypto/tls/#pkg-constants
+var TLSProtocols = map[string]uint16{
+	"ssl3.0": tls.VersionSSL30,
+	"tls1.0": tls.VersionTLS10,
+	"tls1.1": tls.VersionTLS11,
+	"tls1.2": tls.VersionTLS12,
+}
+
+// https://golang.org/pkg/crypto/tls/#CurveID
+var TLSCurves = map[string]tls.CurveID{
+	"P256": tls.CurveP256,
+	"P384": tls.CurveP384,
+	"P521": tls.CurveP521,
+}
+
+// https://golang.org/pkg/crypto/tls/#ClientAuthType
+var TLSClientAuthTypes = map[string]tls.ClientAuthType{
+	"NoClientCert":               tls.NoClientCert,
+	"RequestClientCert":          tls.RequestClientCert,
+	"RequireAnyClientCert":       tls.RequireAnyClientCert,
+	"VerifyClientCertIfGiven":    tls.VerifyClientCertIfGiven,
+	"RequireAndVerifyClientCert": tls.RequireAndVerifyClientCert,
 }
 
 // Unmarshalls json data into AppConfig struct and any other initialization of the struct
@@ -278,11 +356,16 @@ func (c *AppConfig) setBackendDefaults() error {
 // All events are fired and run synchronously
 func (sc *ServerConfig) emitChangeEvents(oldServer *ServerConfig, app Guerrilla) {
 	// get a list of changes
-	changes := getDiff(
+	changes := getChanges(
 		*oldServer,
 		*sc,
 	)
-	if len(changes) > 0 {
+	tlsChanges := getChanges(
+		(*oldServer).TLS,
+		(*sc).TLS,
+	)
+
+	if len(changes) > 0 || len(tlsChanges) > 0 {
 		// something changed in the server config
 		app.Publish(EventConfigServerConfig, sc)
 	}
@@ -313,22 +396,7 @@ func (sc *ServerConfig) emitChangeEvents(oldServer *ServerConfig, app Guerrilla)
 		app.Publish(EventConfigServerMaxClients, sc)
 	}
 
-	// tls changed
-	if ok := func() bool {
-		if _, ok := changes["PrivateKeyFile"]; ok {
-			return true
-		}
-		if _, ok := changes["PublicKeyFile"]; ok {
-			return true
-		}
-		if _, ok := changes["StartTLSOn"]; ok {
-			return true
-		}
-		if _, ok := changes["TLSAlwaysOn"]; ok {
-			return true
-		}
-		return false
-	}(); ok {
+	if len(tlsChanges) > 0 {
 		app.Publish(EventConfigServerTLSConfig, sc)
 	}
 }
@@ -342,37 +410,31 @@ func (sc *ServerConfig) loadTlsKeyTimestamps() error {
 				iface,
 				err.Error()))
 	}
-	if info, err := os.Stat(sc.PrivateKeyFile); err == nil {
-		sc._privateKeyFile_mtime = info.ModTime().Second()
+	if info, err := os.Stat(sc.TLS.PrivateKeyFile); err == nil {
+		sc.TLS._privateKeyFile_mtime = info.ModTime().Unix()
 	} else {
 		return statErr(sc.ListenInterface, err)
 	}
-	if info, err := os.Stat(sc.PublicKeyFile); err == nil {
-		sc._publicKeyFile_mtime = info.ModTime().Second()
+	if info, err := os.Stat(sc.TLS.PublicKeyFile); err == nil {
+		sc.TLS._publicKeyFile_mtime = info.ModTime().Unix()
 	} else {
 		return statErr(sc.ListenInterface, err)
 	}
 	return nil
 }
 
-// Gets the timestamp of the TLS certificates. Returns a unix time of when they were last modified
-// when the config was read. We use this info to determine if TLS needs to be re-loaded.
-func (sc *ServerConfig) getTlsKeyTimestamps() (int, int) {
-	return sc._privateKeyFile_mtime, sc._publicKeyFile_mtime
-}
-
 // Validate validates the server's configuration.
 func (sc *ServerConfig) Validate() error {
 	var errs Errors
 
-	if sc.StartTLSOn || sc.TLSAlwaysOn {
-		if sc.PublicKeyFile == "" {
+	if sc.TLS.StartTLSOn || sc.TLS.AlwaysOn {
+		if sc.TLS.PublicKeyFile == "" {
 			errs = append(errs, errors.New("PublicKeyFile is empty"))
 		}
-		if sc.PrivateKeyFile == "" {
+		if sc.TLS.PrivateKeyFile == "" {
 			errs = append(errs, errors.New("PrivateKeyFile is empty"))
 		}
-		if _, err := tls.LoadX509KeyPair(sc.PublicKeyFile, sc.PrivateKeyFile); err != nil {
+		if _, err := tls.LoadX509KeyPair(sc.TLS.PublicKeyFile, sc.TLS.PrivateKeyFile); err != nil {
 			errs = append(errs,
 				errors.New(fmt.Sprintf("cannot use TLS config for [%s], %v", sc.ListenInterface, err)))
 		}
@@ -384,28 +446,42 @@ func (sc *ServerConfig) Validate() error {
 	return nil
 }
 
-// Returns a diff between struct a & struct b.
+// Gets the timestamp of the TLS certificates. Returns a unix time of when they were last modified
+// when the config was read. We use this info to determine if TLS needs to be re-loaded.
+func (stc *ServerTLSConfig) getTlsKeyTimestamps() (int64, int64) {
+	return stc._privateKeyFile_mtime, stc._publicKeyFile_mtime
+}
+
+// Returns value changes between struct a & struct b.
 // Results are returned in a map, where each key is the name of the field that was different.
 // a and b are struct values, must not be pointer
 // and of the same struct type
-func getDiff(a interface{}, b interface{}) map[string]interface{} {
+func getChanges(a interface{}, b interface{}) map[string]interface{} {
 	ret := make(map[string]interface{}, 5)
 	compareWith := structtomap(b)
 	for key, val := range structtomap(a) {
+		if sliceOfStr, ok := val.([]string); ok {
+			val, _ = json.Marshal(sliceOfStr)
+			val = string(val.([]uint8))
+		}
+		if sliceOfStr, ok := compareWith[key].([]string); ok {
+			compareWith[key], _ = json.Marshal(sliceOfStr)
+			compareWith[key] = string(compareWith[key].([]uint8))
+		}
 		if val != compareWith[key] {
 			ret[key] = compareWith[key]
 		}
 	}
-	// detect tls changes (have the key files been modified?)
-	if oldServer, ok := a.(ServerConfig); ok {
-		t1, t2 := oldServer.getTlsKeyTimestamps()
-		if newServer, ok := b.(ServerConfig); ok {
-			t3, t4 := newServer.getTlsKeyTimestamps()
+	// detect changes to TLS keys (have the key files been modified?)
+	if oldTLS, ok := a.(ServerTLSConfig); ok {
+		t1, t2 := oldTLS.getTlsKeyTimestamps()
+		if newTLS, ok := b.(ServerTLSConfig); ok {
+			t3, t4 := newTLS.getTlsKeyTimestamps()
 			if t1 != t3 {
-				ret["PrivateKeyFile"] = newServer.PrivateKeyFile
+				ret["PrivateKeyFile"] = newTLS.PrivateKeyFile
 			}
 			if t2 != t4 {
-				ret["PublicKeyFile"] = newServer.PublicKeyFile
+				ret["PublicKeyFile"] = newTLS.PublicKeyFile
 			}
 		}
 	}
@@ -413,7 +489,8 @@ func getDiff(a interface{}, b interface{}) map[string]interface{} {
 }
 
 // Convert fields of a struct to a map
-// only able to convert int, bool and string; not recursive
+// only able to convert int, bool, slice-of-strings and string; not recursive
+// slices are marshal'd to json for convenient comparison later
 func structtomap(obj interface{}) map[string]interface{} {
 	ret := make(map[string]interface{}, 0)
 	v := reflect.ValueOf(obj)
@@ -421,9 +498,11 @@ func structtomap(obj interface{}) map[string]interface{} {
 	for index := 0; index < v.NumField(); index++ {
 		vField := v.Field(index)
 		fName := t.Field(index).Name
-
-		switch vField.Kind() {
+		k := vField.Kind()
+		switch k {
 		case reflect.Int:
+			fallthrough
+		case reflect.Int64:
 			value := vField.Int()
 			ret[fName] = value
 		case reflect.String:
@@ -432,6 +511,8 @@ func structtomap(obj interface{}) map[string]interface{} {
 		case reflect.Bool:
 			value := vField.Bool()
 			ret[fName] = value
+		case reflect.Slice:
+			ret[fName] = vField.Interface().([]string)
 		}
 	}
 	return ret

+ 62 - 50
config_test.go

@@ -33,55 +33,59 @@ var configJsonA = `
             "is_enabled" : true,
             "host_name":"mail.guerrillamail.com",
             "max_size": 100017,
-            "private_key_file":"config_test.go",
-            "public_key_file":"config_test.go",
             "timeout":160,
             "listen_interface":"127.0.0.1:2526",
-            "start_tls_on":false,
-            "tls_always_on":false,
-            "max_clients": 2
+            "max_clients": 2,
+			"tls" : {
+				"start_tls_on":false,
+            	"tls_always_on":false,
+				"private_key_file":"config_test.go",
+            	"public_key_file":"config_test.go"
+			}
         },
-
         {
             "is_enabled" : true,
             "host_name":"mail2.guerrillamail.com",
             "max_size":1000001,
-            "private_key_file":"./tests/mail2.guerrillamail.com.key.pem",
-            "public_key_file":"./tests/mail2.guerrillamail.com.cert.pem",
             "timeout":180,
             "listen_interface":"127.0.0.1:2527",
-            "start_tls_on":true,
-            "tls_always_on":false,
-            "max_clients":1
+			"max_clients":1,
+			"tls" : {
+ 				"private_key_file":"./tests/mail2.guerrillamail.com.key.pem",
+            	"public_key_file":"./tests/mail2.guerrillamail.com.cert.pem",
+				"tls_always_on":false,
+            	"start_tls_on":true
+			}
         },
 
         {
             "is_enabled" : true,
             "host_name":"mail.stopme.com",
-            "max_size": 100017,
-            "private_key_file":"config_test.go",
-            "public_key_file":"config_test.go",
+            "max_size": 100017, 
             "timeout":160,
-            "listen_interface":"127.0.0.1:9999",
-            "start_tls_on":false,
-            "tls_always_on":false,
-            "max_clients": 2
+            "listen_interface":"127.0.0.1:9999", 
+            "max_clients": 2,
+			"tls" : {
+				"private_key_file":"config_test.go",
+            	"public_key_file":"config_test.go",
+				"start_tls_on":false,
+            	"tls_always_on":false
+			}
         },
-
         {
             "is_enabled" : true,
             "host_name":"mail.disableme.com",
             "max_size": 100017,
-            "private_key_file":"config_test.go",
-            "public_key_file":"config_test.go",
             "timeout":160,
             "listen_interface":"127.0.0.1:3333",
-            "start_tls_on":false,
-            "tls_always_on":false,
-            "max_clients": 2
+            "max_clients": 2,
+			"tls" : { 
+				"private_key_file":"config_test.go",
+            	"public_key_file":"config_test.go",
+				"start_tls_on":false,
+				"tls_always_on":false
+			}
         }
-
-
     ]
 }
 `
@@ -106,52 +110,60 @@ var configJsonB = `
             "is_enabled" : true,
             "host_name":"mail.guerrillamail.com",
             "max_size": 100017,
-            "private_key_file":"config_test.go",
-            "public_key_file":"config_test.go",
             "timeout":161,
             "listen_interface":"127.0.0.1:2526",
-            "start_tls_on":false,
-            "tls_always_on":true,
-            "max_clients": 3
+            "max_clients": 3,
+			"tls" : {
+ 				"private_key_file":"./config_test.go",
+            	"public_key_file":"./config_test.go",
+				"start_tls_on":false,
+            	"tls_always_on":true
+			}
         },
         {
             "is_enabled" : true,
             "host_name":"mail2.guerrillamail.com",
             "max_size": 100017,
-            "private_key_file":"./tests/mail2.guerrillamail.com.key.pem",
-            "public_key_file": "./tests/mail2.guerrillamail.com.cert.pem",
             "timeout":160,
             "listen_interface":"127.0.0.1:2527",
-            "start_tls_on":true,
-            "tls_always_on":false,
             "log_file" : "./tests/testlog",
-            "max_clients": 2
+            "max_clients": 2,
+			"tls" : {
+				"private_key_file":"./tests/mail2.guerrillamail.com.key.pem",
+            	"public_key_file": "./tests/mail2.guerrillamail.com.cert.pem",
+				"start_tls_on":true,
+            	"tls_always_on":false
+			}
         },
 
         {
             "is_enabled" : true,
             "host_name":"mail.guerrillamail.com",
             "max_size":1000001,
-            "private_key_file":"config_test.go",
-            "public_key_file":"config_test.go",
             "timeout":180,
             "listen_interface":"127.0.0.1:4654",
-            "start_tls_on":false,
-            "tls_always_on":false,
-            "max_clients":1
+            "max_clients":1,
+			"tls" : {
+				"private_key_file":"config_test.go",
+				"public_key_file":"config_test.go",
+				"start_tls_on":false,
+            	"tls_always_on":false
+			}
         },
 
         {
             "is_enabled" : false,
             "host_name":"mail.disbaleme.com",
             "max_size": 100017,
-            "private_key_file":"config_test.go",
-            "public_key_file":"config_test.go",
             "timeout":160,
             "listen_interface":"127.0.0.1:3333",
-            "start_tls_on":true,
-            "tls_always_on":false,
-            "max_clients": 2
+            "max_clients": 2,
+			"tls" : {
+				"private_key_file":"config_test.go",
+            	"public_key_file":"config_test.go",
+				"start_tls_on":true,
+            	"tls_always_on":false
+			}
         }
     ]
 }
@@ -169,8 +181,8 @@ func TestConfigLoad(t *testing.T) {
 		t.SkipNow()
 	}
 	// did we got the timestamps?
-	if ac.Servers[0]._privateKeyFile_mtime <= 0 {
-		t.Error("failed to read timestamp for _privateKeyFile_mtime, got", ac.Servers[0]._privateKeyFile_mtime)
+	if ac.Servers[0].TLS._privateKeyFile_mtime <= 0 {
+		t.Error("failed to read timestamp for _privateKeyFile_mtime, got", ac.Servers[0].TLS._privateKeyFile_mtime)
 	}
 }
 
@@ -209,8 +221,8 @@ func TestConfigChangeEvents(t *testing.T) {
 	// simulate timestamp change
 
 	time.Sleep(time.Second + time.Millisecond*500)
-	os.Chtimes(oldconf.Servers[1].PrivateKeyFile, time.Now(), time.Now())
-	os.Chtimes(oldconf.Servers[1].PublicKeyFile, time.Now(), time.Now())
+	os.Chtimes(oldconf.Servers[1].TLS.PrivateKeyFile, time.Now(), time.Now())
+	os.Chtimes(oldconf.Servers[1].TLS.PublicKeyFile, time.Now(), time.Now())
 	newconf := &AppConfig{}
 	newconf.Load([]byte(configJsonB))
 	newconf.Servers[0].LogFile = log.OutputOff.String() // test for log file change

+ 6 - 6
event.go

@@ -68,20 +68,20 @@ func (e Event) String() string {
 }
 
 type EventHandler struct {
-	*evbus.EventBus
+	evbus.Bus
 }
 
 func (h *EventHandler) Subscribe(topic Event, fn interface{}) error {
-	if h.EventBus == nil {
-		h.EventBus = evbus.New()
+	if h.Bus == nil {
+		h.Bus = evbus.New()
 	}
-	return h.EventBus.Subscribe(topic.String(), fn)
+	return h.Bus.Subscribe(topic.String(), fn)
 }
 
 func (h *EventHandler) Publish(topic Event, args ...interface{}) {
-	h.EventBus.Publish(topic.String(), args...)
+	h.Bus.Publish(topic.String(), args...)
 }
 
 func (h *EventHandler) Unsubscribe(topic Event, handler interface{}) error {
-	return h.EventBus.Unsubscribe(topic.String(), handler)
+	return h.Bus.Unsubscribe(topic.String(), handler)
 }

+ 43 - 6
glide.lock

@@ -1,8 +1,8 @@
-hash: edbacc9b8ae3fcad4c01969c3efc5d815d79ffdc544d0bd56c501018696c2285
-updated: 2017-03-17T11:29:21.745184616+11:00
+hash: d845af9d7a26647c61c850a305d94006a0528611a1ae81eccea766c432e7aac0
+updated: 2018-05-31T13:04:27.585656465+10:00
 imports:
 - name: github.com/asaskevich/EventBus
-  version: ab9e5ceb2cc1ca6f36a5813c928c534e837681c2
+  version: 68a521d7cbbb7a859c2608b06342f384b3bd5f5a
 - name: github.com/garyburd/redigo
   version: 8873b2f1995f59d4bcdd2b0dc9858e2cb9bf0c13
   subpackages:
@@ -10,6 +10,10 @@ imports:
   - redis
 - name: github.com/go-sql-driver/mysql
   version: a0583e0143b1624142adab07e0e97fe106d99561
+- name: github.com/gomodule/redigo
+  version: 8873b2f1995f59d4bcdd2b0dc9858e2cb9bf0c13
+  subpackages:
+  - redis
 - name: github.com/gorilla/context
   version: 08b5f424b9271eedf6f9f0ce86cb9396ed337a42
 - name: github.com/gorilla/mux
@@ -19,19 +23,52 @@ imports:
 - name: github.com/inconshreveable/mousetrap
   version: 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75
 - name: github.com/rakyll/statik
-  version: 274df120e9065bdd08eb1120e0375e3dc1ae8465
+  version: fd36b3595eb2ec8da4b8153b107f7ea08504899d
   subpackages:
   - fs
+- name: github.com/sirupsen/logrus
+  version: c155da19408a8799da419ed3eeb0cb5db0ad5dbc
 - name: github.com/Sirupsen/logrus
-  version: ba1b36c82c5e05c4f912a88eab0dcd91a171688f
+  version: d682213848ed68c0a260ca37d6dd5ace8423f5ba
 - name: github.com/spf13/cobra
   version: b62566898a99f2db9c68ed0026aa0a052e59678d
 - name: github.com/spf13/pflag
   version: 25f8b5b07aece3207895bf19f7ab517eb3b22a40
+- name: golang.org/x/crypto
+  version: ab813273cd59e1333f7ae7bff5d027d4aadf528c
+  subpackages:
+  - ssh/terminal
+- name: golang.org/x/net
+  version: 1e491301e022f8f977054da4c2d852decd59571f
+  subpackages:
+  - html
+  - html/atom
+  - html/charset
 - name: golang.org/x/sys
-  version: 478fcf54317e52ab69f40bb4c7a1520288d7f7ea
+  version: 7dca6fe1f43775aa6d1334576870ff63f978f539
   subpackages:
   - unix
+  - windows
+- name: golang.org/x/text
+  version: 5c1cf69b5978e5a34c5f9ba09a83e56acc4b7877
+  subpackages:
+  - encoding
+  - encoding/charmap
+  - encoding/htmlindex
+  - encoding/internal
+  - encoding/internal/identifier
+  - encoding/japanese
+  - encoding/korean
+  - encoding/simplifiedchinese
+  - encoding/traditionalchinese
+  - encoding/unicode
+  - internal/language
+  - internal/language/compact
+  - internal/tag
+  - internal/utf8internal
+  - language
+  - runes
+  - transform
 - name: gopkg.in/iconv.v1
   version: 16a760eb7e186ae0e3aedda00d4a1daa4d0701d8
 testImports: []

+ 9 - 4
glide.yaml

@@ -1,8 +1,8 @@
 package: github.com/flashmob/go-guerrilla
 import:
-- package: github.com/Sirupsen/logrus
-  version: ~0.11.0
-- package: github.com/garyburd/redigo
+- package: github.com/sirupsen/logrus
+  version: ~1.0.4
+- package: github.com/gomodule/redigo
   version: ~1.0.0
   subpackages:
   - redis
@@ -14,10 +14,15 @@ import:
   subpackages:
   - fs
 - package: github.com/asaskevich/EventBus
-  version: ab9e5ceb2cc1ca6f36a5813c928c534e837681c2
+  version: 68a521d7cbbb7a859c2608b06342f384b3bd5f5a
 - package: github.com/go-sql-driver/mysql
   version: ^1.3.0
 - package: github.com/gorilla/mux
   version: ~1.3.0
 - package: github.com/gorilla/websocket
   version: ~1.1.0
+- package: golang.org/x/sys
+  version: 7dca6fe1f43775aa6d1334576870ff63f978f539
+- package: golang.org/x/net
+  subpackages:
+  - html/charset

+ 25 - 11
goguerrilla.conf.sample

@@ -20,34 +20,48 @@
         "log_received_mails": true,
         "save_workers_size": 1,
         "save_process" : "HeadersParser|Header|Debugger",
-        "primary_mail_host" : "mail.example.com"
+        "primary_mail_host" : "mail.example.com",
+        "gw_save_timeout" : "30s",
+        "gw_val_rcpt_timeout" : "3s"
     },
     "servers" : [
         {
             "is_enabled" : true,
             "host_name":"mail.test.com",
             "max_size": 1000000,
-            "private_key_file":"/path/to/pem/file/test.com.key",
-            "public_key_file":"/path/to/pem/file/test.com.crt",
             "timeout":180,
             "listen_interface":"127.0.0.1:25",
-            "start_tls_on":true,
-            "tls_always_on":false,
             "max_clients": 1000,
-            "log_file" : "stderr"
+            "log_file" : "stderr",
+            "tls" : {
+                "start_tls_on":true,
+                "tls_always_on":false,
+                "private_key_file":"/path/to/pem/file/test.com.key",
+                "public_key_file":"/path/to/pem/file/test.com.crt",
+                "protocols" : ["ssl3.0", "tls1.2"],
+                "ciphers" : ["TLS_FALLBACK_SCSV", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305", "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305", "TLS_RSA_WITH_RC4_128_SHA", "TLS_RSA_WITH_AES_128_GCM_SHA256", "TLS_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA", "TLS_ECDHE_RSA_WITH_RC4_128_SHA", "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384"],
+                "curves" : ["P256", "P384", "P521", "X25519"],
+                "client_auth_type" : "NoClientCert"
+            }
         },
         {
             "is_enabled" : false,
             "host_name":"mail.test.com",
             "max_size":1000000,
-            "private_key_file":"/path/to/pem/file/test.com.key",
-            "public_key_file":"/path/to/pem/file/test.com.crt",
             "timeout":180,
             "listen_interface":"127.0.0.1:465",
-            "start_tls_on":false,
-            "tls_always_on":true,
             "max_clients":500,
-            "log_file" : "stderr"
+            "log_file" : "stderr",
+            "tls" : {
+                "private_key_file":"/path/to/pem/file/test.com.key",
+                "public_key_file":"/path/to/pem/file/test.com.crt",
+                 "start_tls_on":false,
+                 "tls_always_on":true,
+                 "protocols" : ["ssl3.0", "tls1.2"],
+                 "ciphers" : ["TLS_FALLBACK_SCSV", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305", "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305", "TLS_RSA_WITH_RC4_128_SHA", "TLS_RSA_WITH_AES_128_GCM_SHA256", "TLS_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA", "TLS_ECDHE_RSA_WITH_RC4_128_SHA", "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384"],
+                 "curves" : ["P256", "P384", "P521", "X25519"],
+                 "client_auth_type" : "NoClientCert"
+            }
         }
     ]
 }

+ 1 - 0
guerrilla.go

@@ -259,6 +259,7 @@ func (g *guerrilla) subscribeEvents() {
 	// server config was updated
 	g.Subscribe(EventConfigServerConfig, func(sc *ServerConfig) {
 		g.setServerConfig(sc)
+		g.mainlog().Infof("server %s config change event, a new config has been saved", sc.ListenInterface)
 	})
 
 	// add a new server to the config & start

+ 1 - 1
log/hook.go

@@ -2,7 +2,7 @@ package log
 
 import (
 	"bufio"
-	log "github.com/Sirupsen/logrus"
+	log "github.com/sirupsen/logrus"
 	"io"
 	"io/ioutil"
 	"os"

+ 1 - 1
log/log.go

@@ -1,7 +1,7 @@
 package log
 
 import (
-	log "github.com/Sirupsen/logrus"
+	log "github.com/sirupsen/logrus"
 	"io"
 	"io/ioutil"
 	"net"

+ 20 - 0
mail/encoding/encoding.go

@@ -0,0 +1,20 @@
+// encoding enables using golang.org/x/net/html/charset for converting 7bit to UTF-8.
+// golang.org/x/net/html/charset supports a larger range of encodings.
+// when importing, place an underscore _ in front to import for side-effects
+
+package encoding
+
+import (
+	"io"
+
+	"github.com/flashmob/go-guerrilla/mail"
+	cs "golang.org/x/net/html/charset"
+)
+
+func init() {
+
+	mail.Dec.CharsetReader = func(charset string, input io.Reader) (io.Reader, error) {
+		return cs.NewReaderLabel(charset, input)
+	}
+
+}

+ 19 - 0
mail/encoding/encoding_test.go

@@ -0,0 +1,19 @@
+package encoding
+
+import (
+	"github.com/flashmob/go-guerrilla/mail"
+	"strings"
+	"testing"
+)
+
+// This will use the golang.org/x/net/html/charset encoder
+func TestEncodingMimeHeaderDecode(t *testing.T) {
+	str := mail.MimeHeaderDecode("=?ISO-2022-JP?B?GyRCIVo9dztSOWJAOCVBJWMbKEI=?=")
+	if i := strings.Index(str, "【女子高生チャ"); i != 0 {
+		t.Error("expecting 【女子高生チャ, got:", str)
+	}
+	str = mail.MimeHeaderDecode("=?ISO-8859-1?Q?Andr=E9?= Pirard <[email protected]>")
+	if strings.Index(str, "André Pirard") != 0 {
+		t.Error("expecting André Pirard, got:", str)
+	}
+}

+ 66 - 127
mail/envelope.go

@@ -4,21 +4,29 @@ import (
 	"bufio"
 	"bytes"
 	"crypto/md5"
-	"encoding/base64"
 	"errors"
 	"fmt"
-	"gopkg.in/iconv.v1"
 	"io"
-	"io/ioutil"
-	"mime/quotedprintable"
+	"mime"
 	"net/mail"
 	"net/textproto"
-	"regexp"
 	"strings"
 	"sync"
 	"time"
 )
 
+// A WordDecoder decodes MIME headers containing RFC 2047 encoded-words.
+// Used by the MimeHeaderDecode function.
+// It's exposed public so that an alternative decoder can be set, eg Gnu iconv
+// by importing the mail/inconv package.
+// Another alternative would be to use https://godoc.org/golang.org/x/text/encoding
+var Dec mime.WordDecoder
+
+func init() {
+	// use the default decoder, without Gnu inconv. Import the mail/inconv package to use iconv.
+	Dec = mime.WordDecoder{}
+}
+
 const maxHeaderChunk = 1 + (3 << 10) // 3KB
 
 // Address encodes an email address of the form `<user@host>`
@@ -104,7 +112,7 @@ func queuedID(clientID uint64) string {
 func (e *Envelope) ParseHeaders() error {
 	var err error
 	if e.Header != nil {
-		return errors.New("Headers already parsed")
+		return errors.New("headers already parsed")
 	}
 	buf := bytes.NewBuffer(e.Data.Bytes())
 	// find where the header ends, assuming that over 30 kb would be max
@@ -153,6 +161,12 @@ func (e *Envelope) String() string {
 
 // ResetTransaction is called when the transaction is reset (keeping the connection open)
 func (e *Envelope) ResetTransaction() {
+
+	// ensure not processing by the backend, will only get lock if finished, otherwise block
+	e.Lock()
+	// got the lock, it means processing finished
+	e.Unlock()
+
 	e.MailFrom = Address{}
 	e.RcptTo = []Address{}
 	// reset the data buffer, keep it allocated
@@ -186,107 +200,55 @@ func (e *Envelope) PopRcpt() Address {
 	return ret
 }
 
-var mimeRegex, _ = regexp.Compile(`=\?(.+?)\?([QBqp])\?(.+?)\?=`)
-
-// Decode strings in Mime header format
-// eg. =?ISO-2022-JP?B?GyRCIVo9dztSOWJAOCVBJWMbKEI=?=
-// This function uses GNU iconv under the hood, for more charset support than in Go's library
+// Converts 7 bit encoded mime header strings to UTF-8
 func MimeHeaderDecode(str string) string {
+	state := 0
+	var buf bytes.Buffer
+	var out []byte
+	for i := 0; i < len(str); i++ {
+		switch state {
+		case 0:
+			if str[i] == '=' {
+				buf.WriteByte(str[i])
+				state = 1
+			} else {
+				out = append(out, str[i])
+			}
+		case 1:
+			if str[i] == '?' {
+				buf.WriteByte(str[i])
+				state = 2
+			} else {
+				out = append(out, str[i])
+				buf.Reset()
+				state = 0
+			}
 
-	matched := mimeRegex.FindAllStringSubmatch(str, -1)
-	var charset, encoding, payload string
-	if matched != nil {
-		for i := 0; i < len(matched); i++ {
-			if len(matched[i]) > 2 {
-				charset = matched[i][1]
-				encoding = strings.ToUpper(matched[i][2])
-				payload = matched[i][3]
-				switch encoding {
-				case "B":
-					str = strings.Replace(
-						str,
-						matched[i][0],
-						MailTransportDecode(payload, "base64", charset),
-						1)
-				case "Q":
-					str = strings.Replace(
-						str,
-						matched[i][0],
-						MailTransportDecode(payload, "quoted-printable", charset),
-						1)
+		case 2:
+			if str[i] == ' ' {
+				d, err := Dec.Decode(buf.String())
+				if err == nil {
+					out = append(out, []byte(d)...)
+				} else {
+					out = append(out, buf.Bytes()...)
 				}
+				out = append(out, ' ')
+				buf.Reset()
+				state = 0
+			} else {
+				buf.WriteByte(str[i])
 			}
 		}
 	}
-	return str
-}
-
-// decode from 7bit to 8bit UTF-8
-// encodingType can be "base64" or "quoted-printable"
-func MailTransportDecode(str string, encodingType string, charset string) string {
-	if charset == "" {
-		charset = "UTF-8"
-	} else {
-		charset = strings.ToUpper(charset)
-	}
-	if encodingType == "base64" {
-		str = fromBase64(str)
-	} else if encodingType == "quoted-printable" {
-		str = fromQuotedP(str)
-	}
-
-	if charset != "UTF-8" {
-		charset = fixCharset(charset)
-		// iconv is pretty good at what it does
-		if cd, err := iconv.Open("UTF-8", charset); err == nil {
-			defer func() {
-				cd.Close()
-				if r := recover(); r != nil {
-					//logln(1, fmt.Sprintf("Recovered in %v", r))
-				}
-			}()
-			// eg. charset can be "ISO-2022-JP"
-			return cd.ConvString(str)
+	if buf.Len() > 0 {
+		d, err := Dec.Decode(buf.String())
+		if err == nil {
+			out = append(out, []byte(d)...)
+		} else {
+			out = append(out, buf.Bytes()...)
 		}
-
 	}
-	return str
-}
-
-func fromBase64(data string) string {
-	buf := bytes.NewBufferString(data)
-	decoder := base64.NewDecoder(base64.StdEncoding, buf)
-	res, _ := ioutil.ReadAll(decoder)
-	return string(res)
-}
-
-func fromQuotedP(data string) string {
-	res, _ := ioutil.ReadAll(quotedprintable.NewReader(strings.NewReader(data)))
-	return string(res)
-}
-
-var charsetRegex, _ = regexp.Compile(`[_:.\/\\]`)
-
-func fixCharset(charset string) string {
-	fixed_charset := charsetRegex.ReplaceAllString(charset, "-")
-	// Fix charset
-	// borrowed from http://squirrelmail.svn.sourceforge.net/viewvc/squirrelmail/trunk/squirrelmail/include/languages.php?revision=13765&view=markup
-	// OE ks_c_5601_1987 > cp949
-	fixed_charset = strings.Replace(fixed_charset, "ks-c-5601-1987", "cp949", -1)
-	// Moz x-euc-tw > euc-tw
-	fixed_charset = strings.Replace(fixed_charset, "x-euc", "euc", -1)
-	// Moz x-windows-949 > cp949
-	fixed_charset = strings.Replace(fixed_charset, "x-windows_", "cp", -1)
-	// windows-125x and cp125x charsets
-	fixed_charset = strings.Replace(fixed_charset, "windows-", "cp", -1)
-	// ibm > cp
-	fixed_charset = strings.Replace(fixed_charset, "ibm", "cp", -1)
-	// iso-8859-8-i -> iso-8859-8
-	fixed_charset = strings.Replace(fixed_charset, "iso-8859-8-i", "iso-8859-8", -1)
-	if charset != fixed_charset {
-		return fixed_charset
-	}
-	return charset
+	return string(out)
 }
 
 // Envelopes have their own pool
@@ -318,36 +280,13 @@ func (p *Pool) Borrow(remoteAddr string, clientID uint64) *Envelope {
 }
 
 // Return returns an envelope back to the envelope pool
-// Note that an envelope will not be recycled while it still is
-// processing
+// Make sure that envelope finished processing before calling this
 func (p *Pool) Return(e *Envelope) {
-	// we down't want to recycle an envelope that may still be processing
-	isUnlocked := func() <-chan bool {
-		signal := make(chan bool)
-		// make sure envelope finished processing
-		go func() {
-			// lock will block if still processing
-			e.Lock()
-			// got the lock, it means processing finished
-			e.Unlock()
-			// generate a signal
-			signal <- true
-		}()
-		return signal
-	}()
-
 	select {
-	case <-time.After(time.Second * 30):
-		// envelope still processing, we can't recycle it.
-	case <-isUnlocked:
-		// The envelope was _unlocked_, it finished processing
-		// put back in the pool or destroy
-		select {
-		case p.pool <- e:
-			//placed envelope back in pool
-		default:
-			// pool is full, don't return
-		}
+	case p.pool <- e:
+		//placed envelope back in pool
+	default:
+		// pool is full, discard it
 	}
 	// take a value off the semaphore to make room for more envelopes
 	<-p.sem

+ 13 - 3
mail/envelope_test.go

@@ -6,10 +6,20 @@ import (
 	"testing"
 )
 
+// Test MimeHeader decoding, not using iconv
 func TestMimeHeaderDecode(t *testing.T) {
-	str := MimeHeaderDecode("=?ISO-2022-JP?B?GyRCIVo9dztSOWJAOCVBJWMbKEI=?=")
-	if i := strings.Index(str, "【女子高生チャ"); i != 0 {
-		t.Error("expecting 【女子高生チャ, got:", str)
+
+	/*
+		Normally this would fail if not using iconv
+		str := MimeHeaderDecode("=?ISO-2022-JP?B?GyRCIVo9dztSOWJAOCVBJWMbKEI=?=")
+		if i := strings.Index(str, "【女子高生チャ"); i != 0 {
+			t.Error("expecting 【女子高生チャ, got:", str)
+		}
+	*/
+
+	str := MimeHeaderDecode("=?utf-8?B?55So5oi34oCcRXBpZGVtaW9sb2d5IGluIG51cnNpbmcgYW5kIGg=?=  =?utf-8?B?ZWFsdGggY2FyZSBlQm9vayByZWFkL2F1ZGlvIGlkOm8=?=  =?utf-8?B?cTNqZWVr4oCd5Zyo572R56uZ4oCcU1BZ5Lit5paH5a6Y5pa5572R56uZ4oCd?=  =?utf-8?B?55qE5biQ5Y+36K+m5oOF?=")
+	if i := strings.Index(str, "用户“Epidemiology in nursing and h  ealth care eBook read/audio id:o  q3jeek”在网站“SPY中文官方网站”  的帐号详情"); i != 0 {
+		t.Error("expecting 用户“Epidemiology in nursing and h  ealth care eBook read/audio id:o  q3jeek”在网站“SPY中文官方网站”  的帐号详情, got:", str)
 	}
 	str = MimeHeaderDecode("=?ISO-8859-1?Q?Andr=E9?= Pirard <[email protected]>")
 	if strings.Index(str, "André Pirard") != 0 {

+ 24 - 0
mail/iconv/iconv.go

@@ -0,0 +1,24 @@
+// iconv enables using GNU iconv for converting 7bit to UTF-8.
+// iconv supports a larger range of encodings.
+// It's a cgo package, the build system needs have Gnu library headers available.
+// when importing, place an underscore _ in front to import for side-effects
+package iconv
+
+import (
+	"fmt"
+	"io"
+
+	"github.com/flashmob/go-guerrilla/mail"
+	ico "gopkg.in/iconv.v1"
+)
+
+func init() {
+	mail.Dec.CharsetReader = func(charset string, input io.Reader) (io.Reader, error) {
+		if cd, err := ico.Open("UTF-8", charset); err == nil {
+			r := ico.NewReader(cd, input, 32)
+			return r, nil
+		}
+		return nil, fmt.Errorf("unhandled charset %q", charset)
+	}
+
+}

+ 19 - 0
mail/iconv/iconv_test.go

@@ -0,0 +1,19 @@
+package iconv
+
+import (
+	"github.com/flashmob/go-guerrilla/mail"
+	"strings"
+	"testing"
+)
+
+// This will use the iconv encoder
+func TestIconvMimeHeaderDecode(t *testing.T) {
+	str := mail.MimeHeaderDecode("=?ISO-2022-JP?B?GyRCIVo9dztSOWJAOCVBJWMbKEI=?=")
+	if i := strings.Index(str, "【女子高生チャ"); i != 0 {
+		t.Error("expecting 【女子高生チャ, got:", str)
+	}
+	str = mail.MimeHeaderDecode("=?ISO-8859-1?Q?Andr=E9?= Pirard <[email protected]>")
+	if strings.Index(str, "André Pirard") != 0 {
+		t.Error("expecting André Pirard, got:", str)
+	}
+}

+ 55 - 11
server.go

@@ -11,10 +11,12 @@ import (
 	"sync/atomic"
 	"time"
 
+	"crypto/x509"
 	"github.com/flashmob/go-guerrilla/backends"
 	"github.com/flashmob/go-guerrilla/log"
 	"github.com/flashmob/go-guerrilla/mail"
 	"github.com/flashmob/go-guerrilla/response"
+	"io/ioutil"
 )
 
 const (
@@ -100,8 +102,8 @@ func newServer(sc *ServerConfig, b backends.Backend, l log.Logger) (*server, err
 
 func (s *server) configureSSL() error {
 	sConfig := s.configStore.Load().(ServerConfig)
-	if sConfig.TLSAlwaysOn || sConfig.StartTLSOn {
-		cert, err := tls.LoadX509KeyPair(sConfig.PublicKeyFile, sConfig.PrivateKeyFile)
+	if sConfig.TLS.AlwaysOn || sConfig.TLS.StartTLSOn {
+		cert, err := tls.LoadX509KeyPair(sConfig.TLS.PublicKeyFile, sConfig.TLS.PrivateKeyFile)
 		if err != nil {
 			return fmt.Errorf("error while loading the certificate: %s", err)
 		}
@@ -110,6 +112,47 @@ func (s *server) configureSSL() error {
 			ClientAuth:   tls.VerifyClientCertIfGiven,
 			ServerName:   sConfig.Hostname,
 		}
+		if len(sConfig.TLS.Protocols) > 0 {
+			if min, ok := TLSProtocols[sConfig.TLS.Protocols[0]]; ok {
+				tlsConfig.MinVersion = min
+			}
+		}
+		if len(sConfig.TLS.Protocols) > 1 {
+			if max, ok := TLSProtocols[sConfig.TLS.Protocols[1]]; ok {
+				tlsConfig.MaxVersion = max
+			}
+		}
+		if len(sConfig.TLS.Ciphers) > 0 {
+			for _, val := range sConfig.TLS.Ciphers {
+				if c, ok := TLSCiphers[val]; ok {
+					tlsConfig.CipherSuites = append(tlsConfig.CipherSuites, c)
+				}
+			}
+		}
+		if len(sConfig.TLS.Curves) > 0 {
+			for _, val := range sConfig.TLS.Curves {
+				if c, ok := TLSCurves[val]; ok {
+					tlsConfig.CurvePreferences = append(tlsConfig.CurvePreferences, c)
+				}
+			}
+		}
+		if len(sConfig.TLS.RootCAs) > 0 {
+			caCert, err := ioutil.ReadFile(sConfig.TLS.RootCAs)
+			if err != nil {
+				s.log().WithError(err).Errorf("failed opening TLSRootCAs file [%s]", sConfig.TLS.RootCAs)
+			} else {
+				caCertPool := x509.NewCertPool()
+				caCertPool.AppendCertsFromPEM(caCert)
+				tlsConfig.RootCAs = caCertPool
+			}
+
+		}
+		if len(sConfig.TLS.ClientAuthType) > 0 {
+			if ca, ok := TLSClientAuthTypes[sConfig.TLS.ClientAuthType]; ok {
+				tlsConfig.ClientAuth = ca
+			}
+		}
+		tlsConfig.PreferServerCipherSuites = sConfig.TLS.PreferServerCipherSuites
 		tlsConfig.Rand = rand.Reader
 		s.tlsConfigStore.Store(tlsConfig)
 	}
@@ -231,9 +274,15 @@ func (server *server) GetActiveClientsCount() int {
 }
 
 // Verifies that the host is a valid recipient.
+// host checking turned off if there is a single entry and it's a dot.
 func (server *server) allowsHost(host string) bool {
 	server.hosts.Lock()
 	defer server.hosts.Unlock()
+	if len(server.hosts.table) == 1 {
+		if _, ok := server.hosts.table["."]; ok {
+			return true
+		}
+	}
 	if _, ok := server.hosts.table[strings.ToLower(host)]; ok {
 		return true
 	}
@@ -307,7 +356,7 @@ func (server *server) handleClient(client *client) {
 	// Also, Last line has no dash -
 	help := "250 HELP"
 
-	if sc.TLSAlwaysOn {
+	if sc.TLS.AlwaysOn {
 		tlsConfig, ok := server.tlsConfigStore.Load().(*tls.Config)
 		if !ok {
 			server.mainlog().Error("Failed to load *tls.Config")
@@ -319,7 +368,7 @@ func (server *server) handleClient(client *client) {
 			client.kill()
 		}
 	}
-	if !sc.StartTLSOn {
+	if !sc.TLS.StartTLSOn {
 		// STARTTLS turned off, don't advertise it
 		advertiseTLS = ""
 	}
@@ -446,7 +495,6 @@ func (server *server) handleClient(client *client) {
 						} else {
 							client.sendResponse(response.Canned.SuccessRcptCmd)
 						}
-
 					}
 				}
 
@@ -465,10 +513,6 @@ func (server *server) handleClient(client *client) {
 				client.kill()
 
 			case strings.Index(cmd, "DATA") == 0:
-				if client.MailFrom.IsEmpty() {
-					client.sendResponse(response.Canned.FailNoSenderDataCmd)
-					break
-				}
 				if len(client.RcptTo) == 0 {
 					client.sendResponse(response.Canned.FailNoRecipientsDataCmd)
 					break
@@ -476,7 +520,7 @@ func (server *server) handleClient(client *client) {
 				client.sendResponse(response.Canned.SuccessDataCmd)
 				client.state = ClientData
 
-			case sc.StartTLSOn && strings.Index(cmd, "STARTTLS") == 0:
+			case sc.TLS.StartTLSOn && strings.Index(cmd, "STARTTLS") == 0:
 
 				client.sendResponse(response.Canned.SuccessStartTLSCmd)
 				client.state = ClientStartTLS
@@ -533,7 +577,7 @@ func (server *server) handleClient(client *client) {
 			client.resetTransaction()
 
 		case ClientStartTLS:
-			if !client.TLS && sc.StartTLSOn {
+			if !client.TLS && sc.TLS.StartTLSOn {
 				tlsConfig, ok := server.tlsConfigStore.Load().(*tls.Config)
 				if !ok {
 					server.mainlog().Error("Failed to load *tls.Config")

+ 281 - 7
server_test.go

@@ -8,24 +8,31 @@ import (
 	"strings"
 	"sync"
 
+	"crypto/tls"
+	"fmt"
 	"github.com/flashmob/go-guerrilla/backends"
 	"github.com/flashmob/go-guerrilla/log"
 	"github.com/flashmob/go-guerrilla/mail"
 	"github.com/flashmob/go-guerrilla/mocks"
+	"io/ioutil"
+	"net"
+	"os"
 )
 
 // getMockServerConfig gets a mock ServerConfig struct used for creating a new server
 func getMockServerConfig() *ServerConfig {
 	sc := &ServerConfig{
-		IsEnabled:       true, // not tested here
-		Hostname:        "saggydimes.test.com",
-		MaxSize:         1024, // smtp message max size
-		PrivateKeyFile:  "./tests/mail.guerrillamail.com.key.pem",
-		PublicKeyFile:   "./tests/mail.guerrillamail.com.cert.pem",
+		IsEnabled: true, // not tested here
+		Hostname:  "saggydimes.test.com",
+		MaxSize:   1024, // smtp message max size
+		TLS: ServerTLSConfig{
+			PrivateKeyFile: "./tests/mail.guerrillamail.com.key.pem",
+			PublicKeyFile:  "./tests/mail.guerrillamail.com.cert.pem",
+			StartTLSOn:     true,
+			AlwaysOn:       false,
+		},
 		Timeout:         5,
 		ListenInterface: "127.0.0.1:2529",
-		StartTLSOn:      true,
-		TLSAlwaysOn:     false,
 		MaxClients:      30, // not tested here
 		LogFile:         "./tests/testlog",
 	}
@@ -58,6 +65,150 @@ func getMockServerConn(sc *ServerConfig, t *testing.T) (*mocks.Conn, *server) {
 	return conn, server
 }
 
+// test the RootCAs tls config setting
+var rootCAPK = `-----BEGIN CERTIFICATE-----
+MIIDqjCCApKgAwIBAgIJALh2TrsBR5MiMA0GCSqGSIb3DQEBCwUAMGkxCzAJBgNV
+BAYTAlVTMQswCQYDVQQIDAJDQTEWMBQGA1UEBwwNTW91bnRhaW4gVmlldzEhMB8G
+A1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRIwEAYDVQQDDAlsb2NhbGhv
+c3QwIBcNMTgwNTE4MDYzOTU2WhgPMjExODA0MjQwNjM5NTZaMGkxCzAJBgNVBAYT
+AlVTMQswCQYDVQQIDAJDQTEWMBQGA1UEBwwNTW91bnRhaW4gVmlldzEhMB8GA1UE
+CgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRIwEAYDVQQDDAlsb2NhbGhvc3Qw
+ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDCcb0ulYT1o5ysor5UtWYW
+q/ZY3PyK3/4YBZq5JoX4xk7GNQQ+3p/Km7QPoBXfgjFLZXEV2R0bE5hHMXfLa5Xb
+64acb9VqCqDvPFXcaNP4rEdBKDVN2p0PEi917tcKBSrZn5Yl+iOhtcBpQDvhHgn/
+9MdmIAKB3+yK+4l9YhT40XfDXCQqzfg4XcNaEgTzZHcDJz+KjWJuJChprcx27MTI
+Ndxs9nmFA2rK16rjgjtwjZ4t9dXsljdOcx59s6dIQ0GnEM8qdKxi/vEx4+M/hbGf
+v7H75LsuKRrVJINAmfy9fmc6VAXjFU0ZVxGK5eVnzsh/hY08TSSrlCCKAJpksjJz
+AgMBAAGjUzBRMB0GA1UdDgQWBBSZsYWs+8FYe4z4c6LLmFB4TeeV/jAfBgNVHSME
+GDAWgBSZsYWs+8FYe4z4c6LLmFB4TeeV/jAPBgNVHRMBAf8EBTADAQH/MA0GCSqG
+SIb3DQEBCwUAA4IBAQAcXt/FaILkOCMj8bTUx42vi2N9ZTiEuRbYi24IyGOokbDR
+pSsIxiz+HDPUuX6/X/mHidl24aS9wdv5JTXMr44/BeGK1WC7gMueZBxAqONpaG1Q
+VU0e3q1YwXKcupKQ7kVWl0fuY3licv0+s4zBcTLKkmWAYqsb/n0KtCMyqewi+Rqa
+Zj5Z3OcWOq9Ad9fZWKcG8k/sgeTk9z0X1mZcEyWWxqsUmxvN+SdWLoug1xJVVbMN
+CipZ0vBIi9KOhQgzuIFhoTcd6myUtov52/EFqlX6UuFpY2gEWw/f/yu+SI08v4w9
+KwxgAKBkhx2JYZKtu1EsPIMDyS0aahcDnHqnrGAi
+-----END CERTIFICATE-----`
+
+var clientPrvKey = `-----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEA5ZLmMBdKkVyVmN0VhDSFGvgKp24ejHPCv+wfuf3vlU9cwKfH
+R3vejleZAVRcidscfA0Jsub/Glsr0XwecagtpvTI+Fp1ik6sICOz+VW3958qaAi8
+TjbUMjcDHJeSLcjr725CH5uIvhRzR+daYaJQhAcL2MEt8M9WIF6AjtDZEH9R6oM8
+t5FkO0amImlnipYXNBFghmzkZzfGXXRQLw2A+u6keLcjCrn9h2BaofGIjQfYcu/3
+fH4cIFR4z/soGKameqnCUz7dWmbf4tAI+8QR0VXXBKhiHDm98tPSeH994hC52Uul
+rjEVcM5Uox5hazS2PK06oSc1YuFZONqeeGqj6wIDAQABAoIBADERzRHKaK3ZVEBw
+QQEZGLpC+kP/TZhHxgCvv7hJhsQrSnADbJzi5RcXsiSOm5j7tILvZntO1IgVpLAK
+D5fLkrZ069/pteXyGuhjuTw6DjBnXPEPrPAq2ABDse6SlzQiFgv/TTLkU74NMPbV
+hIQJ5ZvSxb12zRMDviz9Bg2ApmTX6k2iPjQBnEHgKzb64IdMcEb5HE1qNt0v0lRA
+sGBMZZKQWbt2m0pSbAbnB3S9GcpJkRgFFMdTaUScIWO6ICT2hBP2pw2/4M2Zrmlt
+bsyWu9uswBzhvu+/pg2E66V6mji0EzDMlXqjlO5jro6t7P33t1zkd/i/ykKmtDLp
+IpR94UECgYEA9Y4EIjOyaBWJ6TRQ6a/tehGPbwIOgvEiTYXRJqdU49qn/i4YZjSm
+F4iibJz+JeOIQXSwa9F7gRlaspIuHgIJoer7BrITMuhr+afqMLkxK0pijul/qAbm
+HdpFn8IxjpNu4/GoAENbEVy50SMST9yWh5ulEkHHftd4/NJKoJQ2PZ8CgYEA71bb
+lFVh1MFclxRKECmpyoqUAzwGlMoHJy/jaBYuWG4X7rzxqDRrgPH3as6gXpRiSZ+K
+5fC+wcU7dKnHtJOkBDk6J5ev2+hbwg+yq3w4+l3bPDvf2TJyXjXjRDZo12pxFD58
+ybCOF6ItbIDXqT5pvo3PMjgMwu1Ycie+h6hA3jUCgYEAsq93XpQT/R2/T44cWxEE
+VFG2+GacvLhP5+26ttAJPA1/Nb3BT458Vp+84iCT6GpcWpVZU/wKTXVvxIYPPRLq
+g4MEzGiFBASRngiMqIv6ta/ZbHmJxXHPvmV5SLn9aezrQsA1KovZFxdMuF03FBpH
+B8NBKbnoO+r8Ra2ZVKTFm60CgYAZw8Dpi/N3IsWj4eRDLyj/C8H5Qyn2NHVmq4oQ
+d2rPzDI5Wg+tqs7z15hp4Ap1hAW8pTcfn7X5SBEpculzr/0VE1AGWRbuVmoiTuxN
+95ZupVHnfw6O5BZZu/VWL4FDx0qbAksOrznso2G+b3RH3NcnUz69yjjddw1xZIPn
+OJ6bDQKBgDUcWYu/2amU18D5vJpppUgRq2084WPUeXsaniTbmWfOC8NAn8CKLY0N
+V4yGSu98apDuqEVqL0VFQEgqK+5KTvRdXXYi36XYRbbVUgV13xveq2YTvjNbPM60
+QWG9YmgH7hVYGusuh5nQeS0qiIpwyws2H5mBVrGXrQ1Xb0MLWj8/
+-----END RSA PRIVATE KEY-----`
+
+// signed using the Root (rootCAPK)
+var clientPubKey = `-----BEGIN CERTIFICATE-----
+MIIDWDCCAkACCQCHoh4OvUySOzANBgkqhkiG9w0BAQsFADBpMQswCQYDVQQGEwJV
+UzELMAkGA1UECAwCQ0ExFjAUBgNVBAcMDU1vdW50YWluIFZpZXcxITAfBgNVBAoM
+GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDESMBAGA1UEAwwJbG9jYWxob3N0MCAX
+DTE4MDUxODA2NDQ0NVoYDzMwMTcwOTE4MDY0NDQ1WjBxMQswCQYDVQQGEwJVUzET
+MBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91bnRhaW4gVmlldzEhMB8G
+A1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRIwEAYDVQQDDAlsb2NhbGhv
+c3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDlkuYwF0qRXJWY3RWE
+NIUa+Aqnbh6Mc8K/7B+5/e+VT1zAp8dHe96OV5kBVFyJ2xx8DQmy5v8aWyvRfB5x
+qC2m9Mj4WnWKTqwgI7P5Vbf3nypoCLxONtQyNwMcl5ItyOvvbkIfm4i+FHNH51ph
+olCEBwvYwS3wz1YgXoCO0NkQf1Hqgzy3kWQ7RqYiaWeKlhc0EWCGbORnN8ZddFAv
+DYD67qR4tyMKuf2HYFqh8YiNB9hy7/d8fhwgVHjP+ygYpqZ6qcJTPt1aZt/i0Aj7
+xBHRVdcEqGIcOb3y09J4f33iELnZS6WuMRVwzlSjHmFrNLY8rTqhJzVi4Vk42p54
+aqPrAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAIQmlo8iCpyYggkbpfDmThBPHfy1
+cZcCi/tRFoFe1ccwn2ezLMIKmW38ZebiroawwqrZgU6AP+dMxVKLMjpyLPSrpFKa
+3o/LbVF7qMfH8/y2q8t7javd6rxoENH9uxLyHhauzI1iWy0whoDWBNiZrPBTBCjq
+jDGZARZqGyrPeXi+RNe1cMvZCxAFy7gqEtWFLWWrp0gYNPvxkHhhQBrUcF+8T/Nf
+9G4hKZSN/KAgC0CNBVuNrdyNc3l8H66BfwwL5X0+pesBYZM+MEfmBZOo+p7OWx2r
+ug8tR8eSL1vGleONtFRBUVG7NbtjhBf9FhvPZcSRR10od/vWHku9E01i4xg=
+-----END CERTIFICATE-----`
+
+func TestTLSConfig(t *testing.T) {
+
+	if err := ioutil.WriteFile("rootca.test.pem", []byte(rootCAPK), 0644); err != nil {
+		t.Fatal("couldn't create rootca.test.pem file.", err)
+		return
+	}
+	if err := ioutil.WriteFile("client.test.key", []byte(clientPrvKey), 0644); err != nil {
+		t.Fatal("couldn't create client.test.key file.", err)
+		return
+	}
+	if err := ioutil.WriteFile("client.test.pem", []byte(clientPubKey), 0644); err != nil {
+		t.Fatal("couldn't create client.test.pem file.", err)
+		return
+	}
+
+	s := server{}
+	s.setConfig(&ServerConfig{
+		TLS: ServerTLSConfig{
+			StartTLSOn:     true,
+			PrivateKeyFile: "client.test.key",
+			PublicKeyFile:  "client.test.pem",
+			RootCAs:        "rootca.test.pem",
+			ClientAuthType: "NoClientCert",
+			Curves:         []string{"P521", "P384"},
+			Ciphers:        []string{"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA"},
+			Protocols:      []string{"tls1.0", "tls1.2"},
+		},
+	})
+	s.configureSSL()
+
+	c := s.tlsConfigStore.Load().(*tls.Config)
+
+	if len(c.CurvePreferences) != 2 {
+		t.Error("c.CurvePreferences should have two elements")
+	} else if c.CurvePreferences[0] != tls.CurveP521 && c.CurvePreferences[1] != tls.CurveP384 {
+		t.Error("c.CurvePreferences curves not setup")
+	}
+	if strings.Index(string(c.RootCAs.Subjects()[0]), "Mountain View") == -1 {
+		t.Error("c.RootCAs not correctly set")
+	}
+	if c.ClientAuth != tls.NoClientCert {
+		t.Error("c.ClientAuth should be tls.NoClientCert")
+	}
+
+	if len(c.CipherSuites) != 2 {
+		t.Error("c.CipherSuites length should be 2")
+	}
+
+	if c.CipherSuites[0] != tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 && c.CipherSuites[1] != tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA {
+		t.Error("c.CipherSuites not correctly set ")
+	}
+
+	if c.MinVersion != tls.VersionTLS10 {
+		t.Error("c.MinVersion should be tls.VersionTLS10")
+	}
+
+	if c.MaxVersion != tls.VersionTLS12 {
+		t.Error("c.MinVersion should be tls.VersionTLS10")
+	}
+
+	if c.PreferServerCipherSuites != false {
+		t.Error("PreferServerCipherSuites should be false")
+	}
+
+	os.Remove("rootca.test.pem")
+	os.Remove("client.test.key")
+	os.Remove("client.test.pem")
+
+}
+
 func TestHandleClient(t *testing.T) {
 	var mainlog log.Logger
 	var logOpenError error
@@ -144,5 +295,128 @@ func TestXClient(t *testing.T) {
 	wg.Wait() // wait for handleClient to exit
 }
 
+// The backend gateway should time out after 1 second because it sleeps for 2 sec.
+// The transaction should wait until finished, and then test to see if we can do
+// a second transaction
+func TestGatewayTimeout(t *testing.T) {
+
+	bcfg := backends.BackendConfig{
+		"save_workers_size":   1,
+		"save_process":        "HeadersParser|Debugger",
+		"log_received_mails":  true,
+		"primary_mail_host":   "example.com",
+		"gw_save_timeout":     "1s",
+		"gw_val_rcpt_timeout": "1s",
+		"sleep_seconds":       2,
+	}
+
+	cfg := &AppConfig{
+		LogFile:      log.OutputOff.String(),
+		AllowedHosts: []string{"grr.la"},
+	}
+	cfg.BackendConfig = bcfg
+
+	d := Daemon{Config: cfg}
+	err := d.Start()
+
+	if err != nil {
+		t.Error("server didn't start")
+	} else {
+
+		conn, err := net.Dial("tcp", "127.0.0.1:2525")
+		if err != nil {
+
+			return
+		}
+		in := bufio.NewReader(conn)
+		str, err := in.ReadString('\n')
+		fmt.Fprint(conn, "HELO host\r\n")
+		str, err = in.ReadString('\n')
+		// perform 2 transactions
+		// both should panic.
+		for i := 0; i < 2; i++ {
+			fmt.Fprint(conn, "MAIL FROM:<[email protected]>r\r\n")
+			str, err = in.ReadString('\n')
+			fmt.Fprint(conn, "RCPT TO:[email protected]\r\n")
+			str, err = in.ReadString('\n')
+			fmt.Fprint(conn, "DATA\r\n")
+			str, err = in.ReadString('\n')
+			fmt.Fprint(conn, "Subject: Test subject\r\n")
+			fmt.Fprint(conn, "\r\n")
+			fmt.Fprint(conn, "A an email body\r\n")
+			fmt.Fprint(conn, ".\r\n")
+			str, err = in.ReadString('\n')
+			expect := "transaction timeout"
+			if strings.Index(str, expect) == -1 {
+				t.Error("Expected the reply to have'", expect, "'but got", str)
+			}
+		}
+		_ = str
+
+		d.Shutdown()
+	}
+}
+
+// The processor will panic and gateway should recover from it
+func TestGatewayPanic(t *testing.T) {
+	bcfg := backends.BackendConfig{
+		"save_workers_size":   1,
+		"save_process":        "HeadersParser|Debugger",
+		"log_received_mails":  true,
+		"primary_mail_host":   "example.com",
+		"gw_save_timeout":     "2s",
+		"gw_val_rcpt_timeout": "2s",
+		"sleep_seconds":       1,
+	}
+
+	cfg := &AppConfig{
+		LogFile:      log.OutputOff.String(),
+		AllowedHosts: []string{"grr.la"},
+	}
+	cfg.BackendConfig = bcfg
+
+	d := Daemon{Config: cfg}
+	err := d.Start()
+
+	if err != nil {
+		t.Error("server didn't start")
+	} else {
+
+		conn, err := net.Dial("tcp", "127.0.0.1:2525")
+		if err != nil {
+
+			return
+		}
+		in := bufio.NewReader(conn)
+		str, err := in.ReadString('\n')
+		fmt.Fprint(conn, "HELO host\r\n")
+		str, err = in.ReadString('\n')
+		// perform 2 transactions
+		// both should timeout. The reason why 2 is because we want to make
+		// sure that the client waits until processing finishes, and the
+		// timeout event is captured.
+		for i := 0; i < 2; i++ {
+			fmt.Fprint(conn, "MAIL FROM:<[email protected]>r\r\n")
+			str, err = in.ReadString('\n')
+			fmt.Fprint(conn, "RCPT TO:[email protected]\r\n")
+			str, err = in.ReadString('\n')
+			fmt.Fprint(conn, "DATA\r\n")
+			str, err = in.ReadString('\n')
+			fmt.Fprint(conn, "Subject: Test subject\r\n")
+			fmt.Fprint(conn, "\r\n")
+			fmt.Fprint(conn, "A an email body\r\n")
+			fmt.Fprint(conn, ".\r\n")
+			str, err = in.ReadString('\n')
+			expect := "storage failed"
+			if strings.Index(str, expect) == -1 {
+				t.Error("Expected the reply to have'", expect, "'but got", str)
+			}
+		}
+		_ = str
+		d.Shutdown()
+	}
+
+}
+
 // TODO
 // - test github issue #44 and #42

+ 1 - 1
tests/client.go

@@ -14,7 +14,7 @@ func Connect(serverConfig guerrilla.ServerConfig, deadline time.Duration) (net.C
 	var bufin *bufio.Reader
 	var conn net.Conn
 	var err error
-	if serverConfig.TLSAlwaysOn {
+	if serverConfig.TLS.AlwaysOn {
 		// start tls automatically
 		conn, err = tls.Dial("tcp", serverConfig.ListenInterface, &tls.Config{
 			InsecureSkipVerify: true,

+ 20 - 16
tests/guerrilla_test.go

@@ -33,13 +33,13 @@ import (
 	"net"
 	"strings"
 
-	"github.com/flashmob/go-guerrilla/tests/testcert"
 	"os"
+
+	"github.com/flashmob/go-guerrilla/tests/testcert"
 )
 
 type TestConfig struct {
 	guerrilla.AppConfig
-	BackendName   string                 `json:"backend_name"`
 	BackendConfig map[string]interface{} `json:"backend_config"`
 }
 
@@ -85,28 +85,32 @@ var configJson = `
             "is_enabled" : true,
             "host_name":"mail.guerrillamail.com",
             "max_size": 100017,
-            "private_key_file":"/vagrant/projects/htdocs/guerrilla/config/ssl/guerrillamail.com.key",
-            "public_key_file":"/vagrant/projects/htdocs/guerrilla/config/ssl/guerrillamail.com.crt",
             "timeout":160,
-            "listen_interface":"127.0.0.1:2526",
-            "start_tls_on":true,
-            "tls_always_on":false,
+            "listen_interface":"127.0.0.1:2526", 
             "max_clients": 2,
-            "log_file" : ""
+            "log_file" : "",
+			"tls" : {
+				"private_key_file":"/vagrant/projects/htdocs/guerrilla/config/ssl/guerrillamail.com.key",
+            	"public_key_file":"/vagrant/projects/htdocs/guerrilla/config/ssl/guerrillamail.com.crt",
+				"start_tls_on":true,
+            	"tls_always_on":false
+			}
         },
 
         {
             "is_enabled" : true,
             "host_name":"mail.guerrillamail.com",
             "max_size":1000001,
-            "private_key_file":"/vagrant/projects/htdocs/guerrilla/config/ssl/guerrillamail.com.key",
-            "public_key_file":"/vagrant/projects/htdocs/guerrilla/config/ssl/guerrillamail.com.crt",
             "timeout":180,
             "listen_interface":"127.0.0.1:4654",
-            "start_tls_on":false,
-            "tls_always_on":true,
             "max_clients":1,
-            "log_file" : ""
+            "log_file" : "",
+			"tls" : {
+				"private_key_file":"/vagrant/projects/htdocs/guerrilla/config/ssl/guerrillamail.com.key",
+            	"public_key_file":"/vagrant/projects/htdocs/guerrilla/config/ssl/guerrillamail.com.crt",
+				"start_tls_on":false,
+            	"tls_always_on":true
+			}
         }
     ]
 }
@@ -124,8 +128,8 @@ func getBackend(backendConfig map[string]interface{}, l log.Logger) (backends.Ba
 func setupCerts(c *TestConfig) {
 	for i := range c.Servers {
 		testcert.GenerateCert(c.Servers[i].Hostname, "", 365*24*time.Hour, false, 2048, "P256", "./")
-		c.Servers[i].PrivateKeyFile = c.Servers[i].Hostname + ".key.pem"
-		c.Servers[i].PublicKeyFile = c.Servers[i].Hostname + ".cert.pem"
+		c.Servers[i].TLS.PrivateKeyFile = c.Servers[i].Hostname + ".key.pem"
+		c.Servers[i].TLS.PublicKeyFile = c.Servers[i].Hostname + ".cert.pem"
 	}
 }
 
@@ -1139,7 +1143,7 @@ func TestFuzz86f25b86b09897aed8f6c2aa5b5ee1557358a6de(t *testing.T) {
 				conn,
 				bufin,
 				"DATA\r\n")
-			expected := "503 5.5.1 Error: No sender"
+			expected := "503 5.5.1 Error: No recipients"
 			if strings.Index(response, expected) != 0 {
 				t.Error("Server did not respond with", expected, ", it said:"+response, err)
 			}

+ 17 - 0
tls_go1.8.go

@@ -0,0 +1,17 @@
+// +build go1.8
+
+package guerrilla
+
+import "crypto/tls"
+
+// add ciphers introduced since Go 1.8
+func init() {
+	TLSCiphers["TLS_RSA_WITH_AES_128_CBC_SHA256"] = tls.TLS_RSA_WITH_AES_128_CBC_SHA256
+	TLSCiphers["TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256"] = tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256
+	TLSCiphers["TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256"] = tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256
+	TLSCiphers["TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"] = tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
+	TLSCiphers["TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305"] = tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305
+	TLSCiphers["TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305"] = tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305
+
+	TLSCurves["X25519"] = tls.X25519
+}