Skip to content

Commit cdc4159

Browse files
authored
feat(github): support GPG verification (#7996 close #7986)
* feat(github): support GPG verification * chore
1 parent 79bef0b commit cdc4159

File tree

6 files changed

+193
-69
lines changed

6 files changed

+193
-69
lines changed

drivers/github/driver.go

Lines changed: 83 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package github
33
import (
44
"context"
55
"encoding/base64"
6-
"errors"
76
"fmt"
87
"io"
98
"net/http"
@@ -12,12 +11,14 @@ import (
1211
"sync"
1312
"text/template"
1413

14+
"github.com/ProtonMail/go-crypto/openpgp"
1515
"github.com/alist-org/alist/v3/drivers/base"
1616
"github.com/alist-org/alist/v3/internal/driver"
1717
"github.com/alist-org/alist/v3/internal/errs"
1818
"github.com/alist-org/alist/v3/internal/model"
1919
"github.com/alist-org/alist/v3/pkg/utils"
2020
"github.com/go-resty/resty/v2"
21+
"github.com/pkg/errors"
2122
log "github.com/sirupsen/logrus"
2223
)
2324

@@ -33,6 +34,7 @@ type Github struct {
3334
moveMsgTmpl *template.Template
3435
isOnBranch bool
3536
commitMutex sync.Mutex
37+
pgpEntity *openpgp.Entity
3638
}
3739

3840
func (d *Github) Config() driver.Config {
@@ -102,6 +104,26 @@ func (d *Github) Init(ctx context.Context) error {
102104
_, err = d.getBranchHead()
103105
d.isOnBranch = err == nil
104106
}
107+
if d.GPGPrivateKey != "" {
108+
if d.CommitterName == "" || d.AuthorName == "" {
109+
user, e := d.getAuthenticatedUser()
110+
if e != nil {
111+
return e
112+
}
113+
if d.CommitterName == "" {
114+
d.CommitterName = user.Name
115+
d.CommitterEmail = user.Email
116+
}
117+
if d.AuthorName == "" {
118+
d.AuthorName = user.Name
119+
d.AuthorEmail = user.Email
120+
}
121+
}
122+
d.pgpEntity, err = loadPrivateKey(d.GPGPrivateKey, d.GPGKeyPassphrase)
123+
if err != nil {
124+
return err
125+
}
126+
}
105127
return nil
106128
}
107129

@@ -174,10 +196,39 @@ func (d *Github) MakeDir(ctx context.Context, parentDir model.Obj, dirName strin
174196
if parent.Entries == nil {
175197
return errs.NotFolder
176198
}
177-
// if parent folder contains .gitkeep only, mark it and delete .gitkeep later
178-
gitKeepSha := ""
199+
subDirSha, err := d.newTree("", []interface{}{
200+
map[string]string{
201+
"path": ".gitkeep",
202+
"mode": "100644",
203+
"type": "blob",
204+
"content": "",
205+
},
206+
})
207+
if err != nil {
208+
return err
209+
}
210+
newTree := make([]interface{}, 0, 2)
211+
newTree = append(newTree, TreeObjReq{
212+
Path: dirName,
213+
Mode: "040000",
214+
Type: "tree",
215+
Sha: subDirSha,
216+
})
179217
if len(parent.Entries) == 1 && parent.Entries[0].Name == ".gitkeep" {
180-
gitKeepSha = parent.Entries[0].Sha
218+
newTree = append(newTree, TreeObjReq{
219+
Path: ".gitkeep",
220+
Mode: "100644",
221+
Type: "blob",
222+
Sha: nil,
223+
})
224+
}
225+
newSha, err := d.newTree(parent.Sha, newTree)
226+
if err != nil {
227+
return err
228+
}
229+
rootSha, err := d.renewParentTrees(parentDir.GetPath(), parent.Sha, newSha, "/")
230+
if err != nil {
231+
return err
181232
}
182233

183234
commitMessage, err := getMessage(d.mkdirMsgTmpl, &MessageTemplateVars{
@@ -190,13 +241,7 @@ func (d *Github) MakeDir(ctx context.Context, parentDir model.Obj, dirName strin
190241
if err != nil {
191242
return err
192243
}
193-
if err = d.createGitKeep(stdpath.Join(parentDir.GetPath(), dirName), commitMessage); err != nil {
194-
return err
195-
}
196-
if gitKeepSha != "" {
197-
err = d.delete(stdpath.Join(parentDir.GetPath(), ".gitkeep"), gitKeepSha, commitMessage)
198-
}
199-
return err
244+
return d.commit(commitMessage, rootSha)
200245
}
201246

202247
func (d *Github) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
@@ -639,24 +684,6 @@ func (d *Github) get(path string) (*Object, error) {
639684
return &resp, err
640685
}
641686

642-
func (d *Github) createGitKeep(path, message string) error {
643-
body := map[string]interface{}{
644-
"message": message,
645-
"content": "",
646-
"branch": d.Ref,
647-
}
648-
d.addCommitterAndAuthor(&body)
649-
650-
res, err := d.client.R().SetBody(body).Put(d.getContentApiUrl(stdpath.Join(path, ".gitkeep")))
651-
if err != nil {
652-
return err
653-
}
654-
if res.StatusCode() != 200 && res.StatusCode() != 201 {
655-
return toErr(res)
656-
}
657-
return nil
658-
}
659-
660687
func (d *Github) putBlob(ctx context.Context, s model.FileStreamer, up driver.UpdateProgress) (string, error) {
661688
beforeContent := "{\"encoding\":\"base64\",\"content\":\""
662689
afterContent := "\"}"
@@ -717,23 +744,6 @@ func (d *Github) putBlob(ctx context.Context, s model.FileStreamer, up driver.Up
717744
return resp.Sha, nil
718745
}
719746

720-
func (d *Github) delete(path, sha, message string) error {
721-
body := map[string]interface{}{
722-
"message": message,
723-
"sha": sha,
724-
"branch": d.Ref,
725-
}
726-
d.addCommitterAndAuthor(&body)
727-
res, err := d.client.R().SetBody(body).Delete(d.getContentApiUrl(path))
728-
if err != nil {
729-
return err
730-
}
731-
if res.StatusCode() != 200 {
732-
return toErr(res)
733-
}
734-
return nil
735-
}
736-
737747
func (d *Github) renewParentTrees(path, prevSha, curSha, until string) (string, error) {
738748
for path != until {
739749
path = stdpath.Dir(path)
@@ -795,11 +805,11 @@ func (d *Github) getTreeDirectly(path string) (*TreeResp, string, error) {
795805
}
796806

797807
func (d *Github) newTree(baseSha string, tree []interface{}) (string, error) {
798-
res, err := d.client.R().
799-
SetBody(&TreeReq{
800-
BaseTree: baseSha,
801-
Trees: tree,
802-
}).
808+
body := &TreeReq{Trees: tree}
809+
if baseSha != "" {
810+
body.BaseTree = baseSha
811+
}
812+
res, err := d.client.R().SetBody(body).
803813
Post(fmt.Sprintf("https://api.github.com/repos/%s/%s/git/trees", d.Owner, d.Repo))
804814
if err != nil {
805815
return "", err
@@ -822,6 +832,13 @@ func (d *Github) commit(message, treeSha string) error {
822832
"parents": []string{oldCommit},
823833
}
824834
d.addCommitterAndAuthor(&body)
835+
if d.pgpEntity != nil {
836+
signature, e := signCommit(&body, d.pgpEntity)
837+
if e != nil {
838+
return e
839+
}
840+
body["signature"] = signature
841+
}
825842
res, err := d.client.R().SetBody(body).Post(fmt.Sprintf("https://api.github.com/repos/%s/%s/git/commits", d.Owner, d.Repo))
826843
if err != nil {
827844
return err
@@ -925,6 +942,21 @@ func (d *Github) getRepo() (*RepoResp, error) {
925942
return &resp, nil
926943
}
927944

945+
func (d *Github) getAuthenticatedUser() (*UserResp, error) {
946+
res, err := d.client.R().Get("https://api.github.com/user")
947+
if err != nil {
948+
return nil, err
949+
}
950+
if res.StatusCode() != 200 {
951+
return nil, toErr(res)
952+
}
953+
resp := &UserResp{}
954+
if err = utils.Json.Unmarshal(res.Body(), resp); err != nil {
955+
return nil, err
956+
}
957+
return resp, nil
958+
}
959+
928960
func (d *Github) addCommitterAndAuthor(m *map[string]interface{}) {
929961
if d.CommitterName != "" {
930962
committer := map[string]string{

drivers/github/meta.go

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,23 @@ import (
77

88
type Addition struct {
99
driver.RootPath
10-
Token string `json:"token" type:"string"`
11-
Owner string `json:"owner" type:"string" required:"true"`
12-
Repo string `json:"repo" type:"string" required:"true"`
13-
Ref string `json:"ref" type:"string" help:"A branch, a tag or a commit SHA, main branch by default."`
14-
GitHubProxy string `json:"gh_proxy" type:"string" help:"GitHub proxy, e.g. https://ghproxy.net/raw.githubusercontent.com or https://gh-proxy.com/raw.githubusercontent.com"`
15-
CommitterName string `json:"committer_name" type:"string"`
16-
CommitterEmail string `json:"committer_email" type:"string"`
17-
AuthorName string `json:"author_name" type:"string"`
18-
AuthorEmail string `json:"author_email" type:"string"`
19-
MkdirCommitMsg string `json:"mkdir_commit_message" type:"text" default:"{{.UserName}} mkdir {{.ObjPath}}"`
20-
DeleteCommitMsg string `json:"delete_commit_message" type:"text" default:"{{.UserName}} remove {{.ObjPath}}"`
21-
PutCommitMsg string `json:"put_commit_message" type:"text" default:"{{.UserName}} upload {{.ObjPath}}"`
22-
RenameCommitMsg string `json:"rename_commit_message" type:"text" default:"{{.UserName}} rename {{.ObjPath}} to {{.TargetName}}"`
23-
CopyCommitMsg string `json:"copy_commit_message" type:"text" default:"{{.UserName}} copy {{.ObjPath}} to {{.TargetPath}}"`
24-
MoveCommitMsg string `json:"move_commit_message" type:"text" default:"{{.UserName}} move {{.ObjPath}} to {{.TargetPath}}"`
10+
Token string `json:"token" type:"string" required:"true"`
11+
Owner string `json:"owner" type:"string" required:"true"`
12+
Repo string `json:"repo" type:"string" required:"true"`
13+
Ref string `json:"ref" type:"string" help:"A branch, a tag or a commit SHA, main branch by default."`
14+
GitHubProxy string `json:"gh_proxy" type:"string" help:"GitHub proxy, e.g. https://ghproxy.net/raw.githubusercontent.com or https://gh-proxy.com/raw.githubusercontent.com"`
15+
GPGPrivateKey string `json:"gpg_private_key" type:"text"`
16+
GPGKeyPassphrase string `json:"gpg_key_passphrase" type:"string"`
17+
CommitterName string `json:"committer_name" type:"string"`
18+
CommitterEmail string `json:"committer_email" type:"string"`
19+
AuthorName string `json:"author_name" type:"string"`
20+
AuthorEmail string `json:"author_email" type:"string"`
21+
MkdirCommitMsg string `json:"mkdir_commit_message" type:"text" default:"{{.UserName}} mkdir {{.ObjPath}}"`
22+
DeleteCommitMsg string `json:"delete_commit_message" type:"text" default:"{{.UserName}} remove {{.ObjPath}}"`
23+
PutCommitMsg string `json:"put_commit_message" type:"text" default:"{{.UserName}} upload {{.ObjPath}}"`
24+
RenameCommitMsg string `json:"rename_commit_message" type:"text" default:"{{.UserName}} rename {{.ObjPath}} to {{.TargetName}}"`
25+
CopyCommitMsg string `json:"copy_commit_message" type:"text" default:"{{.UserName}} copy {{.ObjPath}} to {{.TargetPath}}"`
26+
MoveCommitMsg string `json:"move_commit_message" type:"text" default:"{{.UserName}} move {{.ObjPath}} to {{.TargetPath}}"`
2527
}
2628

2729
var config = driver.Config{

drivers/github/types.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ type TreeResp struct {
7979
}
8080

8181
type TreeReq struct {
82-
BaseTree string `json:"base_tree"`
82+
BaseTree interface{} `json:"base_tree,omitempty"`
8383
Trees []interface{} `json:"tree"`
8484
}
8585

@@ -100,3 +100,8 @@ type UpdateRefReq struct {
100100
type RepoResp struct {
101101
DefaultBranch string `json:"default_branch"`
102102
}
103+
104+
type UserResp struct {
105+
Name string `json:"name"`
106+
Email string `json:"email"`
107+
}

drivers/github/util.go

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
package github
22

33
import (
4+
"bytes"
45
"context"
56
"errors"
67
"fmt"
8+
"io"
9+
"strings"
10+
"text/template"
11+
"time"
12+
13+
"github.com/ProtonMail/go-crypto/openpgp"
14+
"github.com/ProtonMail/go-crypto/openpgp/armor"
715
"github.com/alist-org/alist/v3/internal/model"
816
"github.com/alist-org/alist/v3/pkg/utils"
917
"github.com/go-resty/resty/v2"
10-
"strings"
11-
"text/template"
1218
)
1319

1420
type MessageTemplateVars struct {
@@ -97,3 +103,65 @@ func getUsername(ctx context.Context) string {
97103
}
98104
return user.Username
99105
}
106+
107+
func loadPrivateKey(key, passphrase string) (*openpgp.Entity, error) {
108+
entityList, err := openpgp.ReadArmoredKeyRing(strings.NewReader(key))
109+
if err != nil {
110+
return nil, err
111+
}
112+
if len(entityList) < 1 {
113+
return nil, fmt.Errorf("no keys found in key ring")
114+
}
115+
entity := entityList[0]
116+
117+
pass := []byte(passphrase)
118+
if entity.PrivateKey != nil && entity.PrivateKey.Encrypted {
119+
if err = entity.PrivateKey.Decrypt(pass); err != nil {
120+
return nil, fmt.Errorf("password incorrect: %+v", err)
121+
}
122+
}
123+
for _, subKey := range entity.Subkeys {
124+
if subKey.PrivateKey != nil && subKey.PrivateKey.Encrypted {
125+
if err = subKey.PrivateKey.Decrypt(pass); err != nil {
126+
return nil, fmt.Errorf("password incorrect: %+v", err)
127+
}
128+
}
129+
}
130+
return entity, nil
131+
}
132+
133+
func signCommit(m *map[string]interface{}, entity *openpgp.Entity) (string, error) {
134+
var commit strings.Builder
135+
commit.WriteString(fmt.Sprintf("tree %s\n", (*m)["tree"].(string)))
136+
parents := (*m)["parents"].([]string)
137+
for _, p := range parents {
138+
commit.WriteString(fmt.Sprintf("parent %s\n", p))
139+
}
140+
now := time.Now()
141+
_, offset := now.Zone()
142+
hour := offset / 3600
143+
author := (*m)["author"].(map[string]string)
144+
commit.WriteString(fmt.Sprintf("author %s <%s> %d %+03d00\n", author["name"], author["email"], now.Unix(), hour))
145+
author["date"] = now.Format(time.RFC3339)
146+
committer := (*m)["committer"].(map[string]string)
147+
commit.WriteString(fmt.Sprintf("committer %s <%s> %d %+03d00\n", committer["name"], committer["email"], now.Unix(), hour))
148+
committer["date"] = now.Format(time.RFC3339)
149+
commit.WriteString(fmt.Sprintf("\n%s", (*m)["message"].(string)))
150+
data := commit.String()
151+
152+
var sigBuffer bytes.Buffer
153+
err := openpgp.DetachSign(&sigBuffer, entity, strings.NewReader(data), nil)
154+
if err != nil {
155+
return "", fmt.Errorf("signing failed: %v", err)
156+
}
157+
var armoredSig bytes.Buffer
158+
armorWriter, err := armor.Encode(&armoredSig, "PGP SIGNATURE", nil)
159+
if err != nil {
160+
return "", err
161+
}
162+
if _, err = io.Copy(armorWriter, &sigBuffer); err != nil {
163+
return "", err
164+
}
165+
_ = armorWriter.Close()
166+
return armoredSig.String(), nil
167+
}

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ toolchain go1.23.1
77
require (
88
github.com/KirCute/ftpserverlib-pasvportmap v1.25.0
99
github.com/KirCute/sftpd-alist v0.0.12
10+
github.com/ProtonMail/go-crypto v1.0.0
1011
github.com/SheltonZhu/115driver v1.0.34
1112
github.com/Xhofe/go-cache v0.0.0-20240804043513-b1a71927bc21
1213
github.com/Xhofe/rateg v0.0.0-20230728072201-251a4e1adad4
@@ -90,6 +91,7 @@ require (
9091
github.com/bytedance/sonic/loader v0.1.1 // indirect
9192
github.com/charmbracelet/x/ansi v0.2.3 // indirect
9293
github.com/charmbracelet/x/term v0.2.0 // indirect
94+
github.com/cloudflare/circl v1.3.7 // indirect
9395
github.com/cloudwego/base64x v0.1.4 // indirect
9496
github.com/cloudwego/iasm v0.2.0 // indirect
9597
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect

0 commit comments

Comments
 (0)