package api import ( "context" "encoding/json" "fmt" "io" "net/http" "strings" "github.com/cli/go-gh/v2/pkg/auth" ) // RESTClient wraps methods for the different types of // API requests that are supported by the server. type RESTClient struct { client *http.Client host string } func DefaultRESTClient() (*RESTClient, error) { return NewRESTClient(ClientOptions{}) } // RESTClient builds a client to send requests to GitHub REST API endpoints. // As part of the configuration a hostname, auth token, default set of headers, // and unix domain socket are resolved from the gh environment configuration. // These behaviors can be overridden using the opts argument. func NewRESTClient(opts ClientOptions) (*RESTClient, error) { if optionsNeedResolution(opts) { var err error opts, err = resolveOptions(opts) if err != nil { return nil, err } } client, err := NewHTTPClient(opts) if err != nil { return nil, err } return &RESTClient{ client: client, host: opts.Host, }, nil } // RequestWithContext issues a request with type specified by method to the // specified path with the specified body. // The response is returned rather than being populated // into a response argument. func (c *RESTClient) RequestWithContext(ctx context.Context, method string, path string, body io.Reader) (*http.Response, error) { url := restURL(c.host, path) req, err := http.NewRequestWithContext(ctx, method, url, body) if err != nil { return nil, err } resp, err := c.client.Do(req) if err != nil { return nil, err } success := resp.StatusCode >= 200 && resp.StatusCode < 300 if !success { defer resp.Body.Close() return nil, HandleHTTPError(resp) } return resp, err } // Request wraps RequestWithContext with context.Background. func (c *RESTClient) Request(method string, path string, body io.Reader) (*http.Response, error) { return c.RequestWithContext(context.Background(), method, path, body) } // DoWithContext issues a request with type specified by method to the // specified path with the specified body. // The response is populated into the response argument. func (c *RESTClient) DoWithContext(ctx context.Context, method string, path string, body io.Reader, response interface{}) error { url := restURL(c.host, path) req, err := http.NewRequestWithContext(ctx, method, url, body) if err != nil { return err } resp, err := c.client.Do(req) if err != nil { return err } success := resp.StatusCode >= 200 && resp.StatusCode < 300 if !success { defer resp.Body.Close() return HandleHTTPError(resp) } if resp.StatusCode == http.StatusNoContent { return nil } defer resp.Body.Close() b, err := io.ReadAll(resp.Body) if err != nil { return err } err = json.Unmarshal(b, &response) if err != nil { return err } return nil } // Do wraps DoWithContext with context.Background. func (c *RESTClient) Do(method string, path string, body io.Reader, response interface{}) error { return c.DoWithContext(context.Background(), method, path, body, response) } // Delete issues a DELETE request to the specified path. // The response is populated into the response argument. func (c *RESTClient) Delete(path string, resp interface{}) error { return c.Do(http.MethodDelete, path, nil, resp) } // Get issues a GET request to the specified path. // The response is populated into the response argument. func (c *RESTClient) Get(path string, resp interface{}) error { return c.Do(http.MethodGet, path, nil, resp) } // Patch issues a PATCH request to the specified path with the specified body. // The response is populated into the response argument. func (c *RESTClient) Patch(path string, body io.Reader, resp interface{}) error { return c.Do(http.MethodPatch, path, body, resp) } // Post issues a POST request to the specified path with the specified body. // The response is populated into the response argument. func (c *RESTClient) Post(path string, body io.Reader, resp interface{}) error { return c.Do(http.MethodPost, path, body, resp) } // Put issues a PUT request to the specified path with the specified body. // The response is populated into the response argument. func (c *RESTClient) Put(path string, body io.Reader, resp interface{}) error { return c.Do(http.MethodPut, path, body, resp) } func restURL(hostname string, pathOrURL string) string { if strings.HasPrefix(pathOrURL, "https://") || strings.HasPrefix(pathOrURL, "http://") { return pathOrURL } return restPrefix(hostname) + pathOrURL } func restPrefix(hostname string) string { if isGarage(hostname) { return fmt.Sprintf("https://%s/api/v3/", hostname) } hostname = auth.NormalizeHostname(hostname) if auth.IsEnterprise(hostname) { return fmt.Sprintf("https://%s/api/v3/", hostname) } if strings.EqualFold(hostname, localhost) { return fmt.Sprintf("http://api.%s/", hostname) } return fmt.Sprintf("https://api.%s/", hostname) }