diff options
| author | Guillermo Serrahima <dev@vylion.com> | 2026-03-20 00:54:07 +0100 |
|---|---|---|
| committer | Guillermo Serrahima <dev@vylion.com> | 2026-03-20 00:54:07 +0100 |
| commit | 1d947b771995399b677c040136ef2cf88b33d4f2 (patch) | |
| tree | 3cdd673f6c27c9a5a481627895d1fee932b85580 | |
| parent | 59545c874d8635c39ccdbecf84f953686dd5c03c (diff) | |
| download | llostre-1d947b771995399b677c040136ef2cf88b33d4f2.tar.xz llostre-1d947b771995399b677c040136ef2cf88b33d4f2.zip | |
readded auth that I removed in my hubris + check for no-unauthorized labels
| -rw-r--r-- | backend/fetchers/app.bsky.actor.go | 11 | ||||
| -rw-r--r-- | backend/fetchers/app.bsky.feed.go | 31 | ||||
| -rw-r--r-- | backend/general/records.go | 67 | ||||
| -rw-r--r-- | backend/main.go | 54 | ||||
| -rw-r--r-- | backend/notes/app.bsky.actor.go | 17 | ||||
| -rw-r--r-- | backend/notes/app.bsky.feed.go | 19 | ||||
| -rw-r--r-- | backend/notes/author.go | 1 | ||||
| -rw-r--r-- | backend/notes/note.go | 20 |
8 files changed, 159 insertions, 61 deletions
diff --git a/backend/fetchers/app.bsky.actor.go b/backend/fetchers/app.bsky.actor.go index e07f5be..0752223 100644 --- a/backend/fetchers/app.bsky.actor.go +++ b/backend/fetchers/app.bsky.actor.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "io" + "llostre/backend/auth" "llostre/backend/general" "llostre/backend/notes" "log/slog" @@ -130,13 +131,13 @@ func FetchHandleAndPDS(did string) (string, string, error) { return handle, pds, nil } -func FetchAppBskyActorProfile(logger *slog.Logger, did, pdsURL, slingshotURL string, cache *Cache) (*notes.Author, error) { +func FetchAppBskyActorProfile(logger *slog.Logger, did, pdsURL, slingshotURL string, session *auth.OAuthState, cache *Cache) (*notes.Author, error) { // construct profile record URI uri := fmt.Sprintf("at://%s/app.bsky.actor.profile/self", did) // try slingshot first if slingshotURL != "" { - rawRecord, err := general.FetchRawRecordByURI(logger, uri, slingshotURL) + rawRecord, err := general.FetchRawRecordByURI(logger, uri, slingshotURL, session) if err != nil { logger.Warn("slingshot profile fetch failed", "did", did, "error", err) } else { @@ -145,7 +146,7 @@ func FetchAppBskyActorProfile(logger *slog.Logger, did, pdsURL, slingshotURL str } // fallback to PDS - rawRecord, err := general.FetchRawRecordByURI(logger, uri, pdsURL) + rawRecord, err := general.FetchRawRecordByURI(logger, uri, pdsURL, session) if err != nil { return nil, err } @@ -153,10 +154,10 @@ func FetchAppBskyActorProfile(logger *slog.Logger, did, pdsURL, slingshotURL str return parseAppBskyActorProfile(did, rawRecord, cache) } -func FetchAppBskyActorProfiles(logger *slog.Logger, dids []string, pdsURL, slingshotURL string, cache *Cache) map[string]notes.Author { +func FetchAppBskyActorProfiles(logger *slog.Logger, dids []string, pdsURL, slingshotURL string, session *auth.OAuthState, cache *Cache) map[string]notes.Author { authors := make(map[string]notes.Author) for _, did := range dids { - author, err := FetchAppBskyActorProfile(logger, did, pdsURL, slingshotURL, cache) + author, err := FetchAppBskyActorProfile(logger, did, pdsURL, slingshotURL, session, cache) if err == nil { authors[did] = *author } diff --git a/backend/fetchers/app.bsky.feed.go b/backend/fetchers/app.bsky.feed.go index 10c08e7..2a38d5a 100644 --- a/backend/fetchers/app.bsky.feed.go +++ b/backend/fetchers/app.bsky.feed.go @@ -3,17 +3,18 @@ package fetchers import ( "encoding/json" "fmt" + "llostre/backend/auth" "llostre/backend/general" "llostre/backend/notes" "log/slog" ) -func FetchAppBskyFeedPosts(logger *slog.Logger, did, pdsURL, slingshotURL string, cache *Cache) ([]notes.Note, error) { +func FetchAppBskyFeedPosts(logger *slog.Logger, did, pdsURL, slingshotURL string, session *auth.OAuthState, cache *Cache) ([]notes.Note, error) { // get author of the note author, found := cache.GetProfile(did) var err error if !found { - author, err = FetchAppBskyActorProfile(logger, did, pdsURL, slingshotURL, cache) + author, err = FetchAppBskyActorProfile(logger, did, pdsURL, slingshotURL, session, cache) } if err != nil { return nil, err @@ -21,7 +22,7 @@ func FetchAppBskyFeedPosts(logger *slog.Logger, did, pdsURL, slingshotURL string // try slingshot first if given if slingshotURL != "" { - records, err := general.FetchRawRecordCollection(logger, did, "app.bsky.feed.post", slingshotURL) + records, err := general.FetchRawRecordCollection(logger, did, "app.bsky.feed.post", slingshotURL, session) if err != nil { logger.Error("Fetching from slingshot failed") } else { @@ -29,7 +30,7 @@ func FetchAppBskyFeedPosts(logger *slog.Logger, did, pdsURL, slingshotURL string } } - records, err := general.FetchRawRecordCollection(logger, did, "app.bsky.feed.post", pdsURL) + records, err := general.FetchRawRecordCollection(logger, did, "app.bsky.feed.post", pdsURL, session) if err != nil { return nil, err } @@ -37,12 +38,22 @@ func FetchAppBskyFeedPosts(logger *slog.Logger, did, pdsURL, slingshotURL string return parseAppBskyFeedPostCollection(logger, records, author, cache) } -func FetchAppBskyFeedPostByURI(logger *slog.Logger, uri, slingshotURL string, cache *Cache) (*notes.Note, error) { - // check cache +func GetAppBskyFeedPostByURI(logger *slog.Logger, uri, slingshotURL string, session *auth.OAuthState, cache *Cache) (*notes.Note, error) { + // check cache, return if it was cached without auth or if it was cached with auth and we have auth if cached, found := cache.Get(uri); found { - return cached, nil + if !cached.AuthorNoUnauth { + return cached, nil + } + + if general.IsValidSession(session) { + return cached, nil + } } + return FetchAppBskyFeedPostByURI(logger, uri, slingshotURL, session, cache) +} + +func FetchAppBskyFeedPostByURI(logger *slog.Logger, uri, slingshotURL string, session *auth.OAuthState, cache *Cache) (*notes.Note, error) { repo, _, _, err := general.ParseURI(uri) // need to resolve DID to PDS @@ -54,7 +65,7 @@ func FetchAppBskyFeedPostByURI(logger *slog.Logger, uri, slingshotURL string, ca // get author of the note author, found := cache.GetProfile(repo) if !found { - author, err = FetchAppBskyActorProfile(logger, repo, pdsURL, slingshotURL, cache) + author, err = FetchAppBskyActorProfile(logger, repo, pdsURL, slingshotURL, session, cache) } if err != nil { return nil, err @@ -62,7 +73,7 @@ func FetchAppBskyFeedPostByURI(logger *slog.Logger, uri, slingshotURL string, ca // try slingshot first if given if slingshotURL != "" { - rawRecord, err := general.FetchRawRecordByURI(logger, uri, slingshotURL) + rawRecord, err := general.FetchRawRecordByURI(logger, uri, slingshotURL, session) if err != nil { logger.Error("fetching from slingshot failed", "error", err) } else { @@ -70,7 +81,7 @@ func FetchAppBskyFeedPostByURI(logger *slog.Logger, uri, slingshotURL string, ca } } - rawRecord, err := general.FetchRawRecordByURI(logger, uri, pdsURL) + rawRecord, err := general.FetchRawRecordByURI(logger, uri, pdsURL, session) if err != nil { logger.Error("Fetching from PDS failed") return nil, err diff --git a/backend/general/records.go b/backend/general/records.go index d623862..ff37785 100644 --- a/backend/general/records.go +++ b/backend/general/records.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "io" + "llostre/backend/auth" "log/slog" "net/http" ) @@ -13,12 +14,27 @@ type RawRecord struct { Value json.RawMessage `json:"value"` } -func FetchRawRecordCollection(logger *slog.Logger, did, collection, baseURL string) ([]RawRecord, error) { +func FetchRawRecordCollection(logger *slog.Logger, did, collection, baseURL string, session *auth.OAuthState) ([]RawRecord, error) { url := fmt.Sprintf("%s/xrpc/com.atproto.repo.listRecords?repo=%s&collection=%s", baseURL, did, collection) logger.Debug("fetching raw record collection", "did", did, "endpoint", baseURL, "collection", collection) - resp, err := http.Get(url) + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + + // only add auth if provided + if session != nil { + dpopProof, err := session.KeyPair.GenerateDPoPProof("GET", url, "") + if err != nil { + return nil, err + } + req.Header.Set("Authorization", "Bearer "+session.AccessToken) + req.Header.Set("DPoP", dpopProof) + } + + resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err } @@ -39,7 +55,7 @@ func FetchRawRecordCollection(logger *slog.Logger, did, collection, baseURL stri return result.Records, nil } -func FetchRawRecordByURI(logger *slog.Logger, uri, baseURL string) (*RawRecord, error) { +func FetchRawRecordByURI(logger *slog.Logger, uri, baseURL string, session *auth.OAuthState) (*RawRecord, error) { repo, collection, rkey, err := ParseURI(uri) if err != nil { return nil, err @@ -50,7 +66,22 @@ func FetchRawRecordByURI(logger *slog.Logger, uri, baseURL string) (*RawRecord, logger.Debug("fetching raw record by URI", "uri", uri, "endpoint", baseURL) - resp, err := http.Get(url) + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + + // only add auth if provided + if session != nil { + dpopProof, err := session.KeyPair.GenerateDPoPProof("GET", url, "") + if err != nil { + return nil, err + } + req.Header.Set("Authorization", "Bearer "+session.AccessToken) + req.Header.Set("DPoP", dpopProof) + } + + resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err } @@ -68,3 +99,31 @@ func FetchRawRecordByURI(logger *slog.Logger, uri, baseURL string) (*RawRecord, return &result, nil } + +func IsValidSession(session *auth.OAuthState) bool { + if session == nil { + return false + } + + url := fmt.Sprintf("%s/xrpc/com.atproto.server.getSession", session.PDS) + + dpopProof, err := session.KeyPair.GenerateDPoPProof("GET", url, "") + if err != nil { + return false + } + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return false + } + req.Header.Set("Authorization", "Bearer "+session.AccessToken) + req.Header.Set("DPoP", dpopProof) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return false + } + defer resp.Body.Close() + + return resp.StatusCode == 200 +} diff --git a/backend/main.go b/backend/main.go index 3c10887..234d57d 100644 --- a/backend/main.go +++ b/backend/main.go @@ -247,6 +247,34 @@ func sessionHandler(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) } +func refreshTokens(session *auth.OAuthState, cookieValue string) error { + // Check for access token refresh + needsRefresh := time.Now().After(session.ExpiresAt) + pds := session.PDS + refreshToken := session.RefreshToken + keypair := session.KeyPair + sessionsMutex.RUnlock() + + if needsRefresh { + newTokens, err := auth.RefreshAccessToken(pds, refreshToken, backendURL+"/callback", keypair) + if err != nil { + sessionsMutex.Lock() + delete(sessions, cookieValue) + sessionsMutex.Unlock() + logger.Error("no authorization servers found for token refresh") + return fmt.Errorf("no authorization servers found") + } + + sessionsMutex.Lock() + session.AccessToken = newTokens.AccessToken + session.RefreshToken = newTokens.RefreshToken + session.ExpiresAt = time.Now().Add(time.Duration(newTokens.ExpiresIn) * time.Second) + sessionsMutex.Unlock() + } + + return nil +} + func postsHandler(w http.ResponseWriter, r *http.Request) { logger.Debug("posts handler start") @@ -272,32 +300,10 @@ func postsHandler(w http.ResponseWriter, r *http.Request) { logger.Debug("session public key", "key", session.KeyPair.PublicKey) } - // Check for access token refresh - needsRefresh := time.Now().After(session.ExpiresAt) - pds := session.PDS - refreshToken := session.RefreshToken - keypair := session.KeyPair - sessionsMutex.RUnlock() - - if needsRefresh { - newTokens, err := auth.RefreshAccessToken(pds, refreshToken, backendURL+"/callback", keypair) - if err != nil { - sessionsMutex.Lock() - delete(sessions, cookie.Value) - sessionsMutex.Unlock() - http.Error(w, "session expired, please login again", http.StatusUnauthorized) - return - } - - sessionsMutex.Lock() - session.AccessToken = newTokens.AccessToken - session.RefreshToken = newTokens.RefreshToken - session.ExpiresAt = time.Now().Add(time.Duration(newTokens.ExpiresIn) * time.Second) - sessionsMutex.Unlock() - } + refreshTokens(session, cookie.Value) // Fetch app.bsky.feed.post records - posts, err := fetchers.FetchAppBskyFeedPosts(logger, session.DID, session.PDS, slingshotURL, cache) + posts, err := fetchers.FetchAppBskyFeedPosts(logger, session.DID, session.PDS, slingshotURL, session, cache) if err != nil { logger.Error("fetching app.bsky.feed.posts failed", "error", err) http.Error(w, err.Error(), http.StatusInternalServerError) diff --git a/backend/notes/app.bsky.actor.go b/backend/notes/app.bsky.actor.go index dc5d536..797a8e8 100644 --- a/backend/notes/app.bsky.actor.go +++ b/backend/notes/app.bsky.actor.go @@ -17,6 +17,12 @@ type AppBskyActorProfileRecord struct { } `json:"ref"` } `json:"banner,omitempty"` Pronouns string `json:"pronouns"` + Labels *struct { + Type string `json:"$type"` + Values []struct { + Val string `json:"val"` + } `json:"values"` + } `json:"labels,omitempty"` } func AuthorFromAppBskyActorProfileRecord(record AppBskyActorProfileRecord, did, handle string) (*Author, error) { @@ -28,12 +34,23 @@ func AuthorFromAppBskyActorProfileRecord(record AppBskyActorProfileRecord, did, bannerURL = fmt.Sprintf("https://cdn.bsky.app/img/banner/plain/%s/%s@jpeg", did, record.Banner.Ref.Link) } + // check for !no-unauthenticated label + noUnauth := false + if record.Labels != nil && record.Labels.Type == "com.atproto.label.defs#selfLabels" { + for _, label := range record.Labels.Values { + if label.Val == "!no-unauthenticated" { + noUnauth = true + } + } + } + author := &Author{ Handle: handle, DisplayName: record.DisplayName, AvatarURL: avatarURL, BannerURL: bannerURL, Pronouns: record.Pronouns, + NoUnauth: noUnauth, } return author, nil diff --git a/backend/notes/app.bsky.feed.go b/backend/notes/app.bsky.feed.go index b5cfd03..961f8d7 100644 --- a/backend/notes/app.bsky.feed.go +++ b/backend/notes/app.bsky.feed.go @@ -89,14 +89,15 @@ func NoteFromAppBskyFeedPostRecord(record AppBskyFeedPostRecord, author *Author) did, _, _, err := general.ParseURI(record.URI) return &Note{ - URI: record.URI, - AuthorDID: did, - AuthorHandle: handle, - AuthorName: displayName, - AvatarURL: avatarURL, - CreatedAt: createdAt, - Text: record.Value.Text, - ReplyTo: replyTo, - Facets: facets, + URI: record.URI, + AuthorDID: did, + AuthorHandle: handle, + AuthorName: displayName, + AuthorNoUnauth: author.NoUnauth, + AvatarURL: avatarURL, + CreatedAt: createdAt, + Text: record.Value.Text, + ReplyTo: replyTo, + Facets: facets, }, nil } diff --git a/backend/notes/author.go b/backend/notes/author.go index 02afabf..644a10c 100644 --- a/backend/notes/author.go +++ b/backend/notes/author.go @@ -6,4 +6,5 @@ type Author struct { AvatarURL string BannerURL string Pronouns string + NoUnauth bool } diff --git a/backend/notes/note.go b/backend/notes/note.go index 7c8de36..3b32aa1 100644 --- a/backend/notes/note.go +++ b/backend/notes/note.go @@ -5,15 +5,17 @@ import ( ) type Note struct { - URI string `json:"uri"` - AuthorDID string `json:"authorDid"` - AuthorHandle string `json:"authorHandle"` - AuthorName string `json:"authorName"` - AvatarURL string `json:"avatarURL"` - Text string `json:"text"` - CreatedAt time.Time `json:"createdAt"` - ReplyTo *string `json:"replyTo,omitempty"` - Facets []Facet `json:"facets,omitempty"` + URI string `json:"uri"` + AuthorDID string `json:"authorDid"` + AuthorHandle string `json:"authorHandle"` + AuthorName string `json:"authorName"` + AuthorNoUnauth bool `json:"authorNoUnauth"` // author has !no-unauthenticated label + AvatarURL string `json:"avatarURL"` + Text string `json:"text"` + CreatedAt time.Time `json:"createdAt"` + ReplyTo *string `json:"replyTo,omitempty"` + Facets []Facet `json:"facets,omitempty"` + Parents []string `json:"parents,omitempty"` } type Facet struct { |
