tools/module/gitea/main.go

399 lines
11 KiB
Go
Raw Normal View History

2022-03-23 00:39:53 +00:00
package gitea
import (
2022-09-09 16:05:46 +01:00
"bytes"
2022-03-23 00:39:53 +00:00
"code.gitea.io/sdk/gitea"
2022-09-09 16:05:46 +01:00
"code.mrmelon54.com/melon/tools/module/gitea/assets"
2022-09-08 14:48:36 +01:00
"code.mrmelon54.com/melon/tools/utils"
"context"
_ "embed"
"encoding/gob"
2022-03-23 00:39:53 +00:00
"fmt"
"github.com/google/uuid"
2022-03-23 00:39:53 +00:00
"github.com/gorilla/mux"
"golang.org/x/oauth2"
2022-09-09 16:05:46 +01:00
"gopkg.in/yaml.v3"
"html/template"
2022-09-09 16:05:46 +01:00
"io"
2022-03-24 21:07:44 +00:00
"net/http"
"os"
2022-09-08 17:22:22 +01:00
"path/filepath"
2022-03-26 11:43:55 +00:00
"strings"
2022-09-09 16:05:46 +01:00
"time"
2022-03-23 00:39:53 +00:00
)
2022-09-09 16:05:46 +01:00
var (
//go:embed pages/index.go.html
indexTemplate string
//go:embed pages/swagger.go.html
swaggerTemplate string
returnCookie = "melon-tools-return-gitea"
)
2022-03-23 00:39:53 +00:00
type Module struct {
sessionWrapper func(cb func(http.ResponseWriter, *http.Request, *utils.State)) func(rw http.ResponseWriter, req *http.Request)
2022-03-28 21:22:34 +01:00
oauthClient *oauth2.Config
2022-03-23 00:39:53 +00:00
}
type giteaKeyType int
const (
2022-03-26 00:26:20 +00:00
KeyOauthClient = giteaKeyType(iota)
KeyUser
KeyState
KeyAccessToken
KeyRefreshToken
)
2022-03-24 21:07:44 +00:00
2022-03-23 00:39:53 +00:00
func New() *Module {
gob.Register(new(giteaKeyType))
2022-03-23 00:39:53 +00:00
return &Module{}
}
2022-03-28 21:22:34 +01:00
func (m *Module) GetName() string { return "Gitea" }
func (m *Module) GetEndpoint() string { return "/gitea" }
func (m *Module) SetupModule(router *mux.Router, f func(cb func(http.ResponseWriter, *http.Request, *utils.State)) func(rw http.ResponseWriter, req *http.Request)) {
m.sessionWrapper = f
m.oauthClient = &oauth2.Config{
ClientID: os.Getenv("GITEA_CLIENT_ID"),
ClientSecret: os.Getenv("GITEA_CLIENT_SECRET"),
Scopes: []string{"openid"},
Endpoint: oauth2.Endpoint{
AuthURL: os.Getenv("GITEA_AUTHORIZE_URL"),
TokenURL: os.Getenv("GITEA_TOKEN_URL"),
},
RedirectURL: os.Getenv("GITEA_REDIRECT_URL"),
}
2022-03-24 21:07:44 +00:00
router.HandleFunc("/", m.getClient(m.homepage))
router.HandleFunc("/login", m.sessionWrapper(m.loginPage))
router.PathPrefix("/spec/{org}/{repo}/{sha}/{spec:.+}").HandlerFunc(m.getClient(m.specPage))
2022-09-09 16:48:18 +01:00
router.PathPrefix("/spec-raw/{org}/{repo}/{sha}/{spec:.+}").HandlerFunc(m.getClient(m.specRawPage))
2022-09-09 16:05:46 +01:00
router.PathPrefix("/swagger").HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
p := filepath.Join("swagger", filepath.Base(req.URL.Path))
open, err := assets.SwaggerAssets.Open(p)
if err != nil {
http.NotFound(rw, req)
return
}
stat, err := open.Stat()
if err != nil {
http.NotFound(rw, req)
return
}
seeker, ok := open.(io.ReadSeeker)
if ok {
http.ServeContent(rw, req, p, stat.ModTime(), seeker)
} else {
http.NotFound(rw, req)
}
})
2022-03-24 21:07:44 +00:00
}
func (m *Module) getClient(cb func(http.ResponseWriter, *http.Request, *utils.State, *gitea.Client)) func(rw http.ResponseWriter, req *http.Request) {
return m.sessionWrapper(func(rw http.ResponseWriter, req *http.Request, state *utils.State) {
2022-03-26 00:26:20 +00:00
if v, ok := utils.GetStateValue[*gitea.Client](state, KeyOauthClient); ok {
cb(rw, req, state, v)
2022-03-24 21:07:44 +00:00
return
}
2022-09-09 16:05:46 +01:00
http.SetCookie(rw, &http.Cookie{
Name: returnCookie,
Value: req.RequestURI,
Path: "/gitea",
Expires: time.Now().Add(time.Hour * 1),
MaxAge: 3600,
})
http.Redirect(rw, req, "/gitea/login", http.StatusTemporaryRedirect)
2022-03-24 21:07:44 +00:00
})
}
func (m *Module) homepage(rw http.ResponseWriter, req *http.Request, state *utils.State, giteaClient *gitea.Client) {
2022-09-09 16:05:46 +01:00
cookie, err := req.Cookie(returnCookie)
if err == nil {
if cookie.Valid() != nil {
http.SetCookie(rw, &http.Cookie{Name: returnCookie, Value: "", Path: "/gitea", Expires: time.Now().Add(-time.Hour), MaxAge: 0})
http.Redirect(rw, req, cookie.Value, http.StatusTemporaryRedirect)
return
}
}
2022-03-24 21:07:44 +00:00
myUser, _, err := giteaClient.GetMyUserInfo()
2022-03-23 00:39:53 +00:00
if err != nil {
2022-03-26 00:26:20 +00:00
state.Del(KeyOauthClient)
2022-03-24 21:07:44 +00:00
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
orgs, _, err := giteaClient.ListMyOrgs(gitea.ListOrgsOptions{})
if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
2022-03-23 00:39:53 +00:00
return
}
orgSimple := make([]struct{ Name string }, len(orgs))
for i, j := range orgs {
orgSimple[i] = struct{ Name string }{j.UserName}
}
2022-03-26 00:26:20 +00:00
selOrg := ""
myOrg := ""
2022-09-08 17:22:22 +01:00
repoSimple := make([]struct {
Name string
Private bool
}, 0)
2022-03-26 00:26:20 +00:00
selRepo := ""
2022-03-26 11:43:55 +00:00
selModule := ""
selCommitTime := ""
selCommitHash := ""
selCommitHashShort := ""
2022-09-09 16:05:46 +01:00
mySpecs := make([]struct {
Name string
Code int
}, 0)
2022-03-26 00:26:20 +00:00
q := req.URL.Query()
if q.Has("org") {
selOrg = q.Get("org")
2022-04-11 18:58:51 +01:00
var repos []*gitea.Repository
2022-09-08 17:22:22 +01:00
if selOrg == "!me" {
myOrg = myUser.UserName
2022-09-08 17:22:22 +01:00
repos, _, err = giteaClient.ListMyRepos(gitea.ListReposOptions{ListOptions: gitea.ListOptions{Page: 0, PageSize: 50}})
2022-04-11 18:58:51 +01:00
} else {
myOrg = selOrg
2022-09-08 17:22:22 +01:00
repos, _, err = giteaClient.ListOrgRepos(myOrg, gitea.ListOrgReposOptions{ListOptions: gitea.ListOptions{Page: 0, PageSize: 50}})
2022-04-11 18:58:51 +01:00
}
2022-03-26 00:26:20 +00:00
if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
2022-09-08 17:22:22 +01:00
repoSimple = make([]struct {
Name string
Private bool
}, len(repos))
2022-03-26 00:26:20 +00:00
for i, j := range repos {
2022-09-08 17:22:22 +01:00
repoSimple[i] = struct {
Name string
Private bool
}{Name: j.Name, Private: j.Private}
2022-03-26 00:26:20 +00:00
}
if q.Has("repo") {
selRepo = q.Get("repo")
2022-09-08 17:22:22 +01:00
repo, resp, err := giteaClient.GetRepo(myOrg, selRepo)
2022-03-26 00:26:20 +00:00
if err != nil {
2022-09-08 17:22:22 +01:00
if resp.StatusCode != http.StatusNotFound {
http.Error(rw, "GetRepo: "+err.Error(), http.StatusInternalServerError)
return
}
2022-03-26 00:26:20 +00:00
}
2022-09-08 17:22:22 +01:00
refs, resp, err := giteaClient.GetRepoRefs(myOrg, selRepo, "heads/"+repo.DefaultBranch)
2022-03-26 11:43:55 +00:00
if err != nil {
2022-09-08 17:22:22 +01:00
if resp.StatusCode != http.StatusNotFound {
http.Error(rw, "GetRepoRefs: "+err.Error(), http.StatusInternalServerError)
return
}
refs = make([]*gitea.Reference, 0)
2022-03-26 11:43:55 +00:00
}
if len(refs) == 1 {
ref := refs[0]
commit, _, err := giteaClient.GetSingleCommit(myOrg, selRepo, ref.Object.SHA)
2022-03-26 11:43:55 +00:00
if err != nil {
2022-05-16 12:35:31 +01:00
http.Error(rw, "GetSingleCommit: "+err.Error(), http.StatusInternalServerError)
2022-03-26 11:43:55 +00:00
return
}
2022-09-08 17:22:22 +01:00
2022-04-05 09:15:42 +01:00
selCommitTime = commit.CommitMeta.Created.UTC().Format("20060102150405")
selCommitHash = commit.CommitMeta.SHA
selCommitHashShort = commit.CommitMeta.SHA[:12]
2022-09-08 17:22:22 +01:00
goMod, resp, err := giteaClient.GetFile(myOrg, selRepo, ref.Object.SHA, "go.mod")
2022-03-26 11:43:55 +00:00
if err != nil {
2022-09-08 17:22:22 +01:00
if resp.StatusCode != http.StatusNotFound {
http.Error(rw, "go.mod: "+err.Error(), http.StatusInternalServerError)
return
}
2022-03-26 11:43:55 +00:00
}
goModStr := string(goMod)
goModIdx := strings.Index(goModStr, "\n")
goModLine := goModStr[:goModIdx]
goModSpace := strings.Index(goModLine, " ")
selModule = goModLine[goModSpace+1:]
2022-09-09 16:05:46 +01:00
if resp.StatusCode == http.StatusNotFound {
selModule = ""
}
2022-09-08 17:22:22 +01:00
trees, resp, err := giteaClient.GetTrees(myOrg, selRepo, ref.Object.SHA, true)
if err != nil {
if resp.StatusCode != http.StatusNotFound {
http.Error(rw, "%s: "+err.Error(), http.StatusInternalServerError)
return
}
}
2022-09-09 16:05:46 +01:00
for _, i := range trees.Entries {
switch filepath.Ext(i.Path) {
2022-09-08 17:22:22 +01:00
case ".yml", ".yaml":
2022-09-09 16:05:46 +01:00
file, resp, err := giteaClient.GetFile(myOrg, selRepo, ref.Object.SHA, i.Path)
if err != nil {
switch resp.StatusCode {
case http.StatusForbidden, http.StatusUnauthorized, http.StatusNotFound:
mySpecs = append(mySpecs, struct {
Name string
Code int
}{Name: i.Path, Code: resp.StatusCode})
}
continue
}
a := struct {
OpenAPI string `yaml:"openapi"`
}{}
err = yaml.Unmarshal(file, &a)
if err != nil || a.OpenAPI == "" {
continue
}
mySpecs = append(mySpecs, struct {
Name string
Code int
}{Name: i.Path, Code: http.StatusOK})
2022-09-08 17:22:22 +01:00
}
}
2022-03-26 00:26:20 +00:00
}
}
}
tmp, err := template.New("homepage").Parse(indexTemplate)
if err != nil {
fmt.Println("Template parse error:", err)
return
}
err = tmp.Execute(rw, struct {
2022-09-08 17:22:22 +01:00
Username string
Orgs []struct{ Name string }
Repos []struct {
Name string
Private bool
}
MyOrg string
SelOrg string
SelRepo string
ShowOrg bool
SelModule string
ShowGoMod bool
CommitTime string
CommitHash string
CommitHashShort string
ShowSpec bool
Specs []struct {
2022-09-09 16:05:46 +01:00
Name string
Code int
}
}{
Username: myUser.UserName,
Orgs: orgSimple,
Repos: repoSimple,
MyOrg: myOrg,
SelOrg: selOrg,
SelRepo: selRepo,
SelModule: selModule,
ShowOrg: myOrg != "",
ShowGoMod: selModule != "",
CommitTime: selCommitTime,
CommitHash: selCommitHash,
CommitHashShort: selCommitHashShort,
ShowSpec: len(mySpecs) > 0,
Specs: mySpecs,
})
if err != nil {
fmt.Println("Template execute error:", err)
return
}
}
func (m *Module) loginPage(rw http.ResponseWriter, req *http.Request, state *utils.State) {
2022-03-26 00:26:20 +00:00
if myUser, ok := utils.GetStateValue[*string](state, KeyUser); ok {
if myUser != nil {
http.Redirect(rw, req, "/gitea", http.StatusTemporaryRedirect)
return
}
}
2022-03-26 00:26:20 +00:00
if flowState, ok := utils.GetStateValue[uuid.UUID](state, KeyState); ok {
q := req.URL.Query()
if q.Has("code") && q.Has("state") {
if q.Get("state") == flowState.String() {
exchange, err := m.oauthClient.Exchange(context.Background(), q.Get("code"))
if err != nil {
fmt.Println("Exchange token error:", err)
return
}
c, err := gitea.NewClient(os.Getenv("GITEA_SERVER"), gitea.SetToken(exchange.AccessToken))
if err != nil {
fmt.Println("Create client error:", err)
return
}
2022-03-26 00:26:20 +00:00
state.Put(KeyOauthClient, c)
state.Put(KeyAccessToken, exchange.AccessToken)
state.Put(KeyRefreshToken, exchange.RefreshToken)
http.Redirect(rw, req, "/gitea", http.StatusTemporaryRedirect)
return
}
http.Error(rw, "OAuth flow state doesn't match\n", http.StatusBadRequest)
return
}
}
flowState := uuid.New()
2022-03-26 00:26:20 +00:00
state.Put(KeyState, flowState)
http.Redirect(rw, req, m.oauthClient.AuthCodeURL(flowState.String(), oauth2.AccessTypeOffline), http.StatusTemporaryRedirect)
2022-03-23 00:39:53 +00:00
}
func (m *Module) specPage(rw http.ResponseWriter, req *http.Request, state *utils.State, giteaClient *gitea.Client) {
vars := mux.Vars(req)
myOrg := vars["org"]
myRepo := vars["repo"]
mySha := vars["sha"]
mySpec := vars["spec"]
contents, _, err := giteaClient.GetContents(myOrg, myRepo, mySha, mySpec)
if err != nil {
http.Error(rw, "OpenAPI spec: "+err.Error(), http.StatusInternalServerError)
return
}
tmp, err := template.New("swagger").Parse(swaggerTemplate)
if err != nil {
fmt.Println("Template parse error:", err)
return
}
err = tmp.Execute(rw, struct {
LoadUrls []struct {
Url string `json:"url"`
Name string `json:"name"`
2022-03-23 00:39:53 +00:00
}
LoadMain string
}{
LoadUrls: []struct {
Url string `json:"url"`
Name string `json:"name"`
}{
2022-09-09 16:48:18 +01:00
{Url: fmt.Sprintf("/gitea/spec-raw/%s/%s/%s/%s", myOrg, myRepo, mySha, mySpec), Name: contents.Name},
},
LoadMain: contents.Name,
})
if err != nil {
fmt.Println("Template execute error:", err)
return
2022-03-23 00:39:53 +00:00
}
}
2022-09-09 16:48:18 +01:00
func (m *Module) specRawPage(rw http.ResponseWriter, req *http.Request, state *utils.State, giteaClient *gitea.Client) {
vars := mux.Vars(req)
myOrg := vars["org"]
myRepo := vars["repo"]
mySha := vars["sha"]
mySpec := vars["spec"]
open, _, err := giteaClient.GetFile(myOrg, myRepo, mySha, mySpec)
if err != nil {
http.Error(rw, "OpenAPI spec raw: "+err.Error(), http.StatusInternalServerError)
return
}
http.ServeContent(rw, req, mySpec, time.Now(), bytes.NewReader(open))
}