diff --git a/Makefile b/Makefile index 2a579e9..5bc3657 100644 --- a/Makefile +++ b/Makefile @@ -11,6 +11,8 @@ release: release-deps gox deps: + go get github.com/ian-kent/gotcha/... + go get github.com/ian-kent/go-log/... go get github.com/jteeuwen/go-bindata/... go get labix.org/v2/mgo diff --git a/mailhog/http/api/v1.go b/mailhog/http/api/v1.go index c91935a..400e675 100644 --- a/mailhog/http/api/v1.go +++ b/mailhog/http/api/v1.go @@ -4,28 +4,25 @@ import ( "log" "encoding/json" "fmt" - "net" - "net/http" "net/smtp" "strconv" - "bufio" "strings" "github.com/ian-kent/Go-MailHog/mailhog/data" "github.com/ian-kent/Go-MailHog/mailhog/config" "github.com/ian-kent/Go-MailHog/mailhog/storage" - "github.com/ian-kent/Go-MailHog/mailhog/http/router" + gotcha "github.com/ian-kent/gotcha/app" + "github.com/ian-kent/gotcha/http" ) type APIv1 struct { config *config.Config - exitChannel chan int - server *http.Server eventlisteners []*EventListener + app *gotcha.App } type EventListener struct { - conn net.Conn - bufrw *bufio.ReadWriter + session *http.Session + ch chan []byte } type ReleaseConfig struct { @@ -34,25 +31,24 @@ type ReleaseConfig struct { Port string } -func CreateAPIv1(exitCh chan int, conf *config.Config, server *http.Server) *APIv1 { +func CreateAPIv1(conf *config.Config, app *gotcha.App) *APIv1 { log.Println("Creating API v1") apiv1 := &APIv1{ config: conf, - exitChannel: exitCh, - server: server, eventlisteners: make([]*EventListener, 0), + app: app, } - r := server.Handler.(*router.Router) + r := app.Router - r.Get("^/api/v1/messages/?$", apiv1.messages) - r.Delete("^/api/v1/messages/?$", apiv1.delete_all) - r.Get("^/api/v1/messages/([0-9a-f]+)/?$", apiv1.message) - r.Delete("^/api/v1/messages/([0-9a-f]+)/?$", apiv1.delete_one) - r.Get("^/api/v1/messages/([0-9a-f]+)/download/?$", apiv1.download) - r.Get("^/api/v1/messages/([0-9a-f]+)/mime/part/(\\d+)/download/?$", apiv1.download_part) - r.Post("^/api/v1/messages/([0-9a-f]+)/release/?$", apiv1.release_one) - r.Get("^/api/v1/events/?$", apiv1.eventstream) + r.Get("/api/v1/messages/?", apiv1.messages) + r.Delete("/api/v1/messages/?", apiv1.delete_all) + r.Get("/api/v1/messages/(?P[0-9a-f]+)/?", apiv1.message) + r.Delete("/api/v1/messages/(?P[0-9a-f]+)/?", apiv1.delete_one) + r.Get("/api/v1/messages/(?P[0-9a-f]+)/download/?", apiv1.download) + r.Get("/api/v1/messages/(?P[0-9a-f]+)/mime/part/(\\d+)/download/?", apiv1.download_part) + r.Post("/api/v1/messages/(?P[0-9a-f]+)/release/?", apiv1.release_one) + r.Get("/api/v1/events/?", apiv1.eventstream) go func() { for { @@ -63,8 +59,6 @@ func CreateAPIv1(exitCh chan int, conf *config.Config, server *http.Server) *API json := string(bytes) log.Printf("Sending content: %s\n", json) apiv1.broadcast(json) - case <- exitCh: - break; } } }() @@ -75,7 +69,7 @@ func CreateAPIv1(exitCh chan int, conf *config.Config, server *http.Server) *API func (apiv1 *APIv1) broadcast(json string) { log.Println("[APIv1] BROADCAST /api/v1/events") for _, l := range apiv1.eventlisteners { - log.Printf("Sending to connection: %s\n", l.conn.RemoteAddr()) + log.Printf("Sending to connection: %s\n", l.session.Request.RemoteAddr) lines := strings.Split(json, "\n") data := "" @@ -85,49 +79,32 @@ func (apiv1 *APIv1) broadcast(json string) { data += "\n" size := fmt.Sprintf("%X", len(data) + 1) - l.bufrw.Write([]byte(size + "\r\n")) + l.ch <- []byte(size + "\r\n") lines = strings.Split(data, "\n") for _, ln := range lines { - l.bufrw.Write([]byte(ln + "\n")) - } - _, err := l.bufrw.Write([]byte("\r\n")) - - if err != nil { - log.Printf("Error writing to connection: %s\n", err) - l.conn.Close() - // TODO remove from array - } - - err = l.bufrw.Flush() - if err != nil { - log.Printf("Error flushing buffer: %s\n", err) - l.conn.Close() - // TODO remove from array + l.ch <- []byte(ln + "\n") } + l.ch <- []byte("\r\n") } } -func (apiv1 *APIv1) eventstream(w http.ResponseWriter, r *http.Request, route *router.Route) { +func (apiv1 *APIv1) eventstream(session *http.Session) { log.Println("[APIv1] GET /api/v1/events") - w.Header().Set("Content-Type", "text/event-stream") - w.Header().Set("Cache-Control", "no-cache") - w.Header().Set("Connection", "keep-alive") - w.Write([]byte("\n\n")) - hj, ok := w.(http.Hijacker) - if !ok { - log.Println("[APIv1] Connection hijack failed") - return - } - conn, bufrw, err := hj.Hijack() - if err != nil { - log.Println("[APIv1] Connection hijack failed") - return - } - apiv1.eventlisteners = append(apiv1.eventlisteners, &EventListener{conn, bufrw}) + + apiv1.eventlisteners = append(apiv1.eventlisteners, &EventListener{ + session, + session.Response.Chunked(), + }) + + session.Response.Headers.Add("Content-Type", "text/event-stream") + session.Response.Headers.Add("Cache-Control", "no-cache") + session.Response.Headers.Add("Connection", "keep-alive") + session.Response.Write([]byte("\n\n")) + session.Response.Send() } -func (apiv1 *APIv1) messages(w http.ResponseWriter, r *http.Request, route *router.Route) { +func (apiv1 *APIv1) messages(session *http.Session) { log.Println("[APIv1] GET /api/v1/messages") // TODO start, limit @@ -135,122 +112,118 @@ func (apiv1 *APIv1) messages(w http.ResponseWriter, r *http.Request, route *rout case *storage.MongoDB: messages, _ := apiv1.config.Storage.(*storage.MongoDB).List(0, 1000) bytes, _ := json.Marshal(messages) - w.Header().Set("Content-Type", "text/json") - w.Write(bytes) + session.Response.Headers.Add("Content-Type", "text/json") + session.Response.Write(bytes) case *storage.Memory: messages, _ := apiv1.config.Storage.(*storage.Memory).List(0, 1000) bytes, _ := json.Marshal(messages) - w.Header().Set("Content-Type", "text/json") - w.Write(bytes) + session.Response.Headers.Add("Content-Type", "text/json") + session.Response.Write(bytes) default: - w.WriteHeader(500) + session.Response.Status = 500 } } -func (apiv1 *APIv1) message(w http.ResponseWriter, r *http.Request, route *router.Route) { - - match := route.Pattern.FindStringSubmatch(r.URL.Path) - id := match[1] +func (apiv1 *APIv1) message(session *http.Session) { + id := session.Stash["id"].(string) log.Printf("[APIv1] GET /api/v1/messages/%s\n", id) switch apiv1.config.Storage.(type) { case *storage.MongoDB: message, _ := apiv1.config.Storage.(*storage.MongoDB).Load(id) bytes, _ := json.Marshal(message) - w.Header().Set("Content-Type", "text/json") - w.Write(bytes) + session.Response.Headers.Add("Content-Type", "text/json") + session.Response.Write(bytes) case *storage.Memory: message, _ := apiv1.config.Storage.(*storage.Memory).Load(id) bytes, _ := json.Marshal(message) - w.Header().Set("Content-Type", "text/json") - w.Write(bytes) + session.Response.Headers.Add("Content-Type", "text/json") + session.Response.Write(bytes) default: - w.WriteHeader(500) + session.Response.Status = 500 } } -func (apiv1 *APIv1) download(w http.ResponseWriter, r *http.Request, route *router.Route) { - match := route.Pattern.FindStringSubmatch(r.URL.Path) - id := match[1] - log.Printf("[APIv1] GET /api/v1/messages/%s/download\n", id) +func (apiv1 *APIv1) download(session *http.Session) { + id := session.Stash["id"].(string) + log.Printf("[APIv1] GET /api/v1/messages/%s\n", id) - w.Header().Set("Content-Type", "message/rfc822") - w.Header().Set("Content-Disposition", "attachment; filename=\"" + id + ".eml\"") + session.Response.Headers.Add("Content-Type", "message/rfc822") + session.Response.Headers.Add("Content-Disposition", "attachment; filename=\"" + id + ".eml\"") switch apiv1.config.Storage.(type) { case *storage.MongoDB: message, _ := apiv1.config.Storage.(*storage.MongoDB).Load(id) for h, l := range message.Content.Headers { for _, v := range l { - w.Write([]byte(h + ": " + v + "\r\n")) + session.Response.Write([]byte(h + ": " + v + "\r\n")) } } - w.Write([]byte("\r\n" + message.Content.Body)) + session.Response.Write([]byte("\r\n" + message.Content.Body)) case *storage.Memory: message, _ := apiv1.config.Storage.(*storage.Memory).Load(id) for h, l := range message.Content.Headers { for _, v := range l { - w.Write([]byte(h + ": " + v + "\r\n")) + session.Response.Write([]byte(h + ": " + v + "\r\n")) } } - w.Write([]byte("\r\n" + message.Content.Body)) + session.Response.Write([]byte("\r\n" + message.Content.Body)) default: - w.WriteHeader(500) + session.Response.Status = 500 } } -func (apiv1 *APIv1) download_part(w http.ResponseWriter, r *http.Request, route *router.Route) { - match := route.Pattern.FindStringSubmatch(r.URL.Path) - id := match[1] - part, _ := strconv.Atoi(match[2]) +func (apiv1 *APIv1) download_part(session *http.Session) { + id := session.Stash["id"].(string) + part, _ := strconv.Atoi(session.Stash["part"].(string)) log.Printf("[APIv1] GET /api/v1/messages/%s/mime/part/%d/download\n", id, part) // TODO extension from content-type? - w.Header().Set("Content-Disposition", "attachment; filename=\"" + id + "-part-" + match[2] + "\"") + session.Response.Headers.Add("Content-Disposition", "attachment; filename=\"" + id + "-part-" + strconv.Itoa(part) + "\"") switch apiv1.config.Storage.(type) { case *storage.MongoDB: message, _ := apiv1.config.Storage.(*storage.MongoDB).Load(id) for h, l := range message.MIME.Parts[part].Headers { for _, v := range l { - w.Header().Set(h, v) + session.Response.Headers.Add(h, v) } } - w.Write([]byte("\r\n" + message.MIME.Parts[part].Body)) + session.Response.Write([]byte("\r\n" + message.MIME.Parts[part].Body)) case *storage.Memory: message, _ := apiv1.config.Storage.(*storage.Memory).Load(id) for h, l := range message.MIME.Parts[part].Headers { for _, v := range l { - w.Header().Set(h, v) + session.Response.Headers.Add(h, v) } } - w.Write([]byte("\r\n" + message.MIME.Parts[part].Body)) + session.Response.Write([]byte("\r\n" + message.MIME.Parts[part].Body)) default: - w.WriteHeader(500) + session.Response.Status = 500 } } -func (apiv1 *APIv1) delete_all(w http.ResponseWriter, r *http.Request, route *router.Route) { +func (apiv1 *APIv1) delete_all(session *http.Session) { log.Println("[APIv1] POST /api/v1/messages") - w.Header().Set("Content-Type", "text/json") + session.Response.Headers.Add("Content-Type", "text/json") switch apiv1.config.Storage.(type) { case *storage.MongoDB: apiv1.config.Storage.(*storage.MongoDB).DeleteAll() case *storage.Memory: apiv1.config.Storage.(*storage.Memory).DeleteAll() default: - w.WriteHeader(500) + session.Response.Status = 500 + return } } -func (apiv1 *APIv1) release_one(w http.ResponseWriter, r *http.Request, route *router.Route) { - match := route.Pattern.FindStringSubmatch(r.URL.Path) - id := match[1] +func (apiv1 *APIv1) release_one(session *http.Session) { + id := session.Stash["id"].(string) log.Printf("[APIv1] POST /api/v1/messages/%s/release\n", id) - w.Header().Set("Content-Type", "text/json") + session.Response.Headers.Add("Content-Type", "text/json") var msg = &data.Message{} switch apiv1.config.Storage.(type) { case *storage.MongoDB: @@ -258,16 +231,17 @@ func (apiv1 *APIv1) release_one(w http.ResponseWriter, r *http.Request, route *r case *storage.Memory: msg, _ = apiv1.config.Storage.(*storage.Memory).Load(id) default: - w.WriteHeader(500) + session.Response.Status = 500 + return } - decoder := json.NewDecoder(r.Body) + decoder := json.NewDecoder(session.Request.Body()) var cfg ReleaseConfig err := decoder.Decode(&cfg) if err != nil { log.Printf("Error decoding request body: %s", err) - w.WriteHeader(500) - w.Write([]byte("Error decoding request body")) + session.Response.Status = 500 + session.Response.Write([]byte("Error decoding request body")) return } @@ -285,24 +259,23 @@ func (apiv1 *APIv1) release_one(w http.ResponseWriter, r *http.Request, route *r err = smtp.SendMail(cfg.Host + ":" + cfg.Port, nil, "nobody@" + apiv1.config.Hostname, []string{cfg.Email}, bytes) if err != nil { log.Printf("Failed to release message: %s", err) - w.WriteHeader(500) + session.Response.Status = 500 return } log.Printf("Message released successfully") } -func (apiv1 *APIv1) delete_one(w http.ResponseWriter, r *http.Request, route *router.Route) { - match := route.Pattern.FindStringSubmatch(r.URL.Path) - id := match[1] +func (apiv1 *APIv1) delete_one(session *http.Session) { + id := session.Stash["id"].(string) log.Printf("[APIv1] POST /api/v1/messages/%s/delete\n", id) - w.Header().Set("Content-Type", "text/json") + session.Response.Headers.Add("Content-Type", "text/json") switch apiv1.config.Storage.(type) { case *storage.MongoDB: apiv1.config.Storage.(*storage.MongoDB).DeleteOne(id) case *storage.Memory: apiv1.config.Storage.(*storage.Memory).DeleteOne(id) default: - w.WriteHeader(500) + session.Response.Status = 500 } } diff --git a/mailhog/http/router/regexp.go b/mailhog/http/router/regexp.go deleted file mode 100644 index dcf06c2..0000000 --- a/mailhog/http/router/regexp.go +++ /dev/null @@ -1,79 +0,0 @@ -package router; - -import ( - "regexp" - "net/http" -) - -// http://stackoverflow.com/questions/6564558/wildcards-in-the-pattern-for-http-handlefunc - -type Route struct { - Methods map[string]int - Pattern *regexp.Regexp - Handler HandlerFunc -} - -type HandlerFunc func(http.ResponseWriter, *http.Request, *Route) - -//type Handler http.Handler -func (f HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request, route *Route) { - f(w, r, route) -} - -type Router struct { - routes []*Route -} - -func (h *Router) Get(pattern string, handler HandlerFunc) { - h.Handler([]string{"GET"}, regexp.MustCompile(pattern), handler) -} - -func (h *Router) Post(pattern string, handler HandlerFunc) { - h.Handler([]string{"POST"}, regexp.MustCompile(pattern), handler) -} - -func (h *Router) Put(pattern string, handler HandlerFunc) { - h.Handler([]string{"PUT"}, regexp.MustCompile(pattern), handler) -} - -func (h *Router) Delete(pattern string, handler HandlerFunc) { - h.Handler([]string{"DELETE"}, regexp.MustCompile(pattern), handler) -} - -func (h *Router) Patch(pattern string, handler HandlerFunc) { - h.Handler([]string{"PATCH"}, regexp.MustCompile(pattern), handler) -} - -func (h *Router) Options(pattern string, handler HandlerFunc) { - h.Handler([]string{"OPTIONS"}, regexp.MustCompile(pattern), handler) -} - -func (h *Router) Handler(methods []string, pattern *regexp.Regexp, handler HandlerFunc) { - m := make(map[string]int,0) - for _, v := range methods { - m[v] = 1 - } - h.routes = append(h.routes, &Route{m, pattern, handler}) -} - -func (h *Router) HandleFunc(methods []string, pattern *regexp.Regexp, handler func(http.ResponseWriter, *http.Request, *Route)) { - m := make(map[string]int,0) - for _, v := range methods { - m[v] = 1 - } - h.routes = append(h.routes, &Route{m, pattern, HandlerFunc(handler)}) -} - -func (h *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) { - for _, route := range h.routes { - if route.Pattern.MatchString(r.URL.Path) { - _, ok := route.Methods[r.Method] - if ok { - route.Handler.ServeHTTP(w, r, route) - return - } - } - } - // no pattern matched; send 404 response - http.NotFound(w, r) -} diff --git a/mailhog/http/server.go b/mailhog/http/server.go index e331b48..42fe721 100644 --- a/mailhog/http/server.go +++ b/mailhog/http/server.go @@ -1,86 +1,14 @@ package http import ( - "net/http" - "strings" - "log" - "github.com/ian-kent/Go-MailHog/mailhog/config" - "github.com/ian-kent/Go-MailHog/mailhog/http/api" - "github.com/ian-kent/Go-MailHog/mailhog/http/router" + "github.com/ian-kent/gotcha/http" + "html/template" ) -var exitChannel chan int -var cfg *config.Config +func Index(session *http.Session) { + html, _ := session.RenderTemplate("index.html") -// TODO clean this mess up - -func web_exit(w http.ResponseWriter, r *http.Request, route *router.Route) { - web_headers(w) - w.Write([]byte("Exiting MailHog!")) - exitChannel <- 1 -} - -func web_index(w http.ResponseWriter, r *http.Request, route *router.Route) { - web_headers(w) - data, _ := cfg.Assets("assets/templates/index.html") - w.Write([]byte(web_render(string(data)))) -} - -func web_static(w http.ResponseWriter, r *http.Request, route *router.Route) { - match := route.Pattern.FindStringSubmatch(r.URL.Path) - file := match[1] - log.Printf("[HTTP] GET %s\n", file) - - if strings.HasSuffix(file, ".gif") { - w.Header().Set("Content-Type", "image/gif") - } else if strings.HasSuffix(file, ".png") { - w.Header().Set("Content-Type", "image/png") - } else if strings.HasSuffix(file, ".js") { - w.Header().Set("Content-Type", "text/javascript") - } else { - w.Header().Set("Content-Type", "text/plain") - } - - data, err := cfg.Assets("assets" + file) - if err != nil { - w.WriteHeader(404) - return - } - - w.Write(data) -} - -func web_render(content string) string { - data, _ := cfg.Assets("assets/templates/layout.html") - layout := string(data) - html := strings.Replace(layout, "<%= content %>", content, -1) - // TODO clean this up - html = strings.Replace(html, "<%= config[Hostname] %>", cfg.Hostname, -1) - return html -} - -func web_headers(w http.ResponseWriter) { - w.Header().Set("Content-Type", "text/html") -} - -func Start(exitCh chan int, conf *config.Config) { - exitChannel = exitCh - cfg = conf - - r := &router.Router{} - server := &http.Server{ - Addr: conf.HTTPBindAddr, - Handler: r, - } - - r.Get("^/exit/?$", web_exit) - r.Get("^(/js/controllers.js)$", web_static) - r.Get("^(/images/hog.png)$", web_static) - r.Get("^(/images/github.png)$", web_static) - r.Get("^(/images/ajax-loader.gif)$", web_static) - r.Get("^/$", web_index) - - api.CreateAPIv1(exitCh, conf, server) - - server.ListenAndServe() + session.Stash["Page"] = "Browse" + session.Stash["Content"] = template.HTML(html) + session.Render("layout.html") } diff --git a/main.go b/main.go index f9b6f46..e3c66c5 100644 --- a/main.go +++ b/main.go @@ -3,12 +3,14 @@ package main import ( "flag" "github.com/ian-kent/Go-MailHog/mailhog/config" - "github.com/ian-kent/Go-MailHog/mailhog/http" "github.com/ian-kent/Go-MailHog/mailhog/smtp" + "github.com/ian-kent/Go-MailHog/mailhog/http/api" "github.com/ian-kent/Go-MailHog/mailhog/storage" - "log" + gotcha "github.com/ian-kent/gotcha/app" + "github.com/ian-kent/go-log/log" "net" "os" + mhhttp "github.com/ian-kent/Go-MailHog/mailhog/http" ) var conf *config.Config @@ -73,8 +75,26 @@ func main() { } func web_listen() { - log.Printf("[HTTP] Binding to address: %s\n", conf.HTTPBindAddr) - http.Start(exitCh, conf) + log.Info("[HTTP] Binding to address: %s", conf.HTTPBindAddr) + + var app = gotcha.Create(Asset) + app.Config.Listen = conf.HTTPBindAddr + + r := app.Router + + r.Get("/images/(?P.*)", r.Static("assets/images/{{file}}")) + r.Get("/js/(?P.*)", r.Static("assets/js/{{file}}")) + r.Get("/", mhhttp.Index) + + api.CreateAPIv1(conf, app) + + app.Config.LeftDelim = ">>"; + app.Config.RightDelim = "<<"; + + app.Start() + + <-make(chan int) + exitCh<-1 } func smtp_listen() *net.TCPListener {