-
Notifications
You must be signed in to change notification settings - Fork 144
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
283 additions
and
57 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
package client | ||
|
||
import ( | ||
"context" | ||
"encoding/base64" | ||
"net/http" | ||
|
||
"github.com/bluesky-social/indigo/atproto/syntax" | ||
) | ||
|
||
type AdminAuth struct { | ||
basicAuthHeader string | ||
} | ||
|
||
func NewAdminAuth(password string) AdminAuth { | ||
header := "Basic" + base64.StdEncoding.EncodeToString([]byte("admin:"+password)) | ||
return AdminAuth{basicAuthHeader: header} | ||
} | ||
|
||
func (a *AdminAuth) DoWithAuth(ctx context.Context, req *http.Request, httpClient *http.Client) (*http.Response, error) { | ||
req.Header.Set("Authorization", a.basicAuthHeader) | ||
return httpClient.Do(req) | ||
} | ||
|
||
// Admin bearer token auth does not involve an account DID | ||
func (a *AdminAuth) AccountDID() syntax.DID { | ||
return "" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
package client | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"io" | ||
"net/http" | ||
|
||
"github.com/bluesky-social/indigo/atproto/syntax" | ||
) | ||
|
||
// NOTE: this is an interface so it can be wrapped/extended. eg, a variant with a bunch of retries, or caching, or whatever. maybe that is too complex and we should have simple struct type, more like the existing `indigo/xrpc` package? hrm. | ||
|
||
type APIClient interface { | ||
// Full-power method for making atproto API requests. | ||
Do(ctx context.Context, req *APIRequest) (*http.Response, error) | ||
|
||
// High-level helper for simple JSON "Query" API calls. | ||
// | ||
// Does not work with all API endpoints. For more control, use the Do() method with APIRequest. | ||
Get(ctx context.Context, endpoint syntax.NSID, params map[string]string) (*json.RawMessage, error) | ||
|
||
// High-level helper for simple JSON-to-JSON "Procedure" API calls. | ||
// | ||
// Does not work with all API endpoints. For more control, use the Do() method with APIRequest. | ||
// TODO: what is the right type for body, to indicate it can be marshaled as JSON? | ||
Post(ctx context.Context, endpoint syntax.NSID, body any) (*json.RawMessage, error) | ||
|
||
// Returns the currently-authenticated account DID, or empty string if not available. | ||
AuthDID() syntax.DID | ||
} | ||
|
||
type APIRequest struct { | ||
HTTPVerb string // TODO: type? | ||
Endpoint syntax.NSID | ||
Body io.Reader | ||
QueryParams map[string]string // TODO: better type for this? | ||
Headers map[string]string | ||
} | ||
|
||
func (r *APIRequest) HTTPRequest(ctx context.Context, host string, headers map[string]string) (*http.Request, error) { | ||
// TODO: use 'url' to safely construct the request URL | ||
u := host + "/xrpc/" + r.Endpoint.String() | ||
// XXX: query params | ||
httpReq, err := http.NewRequestWithContext(ctx, r.HTTPVerb, u, r.Body) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
// first set default headers | ||
if headers != nil { | ||
for k, v := range headers { | ||
httpReq.Header.Set(k, v) | ||
} | ||
} | ||
|
||
// then request-specific take priority (overwrite) | ||
if r.Headers != nil { | ||
for k, v := range r.Headers { | ||
httpReq.Header.Set(k, v) | ||
} | ||
} | ||
|
||
return httpReq, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package client | ||
|
||
import ( | ||
"context" | ||
"net/http" | ||
|
||
"github.com/bluesky-social/indigo/atproto/syntax" | ||
) | ||
|
||
type AuthMethod interface { | ||
DoWithAuth(ctx context.Context, httpReq *http.Request, httpClient *http.Client) (*http.Response, error) | ||
AccountDID() syntax.DID | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
package client | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
"net/http" | ||
|
||
"github.com/bluesky-social/indigo/atproto/syntax" | ||
) | ||
|
||
type BaseAPIClient struct { | ||
HTTPClient *http.Client | ||
Host string | ||
Auth AuthMethod | ||
DefaultHeaders map[string]string | ||
} | ||
|
||
func (c *BaseAPIClient) Get(ctx context.Context, endpoint syntax.NSID, params map[string]string) (*json.RawMessage, error) { | ||
hdr := map[string]string{ | ||
"Accept": "application/json", | ||
} | ||
req := APIRequest{ | ||
HTTPVerb: "GET", | ||
Endpoint: endpoint, | ||
Body: nil, | ||
QueryParams: params, | ||
Headers: hdr, | ||
} | ||
resp, err := c.Do(ctx, req) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
defer resp.Body.Close() | ||
// TODO: duplicate error handling with Post()? | ||
if !(resp.StatusCode >= 200 && resp.StatusCode < 300) { | ||
return nil, fmt.Errorf("non-successful API request status: %d", resp.StatusCode) | ||
} | ||
|
||
var ret json.RawMessage | ||
if err := json.NewDecoder(resp.Body).Decode(&ret); err != nil { | ||
return nil, fmt.Errorf("expected JSON response body: %w", err) | ||
} | ||
return &ret, nil | ||
} | ||
|
||
func (c *BaseAPIClient) Post(ctx context.Context, endpoint syntax.NSID, body any) (*json.RawMessage, error) { | ||
bodyJSON, err := json.Marshal(body) | ||
if err != nil { | ||
return nil, err | ||
} | ||
hdr := map[string]string{ | ||
"Accept": "application/json", | ||
"Content-Type": "application/json", | ||
} | ||
req := APIRequest{ | ||
HTTPVerb: "POST", | ||
Endpoint: endpoint, | ||
Body: bytes.NewReader(bodyJSON), | ||
QueryParams: nil, | ||
Headers: hdr, | ||
} | ||
resp, err := c.Do(ctx, req) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
defer resp.Body.Close() | ||
// TODO: duplicate error handling with Get()? | ||
if !(resp.StatusCode >= 200 && resp.StatusCode < 300) { | ||
return nil, fmt.Errorf("non-successful API request status: %d", resp.StatusCode) | ||
} | ||
|
||
var ret json.RawMessage | ||
if err := json.NewDecoder(resp.Body).Decode(&ret); err != nil { | ||
return nil, fmt.Errorf("expected JSON response body: %w", err) | ||
} | ||
return &ret, nil | ||
} | ||
|
||
func (c *BaseAPIClient) Do(ctx context.Context, req APIRequest) (*http.Response, error) { | ||
httpReq, err := req.HTTPRequest(ctx, c.Host, c.DefaultHeaders) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
var resp *http.Response | ||
if c.Auth != nil { | ||
resp, err = c.Auth.DoWithAuth(ctx, httpReq, c.HTTPClient) | ||
} else { | ||
resp, err = c.HTTPClient.Do(httpReq) | ||
} | ||
if err != nil { | ||
return nil, err | ||
} | ||
// TODO: handle some common response errors: rate-limits, 5xx, auth required, etc | ||
return resp, nil | ||
} | ||
|
||
func (c *BaseAPIClient) AuthDID() syntax.DID { | ||
if c.Auth != nil { | ||
return c.Auth.AccountDID() | ||
} | ||
return "" | ||
} |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
package client | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"errors" | ||
"io" | ||
|
||
"github.com/bluesky-social/indigo/atproto/syntax" | ||
) | ||
|
||
// API for clients which pull data from the public atproto network. | ||
// | ||
// Implementations of this interface might resolve PDS instances for DIDs, and fetch data from there. Or they might talk to an archival relay or other network mirroring service. | ||
type NetClient interface { | ||
// Fetches record JSON, without verification or validation. A version (CID) can optionally be specified; use empty string to fetch the latest. | ||
// Returns the record as JSON, and the CID indicated by the server. Does not verify that the data (as CBOR) matches the CID, and does not cryptographically verify a "proof chain" to the record. | ||
GetRecordJSON(ctx context.Context, aturi syntax.ATURI, version syntax.CID) (*json.RawMessage, *syntax.CID, error) | ||
|
||
// Fetches the indicated record as CBOR, and authenticates it by checking both the cryptographic signature and Merkle Tree hashes from the current repo revision. A version (CID) can optionally be specified; use empty string to fetch the latest. | ||
// Returns the record as CBOR; the CID of the validated record, and the repo commit revision. | ||
VerifyRecordCBOR(ctx context.Context, aturi syntax.ATURI, version syntax.CID) (*[]byte, *syntax.CID, string, error) | ||
|
||
// Fetches repo export (CAR file). Optionally attempts to fetch only the diff "since" an earlier repo revision. | ||
GetRepoCAR(ctx context.Context, did syntax.DID, since string) (*io.Reader, error) | ||
|
||
// Fetches indicated blob. Does not validate the CID. Returns a reader (which calling code is responsible for closing). | ||
GetBlob(ctx context.Context, did syntax.DID, cid syntax.CID) (*io.Reader, error) | ||
CheckAccountStatus(ctx context.Context, did syntax.DID) (*AccountStatus, error) | ||
} | ||
|
||
// XXX: type alias to codegen? or just copy? this is protocol-level | ||
type AccountStatus struct { | ||
} | ||
|
||
func VerifyBlobCID(blob []byte, cid syntax.CID) error { | ||
// XXX: compute hash, check against provided CID | ||
return errors.New("Not Implemented") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
package client | ||
|
||
import ( | ||
"context" | ||
"net/http" | ||
|
||
"github.com/bluesky-social/indigo/atproto/syntax" | ||
) | ||
|
||
type RefreshAuth struct { | ||
AccessToken string | ||
RefreshToken string | ||
DID syntax.DID | ||
// The AuthHost might different from any APIClient host, if there is an entryway involved | ||
AuthHost string | ||
} | ||
|
||
// TODO: | ||
//func NewRefreshAuth(pdsHost, accountIdentifier, password string) (*RefreshAuth, error) { | ||
|
||
func (a *RefreshAuth) DoWithAuth(ctx context.Context, httpReq *http.Request, httpClient *http.Client) (*http.Response, error) { | ||
httpReq.Header.Set("Authorization", "Bearer "+a.AccessToken) | ||
// XXX: check response. if it is 403, because access token is expired, then take a lock and do a refresh | ||
// TODO: when doing a refresh request, copy at least the User-Agent header from httpReq, and re-use httpClient | ||
return httpClient.Do(httpReq) | ||
} | ||
|
||
// Admin bearer token auth does not involve an account DID | ||
func (a *RefreshAuth) AccountDID() syntax.DID { | ||
return a.DID | ||
} |