Exemple #1
0
// 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
}
Exemple #2
0
// 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)
		}
	}
}
Exemple #3
0
/**
 * 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
}
Exemple #4
0
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
}
Exemple #5
0
// 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)

}
Exemple #6
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
}
Exemple #7
0
// 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
}
Exemple #8
0
// 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)
		}
	}
}
Exemple #9
0
// 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)

}
Exemple #10
0
// 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
}
Exemple #11
0
func sendf(c cookoo.Context, l LogLevel, msg string, args ...interface{}) {
	if Level >= l {
		c.Logf(Label[l], msg, args...)
	}
}
Exemple #12
0
// 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)
	}
}