// BufferPost buffers the body of the POST request into the context. // // Params: // // Returns: // - []byte with the content of the request. func BufferPost(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { req := c.Get("http.Request", nil).(*http.Request) var b bytes.Buffer _, err := io.Copy(&b, req.Body) c.Logf("info", "Received POST: %s", b.Bytes()) return b.Bytes(), err }
// Serve creates a new Cookoo web server. // // Important details: // // - A URIPathResolver is used for resolving request names. // - The following datasources are added to the Context: // * url: A URLDatasource (Provides access to parts of the URL) // * path: A PathDatasource (Provides access to parts of a path. E.g. "/foo/bar") // * query: A QueryParameterDatasource (Provides access to URL query parameters.) // * post: A FormValuesDatasource (Provides access to form data or the body of a request.) // - The following context variables are set: // * http.Request: A pointer to the http.Request object // * http.ResponseWriter: The response writer. // * server.Address: The server's address and port (NOT ALWAYS PRESENT) // - The handler includes logic to redirect "not found" errors to a path named "@404" if present. // // Context Params: // // - server.Address: If this key exists in the context, it will be used to determine the host/port the // server runes on. EXPERIMENTAL. Default is ":8080". // // Example: // // package main // // import ( // //This is the path to Cookoo // "github.com/Masterminds/cookoo" // "github.com/Masterminds/cookoo/web" // "fmt" // ) // // func main() { // // Build a new Cookoo app. // registry, router, context := cookoo.Cookoo() // // // Fill the registry. // registry.Route("GET /", "The index").Does(web.Flush, "example"). // Using("content").WithDefault("Hello World") // // // Create a server // web.Serve(reg, router, cookoo.SyncContext(cxt)) // } // // Note that we synchronize the context before passing it into Serve(). This // is optional because each handler gets its own copy of the context already. // However, if commands pass the context to goroutines, the context ought to be // synchronized to avoid race conditions. // // Note that copies of the context are not synchronized with each other. // So by declaring the context synchronized here, you // are not therefore synchronizing across handlers. func Serve(reg *cookoo.Registry, router *cookoo.Router, cxt cookoo.Context) { addr := cxt.Get("server.Address", ":8080").(string) handler := NewCookooHandler(reg, router, cxt) // MPB: I dont think there's any real point in having a multiplexer in // this particular case. The Cookoo handler is mux enough. // // Note that we can always use Cookoo with the built-in multiplexer. It // just doesn't make sense if Cookoo's the only handler on the app. //http.Handle("/", handler) server := &http.Server{Addr: addr} // Instead of mux, set a single default handler. // What we might be losing: // - Handling of non-conforming paths. server.Handler = handler go handleSignals(router, cxt, server) err := server.ListenAndServe() //err := http.ListenAndServe(addr, nil) if err != nil { cxt.Logf("error", "Caught error while serving: %s", err) if router.HasRoute("@crash") { router.HandleRequest("@crash", cxt, false) } } }
/** * Perform authentication. * * Params: * - realm (string): The name of the realm. (Default: "web") * - datasource (string): The name of the datasource that should be used to authenticate. * This datasource must be an `auth.UserDatasource`. (Default: "auth.UserDatasource") * * Context: * - http.Request (*http.Request): The HTTP request. This is usually placed into the * context for you. * - http.ResponseWriter (http.ResponseWriter): The response. This is usually placed * into the context for you. * * Datasource: * - An auth.UserDatasource. By default, this will look for a datasource named * "auth.UserDatasource". This can be overridden by the `datasource` param. * * Returns: * - True if the user authenticated. If not, this will send a 401 and then stop * the current chain. */ func Basic(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { realm := p.Get("realm", "web").(string) dsName := p.Get("datasource", "auth.UserDatasource").(string) req := c.Get("http.Request", nil).(*http.Request) res := c.Get("http.ResponseWriter", nil).(http.ResponseWriter) ds := c.Datasource(dsName).(UserDatasource) authz := strings.TrimSpace(req.Header.Get("Authorization")) if len(authz) == 0 || !strings.Contains(authz, "Basic ") { return sendUnauthorized(realm, res) } user, pass, err := parseBasicString(authz) if err != nil { c.Logf("info", "Basic authentication parsing failed: %s", err) return sendUnauthorized(realm, res) } ok, err := ds.AuthUser(user, pass) if !ok { if err != nil { c.Logf("info", "Basic authentication caused an error: %s", err) } return sendUnauthorized(realm, res) } return ok, err }
func (r *URIPathResolver) subtreeMatch(c cookoo.Context, pathName, pattern string) bool { if pattern == "**" { return true } // Find out how many slashes we have. countSlash := strings.Count(pattern, "/") // '**' matches anything. if countSlash == 0 { c.Logf("warn", "Illegal pattern: %s", pattern) return false } // Add 2 for verb plus trailer. parts := strings.SplitN(pathName, "/", countSlash+1) prefix := strings.Join(parts[0:countSlash], "/") subpattern := strings.Replace(pattern, "/**", "", -1) if ok, err := path.Match(subpattern, prefix); ok && err == nil { return true } else if err != nil { c.Logf("warn", "Parsing path `%s` gave error: %s", err) } return false }
// handleSignals traps kill and interrupt signals and runs shutdown(). func handleSignals(router *cookoo.Router, cxt cookoo.Context, server *http.Server) { sig := make(chan os.Signal, 1) signal.Notify(sig, os.Kill, os.Interrupt) s := <-sig cxt.Logf("info", "Received signal %s. Shutting down.", s) // Not particularly useful on its own. // server.SetKeepAlivesEnabled(false) // TODO: Implement graceful shutdowns. shutdown(router, cxt) os.Exit(0) }
// sendHistory sends the accumulated history to the writer. func sendHistory(c cookoo.Context, writer ResponseWriterFlusher, data [][]byte) (int, error) { c.Logf("info", "Sending history.") var i int var d []byte for i, d = range data { _, err := writer.Write(d) if err != nil { c.Logf("warn", "Failed to write history message: %s", err) return i + 1, nil } writer.Flush() } return i + 1, nil }
// ReplayHistory sends back the history to a subscriber. // // This should be called before the client goes into active listening. // // Params: // - topic (string): The topic to fetch. // // Returns: // - int: The number of history messages sent to the client. func ReplayHistory(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { req := c.Get("http.Request", nil).(*http.Request) res := c.Get("http.ResponseWriter", nil).(ResponseWriterFlusher) medium, _ := getMedium(c) name := p.Get("topic", "").(string) // This does not manage topics. If there is no topic set, we silently fail. if len(name) == 0 { c.Log("info", "No topic name given to ReplayHistory.") return 0, nil } top, ok := medium.Topic(name) if !ok { c.Logf("info", "No topic named %s exists yet. No history replayed.", name) return 0, nil } topic, ok := top.(HistoriedTopic) if !ok { c.Logf("info", "No history for topic %s.", name) res.Header().Add(XHistoryEnabled, "False") return 0, nil } res.Header().Add(XHistoryEnabled, "True") since := req.Header.Get(XHistorySince) max := req.Header.Get(XHistoryLength) // maxLen can be used either on its own or paired with X-History-Since. maxLen := 0 if len(max) > 0 { m, err := parseHistLen(max) if err != nil { c.Logf("info", "failed to parse X-History-Length %s", max) } else { maxLen = m } } if len(since) > 0 { ts, err := parseSince(since) if err != nil { c.Logf("warn", "Failed to parse X-History-Since field %s: %s", since, err) return 0, nil } toSend := topic.Since(ts) // If maxLen is also set, we trim the list by sending the newest. ls := len(toSend) if maxLen > 0 && ls > maxLen { offset := ls - maxLen - 1 toSend = toSend[offset:] } return sendHistory(c, res, toSend) } else if maxLen > 0 { toSend := topic.Last(maxLen) return sendHistory(c, res, toSend) } return 0, nil }
// ServeTLS does the same as Serve, but with SSL support. // // If `server.Address` is not found in the context, the default address is // `:4433`. // // Neither certFile nor keyFile are stored in the context. These values are // considered to be security sensitive. func ServeTLS(reg *cookoo.Registry, router *cookoo.Router, cxt cookoo.Context, certFile, keyFile string) { addr := cxt.Get("server.Address", ":4433").(string) server := &http.Server{Addr: addr} server.Handler = NewCookooHandler(reg, router, cxt) go handleSignals(router, cxt, server) err := server.ListenAndServeTLS(certFile, keyFile) if err != nil { cxt.Logf("error", "Caught error while serving: %s", err) if router.HasRoute("@crash") { router.HandleRequest("@crash", cxt, false) } } }
// Publish sends a new message to a topic. // // Params: // - topic (string): The topic to send to. // - message ([]byte): The message to send. // - withHistory (bool): Turn on history. Default is true. This only takes // effect when the channel is created. // // Datasources: // - This uses the 'drift.Medium' datasource. // // Returns: // func Publish(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { hist := p.Get("withHistory", true).(bool) topic := p.Get("topic", "").(string) if len(topic) == 0 { return nil, errors.New("No topic supplied.") } medium, _ := getMedium(c) // Is there any reason to disallow empty messages? msg := p.Get("message", []byte{}).([]byte) c.Logf("info", "Msg: %s", msg) t := fetchOrCreateTopic(medium, topic, hist, DefaultMaxHistory) return nil, t.Publish(msg) }
// DeleteTopic deletes a topic and its history. // // Params: // - name (string) // // Returns: // func DeleteTopic(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { name := p.Get("topic", "").(string) if len(name) == 0 { return nil, &cookoo.FatalError{"Topic name required."} } m, err := getMedium(c) if err != nil { return nil, &cookoo.FatalError{"No medium."} } err = m.Delete(name) if err != nil { c.Logf("warn", "Failed to delete topic: %s", err) } return nil, nil }
func sendf(c cookoo.Context, l LogLevel, msg string, args ...interface{}) { if Level >= l { c.Logf(Label[l], msg, args...) } }
// shutdown runs an @shutdown route if it's found in the router. func shutdown(router *cookoo.Router, cxt cookoo.Context) { if router.HasRoute("@shutdown") { cxt.Logf("info", "Executing route @shutdown") router.HandleRequest("@shutdown", cxt, false) } }