Пример #1
0
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
}
Пример #2
0
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)
	})
}
Пример #3
0
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
}
Пример #4
0
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
}
Пример #5
0
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
}
Пример #6
0
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
}