Live video on the AT Protocol

Proxy repo requests to correct service host

+196 -2
+151 -2
pkg/spxrpc/com_atproto_repo.go
··· 5 5 "fmt" 6 6 "io" 7 7 "net/http" 8 + "strings" 8 9 9 10 comatprototypes "github.com/bluesky-social/indigo/api/atproto" 10 11 lexutil "github.com/bluesky-social/indigo/lex/util" ··· 18 19 "stream.place/streamplace/pkg/log" 19 20 ) 20 21 22 + func resolveRepoService(ctx context.Context, repo string) (did, service, handle string, err error) { 23 + did = repo 24 + if !strings.HasPrefix(repo, "did:") { 25 + did, err = oatproxy.ResolveHandle(ctx, repo) 26 + if err != nil { 27 + return "", "", "", fmt.Errorf("failed to resolve handle %q: %w", repo, err) 28 + } 29 + } 30 + 31 + service, handle, err = oatproxy.ResolveService(ctx, did) 32 + if err != nil { 33 + return "", "", "", fmt.Errorf("failed to resolve service for did %q: %w", did, err) 34 + } 35 + 36 + return did, service, handle, nil 37 + } 38 + 21 39 func (s *Server) handleComAtprotoRepoUploadBlob(ctx context.Context, r io.Reader, contentType string) (*comatprototypes.RepoUploadBlob_Output, error) { 22 40 ctx, span := otel.Tracer("server").Start(ctx, "handleComAtprotoRepoUploadBlob") 23 41 defer span.End() ··· 43 61 } 44 62 45 63 func (s *Server) handleComAtprotoRepoDescribeRepo(ctx context.Context, repo string) (*comatprototypes.RepoDescribeRepo_Output, error) { 64 + did, svc, handle, err := resolveRepoService(ctx, repo) 65 + if err != nil { 66 + return nil, fmt.Errorf("handleComAtprotoRepoDescribeRepo: %w", err) 67 + } 68 + 69 + // if the service isn't the current host, we proxy the request 70 + if svc != s.cli.PublicHost { 71 + session, client := oatproxy.GetOAuthSession(ctx) 72 + if session != nil { 73 + // authenticated request 74 + var out *comatprototypes.RepoDescribeRepo_Output 75 + params := make(map[string]any) 76 + params["repo"] = repo 77 + 78 + err = client.Do(ctx, xrpc.Query, "application/json", "com.atproto.repo.describeRepo", params, nil, &out) 79 + if err != nil { 80 + log.Error(ctx, "upstream xrpc error", "error", err) 81 + return nil, err 82 + } 83 + return out, nil 84 + } else { 85 + // unauthenticated request 86 + var out comatprototypes.RepoDescribeRepo_Output 87 + params := make(map[string]interface{}) 88 + params["repo"] = repo 89 + 90 + err = makeUnauthenticatedRequest(ctx, svc, "com.atproto.repo.describeRepo", params, &out) 91 + if err != nil { 92 + log.Error(ctx, "upstream xrpc error", "error", err) 93 + return nil, err 94 + } 95 + return &out, nil 96 + } 97 + } 98 + 46 99 return &comatprototypes.RepoDescribeRepo_Output{ 47 - Handle: s.cli.PublicHost, 48 - Did: fmt.Sprintf("did:web:%s", s.cli.PublicHost), 100 + Handle: handle, 101 + Did: did, 49 102 DidDoc: atproto.DIDDoc(s.cli.PublicHost), 50 103 Collections: []string{ 51 104 "com.atproto.lexicon.schema", ··· 55 108 } 56 109 57 110 func (s *Server) handleComAtprotoRepoListRecords(ctx context.Context, collection string, cursor string, limit int, repo string, reverse *bool) (*comatprototypes.RepoListRecords_Output, error) { 111 + _, svc, _, err := resolveRepoService(ctx, repo) 112 + if err != nil { 113 + return nil, fmt.Errorf("handleComAtprotoRepoListRecords: %w", err) 114 + } 115 + // if the service isn't the current host, we proxy the request 116 + if svc != s.cli.PublicHost { 117 + session, client := oatproxy.GetOAuthSession(ctx) 118 + if session != nil { 119 + // authenticated request 120 + var out *comatprototypes.RepoListRecords_Output 121 + xrpcType := xrpc.Procedure 122 + params := make(map[string]any) 123 + params["collection"] = collection 124 + if cursor != "" { 125 + params["cursor"] = cursor 126 + } 127 + if limit != 0 { 128 + params["limit"] = limit 129 + } 130 + if reverse != nil { 131 + params["reverse"] = *reverse 132 + } 133 + err = client.Do(ctx, xrpcType, "application/json", "com.atproto.repo.listRecords", params, nil, &out) 134 + if err != nil { 135 + log.Error(ctx, "upstream xrpc error", "error", err) 136 + return nil, err 137 + } 138 + return out, nil 139 + } else { 140 + // unauthenticated request 141 + var out comatprototypes.RepoListRecords_Output 142 + params := make(map[string]interface{}) 143 + params["collection"] = collection 144 + if cursor != "" { 145 + params["cursor"] = cursor 146 + } 147 + if limit != 0 { 148 + params["limit"] = limit 149 + } 150 + if reverse != nil { 151 + params["reverse"] = *reverse 152 + } 153 + err = makeUnauthenticatedRequest(ctx, svc, "com.atproto.repo.listRecords", params, &out) 154 + if err != nil { 155 + log.Error(ctx, "upstream xrpc error", "error", err) 156 + return nil, err 157 + } 158 + return &out, nil 159 + } 160 + } 161 + 58 162 r, ses, err := atproto.OpenLexiconRepo(ctx) 59 163 if err != nil { 60 164 return nil, fmt.Errorf("handleComAtprotoRepoListRecords: failed to open repo: %w", err) ··· 82 186 } 83 187 84 188 func (s *Server) handleComAtprotoRepoGetRecord(ctx context.Context, c string, collection string, repo string, rkey string) (*comatprototypes.RepoGetRecord_Output, error) { 189 + _, svc, _, err := resolveRepoService(ctx, repo) 190 + if err != nil { 191 + return nil, fmt.Errorf("handleComAtprotoRepoGetRecord: %w", err) 192 + } 193 + 194 + // if the service isn't the current host, we proxy the request 195 + if svc != s.cli.PublicHost { 196 + session, client := oatproxy.GetOAuthSession(ctx) 197 + if session != nil { 198 + // authenticated request 199 + var out *comatprototypes.RepoGetRecord_Output 200 + params := make(map[string]interface{}) 201 + params["repo"] = repo 202 + params["collection"] = collection 203 + params["rkey"] = rkey 204 + if c != "" { 205 + params["cid"] = c 206 + } 207 + 208 + err = client.Do(ctx, xrpc.Query, "application/json", "com.atproto.repo.getRecord", params, nil, &out) 209 + if err != nil { 210 + log.Error(ctx, "upstream xrpc error", "error", err) 211 + return nil, err 212 + } 213 + return out, nil 214 + } else { 215 + // unauthenticated request 216 + var out comatprototypes.RepoGetRecord_Output 217 + params := make(map[string]interface{}) 218 + params["repo"] = repo 219 + params["collection"] = collection 220 + params["rkey"] = rkey 221 + if c != "" { 222 + params["cid"] = c 223 + } 224 + 225 + err = makeUnauthenticatedRequest(ctx, svc, "com.atproto.repo.getRecord", params, &out) 226 + if err != nil { 227 + log.Error(ctx, "upstream xrpc error", "error", err) 228 + return nil, err 229 + } 230 + return &out, nil 231 + } 232 + } 233 + 85 234 r, ses, err := atproto.OpenLexiconRepo(ctx) 86 235 if err != nil { 87 236 return nil, fmt.Errorf("handleComAtprotoRepoGetRecord: failed to open repo: %w", err)
+45
pkg/spxrpc/spxrpc.go
··· 2 2 3 3 import ( 4 4 "context" 5 + "encoding/json" 6 + "fmt" 7 + "io" 5 8 "net/http" 9 + "net/url" 6 10 "time" 7 11 8 12 "github.com/labstack/echo/v4" ··· 10 14 "github.com/slok/go-http-metrics/middleware" 11 15 echomiddleware "github.com/slok/go-http-metrics/middleware/echo" 12 16 "github.com/streamplace/oatproxy/pkg/oatproxy" 17 + "stream.place/streamplace/pkg/aqhttp" 13 18 "stream.place/streamplace/pkg/atproto" 14 19 "stream.place/streamplace/pkg/config" 15 20 "stream.place/streamplace/pkg/log" ··· 59 64 e.GET("/xrpc/*", s.HandleWildcard) 60 65 e.POST("/xrpc/*", s.HandleWildcard) 61 66 return s, nil 67 + } 68 + 69 + func makeUnauthenticatedRequest(ctx context.Context, service, method string, params map[string]interface{}, out interface{}) error { 70 + u, err := url.Parse(fmt.Sprintf("https://%s/xrpc/%s", service, method)) 71 + if err != nil { 72 + return fmt.Errorf("failed to parse URL: %w", err) 73 + } 74 + 75 + // add query parameters 76 + query := u.Query() 77 + for k, v := range params { 78 + query.Set(k, fmt.Sprintf("%v", v)) 79 + } 80 + u.RawQuery = query.Encode() 81 + 82 + req, err := http.NewRequestWithContext(ctx, "GET", u.String(), nil) 83 + if err != nil { 84 + return fmt.Errorf("failed to create request: %w", err) 85 + } 86 + 87 + resp, err := aqhttp.Client.Do(req) 88 + if err != nil { 89 + return fmt.Errorf("failed to make request: %w", err) 90 + } 91 + defer resp.Body.Close() 92 + 93 + if resp.StatusCode != http.StatusOK { 94 + return fmt.Errorf("upstream request failed with status %d", resp.StatusCode) 95 + } 96 + 97 + body, err := io.ReadAll(resp.Body) 98 + if err != nil { 99 + return fmt.Errorf("failed to read response body: %w", err) 100 + } 101 + 102 + if err := json.Unmarshal(body, out); err != nil { 103 + return fmt.Errorf("failed to unmarshal response: %w", err) 104 + } 105 + 106 + return nil 62 107 } 63 108 64 109 func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {