func main() { opts := optparse.Parser( "Usage: matchdb <config.yaml> [options]\n", "matchdb 0.0.1") host := opts.StringConfig("host", "", "the host to bind matchdb to") port := opts.IntConfig("port", 8090, "the port to bind matchdb to [8090]") allocLimit := opts.IntConfig("alloc-limit", 5000000, "maximum allocation limit in kilobytes [5000000]") hashKey := opts.StringConfig("hash-key", "", "16-byte hash key encoded as a 32-byte hex string") awsAccessKey := opts.StringConfig("aws-access-key", "", "the AWS Access Key for DynamoDB") awsSecretKey := opts.StringConfig("aws-secret-key", "", "the AWS Secret Key for DynamoDB") awsRegion := opts.StringConfig("aws-region", "us-east-1", "the AWS Region for DynamoDB [us-east-1]") masterTable := opts.StringConfig("master-table", "", "the DynamoDB table for the master lock") masterTimeout := opts.IntConfig("master-timeout", 6000, "timeout in milliseconds for the master lock [6000]") os.Args[0] = "matchdb" runtime.DefaultOpts("matchdb", opts, os.Args) initHashKey(*hashKey) master := NewMaster("", *awsAccessKey, *awsSecretKey, *awsRegion, *masterTable, time.Duration(*masterTimeout)*time.Millisecond) db := &DB{} db.Run(*host, *port, master, int64(*allocLimit)) }
func main() { // Define the options for the command line and config file options parser. opts := optparse.Parser( "Usage: matchweb <config.yaml> [options]\n", "matchweb 0.0.1") host := opts.StringConfig("host", "", "the host to bind the matchweb to") port := opts.IntConfig("port", 9040, "the port to bind the matchweb to [9040]") redirectURL := opts.StringConfig("redirect-url", "", "the URL that the HTTP Redirector redirects to") redirectorHost := opts.StringConfig("redirector-host", "", "the host to bind the HTTP Redirector to") redirectorPort := opts.IntConfig("redirector-port", 9080, "the port to bind the HTTP Redirector to [9080]") hstsMaxAge := opts.IntConfig("hsts-max-age", 31536000, "max-age value of HSTS in number of seconds [0 (disabled)]") clusterID := opts.StringConfig("cluster-id", "", "the cluster id to use when responding to ping requests") matchdbServer := opts.StringConfig("matchdb-server", "", "the address for a single-node MatchDB server setup") hashKey := opts.StringConfig("hash-key", "", "16-byte hash key encoded as a 32-byte hex string") awsAccessKey := opts.StringConfig("aws-access-key", "", "the AWS Access Key for DynamoDB") awsSecretKey := opts.StringConfig("aws-secret-key", "", "the AWS Secret Key for DynamoDB") awsRegion := opts.StringConfig("aws-region", "us-east-1", "the AWS Region for DynamoDB [us-east-1]") masterTable := opts.StringConfig("master-table", "", "the DynamoDB table for the master lock") masterTimeout := opts.IntConfig("master-timeout", 6000, "timeout in milliseconds for the master lock [6000]") routingTimeout := opts.IntConfig("routing-timeout", 3000, "timeout in milliseconds for routing entries [3000]") publishKey := opts.StringConfig("publish-key", "", "the shared secret for publishing new items") publishClusterID := opts.IntConfig("publish-cluster-id", 0, "the cluster id to use when acknowledging publish requests") upstreamHost := opts.StringConfig("upstream-host", "localhost", "the upstream host to connect to [localhost]") upstreamPort := opts.IntConfig("upstream-port", 8080, "the upstream port to connect to [8080]") upstreamTLS := opts.BoolConfig("upstream-tls", false, "use TLS when connecting to upstream [false]") websocketOrigin := opts.StringConfig("websocket-origin", "", "limit websocket calls to the given origin if specified") maintenanceMode := opts.BoolConfig("maintenance", false, "start up in maintenance mode [false]") // Setup the console log filter. log.ConsoleFilters[logPrefix] = func(items []interface{}) (bool, []interface{}) { return true, items[2 : len(items)-2] } // Parse the command line options. os.Args[0] = "matchweb" runtime.DefaultOpts("matchweb", opts, os.Args) // Initialise the TLS config. tlsconf.Init() // Initialise ping/pong variables. setupPong("matchweb", *clusterID) // Initialise the key for hashing slots. initHashKey(*hashKey) // Ensure required config values. if *publishKey == "" { runtime.Error("The publish-key cannot be empty") } server := &LiveServer{ httpClient: &http.Client{Transport: &http.Transport{TLSClientConfig: tlsconf.Config}}, publishKey: []byte(*publishKey), publishKeyLength: len(*publishKey), websocketOrigin: *websocketOrigin, } setUpstreamInfo(server, *upstreamHost, *upstreamPort, *upstreamTLS, *publishClusterID) if *hstsMaxAge != 0 { server.hstsEnabled = true server.hsts = fmt.Sprintf("max-age=%d", *hstsMaxAge) } // Enable maintenance handling. frontends := []Maintainable{server} handleMaintenance(frontends, *maintenanceMode) // Setup the HTTP Redirector. runRedirector(*redirectorHost, *redirectorPort, *redirectURL, *hstsMaxAge) // Run the Live Server. runHTTP("Live Server", *host, *port, server, "") // Enter the wait loop for the process to be killed. loopForever := make(chan bool, 1) <-loopForever }
func main() { // Define the options for the command line and config file options parser. opts := optparse.New( "Usage: planfile <config.yaml> [options]\n", "planfile 0.0.1") cookieKeyFile := opts.StringConfig("cookie-key-file", "cookie.key", "the file containing the key to sign cookie values [cookie.key]") gaHost := opts.StringConfig("ga-host", "", "the google analytics hostname to use") gaID := opts.StringConfig("ga-id", "", "the google analytics id to use") httpAddr := opts.StringConfig("http-addr", ":8888", "the address to bind the http server [:8888]") oauthID := opts.Required().StringConfig("oauth-id", "", "the oauth client id for github") oauthSecret := opts.Required().StringConfig("oauth-secret", "", "the oauth client secret for github") redirectURL := opts.StringConfig("redirect-url", "/.oauth", "the redirect url for handling oauth [/.oauth]") repository := opts.Required().StringConfig("repository", "", "the username/repository on github") secureMode := opts.BoolConfig("secure-mode", "enable hsts and secure cookies [false]") title := opts.StringConfig("title", "Planfile", "the title for the web app [Planfile]") refreshKey := opts.StringConfig("refresh-key", "", "key for anonymously calling refresh at /.refresh?key=<refresh-key>") refreshOpt := opts.IntConfig("refresh-interval", 1, "the number of through-the-web edits before a full refresh [1]") debug, instanceDirectory, _, logPath, _ = runtime.DefaultOpts("planfile", opts, os.Args, true) service := &oauth.OAuthService{ ClientID: *oauthID, ClientSecret: *oauthSecret, Scope: "public_repo", AuthURL: "https://github.com/login/oauth/authorize", TokenURL: "https://github.com/login/oauth/access_token", RedirectURL: *redirectURL, AcceptHeader: "application/json", } assets := map[string]string{} json.Unmarshal(readFile("assets.json"), &assets) setupPygments() mutex := sync.RWMutex{} repo := &Repo{Path: *repository} err := repo.Load(callGithubAnon) if err != nil { runtime.Exit(1) } repo.Title = *title repo.Updated = time.Now().UTC() repoJSON, err := json.Marshal(repo) if err != nil { runtime.StandardError(err) } refreshCount := 0 refreshInterval := *refreshOpt refreshKeySet := *refreshKey != "" refreshKeyBytes := []byte(*refreshKey) secret := readFile(*cookieKeyFile) newContext := func(w http.ResponseWriter, r *http.Request) *Context { return &Context{ r: r, w: w, secret: secret, secure: *secureMode, } } register := func(path string, handler func(*Context), usegzip ...bool) { gzippable := len(usegzip) > 0 && usegzip[0] http.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { log.Info("serving %s", r.URL) w.Header().Set("Content-Type", "text/html; charset=utf-8") if gzippable && httputil.Parse(r, "Accept-Encoding").Accepts("gzip") { buf := &bytes.Buffer{} enc := gzip.NewWriter(buf) handler(newContext(GzipWriter{enc, w}, r)) enc.Close() w.Header().Set("Content-Encoding", "gzip") w.Header().Set("Content-Length", strconv.Itoa(buf.Len())) buf.WriteTo(w) } else { handler(newContext(w, r)) } }) } anon := []byte(", null, null, '', false") authFalse := []byte("', false") authTrue := []byte("', true") header := []byte(`<!doctype html> <meta charset=utf-8> <title>` + html.EscapeString(*title) + `</title> <link href="//fonts.googleapis.com/css?family=Abel|Coustard:400" rel=stylesheet> <link href=/.static/` + assets["planfile.css"] + ` rel=stylesheet> <body><script>DATA = ['` + *gaHost + `', '` + *gaID + `', `) footer := []byte(`];</script> <script src=/.static/` + assets["planfile.js"] + `></script> <noscript>Sorry, your browser needs <a href=http://enable-javascript.com>JavaScript enabled</a>.</noscript> `) register("/", func(ctx *Context) { mutex.RLock() defer mutex.RUnlock() ctx.Write(header) ctx.Write(repoJSON) avatar := ctx.GetCookie("avatar") user := ctx.GetCookie("user") if avatar != "" && user != "" { ctx.Write([]byte(", '" + user + "', '" + avatar + "', '" + ctx.GetCookie("xsrf"))) if ctx.IsAuthorised(repo) { ctx.Write(authTrue) } else { ctx.Write(authFalse) } } else { ctx.Write(anon) } ctx.Write(footer) }, true) register("/.api", func(ctx *Context) { mutex.RLock() defer mutex.RUnlock() if cb := ctx.FormValue("callback"); cb != "" { ctx.Write([]byte(cb)) ctx.Write([]byte{'('}) ctx.Write(repoJSON) ctx.Write([]byte{')', ';'}) } else { ctx.Write(repoJSON) } }, true) register("/.login", func(ctx *Context) { b := make([]byte, 20) if n, err := rand.Read(b); err != nil || n != 20 { ctx.Error("Couldn't access cryptographic device", err) return } s := hex.EncodeToString(b) ctx.SetCookie("xsrf", s) ctx.Redirect(service.AuthCodeURL(s)) }) register("/.logout", func(ctx *Context) { ctx.ExpireCookie("auth") ctx.ExpireCookie("avatar") ctx.ExpireCookie("token") ctx.ExpireCookie("user") ctx.ExpireCookie("xsrf") ctx.Redirect("/") }) notAuthorised := []byte("ERROR: Not Authorised!") savedHeader := []byte(`<!doctype html> <meta charset=utf-8> <title>` + html.EscapeString(*title) + `</title> <body><script>SAVED="`) savedFooter := []byte(`"</script><script src=/.static/` + assets["planfile.js"] + `></script>`) exportRepo := func(ctx *Context) bool { repo.Updated = time.Now().UTC() repoJSON, err = json.Marshal(repo) if err != nil { ctx.Error("Couldn't encode repo data during refresh", err) return false } return true } refresh := func(ctx *Context) { err := repo.Load(ctx.CreateCallGithub()) if err != nil { log.Error("couldn't rebuild planfile info: %s", err) ctx.Write([]byte("ERROR: " + err.Error())) return } exportRepo(ctx) } saveItem := func(ctx *Context, update bool) { mutex.Lock() defer mutex.Unlock() if !ctx.IsAuthorised(repo) { ctx.Write(notAuthorised) return } if !isEqual([]byte(ctx.FormValue("xsrf")), []byte(ctx.GetCookie("xsrf"))) { ctx.Write(notAuthorised) return } callGithub := ctx.CreateCallGithub() err := repo.UpdateInfo(callGithub) if err != nil { ctx.Error("Couldn't update repo info", err) return } var id, path, message string if update { id = ctx.FormValue("id") path = ctx.FormValue("path") } else { baseID := ctx.FormValue("id") id = baseID count := 0 for repo.Exists(id + ".md") { count += 1 id = fmt.Sprintf("%s%d", baseID, count) } path = id + ".md" } content := strings.Replace(ctx.FormValue("content"), "\r\n", "\n", -1) tags := ctx.FormValue("tags") title := ctx.FormValue("title") redir := "/" if ctx.FormValue("summary") == "yes" { if id != "/" { content = fmt.Sprintf(`--- title: %s --- %s`, title, content) if strings.HasPrefix(id, "summary.") { redir = "/" + id[8:] } else { // Shouldn't ever happen. But just in case... redir = "/" + id } } } else { redir = "/.item." + id content = fmt.Sprintf(`--- id: %s tags: %s title: %s --- %s`, id, tags, title, content) } if title == "" { title = id } if update { message = "update: " + title + "." } else { message = "add: " + title + "." } log.Info("SAVE PATH: %q for %q", path, title) err = repo.Modify(ctx, path, content, message) if err != nil { if update { ctx.Error("<a href='/.refresh'>Try refreshing.</a> Couldn't update item", err) } else { ctx.Error("<a href='/.refresh'>Try refreshing.</a> Couldn't save new item", err) } return } refreshCount++ if refreshCount%refreshInterval == 0 { refresh(ctx) } else { repo.AddPlanfile(path, []byte(content), callGithub) if !exportRepo(ctx) { return } } ctx.Write(savedHeader) ctx.Write([]byte(html.EscapeString(redir))) ctx.Write(savedFooter) } register("/.modify", func(ctx *Context) { saveItem(ctx, true) }) register("/.new", func(ctx *Context) { saveItem(ctx, false) }) register("/.oauth", func(ctx *Context) { s := ctx.FormValue("state") if s == "" { ctx.Redirect("/.login") return } if !isEqual([]byte(s), []byte(ctx.GetCookie("xsrf"))) { ctx.ExpireCookie("xsrf") ctx.Redirect("/.login") return } t := &oauth.Transport{OAuthService: service} tok, err := t.ExchangeAuthorizationCode(ctx.FormValue("code")) if err != nil { ctx.Error("Auth Exchange Error", err) return } jtok, err := json.Marshal(tok) if err != nil { ctx.Error("Couldn't encode token", err) return } ctx.SetCookie("token", hex.EncodeToString(jtok)) ctx.token = tok user := &User{} err = ctx.Call("/user", user, nil, false) if err != nil { ctx.Error("Couldn't load user info", err) return } ctx.SetCookie("avatar", user.AvatarURL) ctx.SetCookie("user", user.Login) ctx.Redirect("/") }) register("/.preview", func(ctx *Context) { rendered, err := renderMarkdown([]byte(ctx.FormValue("content"))) if err != nil { ctx.Error("Couldn't render Markdown", err) return } ctx.Write(rendered) }, true) register("/.refresh", func(ctx *Context) { if !ctx.IsAuthorised(repo) { if !(refreshKeySet && isEqual(refreshKeyBytes, []byte(ctx.FormValue("key")))) { ctx.Write(notAuthorised) return } } mutex.Lock() defer mutex.Unlock() refresh(ctx) ctx.Redirect("/") }) mimetypes := map[string]string{ "css": "text/css", "gif": "image/gif", "ico": "image/x-icon", "jpeg": "image/jpeg", "jpg": "image/jpeg", "js": "text/javascript", "png": "image/png", "swf": "application/x-shockwave-flash", "txt": "text/plain", } registerStatic := func(filepath, urlpath string) { _, ext := rsplit(filepath, ".") ctype, ok := mimetypes[ext] if !ok { ctype = "application/octet-stream" } if debug { register(urlpath, func(ctx *Context) { ctx.SetHeader("Content-Type", ctype) ctx.Write(readFile(filepath)) }, strings.HasPrefix(ctype, "text/")) } else { content := readFile(filepath) register(urlpath, func(ctx *Context) { ctx.SetHeader("Cache-Control", "public, max-age=86400") ctx.SetHeader("Content-Type", ctype) ctx.Write(content) }, strings.HasPrefix(ctype, "text/")) } } for _, path := range assets { registerStatic(filepath.Join(instanceDirectory, "static", path), "/.static/"+path) } wwwPath := filepath.Join(instanceDirectory, "www") if files, err := ioutil.ReadDir(wwwPath); err == nil { for _, file := range files { if !file.IsDir() { registerStatic(filepath.Join(wwwPath, file.Name()), "/"+file.Name()) } } } log.Info("Listening on %s", *httpAddr) server := &http.Server{ Addr: *httpAddr, ReadTimeout: 30 * time.Second, WriteTimeout: 30 * time.Second, } err = server.ListenAndServe() if err != nil { runtime.Error("couldn't bind to tcp socket: %s", err) } }