commit 197817cb0374972bfdededf310f9547fbf9622c0 Author: Jan-Lukas Else Date: Sat Mar 14 22:26:34 2020 +0100 Initial version diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..757fee3 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/.idea \ No newline at end of file diff --git a/config.go b/config.go new file mode 100644 index 0000000..6608fa9 --- /dev/null +++ b/config.go @@ -0,0 +1,48 @@ +package main + +import ( + "errors" + "github.com/caarlos0/env/v6" +) + +type config struct { + Port int `env:"PORT" envDefault:"8080"` + HoneyPots []string `env:"HONEYPOTS" envDefault:"_t_email" envSeparator:","` + DefaultRecipient string `env:"DEFAULT_TO"` + AllowedRecipients []string `env:"ALLOWED_TO" envSeparator:","` + Sender string `env:"EMAIL_FROM"` + SmtpUser string `env:"SMTP_USER"` + SmtpPassword string `env:"SMTP_PASS"` + SmtpHost string `env:"SMTP_HOST"` + SmtpPort int `env:"SMTP_PORT" envDefault:"587"` +} + +func parseConfig() (config, error) { + cfg := config{} + if err := env.Parse(&cfg); err != nil { + return cfg, errors.New("failed to parse config") + } + return cfg, nil +} + +func checkRequiredConfig(cfg config) bool { + if cfg.DefaultRecipient == "" { + return false + } + if len(cfg.AllowedRecipients) < 1 { + return false + } + if cfg.Sender == "" { + return false + } + if cfg.SmtpUser == "" { + return false + } + if cfg.SmtpPassword == "" { + return false + } + if cfg.SmtpHost == "" { + return false + } + return true +} \ No newline at end of file diff --git a/config_test.go b/config_test.go new file mode 100644 index 0000000..142e160 --- /dev/null +++ b/config_test.go @@ -0,0 +1,140 @@ +package main + +import ( + "os" + "reflect" + "testing" +) + +func Test_parseConfig(t *testing.T) { + t.Run("Default config", func(t *testing.T) { + os.Clearenv() + cfg, err := parseConfig() + if err != nil { + t.Error() + return + } + if cfg.Port != 8080 { + t.Error("Default Port not 8080") + } + if len(cfg.HoneyPots) != 1 || cfg.HoneyPots[0] != "_t_email" { + t.Error("Default HoneyPots are wrong") + } + if cfg.SmtpPort != 587 { + t.Error("SMTP Port not 587") + } + }) + t.Run("Correct config parsing", func(t *testing.T) { + os.Clearenv() + _ = os.Setenv("PORT", "1111") + _ = os.Setenv("HONEYPOTS", "pot,abc") + _ = os.Setenv("DEFAULT_TO", "mail@example.com") + _ = os.Setenv("ALLOWED_TO", "mail@example.com,test@example.com") + _ = os.Setenv("EMAIL_FROM", "forms@example.com") + _ = os.Setenv("SMTP_USER", "test@example.com") + _ = os.Setenv("SMTP_PASS", "secret") + _ = os.Setenv("SMTP_HOST", "smtp.example.com") + _ = os.Setenv("SMTP_PORT", "100") + cfg, err := parseConfig() + if err != nil { + t.Error() + return + } + if !reflect.DeepEqual(cfg.Port, 1111) { + t.Error("Port is wrong") + } + if !reflect.DeepEqual(cfg.HoneyPots, []string{"pot", "abc"}) { + t.Error("HoneyPots are wrong") + } + if !reflect.DeepEqual(cfg.DefaultRecipient, "mail@example.com") { + t.Error("DefaultRecipient is wrong") + } + if !reflect.DeepEqual(cfg.AllowedRecipients, []string{"mail@example.com", "test@example.com"}) { + t.Error("AllowedRecipients are wrong") + } + if !reflect.DeepEqual(cfg.Sender, "forms@example.com") { + t.Error("Sender is wrong") + } + if !reflect.DeepEqual(cfg.SmtpUser, "test@example.com") { + t.Error("SMTP user is wrong") + } + if !reflect.DeepEqual(cfg.SmtpPassword, "secret") { + t.Error("SMTP password is wrong") + } + if !reflect.DeepEqual(cfg.SmtpHost, "smtp.example.com") { + t.Error("SMTP host is wrong") + } + if !reflect.DeepEqual(cfg.SmtpPort, 100) { + t.Error("SMTP port is wrong") + } + }) + t.Run("Error when wrong config", func(t *testing.T) { + os.Clearenv() + _ = os.Setenv("PORT", "ABC") + _, err := parseConfig() + if err == nil { + t.Error() + } + }) +} + +func Test_checkRequiredConfig(t *testing.T) { + validConfig := config{ + Port: 8080, + HoneyPots: []string{"_t_email"}, + DefaultRecipient: "mail@example.com", + AllowedRecipients: []string{"mail@example.com"}, + Sender: "forms@example.com", + SmtpUser: "test@example.com", + SmtpPassword: "secret", + SmtpHost: "smtp.example.com", + SmtpPort: 587, + } + t.Run("Valid config", func(t *testing.T) { + if true != checkRequiredConfig(validConfig) { + t.Error() + } + }) + t.Run("Default recipient missing", func(t *testing.T) { + newConfig := validConfig + newConfig.DefaultRecipient = "" + if false != checkRequiredConfig(newConfig) { + t.Error() + } + }) + t.Run("Allowed recipients missing", func(t *testing.T) { + newConfig := validConfig + newConfig.AllowedRecipients = nil + if false != checkRequiredConfig(newConfig) { + t.Error() + } + }) + t.Run("Sender missing", func(t *testing.T) { + newConfig := validConfig + newConfig.Sender = "" + if false != checkRequiredConfig(newConfig) { + t.Error() + } + }) + t.Run("SMTP user missing", func(t *testing.T) { + newConfig := validConfig + newConfig.SmtpUser = "" + if false != checkRequiredConfig(newConfig) { + t.Error() + } + }) + t.Run("SMTP password missing", func(t *testing.T) { + newConfig := validConfig + newConfig.SmtpPassword = "" + if false != checkRequiredConfig(newConfig) { + t.Error() + } + }) + t.Run("SMTP host missing", func(t *testing.T) { + newConfig := validConfig + newConfig.SmtpHost = "" + if false != checkRequiredConfig(newConfig) { + t.Error() + } + }) +} diff --git a/form.html b/form.html new file mode 100644 index 0000000..6f1ed55 --- /dev/null +++ b/form.html @@ -0,0 +1,29 @@ + + + + Test-Form + + +
+ + + + + + +
+ +
+ +
+ +
+ +
+ +
+ +
+ + + diff --git a/forms.go b/forms.go new file mode 100644 index 0000000..1ded36a --- /dev/null +++ b/forms.go @@ -0,0 +1,67 @@ +package main + +import ( + "github.com/microcosm-cc/bluemonday" + "net/http" + "net/url" +) + +type FormValues map[string][]string + +func FormHandler(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet { + _, _ = w.Write([]byte("MailyGo works!")) + return + } + if r.Method != http.MethodPost { + w.WriteHeader(http.StatusMethodNotAllowed) + _, _ = w.Write([]byte("The HTTP method is not allowed, make a POST request")) + return + } + _ = r.ParseForm() + sanitizedForm := sanitizeForm(r.PostForm) + if !isBot(sanitizedForm) { + sendForm(sanitizedForm) + } + sendResponse(sanitizedForm, w) + return +} + +func sanitizeForm(values url.Values) FormValues { + p := bluemonday.StrictPolicy() + sanitizedForm := make(FormValues) + for key, values := range values { + var sanitizedValues []string + for _, value := range values { + sanitizedValues = append(sanitizedValues, p.Sanitize(value)) + } + sanitizedForm[p.Sanitize(key)] = sanitizedValues + } + return sanitizedForm +} + +func isBot(values FormValues) bool { + for _, honeyPot := range appConfig.HoneyPots { + if len(values[honeyPot]) > 0 { + for _, value := range values[honeyPot] { + if value != "" { + return true + } + } + } + } + return false +} + +func sendResponse(values FormValues, w http.ResponseWriter) { + if len(values["_redirectTo"]) == 1 && values["_redirectTo"][0] != "" { + w.Header().Add("Location", values["_redirectTo"][0]) + w.WriteHeader(http.StatusSeeOther) + _, _ = w.Write([]byte("Go to " + values["_redirectTo"][0])) + return + } else { + w.WriteHeader(http.StatusCreated) + _, _ = w.Write([]byte("Submitted form")) + return + } +} \ No newline at end of file diff --git a/forms_test.go b/forms_test.go new file mode 100644 index 0000000..4bd0729 --- /dev/null +++ b/forms_test.go @@ -0,0 +1,118 @@ +package main + +import ( + "net/http" + "net/http/httptest" + "net/url" + "os" + "reflect" + "testing" +) + +func Test_sanitizeForm(t *testing.T) { + t.Run("Sanitize form", func(t *testing.T) { + result := sanitizeForm(url.Values{"Test": {"Test"}}) + want := FormValues{"Test": {"Test"}} + if !reflect.DeepEqual(result, want) { + t.Error() + } + }) +} + +func TestFormHandler(t *testing.T) { + t.Run("GET request to FormHandler", func(t *testing.T) { + req := httptest.NewRequest("GET", "http://example.com/", nil) + w := httptest.NewRecorder() + FormHandler(w, req) + resp := w.Result() + if resp.StatusCode != http.StatusOK { + t.Error() + } + }) + t.Run("POST request to FormHandler", func(t *testing.T) { + req := httptest.NewRequest("POST", "http://example.com/", nil) + w := httptest.NewRecorder() + FormHandler(w, req) + resp := w.Result() + if resp.StatusCode != http.StatusCreated { + t.Error() + } + }) + t.Run("Wrong method request to FormHandler", func(t *testing.T) { + req := httptest.NewRequest("DELETE", "http://example.com/", nil) + w := httptest.NewRecorder() + FormHandler(w, req) + resp := w.Result() + if resp.StatusCode != http.StatusMethodNotAllowed { + t.Error() + } + }) +} + +func Test_isBot(t *testing.T) { + t.Run("No bot", func(t *testing.T) { + os.Clearenv() + result := isBot(FormValues{"_t_email": {""}}) + if !reflect.DeepEqual(result, false) { + t.Error() + } + }) + t.Run("No honeypot", func(t *testing.T) { + os.Clearenv() + result := isBot(FormValues{}) + if !reflect.DeepEqual(result, false) { + t.Error() + } + }) + t.Run("Bot", func(t *testing.T) { + os.Clearenv() + result := isBot(FormValues{"_t_email": {"Test", ""}}) + if !reflect.DeepEqual(result, true) { + t.Error() + } + }) +} + +func Test_sendResponse(t *testing.T) { + t.Run("No redirect", func(t *testing.T) { + values := FormValues{} + w := httptest.NewRecorder() + sendResponse(values, w) + if w.Code != http.StatusCreated { + t.Error() + } + }) + t.Run("No redirect 2", func(t *testing.T) { + values := FormValues{ + "_redirectTo": {""}, + } + w := httptest.NewRecorder() + sendResponse(values, w) + if w.Code != http.StatusCreated { + t.Error() + } + }) + t.Run("No redirect 3", func(t *testing.T) { + values := FormValues{ + "_redirectTo": {"abc", "def"}, + } + w := httptest.NewRecorder() + sendResponse(values, w) + if w.Code != http.StatusCreated { + t.Error() + } + }) + t.Run("Redirect", func(t *testing.T) { + values := FormValues{ + "_redirectTo": {"https://example.com"}, + } + w := httptest.NewRecorder() + sendResponse(values, w) + if w.Code != http.StatusSeeOther { + t.Error() + } + if w.Header().Get("Location") != "https://example.com" { + t.Error() + } + }) +} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b7b08d0 --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module codeberg.org/jlelse/mailygo + +go 1.14 + +require ( + github.com/caarlos0/env/v6 v6.2.1 + github.com/microcosm-cc/bluemonday v1.0.3-0.20191119130333-0a75d7616912 + golang.org/x/net v0.0.0-20200301022130-244492dfa37a // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..33cb3e2 --- /dev/null +++ b/go.sum @@ -0,0 +1,28 @@ +github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= +github.com/caarlos0/env/v6 v6.2.1 h1:/bFpX1dg4TNioJjg7mrQaSrBoQvRfLUHNfXivdFbbEo= +github.com/caarlos0/env/v6 v6.2.1/go.mod h1:3LpmfcAYCG6gCiSgDLaFR5Km1FRpPwFvBbRcjHar6Sw= +github.com/chris-ramon/douceur v0.2.0 h1:IDMEdxlEUUBYBKE4z/mJnFyVXox+MjuEVDJNN27glkU= +github.com/chris-ramon/douceur v0.2.0/go.mod h1:wDW5xjJdeoMm1mRt4sD4c/LbF/mWdEpRXQKjTR8nIBE= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= +github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= +github.com/microcosm-cc/bluemonday v1.0.3-0.20191119130333-0a75d7616912 h1:hJde9rA24hlTcAYSwJoXpDUyGtfKQ/jsofw+WaDqGrI= +github.com/microcosm-cc/bluemonday v1.0.3-0.20191119130333-0a75d7616912/go.mod h1:8iwZnFn2CDDNZ0r6UXhF4xawGvzaqzCRa1n3/lO3W2w= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3 h1:eH6Eip3UpmR+yM/qI9Ijluzb1bNv/cAU/n+6l8tRSis= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/mail.go b/mail.go new file mode 100644 index 0000000..e126cc3 --- /dev/null +++ b/mail.go @@ -0,0 +1,82 @@ +package main + +import ( + "bytes" + "fmt" + "net/smtp" + "strconv" + "strings" + "time" +) + +func sendForm(values FormValues) { + recipient := findRecipient(values) + sendMail(recipient, buildMessage(recipient, time.Now(), values)) +} + +func buildMessage(recipient string, date time.Time, values FormValues) string { + var msgBuffer bytes.Buffer + _, _ = fmt.Fprintf(&msgBuffer, "From: Forms <%s>", appConfig.Sender) + _, _ = fmt.Fprintln(&msgBuffer) + _, _ = fmt.Fprintf(&msgBuffer, "To: %s", recipient) + _, _ = fmt.Fprintln(&msgBuffer) + if replyTo := findReplyTo(values); replyTo != "" { + _, _ = fmt.Fprintf(&msgBuffer, "Reply-To: %s", replyTo) + _, _ = fmt.Fprintln(&msgBuffer) + } + _, _ = fmt.Fprintf(&msgBuffer, "Date: %s", date.Format(time.RFC1123Z)) + _, _ = fmt.Fprintln(&msgBuffer) + _, _ = fmt.Fprintf(&msgBuffer, "Subject: New submission on %s", findFormName(values)) + _, _ = fmt.Fprintln(&msgBuffer) + _, _ = fmt.Fprintln(&msgBuffer) + for key, value := range removeMetaValues(values) { + _, _ = fmt.Fprint(&msgBuffer, key) + _, _ = fmt.Fprint(&msgBuffer, ": ") + _, _ = fmt.Fprintln(&msgBuffer, strings.Join(value, ", ")) + } + return msgBuffer.String() +} + +func sendMail(to, message string) { + auth := smtp.PlainAuth("", appConfig.SmtpUser, appConfig.SmtpPassword, appConfig.SmtpHost) + err := smtp.SendMail(appConfig.SmtpHost+":"+strconv.Itoa(appConfig.SmtpPort), auth, appConfig.Sender, []string{to}, []byte(message)) + if err != nil { + fmt.Println("Failed to send mail:", err.Error()) + } +} + +func findRecipient(values FormValues) string { + if len(values["_to"]) == 1 && values["_to"][0] != "" { + formDefinedRecipient := values["_to"][0] + for _, allowed := range appConfig.AllowedRecipients { + if formDefinedRecipient == allowed { + return formDefinedRecipient + } + } + } + return appConfig.DefaultRecipient +} + +func findFormName(values FormValues) string { + if len(values["_formName"]) == 1 && values["_formName"][0] != "" { + return values["_formName"][0] + } + return "a form" +} + +func findReplyTo(values FormValues) string { + if len(values["_replyTo"]) == 1 && values["_replyTo"][0] != "" { + return values["_replyTo"][0] + } + return "" +} + +func removeMetaValues(values FormValues) FormValues { + cleanedValues := FormValues{} + for key, value := range values { + if !strings.HasPrefix(key, "_") { + cleanedValues[key] = value + } + } + return cleanedValues +} diff --git a/mail_test.go b/mail_test.go new file mode 100644 index 0000000..0a920fd --- /dev/null +++ b/mail_test.go @@ -0,0 +1,137 @@ +package main + +import ( + "os" + "reflect" + "strings" + "testing" + "time" +) + +func Test_findRecipient(t *testing.T) { + prepare := func() { + os.Clearenv() + _ = os.Setenv("ALLOWED_TO", "mail@example.com,test@example.com") + _ = os.Setenv("DEFAULT_TO", "mail@example.com") + appConfig, _ = parseConfig() + } + t.Run("No recipient specified", func(t *testing.T) { + prepare() + values := FormValues{} + result := findRecipient(values) + if result != "mail@example.com" { + t.Error() + } + }) + t.Run("Multiple recipients specified", func(t *testing.T) { + prepare() + values := FormValues{ + "_to": {"abc@example.com", "def@example.com"}, + } + result := findRecipient(values) + if result != "mail@example.com" { + t.Error() + } + }) + t.Run("Allowed recipient specified", func(t *testing.T) { + prepare() + values := FormValues{ + "_to": {"test@example.com"}, + } + result := findRecipient(values) + if result != "test@example.com" { + t.Error() + } + }) + t.Run("Forbidden recipient specified", func(t *testing.T) { + prepare() + values := FormValues{ + "_to": {"forbidden@example.com"}, + } + result := findRecipient(values) + if result != "mail@example.com" { + t.Error() + } + }) +} + +func Test_findFormName(t *testing.T) { + t.Run("No form name", func(t *testing.T) { + if "a form" != findFormName(FormValues{}) { + t.Error() + } + }) + t.Run("Multiple form names", func(t *testing.T) { + if "a form" != findFormName(FormValues{"_formName": {"Test", "ABC"}}) { + t.Error() + } + }) + t.Run("Form name", func(t *testing.T) { + if "Test" != findFormName(FormValues{"_formName": {"Test"}}) { + t.Error() + } + }) +} + +func Test_findReplyTo(t *testing.T) { + t.Run("No replyTo", func(t *testing.T) { + if "" != findReplyTo(FormValues{}) { + t.Error() + } + }) + t.Run("Multiple replyTo", func(t *testing.T) { + if "" != findReplyTo(FormValues{"_replyTo": {"test@example.com", "test2@example.com"}}) { + t.Error() + } + }) + t.Run("replyTo", func(t *testing.T) { + if "test@example.com" != findReplyTo(FormValues{"_replyTo": {"test@example.com"}}) { + t.Error() + } + }) +} + +func Test_removeMetaValues(t *testing.T) { + t.Run("Remove meta values", func(t *testing.T) { + result := removeMetaValues(FormValues{ + "_test": {"abc"}, + "test": {"def"}, + }) + want := FormValues{ + "test": {"def"}, + } + if !reflect.DeepEqual(result, want) { + t.Error() + } + }) +} + +func Test_buildMessage(t *testing.T) { + t.Run("Test message", func(t *testing.T) { + os.Clearenv() + _ = os.Setenv("EMAIL_TO", "mail@example.com") + _ = os.Setenv("ALLOWED_TO", "mail@example.com,test@example.com") + _ = os.Setenv("EMAIL_FROM", "forms@example.com") + appConfig, _ = parseConfig() + values := FormValues{ + "_formName": {"Testform"}, + "_replyTo": {"reply@example.com"}, + "Testkey": {"Testvalue"}, + "Another Key": {"Test", "ABC"}, + } + date := time.Now() + result := buildMessage("test@example.com", date, values) + if !strings.Contains(result, "Reply-To: reply@example.com") { + t.Error() + } + if !strings.Contains(result, "Subject: New submission on Testform") { + t.Error() + } + if !strings.Contains(result, "Testkey: Testvalue") { + t.Error() + } + if !strings.Contains(result, "Another Key: Test, ABC") { + t.Error() + } + }) +} \ No newline at end of file diff --git a/mailygo.go b/mailygo.go new file mode 100644 index 0000000..5ade801 --- /dev/null +++ b/mailygo.go @@ -0,0 +1,25 @@ +package main + +import ( + "log" + "net/http" + "strconv" +) + +var appConfig config + +func init() { + cfg, err := parseConfig() + if err != nil { + log.Fatal(err) + } + appConfig = cfg +} + +func main() { + if !checkRequiredConfig(appConfig) { + log.Fatal("Not all required configurations are set") + } + http.HandleFunc("/", FormHandler) + log.Fatal(http.ListenAndServe(":"+strconv.Itoa(appConfig.Port), nil)) +}