package gwv import ( "bytes" "crypto/tls" "fmt" "golang.org/x/net/http2" "io" "io/ioutil" "net" "net/http" "path/filepath" "regexp" "simonwaldherr.de/go/golibs/as" "simonwaldherr.de/go/golibs/file" "simonwaldherr.de/go/golibs/ssl" "strings" "sync" "time" ) type mimeCtrl int const ( AUTO mimeCtrl = iota HTML JSON ICON PLAIN REDIRECT PROXY DOWNLOAD MANUAL ) type handler func(http.ResponseWriter, *http.Request) (string, int) type HandlerWrapper struct { match *regexp.Regexp handler handler mime mimeCtrl rawre string } type WebServer struct { port int secureport int sslkey string sslcert string spdy bool routes []*HandlerWrapper timeout time.Duration handler404 handler handler500 handler WG sync.WaitGroup stop bool LogChan chan string } func (u *HandlerWrapper) String() string { return fmt.Sprintf( "{\n URL: %v\n Handler: %v\n}", u.match, u.handler, ) } func handlerify(re string, handler handler, mime mimeCtrl) *HandlerWrapper { match := regexp.MustCompile(re) return &HandlerWrapper{ match: match, handler: handler, mime: mime, rawre: re, } } func URL(re string, view handler, handler mimeCtrl) *HandlerWrapper { return handlerify(re, view, handler) } func Download(re string, view handler) *HandlerWrapper { return handlerify(re, view, DOWNLOAD) } var extensions = []string{ "", ".htm", ".html", ".shtml", } func StaticFiles(reqpath string, paths ...string) *HandlerWrapper { return handlerify(reqpath, func(rw http.ResponseWriter, req *http.Request) (string, int) { filename := req.URL.Path[len(reqpath):] for _, path := range paths { if strings.Count(path, "..") != 0 { return "", http.StatusNotFound } for _, ext := range extensions { if file.IsFile(filepath.Join(path, filename) + ext) { http.ServeFile(rw, req, filepath.Join(path, filename)+ext) return "", 0 } } } return "", http.StatusNotFound }, AUTO) } func Favicon(path string) *HandlerWrapper { data, err := file.Read(path) return handlerify("^/favicon.ico$", func(rw http.ResponseWriter, req *http.Request) (string, int) { if err != nil { return "", http.StatusNotFound } return data, http.StatusOK }, ICON) } func Redirect(path, destination string, code int) *HandlerWrapper { return handlerify(path, func(rw http.ResponseWriter, req *http.Request) (string, int) { return destination, code }, REDIRECT) } func Proxy(path, destination string) *HandlerWrapper { re := regexp.MustCompile(path) return handlerify(path, func(rw http.ResponseWriter, req *http.Request) (string, int) { httpClient := http.Client{} body, err := ioutil.ReadAll(req.Body) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return "", http.StatusInternalServerError } req.Body = ioutil.NopCloser(bytes.NewReader(body)) url := fmt.Sprintf("%s%s", destination, re.ReplaceAllString(req.RequestURI, "")) proxyReq, err := http.NewRequest(req.Method, url, bytes.NewReader(body)) proxyReq.Header = req.Header resp, err := httpClient.Do(proxyReq) if err != nil { http.Error(rw, err.Error(), http.StatusBadGateway) return "", http.StatusBadGateway } defer resp.Body.Close() for name, values := range resp.Header { rw.Header()[name] = values } rw.WriteHeader(resp.StatusCode) io.Copy(rw, resp.Body) return "", 0 }, PROXY) } func Robots(data string) *HandlerWrapper { return handlerify("^/robots.txt$", func(rw http.ResponseWriter, req *http.Request) (string, int) { return data, http.StatusOK }, PLAIN) } func Humans(data string) *HandlerWrapper { return handlerify("^/humans.txt$", func(rw http.ResponseWriter, req *http.Request) (string, int) { return data, http.StatusOK }, PLAIN) } func NewWebServer(port int, timeout time.Duration) *WebServer { return &WebServer{ port: port, routes: make([]*HandlerWrapper, 0), timeout: timeout, } } func (GWV *WebServer) InitLogChan() { GWV.LogChan = make(chan string, 128) } func (GWV *WebServer) ConfigSSL(port int, sslkey string, sslcert string, spdy bool) { GWV.secureport = port GWV.sslkey = sslkey GWV.sslcert = sslcert GWV.spdy = spdy } func (GWV *WebServer) URLhandler(patterns ...*HandlerWrapper) { for _, url := range patterns { GWV.routes = append(GWV.routes, url) } } func (GWV *WebServer) ServeHTTP(rw http.ResponseWriter, req *http.Request) { GWV.WG.Add(1) defer GWV.WG.Done() request := req.URL.Path rw.Header().Set("Server", "GWV") for _, route := range GWV.routes { matches := route.match.FindAllStringSubmatch(request, 1) if len(matches) > 0 { resp, status := route.handler(rw, req) switch status { case 0: return case 200, 201, 202, 418: GWV.handle200(rw, req, resp, route, status) return case 301, 302, 303, 307: http.Redirect(rw, req, resp, status) return case 400, 401, 403, 404, 405: GWV.handle404(rw, req, status) return case 500, 501, 502, 503: GWV.handle500(rw, req, status) return } } } GWV.handle404(rw, req, http.StatusNotFound) } func GenerateSSL(options map[string]string) error { return ssl.Generate(options) } func CheckSSL(certPath string, keyPath string) error { return ssl.Check(certPath, keyPath) } func (GWV *WebServer) Start() { GWV.WG.Add(1) defer func() { if r := recover(); r != nil { GWV.logChannelHandler(fmt.Sprint("Recovered in f", r)) } }() httpServer := http.Server{ Addr: ":" + as.String(GWV.port), Handler: GWV, ReadTimeout: GWV.timeout * time.Second, } go func() { var err error GWV.logChannelHandler(fmt.Sprint("Serving HTTP on PORT: ", GWV.port)) listener, err := net.Listen("tcp", httpServer.Addr) for !GWV.stop { err = httpServer.Serve(listener) GWV.extendedErrorHandler("can't start server:", err, true) } GWV.extendedErrorHandler("can't start server:", err, true) }() if GWV.secureport != 0 { if GWV.sslkey == "" || GWV.sslcert == "" || CheckSSL(GWV.sslcert, GWV.sslkey) != nil { options := map[string]string{} options["certPath"] = "ssl.cert" options["keyPath"] = "ssl.key" options["host"] = "*" err := GenerateSSL(options) GWV.extendedErrorHandler("can't generate ssl cert:", err, true) } cert, err := tls.LoadX509KeyPair(GWV.sslcert, GWV.sslkey) GWV.extendedErrorHandler("can't load key pair: ", err, true) httpsServer := http.Server{ Addr: ":" + as.String(GWV.secureport), Handler: GWV, ReadTimeout: GWV.timeout * time.Second, TLSConfig: &tls.Config{ Certificates: []tls.Certificate{cert}, MinVersion: tls.VersionTLS11, }, } go func() { var err error GWV.logChannelHandler(fmt.Sprint("Serving HTTPS on PORT: ", GWV.secureport)) if GWV.spdy { http2.ConfigureServer(&httpsServer, &http2.Server{}) } listener, err := tls.Listen("tcp", httpsServer.Addr, httpsServer.TLSConfig) for !GWV.stop { err = httpsServer.Serve(listener) GWV.extendedErrorHandler("can't start server:", err, true) } GWV.extendedErrorHandler("can't start server:", err, true) }() } } func (GWV *WebServer) Stop() { if !GWV.stop { GWV.stop = true GWV.WG.Done() } }
package gwv import ( "fmt" "log" ) func (GWV *WebServer) extendedErrorHandler(msg string, err error, pnc bool) { if err != nil { if GWV.LogChan != nil { GWV.LogChan <- fmt.Sprint(msg, err) } else { log.Print(msg, err) } if pnc { panic(err) } } } func (GWV *WebServer) logChannelHandler(msg string) { if GWV.LogChan != nil { GWV.LogChan <- msg } else { log.Print(msg) } }
package gwv import ( "encoding/json" "fmt" "io" "mime" "net/http" "path/filepath" ) func (GWV *WebServer) handle200(rw http.ResponseWriter, req *http.Request, resp string, route *HandlerWrapper, code int) { var err error switch route.mime { case HTML: rw.Header().Set("Content-Type", "text/html") break case PLAIN: rw.Header().Set("Content-Type", "text/plain") break case JSON: rw.Header().Set("Content-Type", "application/json") break case AUTO: if len(req.URL.Path) > len(route.rawre) { reqstr := req.URL.Path[len(route.rawre):] ctype := mime.TypeByExtension(filepath.Ext(reqstr)) rw.Header().Set("Content-Type", ctype) } else { rw.Header().Set("Content-Type", "text/plain") } break case MANUAL: return case ICON: rw.Header().Set("Content-Type", "image/x-icon") break case DOWNLOAD: rw.Header().Set("Content-Type", "application/octet-stream") rw.Header().Set("Content-Disposition", "attachment") break default: GWV.logChannelHandler(fmt.Sprint("Unknown handler type: ", route.mime)) break } rw.WriteHeader(code) switch route.mime { case JSON: err = json.NewEncoder(rw).Encode(map[string]string{ "message": resp, }) default: _, err = io.WriteString(rw, resp) } GWV.extendedErrorHandler("Error on WriteString to client: ", err, false) } func (GWV *WebServer) handle404(rw http.ResponseWriter, req *http.Request, code int) { var err error GWV.logChannelHandler(fmt.Sprint("404 on path:", req.URL.Path)) if GWV.handler404 != nil { resp, _ := GWV.handler404(rw, req) rw.WriteHeader(code) _, err = io.WriteString(rw, resp) GWV.extendedErrorHandler("Error on WriteString to client at 404:", err, false) return } http.NotFound(rw, req) return } func (GWV *WebServer) Handler404(fn handler) { GWV.handler404 = fn } func (GWV *WebServer) handle500(rw http.ResponseWriter, req *http.Request, code int) { var err error GWV.logChannelHandler(fmt.Sprint("500 on path:", req.URL.Path)) if GWV.handler500 != nil { resp, _ := GWV.handler500(rw, req) rw.WriteHeader(code) _, err = io.WriteString(rw, resp) GWV.extendedErrorHandler("Error on WriteString to client at 404:", err, false) return } http.Error(rw, "Internal Server Error", http.StatusInternalServerError) return } func (GWV *WebServer) Handler500(fn handler) { GWV.handler500 = fn }
package gwv import ( "encoding/json" "fmt" "io/ioutil" "net/http" "time" ) type Connections struct { clients map[chan string]bool clientips map[string]bool addClient chan chan string removeClient chan chan string Messages chan string } func initRealtimeHub() *Connections { var hub = &Connections{ clients: make(map[chan string]bool), clientips: make(map[string]bool), addClient: make(chan (chan string)), removeClient: make(chan (chan string)), Messages: make(chan string), } go func() { for { select { case s := <-hub.addClient: hub.clients[s] = true case s := <-hub.removeClient: delete(hub.clients, s) case msg := <-hub.Messages: for s := range hub.clients { s <- msg } } } }() return hub } func (GWV *WebServer) InitRealtimeHub() *Connections { var hub = &Connections{ clients: make(map[chan string]bool), clientips: make(map[string]bool), addClient: make(chan (chan string)), removeClient: make(chan (chan string)), Messages: make(chan string), } go func() { for { select { case s := <-hub.addClient: hub.clients[s] = true GWV.logChannelHandler("Added new client") case s := <-hub.removeClient: delete(hub.clients, s) GWV.logChannelHandler("Removed client") case msg := <-hub.Messages: for s := range hub.clients { s <- msg } GWV.logChannelHandler(fmt.Sprintf("Broadcast \"%v\" to %d clients", msg, len(hub.clients))) } } }() return hub } func (hub *Connections) ClientDetails() (int, []string) { var l []string var i int for v, b := range hub.clientips { if b { l = append(l, v) i++ } } return i, l } func SSE(re string, hub *Connections) *HandlerWrapper { return handlerify(re, func(rw http.ResponseWriter, req *http.Request) (string, int) { f, ok := rw.(http.Flusher) if !ok { http.Error(rw, "Streaming not supported!", http.StatusInternalServerError) return "", http.StatusNotFound } var ch = make(chan string, 16) hub.addClient <- ch hub.clientips[req.RemoteAddr] = true defer func() { hub.removeClient <- ch hub.clientips[req.RemoteAddr] = false }() notify := rw.(http.CloseNotifier).CloseNotify() rw.Header().Set("Content-Type", "text/event-stream") rw.Header().Set("Cache-Control", "no-cache") rw.Header().Set("Connection", "keep-alive") for i := 0; i < 1440; { select { case msg := <-ch: jsonData, _ := json.Marshal(msg) str := string(jsonData) fmt.Fprintf(rw, "data: {\"str\": %s, \"time\": \"%v\"}\n\n", str, time.Now()) f.Flush() case <-time.After(time.Second * 45): fmt.Fprintf(rw, "data: {\"str\": \"No Data\"}\n\n") f.Flush() i++ case <-notify: f.Flush() i = 1440 hub.removeClient <- ch } } return "", http.StatusOK }, JSON) } var hubArray = make(map[string]*Connections) func SSEA(re string) *HandlerWrapper { return handlerify(re, func(rw http.ResponseWriter, req *http.Request) (string, int) { requrl := fmt.Sprint(req.URL) if req.Method != "GET" { if req.Method == "POST" { if _, ok := hubArray[requrl]; ok { str, err := ioutil.ReadAll(req.Body) if err == nil { hubArray[requrl].Messages <- string(str) return "", http.StatusAccepted } return "", http.StatusBadRequest } } return "", http.StatusMethodNotAllowed } f, ok := rw.(http.Flusher) if !ok { http.Error(rw, "Streaming not supported!", http.StatusInternalServerError) return "", http.StatusNotFound } if _, ok := hubArray[requrl]; !ok { hubArray[requrl] = initRealtimeHub() } var ch = make(chan string, 16) hubArray[requrl].addClient <- ch hubArray[requrl].clientips[req.RemoteAddr] = true defer func() { hubArray[requrl].removeClient <- ch hubArray[requrl].clientips[req.RemoteAddr] = false }() notify := rw.(http.CloseNotifier).CloseNotify() rw.Header().Set("Content-Type", "text/event-stream") rw.Header().Set("Cache-Control", "no-cache") rw.Header().Set("Connection", "keep-alive") for i := 0; i < 1440; { select { case msg := <-ch: jsonData, _ := json.Marshal(msg) str := string(jsonData) fmt.Fprintf(rw, "data: {\"str\": %s, \"time\": \"%v\"}\n\n", str, time.Now()) f.Flush() case <-time.After(time.Second * 45): fmt.Fprintf(rw, "data: {\"str\": \"No Data\"}\n\n") f.Flush() i++ case <-notify: f.Flush() i = 1440 hubArray[requrl].removeClient <- ch } } return "", http.StatusOK }, JSON) }
package gwv import ( "crypto/aes" "crypto/cipher" "crypto/rand" "encoding/base64" "encoding/json" "net/http" "strings" ) type Cryptor struct { SecretKey []byte CookieName string MakeCookieFunc MakeCookieFunc } func NewSimpleCryptor(secretKey []byte, cookieName string) *Cryptor { return &Cryptor{ SecretKey: secretKey, CookieName: cookieName, MakeCookieFunc: MakeCookieFunc(func(w http.ResponseWriter, r *http.Request) *http.Cookie { return &http.Cookie{ Name: cookieName, Path: "/", MaxAge: 360000, HttpOnly: false, } }), } } // makes an empty cookie, no value type MakeCookieFunc func(w http.ResponseWriter, r *http.Request) *http.Cookie // seralize and encrypt v and write to cookie func (sc *Cryptor) Write(v interface{}, w http.ResponseWriter, r *http.Request) error { // marshall data b, err := json.Marshal(v) if err != nil { return err } // make init vector iv := make([]byte, 16) _, err = rand.Read(iv) if err != nil { return err } block, err := aes.NewCipher(sc.SecretKey) if err != nil { return err } cfb := cipher.NewCFBEncrypter(block, iv) ciphertext := make([]byte, len(b)) cfb.XORKeyStream(ciphertext, b) cookie := sc.MakeCookieFunc(w, r) cookie.Value = base64.RawURLEncoding.EncodeToString(iv) + "," + base64.RawURLEncoding.EncodeToString(ciphertext) http.SetCookie(w, cookie) return nil } func (sc *Cryptor) Read(v interface{}, r *http.Request) error { c, err := r.Cookie(sc.CookieName) if err != nil { return err } cookieValueParts := strings.Split(c.Value, ",") // extract init vector iv, err := base64.RawURLEncoding.DecodeString(cookieValueParts[0]) if err != nil { return err } // extract value b, err := base64.RawURLEncoding.DecodeString(cookieValueParts[1]) if err != nil { return err } block, err := aes.NewCipher(sc.SecretKey) if err != nil { return err } cfb := cipher.NewCFBDecrypter(block, iv) plaintext := make([]byte, len(b)) cfb.XORKeyStream(plaintext, b) err = json.Unmarshal(plaintext, v) if err != nil { return err } return nil } // remove cookie (effectively destroying the session) func (sc *Cryptor) Clear(w http.ResponseWriter, r *http.Request) { c := sc.MakeCookieFunc(w, r) c.MaxAge = -1 http.SetCookie(w, c) }