From a2420359fa00d62b15b93122a735bc02f9b76d4b Mon Sep 17 00:00:00 2001 From: Maxim Lebedev Date: Wed, 16 Feb 2022 20:30:36 +0500 Subject: [PATCH] :card_file_box: Added metadata repository implementations --- internal/metadata/repository.go | 17 ++++ .../metadata/repository/http/http_metadata.go | 90 +++++++++++++++++++ .../repository/memory/memory_metadata.go | 36 ++++++++ 3 files changed, 143 insertions(+) create mode 100644 internal/metadata/repository.go create mode 100644 internal/metadata/repository/http/http_metadata.go create mode 100644 internal/metadata/repository/memory/memory_metadata.go diff --git a/internal/metadata/repository.go b/internal/metadata/repository.go new file mode 100644 index 0000000..f558da5 --- /dev/null +++ b/internal/metadata/repository.go @@ -0,0 +1,17 @@ +package metadata + +import ( + "context" + + "source.toby3d.me/website/indieauth/internal/domain" +) + +type Repository interface { + Get(ctx context.Context, me *domain.Me) (*domain.Metadata, error) +} + +var ErrNotExist error = domain.NewError( + domain.ErrorCodeInvalidClient, + "not found 'indieauth-metadata' endpoint on provided me URL", + "https://indieauth.net/source/#discovery-0", +) diff --git a/internal/metadata/repository/http/http_metadata.go b/internal/metadata/repository/http/http_metadata.go new file mode 100644 index 0000000..0f29e0e --- /dev/null +++ b/internal/metadata/repository/http/http_metadata.go @@ -0,0 +1,90 @@ +package http + +import ( + "context" + "encoding/json" + "fmt" + + http "github.com/valyala/fasthttp" + + "source.toby3d.me/website/indieauth/internal/domain" + "source.toby3d.me/website/indieauth/internal/metadata" + "source.toby3d.me/website/indieauth/internal/util" +) + +type ( + //nolint: tagliatelle,lll + Metadata struct { + Issuer *domain.ClientID `json:"issuer"` + AuthorizationEndpoint *domain.URL `json:"authorization_endpoint"` + IntrospectionEndpoint *domain.URL `json:"introspection_endpoint"` + RevocationEndpoint *domain.URL `json:"revocation_endpoint,omitempty"` + ServiceDocumentation *domain.URL `json:"service_documentation,omitempty"` + TokenEndpoint *domain.URL `json:"token_endpoint"` + UserinfoEndpoint *domain.URL `json:"userinfo_endpoint,omitempty"` + CodeChallengeMethodsSupported []domain.CodeChallengeMethod `json:"code_challenge_methods_supported"` + GrantTypesSupported []domain.GrantType `json:"grant_types_supported,omitempty"` + ResponseTypesSupported []domain.ResponseType `json:"response_types_supported,omitempty"` + ScopesSupported []domain.Scope `json:"scopes_supported,omitempty"` + IntrospectionEndpointAuthMethodsSupported []string `json:"introspection_endpoint_auth_methods_supported,omitempty"` + RevocationEndpointAuthMethodsSupported []string `json:"revocation_endpoint_auth_methods_supported,omitempty"` + AuthorizationResponseIssParameterSupported bool `json:"authorization_response_iss_parameter_supported,omitempty"` + } + + httpMetadataRepository struct { + client *http.Client + } +) + +const DefaultMaxRedirectsCount int = 10 + +func NewHTTPMetadataRepository(client *http.Client) metadata.Repository { + return &httpMetadataRepository{ + client: client, + } +} + +func (repo *httpMetadataRepository) Get(ctx context.Context, me *domain.Me) (*domain.Metadata, error) { + req := http.AcquireRequest() + defer http.ReleaseRequest(req) + req.SetRequestURI(me.String()) + req.Header.SetMethod(http.MethodGet) + + resp := http.AcquireResponse() + defer http.ReleaseResponse(resp) + + if err := repo.client.DoRedirects(req, resp, DefaultMaxRedirectsCount); err != nil { + return nil, fmt.Errorf("failed to make a request to the client: %w", err) + } + + endpoints := util.ExtractEndpoints(resp, "indieauth-metadata") + if len(endpoints) == 0 { + return nil, metadata.ErrNotExist + } + + _, body, err := repo.client.Get(nil, endpoints[len(endpoints)-1].String()) + if err != nil { + return nil, fmt.Errorf("failed to fetch metadata endpoint configuration: %w", err) + } + + data := new(Metadata) + if err = json.Unmarshal(body, data); err != nil { + return nil, fmt.Errorf("cannot unmarshal metadata configuration: %w", err) + } + + return &domain.Metadata{ + AuthorizationEndpoint: data.AuthorizationEndpoint, + AuthorizationResponseIssParameterSupported: data.AuthorizationResponseIssParameterSupported, + CodeChallengeMethodsSupported: data.CodeChallengeMethodsSupported, + GrantTypesSupported: data.GrantTypesSupported, + Issuer: data.Issuer, + ResponseTypesSupported: data.ResponseTypesSupported, + ScopesSupported: data.ScopesSupported, + ServiceDocumentation: data.ServiceDocumentation, + TokenEndpoint: data.TokenEndpoint, + // TODO(toby3d): support extensions? + // Micropub: data.Micropub, + // Microsub: data.Microsub, + // TicketEndpoint: data.TicketEndpoint, + }, nil +} diff --git a/internal/metadata/repository/memory/memory_metadata.go b/internal/metadata/repository/memory/memory_metadata.go new file mode 100644 index 0000000..84ad8ce --- /dev/null +++ b/internal/metadata/repository/memory/memory_metadata.go @@ -0,0 +1,36 @@ +package memory + +import ( + "context" + "path" + "sync" + + "source.toby3d.me/website/indieauth/internal/domain" + "source.toby3d.me/website/indieauth/internal/metadata" +) + +type memoryMetadataRepository struct { + store *sync.Map +} + +const DefaultPathPrefix = "metadata" + +func NewMemoryMetadataRepository(store *sync.Map) metadata.Repository { + return &memoryMetadataRepository{ + store: store, + } +} + +func (repo *memoryMetadataRepository) Get(ctx context.Context, me *domain.Me) (*domain.Metadata, error) { + src, ok := repo.store.Load(path.Join(DefaultPathPrefix, me.String())) + if !ok { + return nil, metadata.ErrNotExist + } + + result, ok := src.(*domain.Metadata) + if !ok { + return nil, metadata.ErrNotExist + } + + return result, nil +}