126 lines
4.2 KiB
Go
126 lines
4.2 KiB
Go
package gitea
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
type RemoteAsset struct {
|
|
ID int64
|
|
Filename string
|
|
Size int64
|
|
DownloadURL string
|
|
}
|
|
|
|
type assetResponse struct {
|
|
ID int64 `json:"id"`
|
|
Name string `json:"name"`
|
|
Size int64 `json:"size"`
|
|
BrowserDownloadURL string `json:"browser_download_url"`
|
|
}
|
|
|
|
func (c Client) listAssets(ctx context.Context, client *http.Client, token string, releaseID int64, apiRoot string) ([]RemoteAsset, error) {
|
|
if releaseID <= 0 {
|
|
return nil, failure(FailureInvalid, "list release assets", 0, errors.New("release ID must be positive"))
|
|
}
|
|
assetsURL := apiRoot + "/releases/" + strconv.FormatInt(releaseID, 10) + "/assets"
|
|
var listed []assetResponse
|
|
if err := c.getJSON(ctx, client, token, "list release assets", assetsURL, &listed); err != nil {
|
|
return nil, err
|
|
}
|
|
if listed == nil {
|
|
return nil, failure(FailureMalformed, "decode release assets", http.StatusOK, errors.New("attachment response must be a JSON array"))
|
|
}
|
|
assets := make([]RemoteAsset, 0, len(listed))
|
|
for _, item := range listed {
|
|
if item.ID <= 0 || item.Name == "" || item.Size < 0 {
|
|
return nil, failure(FailureMalformed, "decode release assets", http.StatusOK, errors.New("attachment entry is missing its ID, name, or size"))
|
|
}
|
|
assets = append(assets, RemoteAsset{ID: item.ID, Filename: item.Name, Size: item.Size, DownloadURL: item.BrowserDownloadURL})
|
|
}
|
|
return assets, nil
|
|
}
|
|
|
|
func (c Client) openAsset(ctx context.Context, client *http.Client, base *url.URL, token, rawURL string) (io.ReadCloser, error) {
|
|
if strings.TrimSpace(rawURL) == "" {
|
|
return nil, failure(FailureUnverifiable, "download release asset", 0, errors.New("attachment has no download URL"))
|
|
}
|
|
assetURL, err := base.Parse(rawURL)
|
|
if err != nil || assetURL.User != nil {
|
|
return nil, failure(FailureUnverifiable, "download release asset", 0, errors.New("invalid attachment URL"))
|
|
}
|
|
assetURL.Fragment = ""
|
|
visited := make(map[string]struct{})
|
|
followed := 0
|
|
for {
|
|
key := assetURL.String()
|
|
if _, exists := visited[key]; exists {
|
|
return nil, failure(FailureUnverifiable, "download release asset", 0, errors.New("asset redirect loop"))
|
|
}
|
|
visited[key] = struct{}{}
|
|
request, err := http.NewRequestWithContext(ctx, http.MethodGet, key, nil)
|
|
if err != nil {
|
|
return nil, failure(FailureUnverifiable, "download release asset", 0, err)
|
|
}
|
|
if sameOrigin(base, assetURL) {
|
|
request.Header.Set("Authorization", "token "+token)
|
|
}
|
|
request.Header.Set("Accept", "application/octet-stream")
|
|
response, err := client.Do(request)
|
|
if err != nil {
|
|
return nil, failure(FailureUnverifiable, "download release asset", 0, err)
|
|
}
|
|
if isRedirect(response.StatusCode) {
|
|
if followed == 10 {
|
|
_ = response.Body.Close()
|
|
return nil, failure(FailureUnverifiable, "redirect release asset: redirect limit exceeded", response.StatusCode, errors.New("at most 10 redirects are allowed"))
|
|
}
|
|
location, locationErr := response.Location()
|
|
_ = response.Body.Close()
|
|
if locationErr != nil || location.User != nil {
|
|
return nil, failure(FailureUnverifiable, "redirect release asset", response.StatusCode, errors.New("invalid attachment redirect"))
|
|
}
|
|
location.Fragment = ""
|
|
assetURL = location
|
|
followed++
|
|
continue
|
|
}
|
|
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
|
_ = response.Body.Close()
|
|
return nil, responseFailure("download release asset", response.StatusCode)
|
|
}
|
|
return response.Body, nil
|
|
}
|
|
}
|
|
|
|
func sameOrigin(base, target *url.URL) bool {
|
|
if target == nil || target.User != nil || target.Host == "" {
|
|
return false
|
|
}
|
|
return strings.EqualFold(base.Scheme, target.Scheme) && strings.EqualFold(base.Hostname(), target.Hostname()) && effectivePort(base) == effectivePort(target)
|
|
}
|
|
|
|
func effectivePort(parsed *url.URL) string {
|
|
if port := parsed.Port(); port != "" {
|
|
return port
|
|
}
|
|
if strings.EqualFold(parsed.Scheme, "https") {
|
|
return "443"
|
|
}
|
|
return "80"
|
|
}
|
|
|
|
func isRedirect(status int) bool {
|
|
switch status {
|
|
case http.StatusMovedPermanently, http.StatusFound, http.StatusSeeOther, http.StatusTemporaryRedirect, http.StatusPermanentRedirect:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|