Compare commits
69 Commits
Author | SHA1 | Date |
---|---|---|
Derrick Hammer | 853de2cb64 | |
Derrick Hammer | 946f57fe4e | |
Derrick Hammer | c345f1c72a | |
Derrick Hammer | e33587b406 | |
Derrick Hammer | bf9e062644 | |
Derrick Hammer | 63e9d93f9b | |
Derrick Hammer | a1bc451d3e | |
Derrick Hammer | 340240575f | |
Derrick Hammer | 8640627536 | |
Derrick Hammer | 6aed2bfaae | |
Derrick Hammer | 7453fa48ee | |
Derrick Hammer | 6ca3e8945c | |
Derrick Hammer | dc9a0acb8e | |
Derrick Hammer | 5345523eec | |
Derrick Hammer | f975cf38e0 | |
Derrick Hammer | 276b43ddd0 | |
Derrick Hammer | 6eab62d128 | |
Derrick Hammer | d132946e40 | |
Derrick Hammer | c607400951 | |
Derrick Hammer | aa52bd2e59 | |
Derrick Hammer | 41b25abaaf | |
Derrick Hammer | 1c1f78559d | |
Derrick Hammer | 608cfad501 | |
Derrick Hammer | 669a6eaf6d | |
Derrick Hammer | 846e5df758 | |
Derrick Hammer | 3565331a58 | |
Derrick Hammer | fb752f1f3a | |
Derrick Hammer | 346849cc8c | |
Derrick Hammer | d962eb5304 | |
Derrick Hammer | 41532495bc | |
Derrick Hammer | d83df43411 | |
Derrick Hammer | e907aa7e99 | |
Derrick Hammer | ae9b036cb5 | |
Derrick Hammer | d5ac5e4b3c | |
Derrick Hammer | e7bfd0c6c9 | |
Derrick Hammer | a439ca8002 | |
Derrick Hammer | 4faa7c9b88 | |
Derrick Hammer | 8dd6e83fae | |
Derrick Hammer | bbe7e8e053 | |
Derrick Hammer | 34898771ab | |
Derrick Hammer | 6d20106bb5 | |
Derrick Hammer | 0cf1b8827a | |
Derrick Hammer | 4b429e6d59 | |
Derrick Hammer | 055f502114 | |
Derrick Hammer | 150c5c6cb2 | |
Derrick Hammer | 5036d0bca4 | |
Derrick Hammer | 0365862777 | |
Derrick Hammer | ea4e4aa00a | |
Derrick Hammer | 8abe1e7981 | |
Derrick Hammer | 0c78013f38 | |
Derrick Hammer | e95732d479 | |
Derrick Hammer | 12c71a26bf | |
Derrick Hammer | 8167255a0a | |
Derrick Hammer | 865c639557 | |
Derrick Hammer | 300b074f86 | |
Derrick Hammer | bf74d45404 | |
Derrick Hammer | 7cfcbad86d | |
Derrick Hammer | e1d8e122ce | |
Derrick Hammer | 4afb7eb108 | |
Derrick Hammer | 0093a15645 | |
Derrick Hammer | d97dd79e59 | |
Derrick Hammer | 64319ee7d0 | |
Derrick Hammer | 90fbbf94e1 | |
Derrick Hammer | 36a661bd71 | |
Derrick Hammer | 9262bb0555 | |
Derrick Hammer | 26f497f062 | |
Derrick Hammer | e8af456476 | |
Derrick Hammer | 5ac86cc75c | |
Derrick Hammer | 4456b54550 |
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2024 LumeWeb
|
||||
Copyright (c) 2024 Hammer Technologies LLC
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/hex"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"git.lumeweb.com/LumeWeb/gitea-github-proxy/config"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type manifest struct {
|
||||
Name string `json:"name"`
|
||||
Url string `json:"url"`
|
||||
HookAttributes hookAttributes `json:"hook_attributes"`
|
||||
Public bool `json:"public"`
|
||||
RedirectURL string `json:"redirect_url"`
|
||||
Version string `json:"version"`
|
||||
DefaultPermissions permissions `json:"default_permissions"`
|
||||
}
|
||||
|
||||
type hookAttributes struct {
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
type permissions struct {
|
||||
Issues string `json:"issues"`
|
||||
Metadata string `json:"metadata"`
|
||||
PullRequests string `json:"pull_requests"`
|
||||
}
|
||||
|
||||
type settingsApi struct {
|
||||
config *config.Config
|
||||
db *gorm.DB
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
type appSecrets struct {
|
||||
PrivateKey string `json:"private_key"`
|
||||
Code string `json:"code"`
|
||||
}
|
||||
|
||||
func newApp() appSecrets {
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Marshal the private key into PKCS#1 ASN.1 DER encoded form
|
||||
pkcs1Bytes := x509.MarshalPKCS1PrivateKey(privateKey)
|
||||
|
||||
// Create a PEM block with the private key
|
||||
privateKeyPEM := &pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Bytes: pkcs1Bytes,
|
||||
}
|
||||
|
||||
return appSecrets{
|
||||
PrivateKey: string(pem.EncodeToMemory(privateKeyPEM)),
|
||||
Code: generateTempCode(),
|
||||
}
|
||||
}
|
||||
|
||||
func generateTempCode() string {
|
||||
bytes := make([]byte, 16)
|
||||
if _, err := rand.Read(bytes); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return hex.EncodeToString(bytes)
|
||||
}
|
||||
|
||||
func generatePayloadSignature(payload []byte, secret string) string {
|
||||
hasher := hmac.New(sha256.New, []byte(secret))
|
||||
hasher.Write(payload)
|
||||
sha := hex.EncodeToString(hasher.Sum(nil))
|
||||
return fmt.Sprintf("sha256=%s", sha)
|
||||
}
|
|
@ -0,0 +1,521 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
gitea "code.gitea.io/sdk/gitea"
|
||||
"fmt"
|
||||
"github.com/google/go-github/v59/github"
|
||||
"time"
|
||||
)
|
||||
|
||||
func stringPtr(s string) *string {
|
||||
return &s
|
||||
}
|
||||
|
||||
func numberPtr(n int) *int {
|
||||
return &n
|
||||
}
|
||||
|
||||
func intPtr(n int) *int {
|
||||
return &n
|
||||
}
|
||||
|
||||
func int64Ptr(n int64) *int64 {
|
||||
return &n
|
||||
}
|
||||
|
||||
func boolPtr(b bool) *bool {
|
||||
return &b
|
||||
}
|
||||
|
||||
func timePtr(t time.Time) *github.Timestamp {
|
||||
timestamp := github.Timestamp{Time: t}
|
||||
return ×tamp
|
||||
}
|
||||
|
||||
func timePtrIfNotNil(t *time.Time) *github.Timestamp {
|
||||
if t == nil {
|
||||
return nil
|
||||
}
|
||||
return timePtr(*t)
|
||||
}
|
||||
|
||||
func convertUser(user *gitea.User) *github.User {
|
||||
if user == nil {
|
||||
return &github.User{}
|
||||
}
|
||||
return &github.User{
|
||||
Login: stringPtr(user.UserName),
|
||||
ID: int64Ptr(user.ID),
|
||||
}
|
||||
}
|
||||
|
||||
func convertCoreUser(user *structs.User) *github.User {
|
||||
if user == nil {
|
||||
return &github.User{}
|
||||
}
|
||||
return &github.User{
|
||||
Login: stringPtr(user.UserName),
|
||||
ID: int64Ptr(user.ID),
|
||||
}
|
||||
}
|
||||
|
||||
func convertUsers(users []*gitea.User) []*github.User {
|
||||
var ghUsers []*github.User
|
||||
for _, user := range users {
|
||||
ghUsers = append(ghUsers, convertUser(user))
|
||||
}
|
||||
return ghUsers
|
||||
}
|
||||
|
||||
func convertCoreUsers(users []*structs.User) []*github.User {
|
||||
var ghUsers []*github.User
|
||||
for _, user := range users {
|
||||
ghUsers = append(ghUsers, convertCoreUser(user))
|
||||
}
|
||||
return ghUsers
|
||||
}
|
||||
|
||||
func convertMilestone(milestone *gitea.Milestone) *github.Milestone {
|
||||
if milestone == nil {
|
||||
return &github.Milestone{}
|
||||
}
|
||||
return &github.Milestone{
|
||||
Title: stringPtr(milestone.Title),
|
||||
}
|
||||
}
|
||||
|
||||
func convertCoreMilestone(milestone *structs.Milestone) *github.Milestone {
|
||||
if milestone == nil {
|
||||
return &github.Milestone{}
|
||||
}
|
||||
return &github.Milestone{
|
||||
Title: stringPtr(milestone.Title),
|
||||
}
|
||||
}
|
||||
|
||||
func convertLabels(labels []*gitea.Label) []*github.Label {
|
||||
if labels == nil {
|
||||
return make([]*github.Label, 0)
|
||||
}
|
||||
var ghLabels []*github.Label
|
||||
for _, label := range labels {
|
||||
if label == nil {
|
||||
continue
|
||||
}
|
||||
ghLabels = append(ghLabels, &github.Label{
|
||||
Name: stringPtr(label.Name),
|
||||
})
|
||||
}
|
||||
return ghLabels
|
||||
}
|
||||
|
||||
func convertCoreLabels(labels []*structs.Label) []*github.Label {
|
||||
if labels == nil {
|
||||
return make([]*github.Label, 0)
|
||||
}
|
||||
var ghLabels []*github.Label
|
||||
for _, label := range labels {
|
||||
if label == nil {
|
||||
continue
|
||||
}
|
||||
ghLabels = append(ghLabels, &github.Label{
|
||||
Name: stringPtr(label.Name),
|
||||
})
|
||||
}
|
||||
return ghLabels
|
||||
}
|
||||
|
||||
func convertPRBranch(branch *gitea.PRBranchInfo) *github.PullRequestBranch {
|
||||
if branch == nil {
|
||||
return &github.PullRequestBranch{}
|
||||
}
|
||||
return &github.PullRequestBranch{
|
||||
Label: stringPtr(branch.Name),
|
||||
Ref: stringPtr(branch.Ref),
|
||||
SHA: stringPtr(branch.Sha),
|
||||
Repo: convertRepo(branch.Repository),
|
||||
}
|
||||
}
|
||||
|
||||
func convertCorePRBranch(branch *structs.PRBranchInfo) *github.PullRequestBranch {
|
||||
if branch == nil {
|
||||
return &github.PullRequestBranch{}
|
||||
}
|
||||
return &github.PullRequestBranch{
|
||||
Label: stringPtr(branch.Name),
|
||||
Ref: stringPtr(branch.Ref),
|
||||
SHA: stringPtr(branch.Sha),
|
||||
Repo: convertCoreRepo(branch.Repository),
|
||||
}
|
||||
}
|
||||
|
||||
func convertRepo(repo *gitea.Repository) *github.Repository {
|
||||
if repo == nil {
|
||||
return &github.Repository{}
|
||||
}
|
||||
return &github.Repository{
|
||||
ID: int64Ptr(repo.ID),
|
||||
Name: stringPtr(repo.Name),
|
||||
Owner: convertUser(repo.Owner),
|
||||
FullName: stringPtr(repo.FullName),
|
||||
Description: stringPtr(repo.Description),
|
||||
Homepage: stringPtr(repo.Website),
|
||||
HTMLURL: stringPtr(repo.HTMLURL),
|
||||
CloneURL: stringPtr(repo.CloneURL),
|
||||
GitURL: stringPtr(repo.CloneURL),
|
||||
SSHURL: stringPtr(repo.SSHURL),
|
||||
DefaultBranch: stringPtr(repo.DefaultBranch),
|
||||
CreatedAt: timePtr(repo.Created),
|
||||
UpdatedAt: timePtr(repo.Updated),
|
||||
Private: boolPtr(repo.Private),
|
||||
Fork: boolPtr(repo.Fork),
|
||||
Size: intPtr(repo.Size),
|
||||
StargazersCount: intPtr(repo.Stars),
|
||||
SubscribersCount: intPtr(repo.Watchers),
|
||||
ForksCount: intPtr(repo.Forks),
|
||||
Watchers: intPtr(repo.Watchers),
|
||||
WatchersCount: intPtr(repo.Stars),
|
||||
OpenIssuesCount: intPtr(repo.OpenIssues),
|
||||
Archived: boolPtr(repo.Archived),
|
||||
}
|
||||
}
|
||||
|
||||
func convertCoreRepo(repo *structs.Repository) *github.Repository {
|
||||
if repo == nil {
|
||||
return &github.Repository{}
|
||||
}
|
||||
return &github.Repository{
|
||||
ID: int64Ptr(repo.ID),
|
||||
Name: stringPtr(repo.Name),
|
||||
Owner: convertCoreUser(repo.Owner),
|
||||
FullName: stringPtr(repo.FullName),
|
||||
Description: stringPtr(repo.Description),
|
||||
Homepage: stringPtr(repo.Website),
|
||||
HTMLURL: stringPtr(repo.HTMLURL),
|
||||
CloneURL: stringPtr(repo.CloneURL),
|
||||
GitURL: stringPtr(repo.CloneURL),
|
||||
SSHURL: stringPtr(repo.SSHURL),
|
||||
DefaultBranch: stringPtr(repo.DefaultBranch),
|
||||
CreatedAt: timePtr(repo.Created),
|
||||
UpdatedAt: timePtr(repo.Updated),
|
||||
Private: boolPtr(repo.Private),
|
||||
Fork: boolPtr(repo.Fork),
|
||||
Size: intPtr(repo.Size),
|
||||
StargazersCount: intPtr(repo.Stars),
|
||||
SubscribersCount: intPtr(repo.Watchers),
|
||||
ForksCount: intPtr(repo.Forks),
|
||||
Watchers: intPtr(repo.Watchers),
|
||||
WatchersCount: intPtr(repo.Stars),
|
||||
OpenIssuesCount: intPtr(repo.OpenIssues),
|
||||
Archived: boolPtr(repo.Archived),
|
||||
}
|
||||
}
|
||||
|
||||
func convertPullRequest(request *gitea.PullRequest) *github.PullRequest {
|
||||
if request == nil {
|
||||
return &github.PullRequest{}
|
||||
}
|
||||
pr := &github.PullRequest{
|
||||
ID: int64Ptr(request.ID),
|
||||
Number: intPtr(int(request.Index)),
|
||||
State: stringPtr(string(request.State)),
|
||||
Title: stringPtr(request.Title),
|
||||
Body: stringPtr(request.Body),
|
||||
CreatedAt: timePtr(*request.Created),
|
||||
UpdatedAt: timePtr(*request.Updated),
|
||||
ClosedAt: timePtrIfNotNil(request.Closed),
|
||||
MergedAt: timePtrIfNotNil(request.Merged),
|
||||
Merged: boolPtr(request.HasMerged),
|
||||
Mergeable: boolPtr(request.Mergeable),
|
||||
MergeCommitSHA: request.MergedCommitID,
|
||||
URL: stringPtr(request.URL),
|
||||
HTMLURL: stringPtr(request.HTMLURL),
|
||||
DiffURL: stringPtr(request.DiffURL),
|
||||
PatchURL: stringPtr(request.PatchURL),
|
||||
Comments: intPtr(request.Comments),
|
||||
Assignee: convertUser(request.Assignee),
|
||||
Assignees: convertUsers(request.Assignees),
|
||||
Milestone: convertMilestone(request.Milestone),
|
||||
Labels: convertLabels(request.Labels),
|
||||
}
|
||||
|
||||
// Convert PR branch info
|
||||
if request.Head != nil {
|
||||
pr.Head = convertPRBranch(request.Head)
|
||||
}
|
||||
if request.Base != nil {
|
||||
pr.Base = convertPRBranch(request.Base)
|
||||
}
|
||||
|
||||
return pr
|
||||
}
|
||||
|
||||
func convertCorePullRequest(request *structs.PullRequest) *github.PullRequest {
|
||||
if request == nil {
|
||||
return &github.PullRequest{}
|
||||
}
|
||||
pr := &github.PullRequest{
|
||||
ID: int64Ptr(request.ID),
|
||||
Number: intPtr(int(request.Index)),
|
||||
State: stringPtr(string(request.State)),
|
||||
Title: stringPtr(request.Title),
|
||||
Body: stringPtr(request.Body),
|
||||
CreatedAt: timePtr(*request.Created),
|
||||
UpdatedAt: timePtr(*request.Updated),
|
||||
ClosedAt: timePtrIfNotNil(request.Closed),
|
||||
MergedAt: timePtrIfNotNil(request.Merged),
|
||||
Merged: boolPtr(request.HasMerged),
|
||||
Mergeable: boolPtr(request.Mergeable),
|
||||
MergeCommitSHA: request.MergedCommitID,
|
||||
URL: stringPtr(request.URL),
|
||||
HTMLURL: stringPtr(request.HTMLURL),
|
||||
DiffURL: stringPtr(request.DiffURL),
|
||||
PatchURL: stringPtr(request.PatchURL),
|
||||
Comments: intPtr(request.Comments),
|
||||
Assignee: convertCoreUser(request.Assignee),
|
||||
Assignees: convertCoreUsers(request.Assignees),
|
||||
Milestone: convertCoreMilestone(request.Milestone),
|
||||
Labels: convertCoreLabels(request.Labels),
|
||||
}
|
||||
|
||||
// Convert PR branch info
|
||||
if request.Head != nil {
|
||||
pr.Head = convertCorePRBranch(request.Head)
|
||||
}
|
||||
if request.Base != nil {
|
||||
pr.Base = convertCorePRBranch(request.Base)
|
||||
}
|
||||
|
||||
return pr
|
||||
}
|
||||
|
||||
func convertChanges(changes *structs.ChangesPayload) *github.EditChange {
|
||||
if changes == nil {
|
||||
return &github.EditChange{}
|
||||
}
|
||||
return &github.EditChange{
|
||||
Title: &github.EditTitle{
|
||||
From: stringPtr(changes.Title.From),
|
||||
},
|
||||
Body: &github.EditBody{
|
||||
From: stringPtr(changes.Body.From),
|
||||
},
|
||||
Base: &github.EditBase{
|
||||
Ref: &github.EditRef{
|
||||
From: stringPtr(changes.Ref.From),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func convertLabel(label *gitea.Label) *github.Label {
|
||||
if label == nil {
|
||||
return &github.Label{}
|
||||
}
|
||||
return &github.Label{
|
||||
ID: int64Ptr(label.ID),
|
||||
Name: stringPtr(label.Name),
|
||||
Color: stringPtr(label.Color),
|
||||
Description: stringPtr(label.Description),
|
||||
URL: stringPtr(label.URL),
|
||||
}
|
||||
}
|
||||
|
||||
func convertCoreLabel(label *structs.Label) *github.Label {
|
||||
if label == nil {
|
||||
return &github.Label{}
|
||||
}
|
||||
return &github.Label{
|
||||
ID: int64Ptr(label.ID),
|
||||
Name: stringPtr(label.Name),
|
||||
Color: stringPtr(label.Color),
|
||||
Description: stringPtr(label.Description),
|
||||
URL: stringPtr(label.URL),
|
||||
}
|
||||
}
|
||||
|
||||
func convertCommitFile(file *gitea.ChangedFile) *github.CommitFile {
|
||||
if file == nil {
|
||||
return &github.CommitFile{}
|
||||
}
|
||||
|
||||
return &github.CommitFile{
|
||||
Filename: stringPtr(file.Filename),
|
||||
PreviousFilename: stringPtr(file.PreviousFilename),
|
||||
Additions: intPtr(file.Additions),
|
||||
Deletions: intPtr(file.Deletions),
|
||||
Changes: intPtr(file.Changes),
|
||||
Status: stringPtr(file.Status),
|
||||
BlobURL: stringPtr(file.ContentsURL),
|
||||
RawURL: stringPtr(file.RawURL),
|
||||
}
|
||||
}
|
||||
|
||||
func convertGitTree(tree *gitea.GitTreeResponse) *github.Tree {
|
||||
if tree == nil {
|
||||
return &github.Tree{}
|
||||
}
|
||||
return &github.Tree{
|
||||
SHA: stringPtr(tree.SHA),
|
||||
Truncated: boolPtr(tree.Truncated),
|
||||
Entries: convertGitEntries(tree.Entries),
|
||||
}
|
||||
}
|
||||
|
||||
func convertGitEntries(entries []gitea.GitEntry) []*github.TreeEntry {
|
||||
if entries == nil {
|
||||
return make([]*github.TreeEntry, 0)
|
||||
}
|
||||
var ghEntries []*github.TreeEntry
|
||||
for _, entry := range entries {
|
||||
ghEntries = append(ghEntries, convertGitEntry(&entry))
|
||||
}
|
||||
return ghEntries
|
||||
}
|
||||
|
||||
func convertGitEntry(s *gitea.GitEntry) *github.TreeEntry {
|
||||
if s == nil {
|
||||
return &github.TreeEntry{}
|
||||
}
|
||||
return &github.TreeEntry{
|
||||
Path: stringPtr(s.Path),
|
||||
Mode: stringPtr(s.Mode),
|
||||
Type: stringPtr(s.Type),
|
||||
Size: intPtr(int(s.Size)),
|
||||
SHA: stringPtr(s.SHA),
|
||||
URL: stringPtr(s.URL),
|
||||
}
|
||||
}
|
||||
|
||||
func convertIssueComment(comment *gitea.Comment, reactions []*gitea.Reaction) *github.IssueComment {
|
||||
if comment == nil {
|
||||
return &github.IssueComment{}
|
||||
}
|
||||
return &github.IssueComment{
|
||||
ID: int64Ptr(comment.ID),
|
||||
Body: stringPtr(comment.Body),
|
||||
CreatedAt: timePtr(comment.Created),
|
||||
UpdatedAt: timePtr(comment.Updated),
|
||||
User: convertUser(comment.Poster),
|
||||
Reactions: convertReactions(reactions),
|
||||
}
|
||||
}
|
||||
|
||||
func convertReactions(reactions []*gitea.Reaction) *github.Reactions {
|
||||
if reactions == nil {
|
||||
return &github.Reactions{}
|
||||
}
|
||||
return &github.Reactions{
|
||||
TotalCount: intPtr(len(reactions)),
|
||||
PlusOne: intPtr(countReaction(reactions, "+1")),
|
||||
MinusOne: intPtr(countReaction(reactions, "-1")),
|
||||
Laugh: intPtr(countReaction(reactions, "laugh")),
|
||||
Confused: intPtr(countReaction(reactions, "confused")),
|
||||
Heart: intPtr(countReaction(reactions, "heart")),
|
||||
Rocket: intPtr(countReaction(reactions, "rocket")),
|
||||
Eyes: intPtr(countReaction(reactions, "eyes")),
|
||||
}
|
||||
}
|
||||
|
||||
func countReaction(reactions []*gitea.Reaction, reactionType string) int {
|
||||
count := 0
|
||||
for _, reaction := range reactions {
|
||||
if reaction.Reaction == reactionType {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func translatePrAction(action structs.HookIssueAction, prefix bool) string {
|
||||
translatedAction := ""
|
||||
|
||||
switch action {
|
||||
case structs.HookIssueOpened:
|
||||
translatedAction = "opened"
|
||||
case structs.HookIssueClosed:
|
||||
translatedAction = "closed"
|
||||
case structs.HookIssueReOpened:
|
||||
translatedAction = "reopened"
|
||||
case structs.HookIssueEdited:
|
||||
translatedAction = "edited"
|
||||
case structs.HookIssueAssigned:
|
||||
translatedAction = "assigned"
|
||||
case structs.HookIssueUnassigned:
|
||||
translatedAction = "unassigned"
|
||||
case structs.HookIssueLabelUpdated:
|
||||
// GitHub does not have a direct "label_updated" event; use "labeled" as the closest action
|
||||
translatedAction = "labeled" // Assuming you handle the update as adding a label
|
||||
case structs.HookIssueLabelCleared:
|
||||
// GitHub does not have a direct "label_cleared" event; use "unlabeled" as the closest action
|
||||
translatedAction = "unlabeled" // Assuming you handle the clearing as removing a label
|
||||
case structs.HookIssueSynchronized:
|
||||
translatedAction = "synchronize"
|
||||
case structs.HookIssueMilestoned:
|
||||
translatedAction = "milestoned"
|
||||
case structs.HookIssueDemilestoned:
|
||||
translatedAction = "demilestoned"
|
||||
case structs.HookIssueReviewed:
|
||||
// GitHub does not have a direct "reviewed" event for PRs; this might be closest to a review submitted
|
||||
translatedAction = "review_submitted" // This is not a direct GitHub event, consider how best to map this action
|
||||
case structs.HookIssueReviewRequested:
|
||||
translatedAction = "review_requested"
|
||||
case structs.HookIssueReviewRequestRemoved:
|
||||
translatedAction = "review_request_removed"
|
||||
default:
|
||||
// Fallback for any unhandled actions
|
||||
translatedAction = "unknown_action"
|
||||
}
|
||||
|
||||
if prefix {
|
||||
translatedAction = fmt.Sprintf("pull_request.%s", translatedAction)
|
||||
}
|
||||
|
||||
return translatedAction
|
||||
}
|
||||
|
||||
func convertRelease(release *gitea.Release) *github.RepositoryRelease {
|
||||
if release == nil {
|
||||
return &github.RepositoryRelease{}
|
||||
}
|
||||
return &github.RepositoryRelease{
|
||||
ID: int64Ptr(release.ID),
|
||||
TagName: stringPtr(release.TagName),
|
||||
TargetCommitish: stringPtr(release.Target),
|
||||
Name: stringPtr(release.Title),
|
||||
Body: stringPtr(release.Note),
|
||||
Draft: boolPtr(release.IsDraft),
|
||||
Prerelease: boolPtr(release.IsPrerelease),
|
||||
CreatedAt: timePtr(release.CreatedAt),
|
||||
PublishedAt: timePtr(release.PublishedAt),
|
||||
Assets: convertReleaseAttachments(release.Attachments),
|
||||
URL: stringPtr(release.URL),
|
||||
ZipballURL: stringPtr(release.ZipURL),
|
||||
TarballURL: stringPtr(release.TarURL),
|
||||
HTMLURL: stringPtr(release.HTMLURL),
|
||||
}
|
||||
}
|
||||
|
||||
func convertReleaseAttachments(attachments []*gitea.Attachment) []*github.ReleaseAsset {
|
||||
if attachments == nil {
|
||||
return make([]*github.ReleaseAsset, 0)
|
||||
}
|
||||
var ghAttachments []*github.ReleaseAsset
|
||||
for _, attachment := range attachments {
|
||||
ghAttachments = append(ghAttachments, convertReleaseAttachment(attachment))
|
||||
}
|
||||
return ghAttachments
|
||||
}
|
||||
func convertReleaseAttachment(attachment *gitea.Attachment) *github.ReleaseAsset {
|
||||
if attachment == nil {
|
||||
return &github.ReleaseAsset{}
|
||||
}
|
||||
return &github.ReleaseAsset{
|
||||
ID: int64Ptr(attachment.ID),
|
||||
Name: stringPtr(attachment.Name),
|
||||
Size: intPtr(int(attachment.Size)),
|
||||
DownloadCount: intPtr(int(attachment.DownloadCount)),
|
||||
BrowserDownloadURL: stringPtr(attachment.DownloadURL),
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"github.com/google/go-github/v59/github"
|
||||
)
|
||||
|
||||
func convertPullRequestEvent(event *structs.PullRequestPayload) *github.PullRequestEvent {
|
||||
if len(event.PullRequest.RequestedReviewers) == 0 {
|
||||
event.PullRequest.RequestedReviewers = append(event.PullRequest.RequestedReviewers, nil)
|
||||
}
|
||||
|
||||
if len(event.PullRequest.Labels) == 0 {
|
||||
event.PullRequest.Labels = append(event.PullRequest.Labels, nil)
|
||||
}
|
||||
|
||||
return &github.PullRequestEvent{
|
||||
Action: stringPtr(translatePrAction(event.Action, false)),
|
||||
PullRequest: convertCorePullRequest(event.PullRequest),
|
||||
Repo: convertCoreRepo(event.Repository),
|
||||
Assignee: convertCoreUser(event.PullRequest.Assignee),
|
||||
Number: intPtr(int(event.Index)),
|
||||
Changes: convertChanges(event.Changes),
|
||||
RequestedReviewer: convertCoreUser(event.PullRequest.RequestedReviewers[0]),
|
||||
Sender: convertCoreUser(event.Sender),
|
||||
Label: convertCoreLabel(event.PullRequest.Labels[0]),
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func deleteCookie(w http.ResponseWriter, name string) {
|
||||
cookie := http.Cookie{
|
||||
Name: name,
|
||||
Path: "/",
|
||||
MaxAge: -1,
|
||||
}
|
||||
|
||||
http.SetCookie(w, &cookie)
|
||||
}
|
||||
|
||||
func setAuthCookie(jwt string, domain string, w http.ResponseWriter) {
|
||||
setCookie(w, AuthCookieName, domain, jwt, int(time.Hour.Seconds()), http.SameSiteNoneMode)
|
||||
}
|
||||
func setCookie(w http.ResponseWriter, name string, domain string, value string, maxAge int, sameSite http.SameSite) {
|
||||
cookie := http.Cookie{
|
||||
Name: name,
|
||||
Domain: domain,
|
||||
Value: value,
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
MaxAge: maxAge,
|
||||
Secure: true,
|
||||
SameSite: sameSite,
|
||||
}
|
||||
|
||||
http.SetCookie(w, &cookie)
|
||||
}
|
||||
|
||||
func getCookie(r *http.Request, name string) string {
|
||||
cookie, err := r.Cookie(name)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return cookie.Value
|
||||
}
|
|
@ -0,0 +1,291 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"git.lumeweb.com/LumeWeb/gitea-github-proxy/config"
|
||||
"git.lumeweb.com/LumeWeb/gitea-github-proxy/db/model"
|
||||
"github.com/golang-jwt/jwt"
|
||||
"github.com/gorilla/mux"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const AUTHED_CONTEXT_KEY = "authed"
|
||||
const REDIRECT_AFTER_AUTH = "redirect-after-auth"
|
||||
const WEBHOOK_CONTEXT_KEY = "webhook"
|
||||
|
||||
const AuthCookieName = "auth-token"
|
||||
|
||||
var _ = jwt.Claims(&standardClaims{})
|
||||
|
||||
type standardClaims struct {
|
||||
Issuer any `json:"iss,omitempty"`
|
||||
ExpiresAt any `json:"exp,omitempty"`
|
||||
jwt.StandardClaims
|
||||
}
|
||||
|
||||
func (s *standardClaims) Valid() error {
|
||||
if timeStr, ok := s.ExpiresAt.(string); ok {
|
||||
t, err := time.Parse(time.RFC3339Nano, timeStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
unixTimestamp := t.Unix()
|
||||
s.ExpiresAt = unixTimestamp
|
||||
}
|
||||
return s.StandardClaims.Valid()
|
||||
}
|
||||
|
||||
func findAuthToken(r *http.Request) string {
|
||||
authHeader := parseAuthTokenHeader(r.Header)
|
||||
|
||||
if authHeader != "" {
|
||||
return authHeader
|
||||
}
|
||||
|
||||
cookie := getCookie(r, AuthCookieName)
|
||||
if cookie != "" {
|
||||
return cookie
|
||||
}
|
||||
|
||||
return r.FormValue(AuthCookieName)
|
||||
}
|
||||
|
||||
func giteaOauthVerifyMiddleware(cfg *config.Config) mux.MiddlewareFunc {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
token := findAuthToken(r)
|
||||
if token == "" {
|
||||
addAuthStatusToRequestServ(false, r, w, next)
|
||||
return
|
||||
}
|
||||
|
||||
client, err := getClient(ClientParams{
|
||||
Config: cfg,
|
||||
AuthToken: token,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
_, _, err = client.AdminListUsers(gitea.AdminListUsersOptions{})
|
||||
if err != nil {
|
||||
addAuthStatusToRequestServ(false, r, w, next)
|
||||
return
|
||||
}
|
||||
|
||||
addAuthStatusToRequestServ(true, r, w, next)
|
||||
})
|
||||
}
|
||||
}
|
||||
func githubRestVerifyMiddleware(db *gorm.DB) mux.MiddlewareFunc {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
token := findAuthToken(r)
|
||||
|
||||
if token != "" {
|
||||
parseToken, _, err := new(jwt.Parser).ParseUnverified(token, jwt.MapClaims{})
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid JWT", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
claims, ok := parseToken.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
http.Error(w, "Invalid JWT", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
var appId string
|
||||
|
||||
switch v := claims["iss"].(type) {
|
||||
case string:
|
||||
appId = v
|
||||
case float64:
|
||||
appId = strconv.FormatFloat(v, 'f', -1, 64)
|
||||
default:
|
||||
http.Error(w, "Invalid JWT", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
appIdInt, err := strconv.Atoi(appId)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid JWT", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
appRecord := &model.Apps{}
|
||||
appRecord.ID = uint(appIdInt)
|
||||
|
||||
if err := db.First(appRecord).Error; err != nil {
|
||||
http.Error(w, "Invalid JWT", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
block, _ := pem.Decode([]byte(appRecord.PrivateKey))
|
||||
if block == nil {
|
||||
// Handle error
|
||||
http.Error(w, "Invalid Private Key", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
// Handle error
|
||||
http.Error(w, "Failed to parse Private Key", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
publicKey := &privateKey.PublicKey
|
||||
|
||||
parseToken, err = jwt.ParseWithClaims(token, &standardClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
|
||||
// Return the RSA public key
|
||||
return publicKey, nil
|
||||
})
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if mux.CurrentRoute(r).GetName() == "app-install-get-access-token" {
|
||||
installId := mux.Vars(r)["installation_id"]
|
||||
|
||||
installIdInt, err := strconv.Atoi(installId)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid Install", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
if appIdInt != installIdInt {
|
||||
http.Error(w, "Invalid Install", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addAuthStatusToRequestServ(true, r, w, next)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func storeWebhookDataMiddleware(logger *zap.Logger) mux.MiddlewareFunc {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var webhook map[string]interface{}
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
logger.Error("Failed to read request body", zap.Error(err))
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
err = json.Unmarshal(body, &webhook)
|
||||
if err != nil {
|
||||
logger.Error("Failed to unmarshal webhook", zap.Error(err))
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if len(webhook) == 0 {
|
||||
logger.Error("Webhook data is empty")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.WithValue(r.Context(), WEBHOOK_CONTEXT_KEY, body)
|
||||
r = r.WithContext(ctx)
|
||||
|
||||
r.Body = io.NopCloser(strings.NewReader(string(body)))
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func requireAuthMiddleware(cfg *config.Config) mux.MiddlewareFunc {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
status := getAuthedStatusFromRequest(r)
|
||||
|
||||
if !status {
|
||||
setCookie(w, REDIRECT_AFTER_AUTH, cfg.Domain, r.Referer(), 0, http.SameSiteLaxMode)
|
||||
http.Redirect(w, r, "/setup", http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func githubRestRequireAuthMiddleware(cfg *config.Config) mux.MiddlewareFunc {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
status := getAuthedStatusFromRequest(r)
|
||||
|
||||
if !status {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func loggingMiddleware(logger *zap.Logger) mux.MiddlewareFunc {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Do stuff here
|
||||
logger.Debug("Request", zap.String("method", r.Method), zap.String("url", r.RequestURI))
|
||||
// Call the next handler, which can be another middleware in the chain, or the final handler.
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
func addAuthStatusToRequestServ(status bool, r *http.Request, w http.ResponseWriter, next http.Handler) {
|
||||
ctx := context.WithValue(r.Context(), AUTHED_CONTEXT_KEY, status)
|
||||
r = r.WithContext(ctx)
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func parseAuthTokenHeader(headers http.Header) string {
|
||||
authHeader := headers.Get("Authorization")
|
||||
if authHeader == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
authHeader = strings.TrimPrefix(authHeader, "Bearer ")
|
||||
authHeader = strings.TrimPrefix(authHeader, "bearer ")
|
||||
authHeader = strings.TrimPrefix(authHeader, "Token ")
|
||||
authHeader = strings.TrimPrefix(authHeader, "token ")
|
||||
|
||||
return authHeader
|
||||
}
|
||||
|
||||
func getAuthedStatusFromRequest(r *http.Request) bool {
|
||||
authed, ok := r.Context().Value(AUTHED_CONTEXT_KEY).(bool)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
return authed
|
||||
}
|
|
@ -0,0 +1,187 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"context"
|
||||
"fmt"
|
||||
"git.lumeweb.com/LumeWeb/gitea-github-proxy/config"
|
||||
"github.com/golang-jwt/jwt"
|
||||
"go.uber.org/fx"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/oauth2"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Oauth struct {
|
||||
cfg *config.Config
|
||||
logger *zap.Logger
|
||||
token *oauth2.Token
|
||||
refresher oauth2.TokenSource
|
||||
keepAliveRunning bool
|
||||
oauthCfg *oauth2.Config
|
||||
}
|
||||
|
||||
func NewOauth(lc fx.Lifecycle, cfg *config.Config, logger *zap.Logger) *Oauth {
|
||||
oa := &Oauth{cfg: cfg, logger: logger}
|
||||
|
||||
lc.Append(fx.Hook{
|
||||
OnStart: func(ctx context.Context) error {
|
||||
oa.config()
|
||||
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
||||
return oa
|
||||
}
|
||||
|
||||
func (o Oauth) config() *oauth2.Config {
|
||||
if o.oauthCfg == nil {
|
||||
o.oauthCfg = &oauth2.Config{
|
||||
ClientID: o.cfg.Oauth.ClientId,
|
||||
ClientSecret: o.cfg.Oauth.ClientSecret,
|
||||
Scopes: []string{"admin"},
|
||||
RedirectURL: fmt.Sprintf("https://%s/setup/callback", o.cfg.Domain),
|
||||
Endpoint: oauth2.Endpoint{
|
||||
TokenURL: fmt.Sprintf("%s/login/oauth/access_token", o.cfg.GiteaUrl),
|
||||
AuthURL: fmt.Sprintf("%s/login/oauth/authorize", o.cfg.GiteaUrl),
|
||||
},
|
||||
}
|
||||
|
||||
}
|
||||
if o.loadToken(o.oauthCfg) {
|
||||
o.keepAlive()
|
||||
}
|
||||
|
||||
return o.oauthCfg
|
||||
}
|
||||
|
||||
func (o Oauth) authUrl() string {
|
||||
return o.config().AuthCodeURL("state")
|
||||
}
|
||||
|
||||
func (o *Oauth) loadToken(config *oauth2.Config) bool {
|
||||
token := &oauth2.Token{}
|
||||
|
||||
if o.cfg.Oauth.Token != "" {
|
||||
o.token = &oauth2.Token{AccessToken: o.cfg.Oauth.Token}
|
||||
}
|
||||
|
||||
if o.cfg.Oauth.RefreshToken != "" {
|
||||
o.token.RefreshToken = o.cfg.Oauth.RefreshToken
|
||||
}
|
||||
|
||||
if o.token != nil {
|
||||
valid := false
|
||||
parseToken, _, err := new(jwt.Parser).ParseUnverified(o.cfg.Oauth.Token, jwt.MapClaims{})
|
||||
|
||||
if err != nil {
|
||||
o.logger.Error("Error parsing token", zap.Error(err))
|
||||
} else {
|
||||
// Assert the token's claims to the desired type (MapClaims in this case)
|
||||
if claims, ok := parseToken.Claims.(jwt.MapClaims); ok {
|
||||
if exp, ok := claims["exp"].(float64); ok {
|
||||
expirationTime := time.Unix(int64(exp), 0)
|
||||
if time.Now().Before(expirationTime) {
|
||||
valid = true
|
||||
o.token.Expiry = expirationTime
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if valid {
|
||||
token = o.token
|
||||
} else {
|
||||
o.logger.Info("Token is expired, ignoring")
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
o.refresher = config.TokenSource(context.Background(), token)
|
||||
return true
|
||||
}
|
||||
|
||||
func (o Oauth) keepAlive() {
|
||||
if o.cfg.Oauth.Token == "" || o.cfg.Oauth.RefreshToken == "" {
|
||||
o.logger.Error("No token or refresh token provided.")
|
||||
return
|
||||
}
|
||||
|
||||
if o.refresher == nil {
|
||||
o.logger.Error("No refresher provided.")
|
||||
return
|
||||
}
|
||||
|
||||
if o.keepAliveRunning {
|
||||
return
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(30 * time.Minute)
|
||||
|
||||
o.keepAliveRunning = true
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
if !o.isTokenValid() {
|
||||
if err := o.refreshToken(); err != nil {
|
||||
o.logger.Error("Error refreshing token", zap.Error(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
func (o *Oauth) isTokenValid() bool {
|
||||
if o.token == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return o.token.Valid()
|
||||
}
|
||||
|
||||
func (o *Oauth) refreshToken() error {
|
||||
o.logger.Info("Refreshing token...")
|
||||
|
||||
token, err := o.refresher.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.token = token
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *Oauth) exchange(code string) (*oauth2.Token, error) {
|
||||
cfg := o.config()
|
||||
token, err := o.config().Exchange(context.Background(), code)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
o.cfg.Oauth.Token = token.AccessToken
|
||||
o.cfg.Oauth.RefreshToken = token.RefreshToken
|
||||
err = config.SaveConfig(o.cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
o.loadToken(cfg)
|
||||
o.keepAlive()
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func (o *Oauth) client() *gitea.Client {
|
||||
client, err := getClient(ClientParams{
|
||||
Config: o.cfg,
|
||||
})
|
||||
if err != nil {
|
||||
o.logger.Fatal("Error creating gitea client", zap.Error(err))
|
||||
}
|
||||
|
||||
return client
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"git.lumeweb.com/LumeWeb/gitea-github-proxy/config"
|
||||
"git.lumeweb.com/LumeWeb/gitea-github-proxy/db/model"
|
||||
"github.com/gorilla/mux"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type manifests struct {
|
||||
config *config.Config
|
||||
db *gorm.DB
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
type createdApp struct {
|
||||
Id uint `json:"id"`
|
||||
PEM string `json:"pem"`
|
||||
WebhookSecret string `json:"webhook_secret"`
|
||||
ClientId string `json:"client_id"`
|
||||
ClientSecret string `json:"client_secret"`
|
||||
HTMLUrl string `json:"html_url"`
|
||||
}
|
||||
|
||||
func newManifests(config *config.Config, db *gorm.DB, logger *zap.Logger) *manifests {
|
||||
return &manifests{
|
||||
config: config,
|
||||
db: db,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *manifests) handlerConversion(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
|
||||
code := vars["code"]
|
||||
|
||||
if len(code) == 0 {
|
||||
http.Error(w, "No code provided", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
appRecord := &model.Apps{Code: code}
|
||||
|
||||
if err := m.db.First(appRecord).Error; err != nil {
|
||||
m.logger.Error("App not found", zap.Error(err))
|
||||
http.Error(w, "App not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
app := createdApp{
|
||||
Id: appRecord.ID,
|
||||
PEM: appRecord.PrivateKey,
|
||||
ClientId: "",
|
||||
ClientSecret: "",
|
||||
WebhookSecret: appRecord.WebhookSecret,
|
||||
HTMLUrl: fmt.Sprintf("https://%s/apps/%s", m.config.Domain, appRecord.Name),
|
||||
}
|
||||
|
||||
appRecord.Code = generateTempCode()
|
||||
tx := m.db.Save(appRecord)
|
||||
if tx.Error != nil {
|
||||
m.logger.Error("Error updating app", zap.Error(tx.Error))
|
||||
http.Error(w, "Error updating app", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
appData, err := json.Marshal(app)
|
||||
if err != nil {
|
||||
m.logger.Error("Error marshalling app", zap.Error(err))
|
||||
http.Error(w, "Error marshalling app", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
w.Write(appData)
|
||||
}
|
||||
|
||||
func setupManifestsRoutes(params RouteParams) {
|
||||
r := params.R
|
||||
|
||||
manifestApi := newManifests(params.Config, params.Db, params.Logger)
|
||||
manifestsRouter := r.PathPrefix("/api/v3/app-manifests").Subrouter()
|
||||
|
||||
manifestsRouter.HandleFunc("/{code}/conversions", manifestApi.handlerConversion).Methods("POST")
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func NewRouter() *mux.Router {
|
||||
r := mux.NewRouter()
|
||||
|
||||
return r
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"git.lumeweb.com/LumeWeb/gitea-github-proxy/config"
|
||||
"github.com/gorilla/mux"
|
||||
"go.uber.org/fx"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type RouteParams struct {
|
||||
fx.In
|
||||
Config *config.Config
|
||||
Db *gorm.DB
|
||||
Logger *zap.Logger
|
||||
R *mux.Router
|
||||
WebhookManager *WebhookManager
|
||||
Oauth *Oauth
|
||||
}
|
||||
|
||||
func SetupRoutes(params RouteParams) {
|
||||
logger := params.Logger
|
||||
r := params.R
|
||||
|
||||
r.Use(loggingMiddleware(logger))
|
||||
|
||||
setupApiRoutes(params)
|
||||
setupSettingsRoutes(params)
|
||||
setupManifestsRoutes(params)
|
||||
setupAppRoutes(params)
|
||||
setupAppInstallRoutes(params)
|
||||
setupWebhookRoutes(params)
|
||||
setupRestRoutes(params)
|
||||
}
|
||||
|
||||
type ClientParams struct {
|
||||
Config *config.Config
|
||||
AuthToken string
|
||||
}
|
||||
|
||||
func getClient(params ClientParams) (*gitea.Client, error) {
|
||||
options := make([]gitea.ClientOption, 0)
|
||||
|
||||
authToken := ""
|
||||
if len(params.AuthToken) > 0 {
|
||||
authToken = params.AuthToken
|
||||
}
|
||||
|
||||
if len(authToken) == 0 {
|
||||
authToken = params.Config.Oauth.Token
|
||||
}
|
||||
|
||||
options = append(options, gitea.SetToken(authToken))
|
||||
|
||||
client, err := gitea.NewClient(params.Config.GiteaUrl, options...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
|
@ -0,0 +1,117 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"git.lumeweb.com/LumeWeb/gitea-github-proxy/config"
|
||||
"git.lumeweb.com/LumeWeb/gitea-github-proxy/db/model"
|
||||
"github.com/golang-jwt/jwt"
|
||||
"github.com/gorilla/mux"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type appApi struct {
|
||||
config *config.Config
|
||||
db *gorm.DB
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
func newAppApi(cfg *config.Config, db *gorm.DB, logger *zap.Logger) *appApi {
|
||||
return &appApi{config: cfg, db: db, logger: logger}
|
||||
}
|
||||
|
||||
func (a *appApi) handlerNewAppInstall(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte("App installations are not needed on this proxy. All webhooks are broadcasted to all registered apps."))
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
}
|
||||
|
||||
func (a *appApi) handlerAppGetAccessToken(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
now := time.Now().Add(-30 * time.Second)
|
||||
expirationTime := now.Add(100 * 365 * 24 * time.Hour)
|
||||
|
||||
appName := mux.Vars(r)["app"]
|
||||
|
||||
appRecord := &model.Apps{}
|
||||
appRecord.Name = appName
|
||||
|
||||
if err := a.db.First(appRecord).Error; err != nil {
|
||||
http.Error(w, "Failed to find app", http.StatusNotFound)
|
||||
a.logger.Error("Failed to find app", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
block, _ := pem.Decode([]byte(appRecord.PrivateKey))
|
||||
if block == nil {
|
||||
http.Error(w, "Failed to parse PEM block containing the key", http.StatusInternalServerError)
|
||||
a.logger.Error("Failed to parse PEM block containing the key")
|
||||
return
|
||||
}
|
||||
|
||||
privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to parse DER encoded private key", http.StatusInternalServerError)
|
||||
a.logger.Error("Failed to parse DER encoded private key", zap.Error(err))
|
||||
}
|
||||
|
||||
claims := jwt.MapClaims{
|
||||
"iss": appRecord.ID,
|
||||
"iat": now.Unix(),
|
||||
"exp": expirationTime,
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
|
||||
|
||||
signedToken, err := token.SignedString(privateKey)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to sign token", http.StatusInternalServerError)
|
||||
a.logger.Error("Failed to sign token", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
out := struct {
|
||||
Token string `json:"token"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
}{
|
||||
Token: signedToken,
|
||||
ExpiresAt: expirationTime,
|
||||
}
|
||||
|
||||
a.respond(w, http.StatusCreated, out)
|
||||
}
|
||||
|
||||
func (r appApi) respond(w http.ResponseWriter, status int, data interface{}) {
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to marshal response", http.StatusInternalServerError)
|
||||
r.logger.Error("Failed to marshal response", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
if data != nil {
|
||||
_, _ = w.Write(jsonData)
|
||||
}
|
||||
}
|
||||
|
||||
func setupAppRoutes(params RouteParams) {
|
||||
logger := params.Logger
|
||||
cfg := params.Config
|
||||
db := params.Db
|
||||
r := params.R
|
||||
|
||||
appApi := newAppApi(cfg, db, logger)
|
||||
appRouter := r.PathPrefix("/apps").Subrouter()
|
||||
|
||||
appRouter.Use(giteaOauthVerifyMiddleware(cfg))
|
||||
appRouter.Use(requireAuthMiddleware(cfg))
|
||||
|
||||
appRouter.HandleFunc("/{app}/installations/new", appApi.handlerNewAppInstall).Methods("GET")
|
||||
appRouter.HandleFunc("/{app}/access_token", appApi.handlerAppGetAccessToken).Methods("GET")
|
||||
}
|
|
@ -0,0 +1,119 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"git.lumeweb.com/LumeWeb/gitea-github-proxy/config"
|
||||
"git.lumeweb.com/LumeWeb/gitea-github-proxy/db/model"
|
||||
"github.com/golang-jwt/jwt"
|
||||
"github.com/gorilla/mux"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type appInstallApi struct {
|
||||
config *config.Config
|
||||
logger *zap.Logger
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func newAppInstallApi(cfg *config.Config, db *gorm.DB, logger *zap.Logger) *appInstallApi {
|
||||
return &appInstallApi{config: cfg, logger: logger, db: db}
|
||||
}
|
||||
|
||||
func (a *appInstallApi) handlerAppGetAccessToken(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
now := time.Now().Add(-30 * time.Second)
|
||||
expirationTime := now.Add(10 * time.Minute)
|
||||
|
||||
appId := mux.Vars(r)["installation_id"]
|
||||
appIdInt, err := strconv.ParseInt(appId, 10, 64)
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to parse app id", http.StatusBadRequest)
|
||||
a.logger.Error("Failed to parse app id", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
appRecord := &model.Apps{}
|
||||
appRecord.ID = uint(appIdInt)
|
||||
|
||||
if err := a.db.First(appRecord).Error; err != nil {
|
||||
http.Error(w, "Failed to find app", http.StatusNotFound)
|
||||
a.logger.Error("Failed to find app", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
block, _ := pem.Decode([]byte(appRecord.PrivateKey))
|
||||
if block == nil {
|
||||
http.Error(w, "Failed to parse PEM block containing the key", http.StatusInternalServerError)
|
||||
a.logger.Error("Failed to parse PEM block containing the key")
|
||||
return
|
||||
}
|
||||
|
||||
privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to parse DER encoded private key", http.StatusInternalServerError)
|
||||
a.logger.Error("Failed to parse DER encoded private key", zap.Error(err))
|
||||
}
|
||||
|
||||
claims := jwt.MapClaims{
|
||||
"iss": appId,
|
||||
"iat": now.Unix(),
|
||||
"exp": expirationTime,
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
|
||||
|
||||
signedToken, err := token.SignedString(privateKey)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to sign token", http.StatusInternalServerError)
|
||||
a.logger.Error("Failed to sign token", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
out := struct {
|
||||
Token string `json:"token"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
}{
|
||||
Token: signedToken,
|
||||
ExpiresAt: expirationTime,
|
||||
}
|
||||
|
||||
a.respond(w, http.StatusCreated, out)
|
||||
|
||||
}
|
||||
|
||||
func (r appInstallApi) respond(w http.ResponseWriter, status int, data interface{}) {
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to marshal response", http.StatusInternalServerError)
|
||||
r.logger.Error("Failed to marshal response", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
if data != nil {
|
||||
_, _ = w.Write(jsonData)
|
||||
}
|
||||
}
|
||||
|
||||
func setupAppInstallRoutes(params RouteParams) {
|
||||
logger := params.Logger
|
||||
cfg := params.Config
|
||||
db := params.Db
|
||||
r := params.R
|
||||
|
||||
appInstallApi := newAppInstallApi(cfg, db, logger)
|
||||
|
||||
appRouter := r.PathPrefix("/api/v3/app").Subrouter()
|
||||
appRouter.Use(githubRestVerifyMiddleware(params.Db))
|
||||
appRouter.Use(githubRestRequireAuthMiddleware(params.Config))
|
||||
|
||||
appRouter.HandleFunc("/installations/{installation_id}/access_tokens", appInstallApi.handlerAppGetAccessToken).Methods("POST").Name("app-install-get-access-token")
|
||||
}
|
|
@ -0,0 +1,473 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
gitea "code.gitea.io/sdk/gitea"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"git.lumeweb.com/LumeWeb/gitea-github-proxy/config"
|
||||
"github.com/google/go-github/v59/github"
|
||||
"github.com/gorilla/mux"
|
||||
"go.uber.org/zap"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type restApi struct {
|
||||
config *config.Config
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
func newRestApi(cfg *config.Config, logger *zap.Logger) *restApi {
|
||||
return &restApi{config: cfg, logger: logger}
|
||||
}
|
||||
|
||||
func (r restApi) handlerGetPullRequestFiles(w http.ResponseWriter, request *http.Request) {
|
||||
vars := mux.Vars(request)
|
||||
|
||||
owner := vars["owner"]
|
||||
repo := vars["repo"]
|
||||
pullNumber := vars["pull_number"]
|
||||
|
||||
client := r.getClientOrError(w)
|
||||
|
||||
if client == nil {
|
||||
return
|
||||
}
|
||||
parsedPullNumber, err := strconv.ParseInt(pullNumber, 10, 64)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to parse pull number", http.StatusBadRequest)
|
||||
r.logger.Error("Failed to parse pull number", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
files, r2, err := client.ListPullRequestFiles(owner, repo, parsedPullNumber, gitea.ListPullRequestFilesOptions{
|
||||
ListOptions: r.getPagingOptions(request),
|
||||
})
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to get pull request files", http.StatusInternalServerError)
|
||||
r.logger.Error("Failed to get pull request files", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
githubFiles := make([]*github.CommitFile, len(files))
|
||||
|
||||
for i, file := range files {
|
||||
githubFiles[i] = convertCommitFile(file)
|
||||
}
|
||||
|
||||
r.sendPagingHeaders(w, r2)
|
||||
r.respond(w, http.StatusOK, githubFiles)
|
||||
}
|
||||
|
||||
func (r restApi) getPagingOptions(request *http.Request) gitea.ListOptions {
|
||||
page, _ := strconv.Atoi(request.URL.Query().Get("page"))
|
||||
perPage, _ := strconv.Atoi(request.URL.Query().Get("per_page"))
|
||||
|
||||
return gitea.ListOptions{
|
||||
Page: page,
|
||||
PageSize: perPage,
|
||||
}
|
||||
}
|
||||
|
||||
func (r restApi) sendPagingHeaders(w http.ResponseWriter, apiResponse *gitea.Response) {
|
||||
links := []string{}
|
||||
|
||||
baseURL := fmt.Sprintf("https://%s", r.config.Domain)
|
||||
|
||||
joinStrings := func(elements []string, separator string) string {
|
||||
if len(elements) == 0 {
|
||||
return ""
|
||||
}
|
||||
result := elements[0]
|
||||
for i := 1; i < len(elements); i++ {
|
||||
result += separator + elements[i]
|
||||
}
|
||||
return result
|
||||
}
|
||||
joinLinks := func(links []string) string {
|
||||
return fmt.Sprintf("%s", joinStrings(links, ", "))
|
||||
}
|
||||
|
||||
if apiResponse.FirstPage > 0 {
|
||||
links = append(links, fmt.Sprintf(`<%s?page=%d>; rel="first"`, baseURL, apiResponse.FirstPage))
|
||||
}
|
||||
if apiResponse.PrevPage > 0 {
|
||||
links = append(links, fmt.Sprintf(`<%s?page=%d>; rel="prev"`, baseURL, apiResponse.PrevPage))
|
||||
}
|
||||
if apiResponse.NextPage > 0 {
|
||||
links = append(links, fmt.Sprintf(`<%s?page=%d>; rel="next"`, baseURL, apiResponse.NextPage))
|
||||
}
|
||||
if apiResponse.LastPage > 0 {
|
||||
links = append(links, fmt.Sprintf(`<%s?page=%d>; rel="last"`, baseURL, apiResponse.LastPage))
|
||||
}
|
||||
|
||||
if len(links) > 0 {
|
||||
w.Header().Add("Link", fmt.Sprintf("%s", joinLinks(links)))
|
||||
}
|
||||
}
|
||||
|
||||
func (r restApi) respond(w http.ResponseWriter, status int, data interface{}) {
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to marshal response", http.StatusInternalServerError)
|
||||
r.logger.Error("Failed to marshal response", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
if data != nil {
|
||||
_, _ = w.Write(jsonData)
|
||||
}
|
||||
}
|
||||
|
||||
func (r restApi) getClientOrError(w http.ResponseWriter) *gitea.Client {
|
||||
client, err := getClient(ClientParams{
|
||||
Config: r.config,
|
||||
})
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to get Gitea client", http.StatusInternalServerError)
|
||||
r.logger.Error("Failed to get Gitea client", zap.Error(err))
|
||||
return nil
|
||||
}
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
func (r restApi) handlerGetTree(w http.ResponseWriter, request *http.Request) {
|
||||
vars := mux.Vars(request)
|
||||
owner := vars["owner"]
|
||||
repo := vars["repo"]
|
||||
treeSha := vars["tree_sha"]
|
||||
recursive := false
|
||||
|
||||
if request.URL.Query().Has("recursive") {
|
||||
recursive = true
|
||||
}
|
||||
|
||||
client := r.getClientOrError(w)
|
||||
|
||||
if client == nil {
|
||||
return
|
||||
}
|
||||
|
||||
tree, r2, err := client.GetTrees(owner, repo, treeSha, recursive)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to get tree", http.StatusInternalServerError)
|
||||
r.logger.Error("Failed to get tree", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
treeResponse := convertGitTree(tree)
|
||||
|
||||
r.sendPagingHeaders(w, r2)
|
||||
r.respond(w, http.StatusOK, treeResponse)
|
||||
}
|
||||
|
||||
func (r restApi) handleGetIssueComments(w http.ResponseWriter, request *http.Request) {
|
||||
vars := mux.Vars(request)
|
||||
owner := vars["owner"]
|
||||
repo := vars["repo"]
|
||||
issueNumber := vars["issue_number"]
|
||||
|
||||
client := r.getClientOrError(w)
|
||||
|
||||
if client == nil {
|
||||
return
|
||||
}
|
||||
|
||||
issueNumberInt, err := strconv.Atoi(issueNumber)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to parse issue number", http.StatusBadRequest)
|
||||
r.logger.Error("Failed to parse issue number", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
comments, r2, err := client.ListIssueComments(owner, repo, int64(issueNumberInt), gitea.ListIssueCommentOptions{ListOptions: r.getPagingOptions(request)})
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to get issue comments", http.StatusInternalServerError)
|
||||
r.logger.Error("Failed to get issue comments", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
ghComments := make([]*github.IssueComment, len(comments))
|
||||
|
||||
for i, comment := range comments {
|
||||
reactions, _, _ := client.GetIssueCommentReactions(owner, repo, comment.ID)
|
||||
ghComments[i] = convertIssueComment(comment, reactions)
|
||||
}
|
||||
|
||||
r.sendPagingHeaders(w, r2)
|
||||
r.respond(w, http.StatusOK, ghComments)
|
||||
|
||||
}
|
||||
|
||||
func (r restApi) handlerCreateIssueComment(w http.ResponseWriter, request *http.Request) {
|
||||
vars := mux.Vars(request)
|
||||
owner := vars["owner"]
|
||||
repo := vars["repo"]
|
||||
issueNumber := vars["issue_number"]
|
||||
|
||||
client := r.getClientOrError(w)
|
||||
|
||||
if client == nil {
|
||||
return
|
||||
}
|
||||
|
||||
ghComment := github.IssueComment{}
|
||||
|
||||
if err := r.parseJsonBody(request, &ghComment); err != nil {
|
||||
http.Error(w, "Failed to parse request body", http.StatusBadRequest)
|
||||
r.logger.Error("Failed to parse request body", zap.Error(err))
|
||||
}
|
||||
|
||||
issueNumberInt, err := strconv.Atoi(issueNumber)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to parse issue number", http.StatusBadRequest)
|
||||
r.logger.Error("Failed to parse issue number", zap.Error(err))
|
||||
return
|
||||
}
|
||||
comment := gitea.CreateIssueCommentOption{
|
||||
Body: ghComment.GetBody(),
|
||||
}
|
||||
|
||||
commentResponse, _, err := client.CreateIssueComment(owner, repo, int64(issueNumberInt), comment)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to create comment", http.StatusInternalServerError)
|
||||
r.logger.Error("Failed to create comment", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
newGhComment := convertIssueComment(commentResponse, nil)
|
||||
|
||||
r.respond(w, http.StatusCreated, newGhComment)
|
||||
}
|
||||
|
||||
func (r restApi) handlerUpdateIssueComment(w http.ResponseWriter, request *http.Request) {
|
||||
vars := mux.Vars(request)
|
||||
owner := vars["owner"]
|
||||
repo := vars["repo"]
|
||||
commentID := vars["comment_id"]
|
||||
|
||||
client := r.getClientOrError(w)
|
||||
|
||||
if client == nil {
|
||||
return
|
||||
}
|
||||
|
||||
ghComment := github.IssueComment{}
|
||||
|
||||
if err := r.parseJsonBody(request, &ghComment); err != nil {
|
||||
http.Error(w, "Failed to parse request body", http.StatusBadRequest)
|
||||
r.logger.Error("Failed to parse request body", zap.Error(err))
|
||||
}
|
||||
|
||||
commentIDInt, err := strconv.Atoi(commentID)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to parse comment ID", http.StatusBadRequest)
|
||||
r.logger.Error("Failed to parse comment ID", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
comment := gitea.EditIssueCommentOption{
|
||||
Body: ghComment.GetBody(),
|
||||
}
|
||||
|
||||
commentResponse, _, err := client.EditIssueComment(owner, repo, int64(commentIDInt), comment)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to update comment", http.StatusInternalServerError)
|
||||
r.logger.Error("Failed to update comment", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
newGhComment := convertIssueComment(commentResponse, nil)
|
||||
|
||||
r.respond(w, http.StatusOK, newGhComment)
|
||||
}
|
||||
|
||||
func (r restApi) handlerCreateRelease(w http.ResponseWriter, request *http.Request) {
|
||||
vars := mux.Vars(request)
|
||||
owner := vars["owner"]
|
||||
repo := vars["repo"]
|
||||
|
||||
client := r.getClientOrError(w)
|
||||
|
||||
if client == nil {
|
||||
return
|
||||
}
|
||||
|
||||
ghRelease := github.RepositoryRelease{}
|
||||
|
||||
if err := r.parseJsonBody(request, &ghRelease); err != nil {
|
||||
http.Error(w, "Failed to parse request body", http.StatusBadRequest)
|
||||
r.logger.Error("Failed to parse request body", zap.Error(err))
|
||||
}
|
||||
|
||||
release := gitea.CreateReleaseOption{
|
||||
TagName: ghRelease.GetTagName(),
|
||||
Target: ghRelease.GetTargetCommitish(),
|
||||
Title: ghRelease.GetName(),
|
||||
Note: ghRelease.GetBody(),
|
||||
IsDraft: ghRelease.GetDraft(),
|
||||
IsPrerelease: ghRelease.GetPrerelease(),
|
||||
}
|
||||
|
||||
releaseResponse, r2, err := client.CreateRelease(owner, repo, release)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to create release", http.StatusInternalServerError)
|
||||
r.logger.Error("Failed to create release", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
newGhRelease := convertRelease(releaseResponse)
|
||||
|
||||
r.sendPagingHeaders(w, r2)
|
||||
r.respond(w, http.StatusCreated, newGhRelease)
|
||||
}
|
||||
|
||||
func (r restApi) handlerCreatePullRequest(w http.ResponseWriter, request *http.Request) {
|
||||
vars := mux.Vars(request)
|
||||
owner := vars["owner"]
|
||||
repo := vars["repo"]
|
||||
|
||||
client := r.getClientOrError(w)
|
||||
|
||||
if client == nil {
|
||||
return
|
||||
}
|
||||
|
||||
ghPullRequest := github.NewPullRequest{}
|
||||
|
||||
if err := r.parseJsonBody(request, &ghPullRequest); err != nil {
|
||||
http.Error(w, "Failed to parse request body", http.StatusBadRequest)
|
||||
r.logger.Error("Failed to parse request body", zap.Error(err))
|
||||
}
|
||||
|
||||
pullRequest := gitea.CreatePullRequestOption{
|
||||
Title: ghPullRequest.GetTitle(),
|
||||
Head: ghPullRequest.GetHead(),
|
||||
Base: ghPullRequest.GetBase(),
|
||||
Body: ghPullRequest.GetBody(),
|
||||
}
|
||||
|
||||
pullRequestResponse, r2, err := client.CreatePullRequest(owner, repo, pullRequest)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to create pull request", http.StatusInternalServerError)
|
||||
r.logger.Error("Failed to create pull request", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
newGhPullRequest := convertPullRequest(pullRequestResponse)
|
||||
|
||||
r.sendPagingHeaders(w, r2)
|
||||
r.respond(w, http.StatusCreated, newGhPullRequest)
|
||||
}
|
||||
|
||||
func (r restApi) handlerUpdatePullRequest(w http.ResponseWriter, request *http.Request) {
|
||||
vars := mux.Vars(request)
|
||||
owner := vars["owner"]
|
||||
repo := vars["repo"]
|
||||
pullNumber := vars["pull_number"]
|
||||
|
||||
parsedPullNumber, err := strconv.ParseInt(pullNumber, 10, 64)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to parse pull number", http.StatusBadRequest)
|
||||
r.logger.Error("Failed to parse pull number", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
client := r.getClientOrError(w)
|
||||
|
||||
if client == nil {
|
||||
return
|
||||
}
|
||||
|
||||
type pullRequestUpdate struct {
|
||||
Title *string `json:"title,omitempty"`
|
||||
Body *string `json:"body,omitempty"`
|
||||
State *string `json:"state,omitempty"`
|
||||
Base *string `json:"base,omitempty"`
|
||||
MaintainerCanModify *bool `json:"maintainer_can_modify,omitempty"`
|
||||
}
|
||||
|
||||
ghPullRequest := pullRequestUpdate{}
|
||||
pullRequest := gitea.EditPullRequestOption{}
|
||||
|
||||
if err := r.parseJsonBody(request, &ghPullRequest); err != nil {
|
||||
http.Error(w, "Failed to parse request body", http.StatusBadRequest)
|
||||
r.logger.Error("Failed to parse request body", zap.Error(err))
|
||||
}
|
||||
|
||||
if ghPullRequest.State != nil {
|
||||
if *ghPullRequest.State != "open" && *ghPullRequest.State != "closed" {
|
||||
http.Error(w, "Invalid state", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
state := *ghPullRequest.State
|
||||
stateType := gitea.StateType(state)
|
||||
|
||||
pullRequest.State = &stateType
|
||||
}
|
||||
|
||||
if ghPullRequest.Title != nil {
|
||||
pullRequest.Title = *ghPullRequest.Title
|
||||
}
|
||||
|
||||
if ghPullRequest.Body != nil {
|
||||
pullRequest.Body = *ghPullRequest.Body
|
||||
}
|
||||
|
||||
pullRequestResponse, r2, err := client.EditPullRequest(owner, repo, parsedPullNumber, pullRequest)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to update pull request", http.StatusInternalServerError)
|
||||
r.logger.Error("Failed to update pull request", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
newGhPullRequest := convertPullRequest(pullRequestResponse)
|
||||
|
||||
r.sendPagingHeaders(w, r2)
|
||||
r.respond(w, http.StatusOK, newGhPullRequest)
|
||||
}
|
||||
|
||||
func (r restApi) parseJsonBody(request *http.Request, obj interface{}) error {
|
||||
if obj == nil {
|
||||
obj = make(map[string]interface{})
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(request.Body)
|
||||
return decoder.Decode(obj)
|
||||
}
|
||||
|
||||
func setupRestRoutes(params RouteParams) {
|
||||
logger := params.Logger
|
||||
cfg := params.Config
|
||||
r := params.R
|
||||
|
||||
restApi := newRestApi(cfg, logger)
|
||||
|
||||
setupRoutes := func(r *mux.Router) {
|
||||
r.HandleFunc("/repos/{owner}/{repo}/pulls/{pull_number}/files", restApi.handlerGetPullRequestFiles).Methods("GET")
|
||||
r.HandleFunc("/repos/{owner}/{repo}/git/trees/{tree_sha}", restApi.handlerGetTree).Methods("GET")
|
||||
|
||||
// Comment routes
|
||||
r.HandleFunc("/repos/{owner}/{repo}/issues/{issue_number}/comments", restApi.handleGetIssueComments).Methods("GET")
|
||||
r.HandleFunc("/repos/{owner}/{repo}/issues/{issue_number}/comments", restApi.handlerCreateIssueComment).Methods("POST")
|
||||
r.HandleFunc("/repos/{owner}/{repo}/issues/comments/{comment_id}", restApi.handlerUpdateIssueComment).Methods("PATCH")
|
||||
|
||||
// Repo Release routes
|
||||
r.HandleFunc("/repos/{owner}/{repo}/releases", restApi.handlerCreateRelease).Methods("POST")
|
||||
|
||||
// Pull Request routes
|
||||
r.HandleFunc("/repos/{owner}/{repo}/pulls", restApi.handlerCreatePullRequest).Methods("POST")
|
||||
r.HandleFunc("/repos/{owner}/{repo}/pulls/{pull_number}", restApi.handlerUpdatePullRequest).Methods("PATCH")
|
||||
}
|
||||
|
||||
restRouter := r.PathPrefix("/api").Subrouter()
|
||||
restRouter.Use(githubRestVerifyMiddleware(params.Db))
|
||||
restRouter.Use(githubRestRequireAuthMiddleware(params.Config))
|
||||
|
||||
setupRoutes(restRouter)
|
||||
setupRoutes(restRouter.PathPrefix("/v3").Subrouter())
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"git.lumeweb.com/LumeWeb/gitea-github-proxy/config"
|
||||
"git.lumeweb.com/LumeWeb/gitea-github-proxy/db/model"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
func newSettingsApi(cfg *config.Config, db *gorm.DB, logger *zap.Logger) *settingsApi {
|
||||
return &settingsApi{config: cfg, db: db, logger: logger}
|
||||
}
|
||||
|
||||
func (s settingsApi) handlerNewApp(w http.ResponseWriter, r *http.Request) {
|
||||
manifestData := r.FormValue("manifest")
|
||||
|
||||
var manifestObj manifest
|
||||
|
||||
err := json.Unmarshal([]byte(manifestData), &manifestObj)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to parse manifest", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
appData := newApp()
|
||||
|
||||
appRecord := &model.Apps{
|
||||
Name: manifestObj.Name,
|
||||
Url: manifestObj.Url,
|
||||
WebhookUrl: manifestObj.HookAttributes.URL,
|
||||
Code: generateTempCode(),
|
||||
WebhookSecret: generateTempCode(),
|
||||
PrivateKey: appData.PrivateKey,
|
||||
}
|
||||
|
||||
tx := s.db.Create(appRecord)
|
||||
if tx.Error != nil {
|
||||
s.logger.Error("Error creating app", zap.Error(tx.Error))
|
||||
http.Error(w, "Error creating app", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if len(manifestObj.RedirectURL) == 0 {
|
||||
s.logger.Error("Redirect URL is required")
|
||||
http.Error(w, "Redirect URL is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
redirectUrl, err := url.Parse(manifestObj.RedirectURL)
|
||||
if err != nil {
|
||||
s.logger.Error("Error parsing redirect URL", zap.Error(err))
|
||||
http.Error(w, "Error parsing redirect URL", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
query := redirectUrl.Query()
|
||||
|
||||
query.Add("code", appRecord.Code)
|
||||
|
||||
if r.URL.Query().Get("state") != "" {
|
||||
query.Add("state", r.URL.Query().Get("state"))
|
||||
}
|
||||
|
||||
redirectUrl.RawQuery = query.Encode()
|
||||
|
||||
http.Redirect(w, r, redirectUrl.String(), http.StatusFound)
|
||||
|
||||
}
|
||||
|
||||
func setupSettingsRoutes(params RouteParams) {
|
||||
r := params.R
|
||||
|
||||
settingsRouter := r.PathPrefix("/settings").Subrouter()
|
||||
settingsRouter.Use(giteaOauthVerifyMiddleware(params.Config))
|
||||
settingsRouter.Use(requireAuthMiddleware(params.Config))
|
||||
|
||||
settingsApi := newSettingsApi(params.Config, params.Db, params.Logger)
|
||||
|
||||
settingsRouter.HandleFunc("/apps/new", settingsApi.handlerNewApp).Methods("POST")
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"git.lumeweb.com/LumeWeb/gitea-github-proxy/config"
|
||||
"go.uber.org/zap"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type setupApi struct {
|
||||
config *config.Config
|
||||
logger *zap.Logger
|
||||
oauth *Oauth
|
||||
}
|
||||
|
||||
func newSetupApi(config *config.Config, logger *zap.Logger, oauth *Oauth) *setupApi {
|
||||
return &setupApi{config: config, logger: logger, oauth: oauth}
|
||||
}
|
||||
|
||||
func (s setupApi) setupHandler(w http.ResponseWriter, r *http.Request) {
|
||||
status := getAuthedStatusFromRequest(r)
|
||||
|
||||
if status {
|
||||
redirectCookie := getCookie(r, REDIRECT_AFTER_AUTH)
|
||||
if redirectCookie != "" {
|
||||
deleteCookie(w, REDIRECT_AFTER_AUTH)
|
||||
http.Redirect(w, r, redirectCookie, http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("Setup is complete, you are authorized to use the proxy."))
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, s.oauth.authUrl(), http.StatusFound)
|
||||
}
|
||||
func (s setupApi) callbackHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Query().Get("error") != "" {
|
||||
http.Error(w, errors.Join(errors.New("Error authorizing with Gitea: "), errors.New(r.URL.Query().Get("error"))).Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
code := r.URL.Query().Get("code")
|
||||
if len(code) == 0 {
|
||||
http.Error(w, "No code provided", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
token, err := s.oauth.exchange(code)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to exchange code for token", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
setAuthCookie(token.AccessToken, s.config.Domain, w)
|
||||
|
||||
http.Redirect(w, r, "/setup", http.StatusFound)
|
||||
}
|
||||
|
||||
func setupApiRoutes(params RouteParams) {
|
||||
r := params.R
|
||||
|
||||
setupRouter := r.PathPrefix("/setup").Subrouter()
|
||||
setupRouter.Use(giteaOauthVerifyMiddleware(params.Config))
|
||||
|
||||
setupApi := newSetupApi(params.Config, params.Logger, params.Oauth)
|
||||
setupRouter.HandleFunc("", setupApi.setupHandler).Methods("GET")
|
||||
setupRouter.HandleFunc("/callback", setupApi.callbackHandler).Methods("GET")
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"encoding/json"
|
||||
"git.lumeweb.com/LumeWeb/gitea-github-proxy/config"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type webhookApi struct {
|
||||
config *config.Config
|
||||
logger *zap.Logger
|
||||
db *gorm.DB
|
||||
whm *WebhookManager
|
||||
}
|
||||
|
||||
func newWebhookApi(cfg *config.Config, db *gorm.DB, logger *zap.Logger, whm *WebhookManager) *webhookApi {
|
||||
return &webhookApi{config: cfg, db: db, logger: logger, whm: whm}
|
||||
}
|
||||
|
||||
func (a *webhookApi) handlePullRequest(w http.ResponseWriter, r *http.Request) {
|
||||
webhook := a.getWebhookData(w, r)
|
||||
|
||||
payload := &structs.PullRequestPayload{}
|
||||
|
||||
err := json.Unmarshal(webhook, payload)
|
||||
if a.unmarshallError(w, err) {
|
||||
return
|
||||
}
|
||||
|
||||
a.whm.HandlePullRequest(payload, r)
|
||||
}
|
||||
|
||||
func setupWebhookRoutes(params RouteParams) {
|
||||
cfg := params.Config
|
||||
db := params.Db
|
||||
logger := params.Logger
|
||||
r := params.R
|
||||
whm := params.WebhookManager
|
||||
|
||||
webhookApi := newWebhookApi(cfg, db, logger, whm)
|
||||
webhookRouter := r.PathPrefix("/api").Subrouter()
|
||||
|
||||
webhookRouter.Use(gitea.VerifyWebhookSignatureMiddleware(cfg.GiteaWebHookSecret))
|
||||
webhookRouter.Use(storeWebhookDataMiddleware(logger))
|
||||
|
||||
webhookRouter.HandleFunc("/webhooks/pull_request", webhookApi.handlePullRequest).Methods("POST")
|
||||
}
|
||||
|
||||
func (a *webhookApi) getWebhookData(w http.ResponseWriter, r *http.Request) []byte {
|
||||
return getWebhookData(r, a.logger)
|
||||
}
|
||||
|
||||
func (a *webhookApi) unmarshallError(w http.ResponseWriter, err error) bool {
|
||||
if err != nil {
|
||||
a.logger.Error("Failed to unmarshal webhook data", zap.Error(err))
|
||||
http.Error(w, "Failed to unmarshal webhook data", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
return err != nil
|
||||
}
|
||||
func getWebhookData(r *http.Request, logger *zap.Logger) []byte {
|
||||
webhook := r.Context().Value(WEBHOOK_CONTEXT_KEY)
|
||||
webhookData, ok := webhook.([]byte)
|
||||
if !ok {
|
||||
logger.Fatal("Failed to get webhook data from context")
|
||||
}
|
||||
|
||||
return webhookData
|
||||
}
|
|
@ -0,0 +1,133 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
giteaTypes "code.gitea.io/gitea/modules/structs"
|
||||
"encoding/json"
|
||||
"git.lumeweb.com/LumeWeb/gitea-github-proxy/config"
|
||||
"git.lumeweb.com/LumeWeb/gitea-github-proxy/db/model"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type WebhookManager struct {
|
||||
config *config.Config
|
||||
db *gorm.DB
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
type IncomingWebhookData struct {
|
||||
Headers http.Header
|
||||
}
|
||||
|
||||
func NewWebhookManager(cfg *config.Config, db *gorm.DB, logger *zap.Logger) *WebhookManager {
|
||||
return &WebhookManager{config: cfg, db: db, logger: logger}
|
||||
}
|
||||
|
||||
func (whm *WebhookManager) HandlePullRequest(request *giteaTypes.PullRequestPayload, r *http.Request) {
|
||||
ghEvent := convertPullRequestEvent(request)
|
||||
githubAction := translatePrAction(request.Action, true)
|
||||
r.Header.Set("X-GitHub-Event", githubAction)
|
||||
|
||||
whm.sendWebhooks(ghEvent, r)
|
||||
}
|
||||
func (whm *WebhookManager) sendWebhooks(request interface{}, r *http.Request) {
|
||||
var apps []model.Apps
|
||||
result := whm.db.Find(&apps)
|
||||
if result.Error != nil {
|
||||
whm.logger.Error("Failed to query apps", zap.Error(result.Error))
|
||||
return
|
||||
}
|
||||
for _, app := range apps {
|
||||
go func(app model.Apps) {
|
||||
|
||||
payloadBytes, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
whm.logger.Error("Failed to marshal payload", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
var rawMap map[string]interface{}
|
||||
err = json.Unmarshal(payloadBytes, &rawMap)
|
||||
if err != nil {
|
||||
whm.logger.Error("Failed to unmarshal payload", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
rawMap["installation"] = struct {
|
||||
ID uint `json:"id"`
|
||||
}{ID: app.ID}
|
||||
|
||||
payloadBytes, err = json.Marshal(rawMap)
|
||||
if err != nil {
|
||||
whm.logger.Error("Failed to marshal payload", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", app.WebhookUrl, bytes.NewBuffer(payloadBytes))
|
||||
if err != nil {
|
||||
whm.logger.Error("Failed to create request", zap.Error(err), zap.String("url", app.WebhookUrl))
|
||||
return
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
for key, values := range r.Header {
|
||||
for _, value := range values {
|
||||
req.Header.Add(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
payload := toNormalizedJson(payloadBytes)
|
||||
|
||||
signature := generatePayloadSignature(payload, app.WebhookSecret)
|
||||
req.Header.Set("X-Hub-Signature-256", signature)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
whm.logger.Error("Failed to send webhook", zap.Error(err), zap.String("url", app.WebhookUrl))
|
||||
return
|
||||
}
|
||||
defer func(Body io.ReadCloser) {
|
||||
err := Body.Close()
|
||||
if err != nil {
|
||||
whm.logger.Error("Failed to close response body", zap.Error(err))
|
||||
}
|
||||
}(resp.Body)
|
||||
|
||||
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
||||
whm.logger.Info("Webhook sent successfully", zap.String("url", app.WebhookUrl))
|
||||
} else {
|
||||
whm.logger.Error("Webhook failed", zap.String("url", app.WebhookUrl), zap.Int("status", resp.StatusCode))
|
||||
}
|
||||
}(app)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Ported from https://github.com/octokit/webhooks.js/blob/984f3f51e077dbc7637aa55406c3bb114dbb7f82/src/to-normalized-json-string.ts
|
||||
*/
|
||||
|
||||
func toNormalizedJson(payload []byte) []byte {
|
||||
jsonString := string(payload)
|
||||
|
||||
// Regex to find Unicode escape sequences
|
||||
re := regexp.MustCompile(`\\u([\da-fA-F]{4})`)
|
||||
|
||||
// Function to convert found sequences to uppercase
|
||||
replaceFunc := func(match string) string {
|
||||
parts := strings.Split(match, "\\u")
|
||||
// Convert the Unicode sequence part to uppercase
|
||||
return parts[0] + "\\u" + strings.ToUpper(parts[1])
|
||||
}
|
||||
|
||||
// Replace the matches in the jsonString
|
||||
normalizedString := re.ReplaceAllStringFunc(jsonString, replaceFunc)
|
||||
|
||||
return []byte(normalizedString)
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"git.lumeweb.com/LumeWeb/gitea-github-proxy/api"
|
||||
"git.lumeweb.com/LumeWeb/gitea-github-proxy/config"
|
||||
"git.lumeweb.com/LumeWeb/gitea-github-proxy/db"
|
||||
"go.uber.org/fx"
|
||||
"go.uber.org/fx/fxevent"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func main() {
|
||||
logger, _ := zap.NewDevelopment()
|
||||
|
||||
fx.New(
|
||||
fx.Supply(logger),
|
||||
fx.WithLogger(func(logger *zap.Logger) fxevent.Logger {
|
||||
log := &fxevent.ZapLogger{Logger: logger}
|
||||
log.UseLogLevel(zapcore.InfoLevel)
|
||||
log.UseErrorLevel(zapcore.ErrorLevel)
|
||||
return log
|
||||
}),
|
||||
config.Module,
|
||||
db.Module,
|
||||
fx.Provide(api.NewOauth),
|
||||
fx.Provide(api.NewRouter),
|
||||
fx.Provide(NewServer),
|
||||
fx.Provide(api.NewWebhookManager),
|
||||
fx.Invoke(api.SetupRoutes),
|
||||
fx.Invoke(func(*http.Server) {}),
|
||||
).Run()
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.lumeweb.com/LumeWeb/gitea-github-proxy/config"
|
||||
"github.com/gorilla/mux"
|
||||
"go.uber.org/fx"
|
||||
"go.uber.org/zap"
|
||||
"net"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type ServerParams struct {
|
||||
fx.In
|
||||
Logger *zap.Logger
|
||||
Router *mux.Router
|
||||
Config *config.Config
|
||||
}
|
||||
|
||||
func NewServer(lc fx.Lifecycle, params ServerParams) (*http.Server, error) {
|
||||
srv := &http.Server{
|
||||
Addr: fmt.Sprintf("%s:%d", params.Config.Host, params.Config.Port),
|
||||
Handler: params.Router,
|
||||
}
|
||||
|
||||
lc.Append(fx.Hook{
|
||||
OnStart: func(ctx context.Context) error {
|
||||
ln, err := net.Listen("tcp", srv.Addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
err := srv.Serve(ln)
|
||||
if err != nil {
|
||||
params.Logger.Fatal("Failed to serve", zap.Error(err))
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
},
|
||||
OnStop: func(ctx context.Context) error {
|
||||
return srv.Shutdown(ctx)
|
||||
},
|
||||
})
|
||||
return srv, nil
|
||||
}
|
|
@ -0,0 +1,158 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"encoding"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"github.com/fatih/structs"
|
||||
"github.com/iancoleman/strcase"
|
||||
"github.com/spf13/viper"
|
||||
"go.uber.org/fx"
|
||||
"go.uber.org/zap"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
)
|
||||
|
||||
type PrivateKey ed25519.PrivateKey
|
||||
|
||||
var (
|
||||
_ encoding.TextUnmarshaler = (*PrivateKey)(nil)
|
||||
_ encoding.TextMarshaler = (*PrivateKey)(nil)
|
||||
)
|
||||
|
||||
func (p *PrivateKey) UnmarshalText(text []byte) error {
|
||||
dec, err := base64.StdEncoding.DecodeString(string(text))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*p = dec
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PrivateKey) MarshalText() ([]byte, error) {
|
||||
return []byte(base64.StdEncoding.EncodeToString(*p)), nil
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Host string `mapstructure:"host" structs:"host"`
|
||||
Port int `mapstructure:"port" structs:"port"`
|
||||
GiteaUrl string `mapstructure:"gitea_url" structs:"gitea_url"`
|
||||
DbPath string `mapstructure:"db_path" structs:"db_path"`
|
||||
Oauth OauthConfig
|
||||
JwtPrivateKey PrivateKey
|
||||
Domain string `mapstructure:"domain" structs:"domain"`
|
||||
GiteaWebHookSecret string `mapstructure:"gitea_webhook_secret" structs:"gitea_webhook_secret"`
|
||||
}
|
||||
|
||||
type OauthConfig struct {
|
||||
Authorization string `mapstructure:"authorization" structs:"authorization"`
|
||||
Token string `mapstructure:"token" structs:"token"`
|
||||
ClientId string `mapstructure:"client_id" structs:"client_id"`
|
||||
ClientSecret string `mapstructure:"client_secret" structs:"client_secret"`
|
||||
RefreshToken string `mapstructure:"refresh_token" structs:"refresh_token"`
|
||||
}
|
||||
|
||||
var Module = fx.Module("config",
|
||||
fx.Options(
|
||||
fx.Provide(NewConfig),
|
||||
),
|
||||
)
|
||||
|
||||
func NewConfig(logger *zap.Logger) *Config {
|
||||
c := &Config{}
|
||||
|
||||
_, sk, err := ed25519.GenerateKey(nil)
|
||||
|
||||
if err != nil {
|
||||
logger.Fatal("Error generating private key", zap.Error(err))
|
||||
}
|
||||
|
||||
// Set the default values
|
||||
viper.SetDefault("host", "localhost")
|
||||
viper.SetDefault("port", 8080)
|
||||
viper.SetDefault("gitea_url", "")
|
||||
viper.SetDefault("db_path", "data.db")
|
||||
viper.SetDefault("jwt_private_key", base64.StdEncoding.EncodeToString(sk))
|
||||
viper.SetDefault("oauth.client_id", "")
|
||||
viper.SetDefault("oauth.client_secret", "")
|
||||
|
||||
viper.AddConfigPath(".")
|
||||
viper.AddConfigPath("/etc/gitea-github-proxy")
|
||||
viper.SetConfigName("config")
|
||||
viper.SetConfigType("yaml")
|
||||
err = viper.ReadInConfig()
|
||||
if err != nil {
|
||||
err = viper.SafeWriteConfig()
|
||||
if err != nil {
|
||||
logger.Fatal("Error writing config file", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
err = viper.Unmarshal(c)
|
||||
if err != nil {
|
||||
logger.Fatal("Error unmarshalling config", zap.Error(err))
|
||||
}
|
||||
|
||||
/* err = viper.UnmarshalKey("jwt_private_key", &c.JwtPrivateKey, viper.DecodeHook(mapstructure.TextUnmarshallerHookFunc()))
|
||||
if err != nil {
|
||||
logger.Fatal("Error unmarshalling jwtPrivateKey", zap.Error(err))
|
||||
}*/
|
||||
|
||||
if len(c.GiteaUrl) == 0 {
|
||||
logger.Fatal("Gitea URL is required")
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func SaveConfig(cfg *Config) error {
|
||||
data := structs.New(cfg)
|
||||
|
||||
for _, field := range data.Fields() {
|
||||
processFields(field, nil)
|
||||
}
|
||||
|
||||
/* if field, ok := interface{}(cfg.JwtPrivateKey).(encoding.TextMarshaler); ok {
|
||||
text, err := field.MarshalText()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
viper.Set("jwt_private_key", string(text))
|
||||
}
|
||||
*/
|
||||
return viper.WriteConfig()
|
||||
}
|
||||
|
||||
func processFields(f *structs.Field, parent *structs.Field) {
|
||||
if f.IsZero() {
|
||||
return
|
||||
}
|
||||
|
||||
if f.Kind() == reflect.Struct {
|
||||
fields := f.Fields()
|
||||
|
||||
if len(fields) > 0 {
|
||||
for _, field := range fields {
|
||||
processFields(field, f)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
name := ""
|
||||
|
||||
if parent != nil {
|
||||
name = fmt.Sprintf("%s.%s", parent.Name(), f.Name())
|
||||
} else {
|
||||
name = f.Name()
|
||||
}
|
||||
|
||||
name = strcase.ToSnakeWithIgnore(name, ".")
|
||||
|
||||
viper.Set(name, f.Value())
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.lumeweb.com/LumeWeb/gitea-github-proxy/config"
|
||||
"git.lumeweb.com/LumeWeb/gitea-github-proxy/db/model"
|
||||
"go.uber.org/fx"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type DatabaseParams struct {
|
||||
fx.In
|
||||
Config *config.Config
|
||||
}
|
||||
|
||||
var Module = fx.Module("db",
|
||||
fx.Options(
|
||||
fx.Provide(NewDatabase),
|
||||
),
|
||||
)
|
||||
|
||||
func NewDatabase(lc fx.Lifecycle, params DatabaseParams) *gorm.DB {
|
||||
db, err := gorm.Open(sqlite.Open(params.Config.DbPath), &gorm.Config{})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
lc.Append(fx.Hook{
|
||||
OnStart: func(ctx context.Context) error {
|
||||
return db.AutoMigrate(
|
||||
&model.Apps{},
|
||||
)
|
||||
},
|
||||
OnStop: func(ctx context.Context) error {
|
||||
sqlDb, err := db.DB()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return sqlDb.Close()
|
||||
},
|
||||
})
|
||||
|
||||
return db
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package model
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
type Apps struct {
|
||||
gorm.Model
|
||||
|
||||
Name string
|
||||
Url string
|
||||
WebhookUrl string
|
||||
WebhookSecret string
|
||||
Code string `gorm:"uniqueIndex"`
|
||||
PrivateKey string
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
module git.lumeweb.com/LumeWeb/gitea-github-proxy
|
||||
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
code.gitea.io/gitea v1.21.5
|
||||
code.gitea.io/sdk/gitea v0.17.1
|
||||
github.com/fatih/structs v1.1.0
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible
|
||||
github.com/google/go-github/v59 v59.0.0
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/iancoleman/strcase v0.3.0
|
||||
github.com/spf13/viper v1.18.2
|
||||
go.uber.org/fx v1.20.1
|
||||
go.uber.org/zap v1.26.0
|
||||
golang.org/x/oauth2 v0.17.0
|
||||
gorm.io/driver/sqlite v1.5.5
|
||||
gorm.io/gorm v1.25.7
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davidmz/go-pageant v1.0.2 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/hashicorp/go-version v1.6.0 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.17 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.11.0 // indirect
|
||||
github.com/spf13/cast v1.6.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
go.uber.org/dig v1.17.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/crypto v0.19.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
|
||||
golang.org/x/sys v0.17.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
google.golang.org/appengine v1.6.8 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
Loading…
Reference in New Issue