Something went wrong. Try again.
[mirror] Scalable static site server for Git forges (like GitHub Pages)
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254package git_pages
import ( "encoding/json" "errors" "fmt" "net/http" "net/url" "strings" "time")
const maxForgeResponseSize = 65536
var errResponseTooLong = errors.New("forge response too long")
func makeGogsAPIRequest( baseURL *url.URL, authorization string, endpoint string,) (*http.Request, *http.Response, error) { request, err := http.NewRequest("GET", baseURL.ResolveReference(&url.URL{ Path: fmt.Sprintf("/api/v1/%s", endpoint), }).String(), nil) if err != nil { panic(err) // misconfiguration } request.Header.Set("Accept", "application/json") request.Header.Set("Authorization", authorization)
httpClient := http.Client{Timeout: 5 * time.Second} response, err := httpClient.Do(request) return request, response, err}
// Gogs, Gitea, and Forgejo all support the same API here.func FetchGogsAuthorizedUser(baseURL *url.URL, authorization string) (*ForgeUser, error) { request, response, err := makeGogsAPIRequest(baseURL, authorization, "user") if err != nil { return nil, AuthError{ http.StatusServiceUnavailable, fmt.Sprintf("cannot fetch authorized forge user: %s", err), } } defer response.Body.Close()
if response.StatusCode != http.StatusOK { return nil, AuthError{ http.StatusServiceUnavailable, fmt.Sprintf( "cannot fetch authorized forge user: GET %s returned %s", request.URL, response.Status, ), } }
var userInfo struct { ID int64 `json:"id"` Login string `json:"login"` } decoder := json.NewDecoder(ReadAtMost(response.Body, maxForgeResponseSize, errResponseTooLong)) if err := decoder.Decode(&userInfo); err != nil { return nil, errors.Join(AuthError{ http.StatusServiceUnavailable, fmt.Sprintf( "cannot fetch authorized forge user: GET %s returned malformed JSON", request.URL, ), }, err) }
origin := request.URL.Hostname() return &ForgeUser{ Origin: &origin, Id: &userInfo.ID, Name: &userInfo.Login, }, nil}
// Gogs, Gitea, and Forgejo all support the same API here.func CheckGogsRepositoryPushPermission(baseURL *url.URL, authorization string) error { ownerAndRepo := strings.TrimSuffix(strings.TrimPrefix(baseURL.Path, "/"), ".git") request, response, err := makeGogsAPIRequest(baseURL, authorization, fmt.Sprintf("repos/%s", ownerAndRepo)) if err != nil { return AuthError{ http.StatusServiceUnavailable, fmt.Sprintf("cannot check repository permissions: %s", err), } } defer response.Body.Close()
if response.StatusCode == http.StatusNotFound { return AuthError{ http.StatusNotFound, fmt.Sprintf("no repository %s", ownerAndRepo), } } else if response.StatusCode == http.StatusUnauthorized { return AuthError{ http.StatusUnauthorized, fmt.Sprintf("no access to %s or invalid token", ownerAndRepo), } } else if response.StatusCode != http.StatusOK { return AuthError{ http.StatusServiceUnavailable, fmt.Sprintf( "cannot check repository permissions: GET %s returned %s", request.URL, response.Status, ), } }
var repositoryInfo struct { FullName string `json:"full_name"` Permissions struct { Push bool `json:"push"` } `json:"permissions"` } decoder := json.NewDecoder(ReadAtMost(response.Body, maxForgeResponseSize, errResponseTooLong)) if err := decoder.Decode(&repositoryInfo); err != nil { return errors.Join(AuthError{ http.StatusServiceUnavailable, fmt.Sprintf( "cannot check repository permissions: GET %s returned malformed JSON", request.URL, ), }, err) }
if repositoryInfo.FullName != ownerAndRepo { return AuthError{ http.StatusUnauthorized, fmt.Sprintf("authorization out-of-date: repository %s has been renamed to %s", ownerAndRepo, repositoryInfo.FullName), } }
if !repositoryInfo.Permissions.Push { return AuthError{ http.StatusUnauthorized, fmt.Sprintf("no push permission for %s", ownerAndRepo), } }
// this token authorizes pushing to the repo, yay! return nil}
type ForgeActionRun struct { TriggerUser *ForgeUser PullRequest *ForgePullRequest // only if `event == "pull_request"`}
type ForgePullRequest struct { OwnerID int64 OwnerName string RepositoryID int64 RepositoryName string Number int64}
// This is a Forgejo-specific API added in https://codeberg.org/forgejo/forgejo/pulls/12727// and available starting in Forgejo vX.Y.func FetchForgejoActionRun(baseURL *url.URL, authorization string) (*ForgeActionRun, error) { request, response, err := makeGogsAPIRequest(baseURL, authorization, "actions/run") if err != nil { return nil, AuthError{ http.StatusServiceUnavailable, fmt.Sprintf("cannot fetch workflow run's pull request: %s", err), } } defer response.Body.Close()
if response.StatusCode == http.StatusForbidden { return nil, AuthError{http.StatusForbidden, "not an automatic actions token"} } else if response.StatusCode == http.StatusUnauthorized { return nil, AuthError{http.StatusUnauthorized, "malformed token"} } else if response.StatusCode == http.StatusNotFound { return nil, AuthError{http.StatusInternalServerError, "endpoint not available"} } else if response.StatusCode != http.StatusOK { return nil, AuthError{ http.StatusServiceUnavailable, fmt.Sprintf( "cannot fetch workflow run's pull request: GET %s returned %s", request.URL, response.Status, ), } }
var runInfo struct { Event string `json:"event"` EventPayload string `json:"event_payload"` TriggerUser struct { ID int64 `json:"id"` Username string `json:"username"` } `json:"trigger_user"` } decoder := json.NewDecoder(ReadAtMost(response.Body, maxForgeResponseSize, errResponseTooLong)) if err := decoder.Decode(&runInfo); err != nil { return nil, errors.Join(AuthError{ http.StatusServiceUnavailable, fmt.Sprintf( "cannot fetch workflow run's pull request: GET %s returned malformed JSON", request.URL, ), }, err) } origin := request.URL.Hostname() actionRun := &ForgeActionRun{ TriggerUser: &ForgeUser{ Origin: &origin, Id: &runInfo.TriggerUser.ID, Name: &runInfo.TriggerUser.Username, }, }
if runInfo.Event == "pull_request" { var eventInfo struct { Number int64 `json:"number"` Repository struct { ID int64 `json:"id"` Name string `json:"name"` Owner struct { ID int64 `json:"id"` Username string `json:"username"` } } } decoder = json.NewDecoder(strings.NewReader(runInfo.EventPayload)) if err := decoder.Decode(&eventInfo); err != nil { return nil, errors.Join(AuthError{ http.StatusServiceUnavailable, fmt.Sprintf( "cannot fetch workflow run's pull request: GET %s returned malformed JSON "+ "in event payload", request.URL, ), }, err) } actionRun.PullRequest = &ForgePullRequest{ OwnerID: eventInfo.Repository.Owner.ID, OwnerName: eventInfo.Repository.Owner.Username, RepositoryID: eventInfo.Repository.ID, RepositoryName: eventInfo.Repository.Name, Number: eventInfo.Number, } } else { // we aren't decoding the other events }
return actionRun, nil}