diff --git a/server/common/common.go b/server/common/common.go new file mode 100644 index 0000000..08e50a8 --- /dev/null +++ b/server/common/common.go @@ -0,0 +1,6 @@ +package common + +const ( + // BotID is the id of bot. + BotID = 0 +) diff --git a/server/route/api/v1/shortcut_service.go b/server/route/api/v1/shortcut_service.go index c1a65f0..3f646b0 100644 --- a/server/route/api/v1/shortcut_service.go +++ b/server/route/api/v1/shortcut_service.go @@ -10,8 +10,6 @@ import ( "github.com/pkg/errors" "golang.org/x/exp/slices" "google.golang.org/grpc/codes" - "google.golang.org/grpc/metadata" - "google.golang.org/grpc/peer" "google.golang.org/grpc/status" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/types/known/emptypb" @@ -110,11 +108,6 @@ func (s *APIV1Service) GetShortcutByName(ctx context.Context, request *v1pb.GetS } } - // Create shortcut view activity. - if err := s.createShortcutViewActivity(ctx, shortcut); err != nil { - fmt.Printf("failed to create activity, err: %v", err) - } - composedShortcut, err := s.convertShortcutFromStorepb(ctx, shortcut) if err != nil { return nil, status.Errorf(codes.Internal, "failed to convert shortcut, err: %v", err) @@ -352,35 +345,6 @@ func mapToAnalyticsSlice(m map[string]int32) []*v1pb.GetShortcutAnalyticsRespons return analyticsSlice } -func (s *APIV1Service) createShortcutViewActivity(ctx context.Context, shortcut *storepb.Shortcut) error { - p, _ := peer.FromContext(ctx) - headers, ok := metadata.FromIncomingContext(ctx) - if !ok { - return errors.New("Failed to get metadata from context") - } - payload := &storepb.ActivityShorcutViewPayload{ - ShortcutId: shortcut.Id, - Ip: p.Addr.String(), - Referer: headers.Get("referer")[0], - UserAgent: headers.Get("user-agent")[0], - } - payloadStr, err := protojson.Marshal(payload) - if err != nil { - return errors.Wrap(err, "Failed to marshal activity payload") - } - activity := &store.Activity{ - CreatorID: BotID, - Type: store.ActivityShortcutView, - Level: store.ActivityInfo, - Payload: string(payloadStr), - } - _, err = s.Store.CreateActivity(ctx, activity) - if err != nil { - return errors.Wrap(err, "Failed to create activity") - } - return nil -} - func (s *APIV1Service) createShortcutCreateActivity(ctx context.Context, shortcut *storepb.Shortcut) error { payload := &storepb.ActivityShorcutCreatePayload{ ShortcutId: shortcut.Id, diff --git a/server/route/api/v1/user_service.go b/server/route/api/v1/user_service.go index c34f95f..d423b88 100644 --- a/server/route/api/v1/user_service.go +++ b/server/route/api/v1/user_service.go @@ -18,11 +18,6 @@ import ( "github.com/yourselfhosted/slash/store" ) -const ( - // BotID is the id of bot. - BotID = 0 -) - func (s *APIV1Service) ListUsers(ctx context.Context, _ *v1pb.ListUsersRequest) (*v1pb.ListUsersResponse, error) { users, err := s.Store.ListUsers(ctx, &store.FindUser{}) if err != nil { diff --git a/server/route/frontend/frontend.go b/server/route/frontend/frontend.go index 25b01ef..68672e6 100644 --- a/server/route/frontend/frontend.go +++ b/server/route/frontend/frontend.go @@ -5,14 +5,18 @@ import ( "embed" "fmt" "io/fs" + "log/slog" "net/http" "strings" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" + "github.com/pkg/errors" + "google.golang.org/protobuf/encoding/protojson" "github.com/yourselfhosted/slash/internal/util" storepb "github.com/yourselfhosted/slash/proto/gen/store" + "github.com/yourselfhosted/slash/server/common" "github.com/yourselfhosted/slash/server/metric" "github.com/yourselfhosted/slash/server/profile" "github.com/yourselfhosted/slash/store" @@ -44,35 +48,34 @@ func (s *FrontendService) Serve(ctx context.Context, e *echo.Echo) { HTML5: true, Filesystem: getFileSystem("dist"), Skipper: func(c echo.Context) bool { - return util.HasPrefixes(c.Path(), "/api", "/slash.api.v1", "/robots.txt", "/sitemap.xml", "/s/:shortcutName", "/c/:collectionName") + return util.HasPrefixes(c.Path(), "/api", "/slash.api.v1", "/s/:shortcutName", "/c/:collectionName") }, })) - g := e.Group("assets") + assetsGroup := e.Group("assets") // Use echo gzip middleware to compress the response. // Reference: https://echo.labstack.com/docs/middleware/gzip - g.Use(middleware.GzipWithConfig(middleware.GzipConfig{ + assetsGroup.Use(middleware.GzipWithConfig(middleware.GzipConfig{ Skipper: func(c echo.Context) bool { - return util.HasPrefixes(c.Path(), "/api", "/slash.api.v1", "/robots.txt", "/sitemap.xml", "/s/:shortcutName", "/c/:collectionName") + return util.HasPrefixes(c.Path(), "/api", "/slash.api.v1", "/s/:shortcutName", "/c/:collectionName") }, Level: 5, })) - g.Use(func(next echo.HandlerFunc) echo.HandlerFunc { + assetsGroup.Use(func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { c.Response().Header().Set(echo.HeaderCacheControl, "max-age=31536000, immutable") return next(c) } }) - g.Use(middleware.StaticWithConfig(middleware.StaticConfig{ + assetsGroup.Use(middleware.StaticWithConfig(middleware.StaticConfig{ HTML5: true, Filesystem: getFileSystem("dist/assets"), Skipper: func(c echo.Context) bool { - return util.HasPrefixes(c.Path(), "/api", "/slash.api.v1", "/robots.txt", "/sitemap.xml", "/s/:shortcutName", "/c/:collectionName") + return util.HasPrefixes(c.Path(), "/api", "/slash.api.v1", "/s/:shortcutName", "/c/:collectionName") }, })) s.registerRoutes(e) - s.registerFileRoutes(ctx, e) } func (s *FrontendService) registerRoutes(e *echo.Echo) { @@ -84,21 +87,20 @@ func (s *FrontendService) registerRoutes(e *echo.Echo) { shortcut, err := s.Store.GetShortcut(ctx, &store.FindShortcut{ Name: &shortcutName, }) - if err != nil { - return c.HTML(http.StatusOK, rawIndexHTML) - } - if shortcut == nil { + // If any error occurs or the shortcut is not found, return the raw `index.html`. + if err != nil || shortcut == nil { return c.HTML(http.StatusOK, rawIndexHTML) } - metric.Enqueue("shortcut view") - // Only set the `Location` header if the link is a valid URI. - if util.ValidateURI(shortcut.Link) { - c.Response().Header().Set("Location", shortcut.Link) + // Create shortcut view activity. + if err := s.createShortcutViewActivity(ctx, c.Request(), shortcut); err != nil { + slog.Warn("failed to create shortcut view activity", slog.String("error", err.Error())) } + metric.Enqueue("shortcut view") + // Inject shortcut metadata into `index.html`. indexHTML := strings.ReplaceAll(rawIndexHTML, headerMetadataPlaceholder, generateShortcutMetadata(shortcut).String()) - return c.HTML(http.StatusPermanentRedirect, indexHTML) + return c.HTML(http.StatusOK, indexHTML) }) e.GET("/c/:collectionName", func(c echo.Context) error { @@ -107,10 +109,8 @@ func (s *FrontendService) registerRoutes(e *echo.Echo) { collection, err := s.Store.GetCollection(ctx, &store.FindCollection{ Name: &collectionName, }) - if err != nil { - return c.HTML(http.StatusOK, rawIndexHTML) - } - if collection == nil { + // If any error occurs or the collection is not found, return the raw `index.html`. + if err != nil || collection == nil { return c.HTML(http.StatusOK, rawIndexHTML) } @@ -121,50 +121,42 @@ func (s *FrontendService) registerRoutes(e *echo.Echo) { }) } -func (s *FrontendService) registerFileRoutes(ctx context.Context, e *echo.Echo) { - workspaceGeneralSetting, err := s.Store.GetWorkspaceGeneralSetting(ctx) +func (s *FrontendService) createShortcutViewActivity(ctx context.Context, request *http.Request, shortcut *storepb.Shortcut) error { + ip := getReadUserIP(request) + referer := request.Header.Get("Referer") + userAgent := request.Header.Get("User-Agent") + payload := &storepb.ActivityShorcutViewPayload{ + ShortcutId: shortcut.Id, + Ip: ip, + Referer: referer, + UserAgent: userAgent, + } + payloadStr, err := protojson.Marshal(payload) if err != nil { - return + return errors.Wrap(err, "Failed to marshal activity payload") } - instanceURL := workspaceGeneralSetting.InstanceUrl - if instanceURL == "" { - return + activity := &store.Activity{ + CreatorID: common.BotID, + Type: store.ActivityShortcutView, + Level: store.ActivityInfo, + Payload: string(payloadStr), } + _, err = s.Store.CreateActivity(ctx, activity) + if err != nil { + return errors.Wrap(err, "Failed to create activity") + } + return nil +} - e.GET("/robots.txt", func(c echo.Context) error { - robotsTxt := fmt.Sprintf(`User-agent: * -Allow: / -Host: %s -Sitemap: %s/sitemap.xml`, instanceURL, instanceURL) - return c.String(http.StatusOK, robotsTxt) - }) - - e.GET("/sitemap.xml", func(c echo.Context) error { - urlsets := []string{} - // Append shortcut list. - shortcuts, err := s.Store.ListShortcuts(ctx, &store.FindShortcut{ - VisibilityList: []store.Visibility{store.VisibilityPublic}, - }) - if err != nil { - return err - } - for _, shortcut := range shortcuts { - urlsets = append(urlsets, fmt.Sprintf(`%s/s/%s`, instanceURL, shortcut.Name)) - } - // Append collection list. - collections, err := s.Store.ListCollections(ctx, &store.FindCollection{ - VisibilityList: []store.Visibility{store.VisibilityPublic}, - }) - if err != nil { - return err - } - for _, collection := range collections { - urlsets = append(urlsets, fmt.Sprintf(`%s/c/%s`, instanceURL, collection.Name)) - } - - sitemap := fmt.Sprintf(`%s`, strings.Join(urlsets, "\n")) - return c.XMLBlob(http.StatusOK, []byte(sitemap)) - }) +func getReadUserIP(r *http.Request) string { + IPAddress := r.Header.Get("X-Real-Ip") + if IPAddress == "" { + IPAddress = r.Header.Get("X-Forwarded-For") + } + if IPAddress == "" { + IPAddress = r.RemoteAddr + } + return IPAddress } func getFileSystem(path string) http.FileSystem {