func (sis *SiteinfoService) Register(srv *ab.Server) error { clientjs := make([]string, len(sis.BaseURLs)) for i, baseurl := range sis.BaseURLs { clientjs[i] = getClientJS(baseurl) } srv.Post("/api/siteinfo", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { d := siteInfoRequest{} ab.MustDecode(r, &d) db := ab.GetDB(r) info, err := sis.getFromDB(db, d.Url) ab.MaybeFail(http.StatusInternalServerError, err) if info.Empty() { info, err = sis.fetchAndSaveSiteinfo(db, d.Url, clientjs) if err != nil { if err == timeoutError { ab.Fail(http.StatusServiceUnavailable, err) } else { ab.Fail(http.StatusBadGateway, err) } } if info.Empty() { info, err = sis.getFromDB(db, d.Url) ab.MaybeFail(http.StatusInternalServerError, err) } } ab.Render(r).JSON(info) })) return nil }
func corsPreflightHandler(baseURL, httpOrigin string) http.Handler { baseurl, err := url.Parse(baseURL) if err != nil { panic(err) } httporigin, err := url.Parse(httpOrigin) if err != nil { panic(err) } return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { origin := r.Header.Get("Origin") method := r.Header.Get("Access-Control-Request-Method") headers := r.Header.Get("Access-Control-Request-Headers") ab.LogTrace(r).Printf("CORS %s %s %s", method, origin, headers) w.Header().Add("Vary", "Origin") w.Header().Add("Vary", "Access-Control-Request-Method") w.Header().Add("Vary", "Access-Control-Request-Headers") if origin == "" || method == "" { ab.Fail(http.StatusBadRequest, nil) } originurl, err := url.Parse(origin) ab.MaybeFail(http.StatusBadRequest, err) if originurl.Host != baseurl.Host && originurl.Host != httporigin.Host { ab.Fail(http.StatusForbidden, nil) } w.Header().Set("Access-Control-Allow-Origin", origin) w.Header().Set("Access-Control-Allow-Methods", method) w.Header().Set("Access-Control-Allow-Headers", headers) w.Header().Set("Access-Control-Allow-Credentials", "true") w.Header().Set("Access-Control-Max-Age", "0") ab.Render(r).SetCode(http.StatusOK) }) }
func userService(ec *ab.EntityController) ab.Service { res := ab.EntityResource(ec, &User{}, ab.EntityResourceConfig{ DisableList: true, }) res.ExtraEndpoints = func(srv *ab.Server) error { srv.Get("/api/user", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { sess := ab.GetSession(r) if sess["uid"] != "" { db := ab.GetDB(r) user, err := ec.Load(db, "user", sess["uid"]) ab.MaybeFail(http.StatusInternalServerError, err) ab.Render(r). JSON(user) } })) return nil } return res }
func logService(ec *ab.EntityController, baseurl string) ab.Service { res := ab.EntityResource(ec, &Log{}, ab.EntityResourceConfig{ DisableList: true, DisableGet: true, DisablePost: true, DisablePut: true, DisableDelete: true, }) res.ExtraEndpoints = func(srv *ab.Server) error { walkthroughPlayed := prometheus.NewCounterFrom(stdprometheus.CounterOpts{ Namespace: "walkhub", Subsystem: "metrics", Name: "walkthrough_played", Help: "Number of walkthrough plays", }, []string{"uuid", "embedorigin"}) walkthroughVisited := prometheus.NewCounterFrom(stdprometheus.CounterOpts{ Namespace: "walkhub", Subsystem: "metrics", Name: "walkthrough_visited", Help: "Number of walkthrough visits", }, []string{"uuid", "embedorigin"}) srv.Post("/api/log/helpcenteropened", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { l := helpCenterOpenedLog{} ab.MustDecode(r, &l) db := ab.GetDB(r) userid := getLogUserID(r, ec) message := fmt.Sprintf("%s has opened the help center on %s", userid, l.URL) ab.MaybeFail(http.StatusInternalServerError, DBLog(db, ec, "helpcenteropened", message)) })) srv.Post("/api/log/walkthroughplayed", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { l := walkthroughPlayedLog{} ab.MustDecode(r, &l) db := ab.GetDB(r) userid := getLogUserID(r, ec) wt, err := LoadActualRevision(db, ec, l.UUID) ab.MaybeFail(http.StatusBadRequest, err) if wt == nil { ab.Fail(http.StatusNotFound, nil) } message := "" embedPart := "" if l.EmbedOrigin != "" { embedPart = "from the help center on " + l.EmbedOrigin + " " } wturl := baseurl + "walkthrough/" + wt.UUID if l.ErrorMessage == "" { message = fmt.Sprintf("%s has played the walkthrough %s<%s|%s>", userid, embedPart, wturl, wt.Name) } else { message = fmt.Sprintf("%s has failed to play the walkthrough %s<%s|%s> with the error message %s", userid, embedPart, wturl, wt.Name, l.ErrorMessage) } ab.MaybeFail(http.StatusInternalServerError, DBLog(db, ec, "walkthroughplayed", message)) walkthroughPlayed. With("uuid", l.UUID). With("embedorigin", l.EmbedOrigin). Add(1) })) srv.Post("/api/log/walkthroughpagevisited", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { l := walkthroughPageVisitedLog{} ab.MustDecode(r, &l) db := ab.GetDB(r) userid := getLogUserID(r, ec) wt, err := LoadActualRevision(db, ec, l.UUID) ab.MaybeFail(http.StatusBadRequest, err) if wt == nil { ab.Fail(http.StatusNotFound, nil) } embedPart := "" if l.EmbedOrigin != "" { embedPart = "embedded on " + l.EmbedOrigin + " " } wturl := baseurl + "walkthrough/" + wt.UUID message := fmt.Sprintf("%s has visited the walkthrough page %s<%s|%s>", userid, embedPart, wturl, wt.Name) ab.MaybeFail(http.StatusInternalServerError, DBLog(db, ec, "walkthroughvisited", message)) walkthroughVisited. With("uuid", l.UUID). With("embedorigin", l.EmbedOrigin). Add(1) })) return nil } return res }
func screeningService(ec *ab.EntityController) ab.Service { res := ab.EntityResource(ec, &Screening{}, ab.EntityResourceConfig{ DisablePost: true, DisableList: true, DisableGet: true, DisablePut: true, DisableDelete: true, }) res.ExtraEndpoints = func(srv *ab.Server) error { srv.Post("/api/walkthrough/:id/screening", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { wid := ab.GetParams(r).ByName("id") data := []string{} ab.MustDecode(r, &data) db := ab.GetDB(r) uid := ab.GetSession(r)["uid"] userEntity, err := ec.Load(db, "user", uid) ab.MaybeFail(http.StatusInternalServerError, err) user := userEntity.(*User) wt, err := LoadActualRevision(db, ec, wid) ab.MaybeFail(http.StatusBadRequest, err) if wt.UID != uid && !user.Admin { ab.Fail(http.StatusForbidden, nil) } if len(data) == 0 || len(data) != len(wt.Steps)-1 { ab.Fail(http.StatusBadRequest, fmt.Errorf("got %d images, expected: %d", len(data), len(wt.Steps)-1)) } screening := &Screening{ WID: wid, UID: uid, Steps: uint(len(wt.Steps) - 1), Created: time.Now(), Published: true, } err = ec.Insert(db, screening) ab.MaybeFail(http.StatusInternalServerError, err) images := map[string][]byte{} for i, d := range data { if d == "" { continue } dataurl, err := dataurl.DecodeString(d) if err != nil { ab.LogTrace(r).Printf("data url error: %s", dataurl) ab.Fail(http.StatusBadRequest, err) } if dataurl.ContentType() != "image/png" { ab.Fail(http.StatusBadRequest, errors.New("not a png")) } fn := screening.ScreenshotPath(uint(i)) images[fn] = dataurl.Data } if len(images) == 0 { ab.Fail(http.StatusBadRequest, errors.New("no images sent")) } for name, content := range images { if err := ioutil.WriteFile(name, content, 0644); err != nil { ab.LogUser(r).Println(err) } } ab.Render(r). SetCode(http.StatusCreated). JSON(screening) }), userLoggedInMiddleware) lock := map[string]chan struct{}{} var mtx sync.Mutex srv.Get("/api/walkthrough/:id/screening", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { wid := ab.GetParams(r).ByName("id") db := ab.GetDB(r) screening, err := LoadActualScreeningForWalkthrough(db, ec, wid) ab.MaybeFail(http.StatusInternalServerError, err) if screening == nil { ab.Fail(http.StatusNotFound, nil) } fn := screening.GIFPath() reply := func() { filelist := make([]string, int(screening.Steps)) for i := uint(0); i < screening.Steps; i++ { filelist[i] = "/" + screening.ScreenshotPath(i) } ab.Render(r).AddOffer("image/gif", func(w http.ResponseWriter) { f, err := os.Open(fn) ab.MaybeFail(http.StatusInternalServerError, err) defer f.Close() io.Copy(w, f) }).JSON(filelist) } if _, err := os.Stat(fn); err == nil { reply() return } // Short-circuits the gif generation process. // If the client wants JSON, there is no need // to go through the GIF generation, which is // an expensive process. if r.Header.Get("Accept") == "application/json" { reply() return } mtx.Lock() l, ok := lock[fn] if ok { mtx.Unlock() select { case <-l: reply() case <-time.After(5 * time.Second): w.Header().Set("Retry-After", "30") ab.Render(r).SetCode(http.StatusServiceUnavailable) } return } l = make(chan struct{}) lock[fn] = l mtx.Unlock() err = screening.createGIF(false) defer func() { mtx.Lock() delete(lock, fn) mtx.Unlock() }() if err != nil { if _, ok := err.(*os.PathError); ok { ab.Fail(http.StatusNotFound, err) } else { ab.Fail(http.StatusInternalServerError, err) } } close(l) reply() })) return nil } return res }
func walkthroughService(ec *ab.EntityController, search *search.SearchService, baseurl string) ab.Service { h := &walkthroughEntityResourceHelper{ controller: ec, } res := ab.EntityResource(ec, &Walkthrough{}, ab.EntityResourceConfig{ PostMiddlewares: []func(http.Handler) http.Handler{userLoggedInMiddleware}, PutMiddlewares: []func(http.Handler) http.Handler{userLoggedInMiddleware}, DeleteMiddlewares: []func(http.Handler) http.Handler{userLoggedInMiddleware}, EntityResourceLister: h, EntityResourceLoader: h, }) res.ExtraEndpoints = func(srv *ab.Server) error { reindexing := false var reindexingMutex sync.RWMutex srv.Post("/api/reindexwalkthroughs", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { reindexingMutex.RLock() idxing := reindexing reindexingMutex.RUnlock() if idxing { ab.Fail(http.StatusServiceUnavailable, errors.New("reindexing is in progress")) } reindexingMutex.Lock() reindexing = true reindexingMutex.Unlock() db := ab.GetDB(r) go func() { defer func() { reindexingMutex.Lock() reindexing = false reindexingMutex.Unlock() }() err := search.PurgeIndex() if err != nil { log.Println(err) return } wts, err := LoadAllActualWalkthroughs(db, ec, 0, 0) if err != nil { log.Println(err) return } for _, wt := range wts { err = search.IndexEntity("walkthrough", wt) if err != nil { log.Println(err) return } } }() ab.Render(r).SetCode(http.StatusAccepted) }), ab.RestrictPrivateAddressMiddleware()) srv.Get("/api/mysites", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { db := ab.GetDB(r) uid := ab.GetSession(r)["uid"] rows, err := db.Query("SELECT DISTINCT steps->0->'arg0' AS site FROM walkthrough WHERE uid = $1 AND published ORDER BY site", uid) ab.MaybeFail(http.StatusInternalServerError, err) defer rows.Close() sites := []string{} for rows.Next() { var site sql.NullString err = rows.Scan(&site) ab.MaybeFail(http.StatusInternalServerError, err) if site.Valid { siteName := site.String // strip surrounding " siteName = siteName[1:] siteName = siteName[:len(siteName)-1] sites = append(sites, siteName) } } ab.Render(r).JSON(sites) }), userLoggedInMiddleware) return nil } res.AddPostEvent(ab.ResourceEventCallback{ BeforeCallback: func(r *http.Request, d ab.Resource) { wt := d.(*Walkthrough) uid := UserDelegate.CurrentUser(r) if wt.UID == "" { wt.UID = uid } if wt.UID != uid { ab.Fail(http.StatusBadRequest, errors.New("invalid user id")) } wt.Updated = time.Now() wt.Revision = "" wt.UUID = "" }, AfterCallback: func(r *http.Request, d ab.Resource) { db := ab.GetDB(r) wt := d.(*Walkthrough) search.IndexEntity("walkthrough", wt) userEntity, err := ec.Load(db, "user", wt.UID) if err != nil { log.Println(err) return } user := userEntity.(*User) startURL := "" if len(wt.Steps) > 0 && wt.Steps[0].Command == "open" { startURL = wt.Steps[0].Arg0 } message := fmt.Sprintf("%s has recorded a Walkthrough (<%s|%s>) on %s", user.Mail, baseurl+"walkthrough/"+wt.UUID, html.EscapeString(wt.Name), html.EscapeString(startURL), ) DBLog(db, ec, "walkthroughrecord", message) }, }) res.AddPutEvent(ab.ResourceEventCallback{ BeforeCallback: func(r *http.Request, d ab.Resource) { db := ab.GetDB(r) wt := d.(*Walkthrough) uid := UserDelegate.CurrentUser(r) currentUserEntity, err := ec.Load(db, "user", uid) ab.MaybeFail(http.StatusBadRequest, err) currentUser := currentUserEntity.(*User) if wt.UID != uid { if !currentUser.Admin { ab.Fail(http.StatusForbidden, nil) } } previousRevision, err := LoadActualRevision(db, ec, wt.UUID) ab.MaybeFail(http.StatusBadRequest, err) if previousRevision == nil { ab.Fail(http.StatusNotFound, nil) } if previousRevision.UID != uid && !currentUser.Admin { ab.Fail(http.StatusForbidden, nil) } wt.Updated = time.Now() wt.Revision = "" }, AfterCallback: func(r *http.Request, d ab.Resource) { search.IndexEntity("walkthrough", d.(*Walkthrough)) }, }) res.AddDeleteEvent(ab.ResourceEventCallback{ InsideCallback: func(r *http.Request, d ab.Resource) { db := ab.GetDB(r) uid := UserDelegate.CurrentUser(r) wt := d.(*Walkthrough) currentUserEntity, err := ec.Load(db, "user", uid) ab.MaybeFail(http.StatusBadRequest, err) currentUser := currentUserEntity.(*User) if wt.UID != uid { if !currentUser.Admin { ab.Fail(http.StatusForbidden, nil) } } }, }) return res }