-37
knotclient/signer.go
-37
knotclient/signer.go
···
7
7
"encoding/hex"
8
8
"encoding/json"
9
9
"fmt"
10
-
"io"
11
-
"log"
12
10
"net/http"
13
11
"net/url"
14
12
"time"
···
103
101
}
104
102
105
103
return s.client.Do(req)
106
-
}
107
-
108
-
func (s *SignedClient) RepoLanguages(ownerDid, repoName, ref string) (*types.RepoLanguageResponse, error) {
109
-
const (
110
-
Method = "GET"
111
-
)
112
-
endpoint := fmt.Sprintf("/%s/%s/languages/%s", ownerDid, repoName, url.PathEscape(ref))
113
-
114
-
req, err := s.newRequest(Method, endpoint, nil)
115
-
if err != nil {
116
-
return nil, err
117
-
}
118
-
119
-
resp, err := s.client.Do(req)
120
-
if err != nil {
121
-
return nil, err
122
-
}
123
-
124
-
var result types.RepoLanguageResponse
125
-
if resp.StatusCode != http.StatusOK {
126
-
log.Println("failed to calculate languages", resp.Status)
127
-
return &types.RepoLanguageResponse{}, nil
128
-
}
129
-
130
-
body, err := io.ReadAll(resp.Body)
131
-
if err != nil {
132
-
return nil, err
133
-
}
134
-
135
-
err = json.Unmarshal(body, &result)
136
-
if err != nil {
137
-
return nil, err
138
-
}
139
-
140
-
return &result, nil
141
104
}
142
105
143
106
func (s *SignedClient) RepoForkAheadBehind(ownerDid, source, name, branch, hiddenRef string) (*http.Response, error) {
+35
knotclient/unsigned.go
+35
knotclient/unsigned.go
···
248
248
249
249
return &formatPatchResponse, nil
250
250
}
251
+
252
+
func (s *UnsignedClient) RepoLanguages(ownerDid, repoName, ref string) (*types.RepoLanguageResponse, error) {
253
+
const (
254
+
Method = "GET"
255
+
)
256
+
endpoint := fmt.Sprintf("/%s/%s/languages/%s", ownerDid, repoName, url.PathEscape(ref))
257
+
258
+
req, err := s.newRequest(Method, endpoint, nil, nil)
259
+
if err != nil {
260
+
return nil, err
261
+
}
262
+
263
+
resp, err := s.client.Do(req)
264
+
if err != nil {
265
+
return nil, err
266
+
}
267
+
268
+
var result types.RepoLanguageResponse
269
+
if resp.StatusCode != http.StatusOK {
270
+
log.Println("failed to calculate languages", resp.Status)
271
+
return &types.RepoLanguageResponse{}, nil
272
+
}
273
+
274
+
body, err := io.ReadAll(resp.Body)
275
+
if err != nil {
276
+
return nil, err
277
+
}
278
+
279
+
err = json.Unmarshal(body, &result)
280
+
if err != nil {
281
+
return nil, err
282
+
}
283
+
284
+
return &result, nil
285
+
}
+236
-177
knotserver/handler.go
+236
-177
knotserver/handler.go
···
1
1
package knotserver
2
2
3
3
import (
4
+
"compress/gzip"
4
5
"context"
6
+
"crypto/sha256"
7
+
"encoding/json"
8
+
"errors"
5
9
"fmt"
6
-
"log/slog"
10
+
"log"
7
11
"net/http"
8
-
"runtime/debug"
12
+
"net/url"
13
+
"path/filepath"
14
+
"strconv"
15
+
"strings"
16
+
"sync"
17
+
"time"
9
18
19
+
securejoin "github.com/cyphar/filepath-securejoin"
20
+
"github.com/gliderlabs/ssh"
10
21
"github.com/go-chi/chi/v5"
11
-
"tangled.sh/tangled.sh/core/idresolver"
12
-
"tangled.sh/tangled.sh/core/jetstream"
13
-
"tangled.sh/tangled.sh/core/knotserver/config"
22
+
"github.com/go-git/go-git/v5/plumbing"
23
+
"github.com/go-git/go-git/v5/plumbing/object"
14
24
"tangled.sh/tangled.sh/core/knotserver/db"
15
-
"tangled.sh/tangled.sh/core/knotserver/xrpc"
16
-
tlog "tangled.sh/tangled.sh/core/log"
17
-
"tangled.sh/tangled.sh/core/notifier"
18
-
"tangled.sh/tangled.sh/core/rbac"
25
+
"tangled.sh/tangled.sh/core/knotserver/git"
19
26
"tangled.sh/tangled.sh/core/types"
20
27
)
21
28
22
-
type Handle struct {
23
-
c *config.Config
24
-
db *db.DB
25
-
jc *jetstream.JetstreamClient
26
-
e *rbac.Enforcer
27
-
l *slog.Logger
28
-
n *notifier.Notifier
29
-
resolver *idresolver.Resolver
29
+
func (h *Handle) Index(w http.ResponseWriter, r *http.Request) {
30
+
w.Write([]byte("This is a knot server. More info at https://tangled.sh"))
30
31
}
31
32
32
-
func Setup(ctx context.Context, c *config.Config, db *db.DB, e *rbac.Enforcer, jc *jetstream.JetstreamClient, l *slog.Logger, n *notifier.Notifier) (http.Handler, error) {
33
-
r := chi.NewRouter()
33
+
func (h *Handle) Capabilities(w http.ResponseWriter, r *http.Request) {
34
+
w.Header().Set("Content-Type", "application/json")
34
35
35
-
h := Handle{
36
-
c: c,
37
-
db: db,
38
-
e: e,
39
-
l: l,
40
-
jc: jc,
41
-
n: n,
42
-
resolver: idresolver.DefaultResolver(),
36
+
capabilities := map[string]any{
37
+
"pull_requests": map[string]any{
38
+
"format_patch": true,
39
+
"patch_submissions": true,
40
+
"branch_submissions": true,
41
+
"fork_submissions": true,
42
+
},
43
+
"xrpc": true,
43
44
}
44
45
45
-
err := e.AddKnot(rbac.ThisServer)
46
+
jsonData, err := json.Marshal(capabilities)
46
47
if err != nil {
47
-
return nil, fmt.Errorf("failed to setup enforcer: %w", err)
48
+
http.Error(w, "Failed to serialize JSON", http.StatusInternalServerError)
49
+
return
48
50
}
49
51
50
-
// configure owner
51
-
if err = h.configureOwner(); err != nil {
52
-
return nil, err
53
-
}
54
-
h.l.Info("owner set", "did", h.c.Server.Owner)
55
-
h.jc.AddDid(h.c.Server.Owner)
52
+
w.Write(jsonData)
53
+
}
54
+
55
+
func (h *Handle) RepoIndex(w http.ResponseWriter, r *http.Request) {
56
+
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
57
+
l := h.l.With("path", path, "handler", "RepoIndex")
58
+
ref := chi.URLParam(r, "ref")
59
+
ref, _ = url.PathUnescape(ref)
56
60
57
-
// configure known-dids in jetstream consumer
58
-
dids, err := h.db.GetAllDids()
61
+
gr, err := git.Open(path, ref)
59
62
if err != nil {
60
-
return nil, fmt.Errorf("failed to get all dids: %w", err)
61
-
}
62
-
for _, d := range dids {
63
-
jc.AddDid(d)
63
+
plain, err2 := git.PlainOpen(path)
64
+
if err2 != nil {
65
+
l.Error("opening repo", "error", err2.Error())
66
+
notFound(w)
67
+
return
68
+
}
69
+
branches, _ := plain.Branches()
70
+
71
+
log.Println(err)
72
+
73
+
if errors.Is(err, plumbing.ErrReferenceNotFound) {
74
+
resp := types.RepoIndexResponse{
75
+
IsEmpty: true,
76
+
Branches: branches,
77
+
}
78
+
writeJSON(w, resp)
79
+
return
80
+
} else {
81
+
l.Error("opening repo", "error", err.Error())
82
+
notFound(w)
83
+
return
84
+
}
64
85
}
65
86
66
-
err = h.jc.StartJetstream(ctx, h.processMessages)
67
-
if err != nil {
68
-
return nil, fmt.Errorf("failed to start jetstream: %w", err)
69
-
}
87
+
var (
88
+
commits []*object.Commit
89
+
total int
90
+
branches []types.Branch
91
+
files []types.NiceTree
92
+
tags []object.Tag
93
+
)
70
94
71
-
r.Get("/", h.Index)
72
-
r.Get("/capabilities", h.Capabilities)
73
-
r.Get("/version", h.Version)
74
-
r.Get("/owner", func(w http.ResponseWriter, r *http.Request) {
75
-
w.Write([]byte(h.c.Server.Owner))
76
-
})
77
-
r.Route("/{did}", func(r chi.Router) {
78
-
// Repo routes
79
-
r.Route("/{name}", func(r chi.Router) {
80
-
r.Route("/collaborator", func(r chi.Router) {
81
-
r.Use(h.VerifySignature)
82
-
r.Post("/add", h.AddRepoCollaborator)
83
-
})
95
+
var wg sync.WaitGroup
96
+
errorsCh := make(chan error, 5)
84
97
85
-
r.Route("/languages", func(r chi.Router) {
86
-
r.With(h.VerifySignature)
87
-
r.Get("/", h.RepoLanguages)
88
-
r.Get("/{ref}", h.RepoLanguages)
89
-
})
98
+
wg.Add(1)
99
+
go func() {
100
+
defer wg.Done()
101
+
cs, err := gr.Commits(0, 60)
102
+
if err != nil {
103
+
errorsCh <- fmt.Errorf("commits: %w", err)
104
+
return
105
+
}
106
+
commits = cs
107
+
}()
90
108
91
-
r.Get("/", h.RepoIndex)
92
-
r.Get("/info/refs", h.InfoRefs)
93
-
r.Post("/git-upload-pack", h.UploadPack)
94
-
r.Post("/git-receive-pack", h.ReceivePack)
95
-
r.Get("/compare/{rev1}/{rev2}", h.Compare) // git diff-tree compare of two objects
109
+
wg.Add(1)
110
+
go func() {
111
+
defer wg.Done()
112
+
t, err := gr.TotalCommits()
113
+
if err != nil {
114
+
errorsCh <- fmt.Errorf("calculating total: %w", err)
115
+
return
116
+
}
117
+
total = t
118
+
}()
96
119
97
-
r.With(h.VerifySignature).Post("/hidden-ref/{forkRef}/{remoteRef}", h.NewHiddenRef)
120
+
wg.Add(1)
121
+
go func() {
122
+
defer wg.Done()
123
+
bs, err := gr.Branches()
124
+
if err != nil {
125
+
errorsCh <- fmt.Errorf("fetching branches: %w", err)
126
+
return
127
+
}
128
+
branches = bs
129
+
}()
98
130
99
-
r.Route("/merge", func(r chi.Router) {
100
-
r.With(h.VerifySignature)
101
-
r.Post("/", h.Merge)
102
-
r.Post("/check", h.MergeCheck)
103
-
})
131
+
wg.Add(1)
132
+
go func() {
133
+
defer wg.Done()
134
+
ts, err := gr.Tags()
135
+
if err != nil {
136
+
errorsCh <- fmt.Errorf("fetching tags: %w", err)
137
+
return
138
+
}
139
+
tags = ts
140
+
}()
104
141
105
-
r.Route("/tree/{ref}", func(r chi.Router) {
106
-
r.Get("/", h.RepoIndex)
107
-
r.Get("/*", h.RepoTree)
108
-
})
142
+
wg.Add(1)
143
+
go func() {
144
+
defer wg.Done()
145
+
fs, err := gr.FileTree(r.Context(), "")
146
+
if err != nil {
147
+
errorsCh <- fmt.Errorf("fetching filetree: %w", err)
148
+
return
149
+
}
150
+
files = fs
151
+
}()
109
152
110
-
r.Route("/blob/{ref}", func(r chi.Router) {
111
-
r.Get("/*", h.Blob)
112
-
})
153
+
wg.Wait()
154
+
close(errorsCh)
113
155
114
-
r.Route("/raw/{ref}", func(r chi.Router) {
115
-
r.Get("/*", h.BlobRaw)
116
-
})
156
+
// show any errors
157
+
for err := range errorsCh {
158
+
l.Error("loading repo", "error", err.Error())
159
+
writeError(w, err.Error(), http.StatusInternalServerError)
160
+
return
161
+
}
117
162
118
-
r.Get("/log/{ref}", h.Log)
119
-
r.Get("/archive/{file}", h.Archive)
120
-
r.Get("/commit/{ref}", h.Diff)
121
-
r.Get("/tags", h.Tags)
122
-
r.Route("/branches", func(r chi.Router) {
123
-
r.Get("/", h.Branches)
124
-
r.Get("/{branch}", h.Branch)
125
-
r.Route("/default", func(r chi.Router) {
126
-
r.Get("/", h.DefaultBranch)
127
-
r.With(h.VerifySignature).Put("/", h.SetDefaultBranch)
128
-
})
129
-
})
130
-
})
131
-
})
163
+
rtags := []*types.TagReference{}
164
+
for _, tag := range tags {
165
+
var target *object.Tag
166
+
if tag.Target != plumbing.ZeroHash {
167
+
target = &tag
168
+
}
169
+
tr := types.TagReference{
170
+
Tag: target,
171
+
}
132
172
133
-
// xrpc apis
134
-
r.Mount("/xrpc", h.XrpcRouter())
173
+
tr.Reference = types.Reference{
174
+
Name: tag.Name,
175
+
Hash: tag.Hash.String(),
176
+
}
135
177
136
-
// Create a new repository.
137
-
r.Route("/repo", func(r chi.Router) {
138
-
r.Use(h.VerifySignature)
139
-
r.Delete("/", h.RemoveRepo)
140
-
r.Route("/fork", func(r chi.Router) {
141
-
r.Post("/", h.RepoFork)
142
-
r.Post("/sync/*", h.RepoForkSync)
143
-
r.Get("/sync/*", h.RepoForkAheadBehind)
144
-
})
145
-
})
178
+
if tag.Message != "" {
179
+
tr.Message = tag.Message
180
+
}
146
181
147
-
r.Route("/member", func(r chi.Router) {
148
-
r.Use(h.VerifySignature)
149
-
r.Put("/add", h.AddMember)
150
-
})
182
+
rtags = append(rtags, &tr)
183
+
}
151
184
152
-
// Socket that streams git oplogs
153
-
r.Get("/events", h.Events)
185
+
var readmeContent string
186
+
var readmeFile string
187
+
for _, readme := range h.c.Repo.Readme {
188
+
content, _ := gr.FileContent(readme)
189
+
if len(content) > 0 {
190
+
readmeContent = string(content)
191
+
readmeFile = readme
192
+
}
193
+
}
154
194
155
-
// Health check. Used for two-way verification with appview.
156
-
r.With(h.VerifySignature).Get("/health", h.Health)
195
+
if ref == "" {
196
+
mainBranch, err := gr.FindMainBranch()
197
+
if err != nil {
198
+
writeError(w, err.Error(), http.StatusInternalServerError)
199
+
l.Error("finding main branch", "error", err.Error())
200
+
return
201
+
}
202
+
ref = mainBranch
203
+
}
157
204
158
-
// All public keys on the knot.
159
-
r.Get("/keys", h.Keys)
205
+
resp := types.RepoIndexResponse{
206
+
IsEmpty: false,
207
+
Ref: ref,
208
+
Commits: commits,
209
+
Description: getDescription(path),
210
+
Readme: readmeContent,
211
+
ReadmeFileName: readmeFile,
212
+
Files: files,
213
+
Branches: branches,
214
+
Tags: rtags,
215
+
TotalCommits: total,
216
+
}
160
217
161
-
return r, nil
218
+
writeJSON(w, resp)
162
219
}
163
220
164
-
func (h *Handle) XrpcRouter() http.Handler {
165
-
logger := tlog.New("knots")
221
+
func (h *Handle) RepoTree(w http.ResponseWriter, r *http.Request) {
222
+
treePath := chi.URLParam(r, "*")
223
+
ref := chi.URLParam(r, "ref")
224
+
ref, _ = url.PathUnescape(ref)
166
225
167
-
serviceAuth := serviceauth.NewServiceAuth(h.l, h.resolver, h.c.Server.Did().String())
226
+
l := h.l.With("handler", "RepoTree", "ref", ref, "treePath", treePath)
168
227
169
-
xrpc := &xrpc.Xrpc{
170
-
Config: h.c,
171
-
Db: h.db,
172
-
Ingester: h.jc,
173
-
Enforcer: h.e,
174
-
Logger: logger,
175
-
Notifier: h.n,
176
-
Resolver: h.resolver,
177
-
ServiceAuth: serviceAuth,
228
+
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
229
+
gr, err := git.Open(path, ref)
230
+
if err != nil {
231
+
notFound(w)
232
+
return
178
233
}
179
-
return xrpc.Router()
180
-
}
181
234
182
-
// version is set during build time.
183
-
var version string
184
-
185
-
func (h *Handle) Version(w http.ResponseWriter, r *http.Request) {
186
-
if version == "" {
187
-
info, ok := debug.ReadBuildInfo()
188
-
if !ok {
189
-
http.Error(w, "failed to read build info", http.StatusInternalServerError)
190
-
return
191
-
}
192
-
193
-
var modVer string
194
-
for _, mod := range info.Deps {
195
-
if mod.Path == "tangled.sh/tangled.sh/knotserver" {
196
-
version = mod.Version
197
-
break
198
-
}
199
-
}
235
+
files, err := gr.FileTree(r.Context(), treePath)
236
+
if err != nil {
237
+
writeError(w, err.Error(), http.StatusInternalServerError)
238
+
l.Error("file tree", "error", err.Error())
239
+
return
240
+
}
200
241
201
-
if modVer == "" {
202
-
version = "unknown"
203
-
}
242
+
resp := types.RepoTreeResponse{
243
+
Ref: ref,
244
+
Parent: treePath,
245
+
Description: getDescription(path),
246
+
DotDot: filepath.Dir(treePath),
247
+
Files: files,
204
248
}
205
249
206
-
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
207
-
fmt.Fprintf(w, "knotserver/%s", version)
250
+
writeJSON(w, resp)
208
251
}
209
252
210
-
func (h *Handle) configureOwner() error {
211
-
cfgOwner := h.c.Server.Owner
253
+
func (h *Handle) BlobRaw(w http.ResponseWriter, r *http.Request) {
254
+
treePath := chi.URLParam(r, "*")
255
+
ref := chi.URLParam(r, "ref")
256
+
ref, _ = url.PathUnescape(ref)
212
257
213
-
rbacDomain := "thisserver"
258
+
l := h.l.With("handler", "BlobRaw", "ref", ref, "treePath", treePath)
214
259
215
-
existing, err := h.e.GetKnotUsersByRole("server:owner", rbacDomain)
260
+
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
261
+
gr, err := git.Open(path, ref)
216
262
if err != nil {
217
-
return err
263
+
notFound(w)
264
+
return
218
265
}
219
266
220
-
switch len(existing) {
221
-
case 0:
222
-
// no owner configured, continue
223
-
case 1:
224
-
// find existing owner
225
-
existingOwner := existing[0]
267
+
contents, err := gr.RawContent(treePath)
268
+
if err != nil {
269
+
writeError(w, err.Error(), http.StatusBadRequest)
270
+
l.Error("file content", "error", err.Error())
271
+
return
272
+
}
273
+
274
+
mimeType := http.DetectContentType(contents)
275
+
276
+
// exception for svg
277
+
if filepath.Ext(treePath) == ".svg" {
278
+
mimeType = "image/svg+xml"
279
+
}
280
+
281
+
contentHash := sha256.Sum256(contents)
282
+
eTag := fmt.Sprintf("\"%x\"", contentHash)
226
283
227
-
// no ownership change, this is okay
228
-
if existingOwner == h.c.Server.Owner {
229
-
break
284
+
// allow image, video, and text/plain files to be served directly
285
+
switch {
286
+
case strings.HasPrefix(mimeType, "image/"), strings.HasPrefix(mimeType, "video/"):
287
+
if clientETag := r.Header.Get("If-None-Match"); clientETag == eTag {
288
+
w.WriteHeader(http.StatusNotModified)
289
+
return
230
290
}
291
+
w.Header().Set("ETag", eTag)
231
292
232
-
// remove existing owner
233
-
err = h.e.RemoveKnotOwner(rbacDomain, existingOwner)
234
-
if err != nil {
235
-
return nil
236
-
}
293
+
case strings.HasPrefix(mimeType, "text/plain"):
294
+
w.Header().Set("Cache-Control", "public, no-cache")
295
+
237
296
default:
238
297
l.Error("attempted to serve disallowed file type", "mimetype", mimeType)
239
298
writeError(w, "only image, video, and text files can be accessed directly", http.StatusForbidden)
+10
-29
knotserver/ingester.go
+10
-29
knotserver/ingester.go
···
8
8
"net/http"
9
9
"net/url"
10
10
"path/filepath"
11
-
"slices"
12
11
"strings"
13
12
14
13
comatproto "github.com/bluesky-social/indigo/api/atproto"
···
103
102
l = l.With("target_branch", record.TargetBranch)
104
103
105
104
if record.Source == nil {
106
-
reason := "not a branch-based pull request"
107
-
l.Info("ignoring pull record", "reason", reason)
108
-
return fmt.Errorf("ignoring pull record: %s", reason)
105
+
return fmt.Errorf("ignoring pull record: not a branch-based pull request")
109
106
}
110
107
111
108
if record.Source.Repo != nil {
112
-
reason := "fork based pull"
113
-
l.Info("ignoring pull record", "reason", reason)
114
-
return fmt.Errorf("ignoring pull record: %s", reason)
115
-
}
116
-
117
-
allDids, err := h.db.GetAllDids()
118
-
if err != nil {
119
-
return err
120
-
}
121
-
122
-
// presently: we only process PRs from collaborators for pipelines
123
-
if !slices.Contains(allDids, did) {
124
-
reason := "not a known did"
125
-
l.Info("rejecting pull record", "reason", reason)
126
-
return fmt.Errorf("rejected pull record: %s, %s", reason, did)
109
+
return fmt.Errorf("ignoring pull record: fork based pull")
127
110
}
128
111
129
112
repoAt, err := syntax.ParseATURI(record.TargetRepo)
130
113
if err != nil {
131
-
return err
114
+
return fmt.Errorf("failed to parse ATURI: %w", err)
132
115
}
133
116
134
117
// resolve this aturi to extract the repo record
···
144
127
145
128
resp, err := comatproto.RepoGetRecord(ctx, &xrpcc, "", tangled.RepoNSID, repoAt.Authority().String(), repoAt.RecordKey().String())
146
129
if err != nil {
147
-
return err
130
+
return fmt.Errorf("failed to resolver repo: %w", err)
148
131
}
149
132
150
133
repo := resp.Value.Val.(*tangled.Repo)
151
134
152
135
if repo.Knot != h.c.Server.Hostname {
153
-
reason := "not this knot"
154
-
l.Info("rejecting pull record", "reason", reason)
155
-
return fmt.Errorf("rejected pull record: %s", reason)
136
+
return fmt.Errorf("rejected pull record: not this knot, %s != %s", repo.Knot, h.c.Server.Hostname)
156
137
}
157
138
158
139
didSlashRepo, err := securejoin.SecureJoin(repo.Owner, repo.Name)
159
140
if err != nil {
160
-
return err
141
+
return fmt.Errorf("failed to construct relative repo path: %w", err)
161
142
}
162
143
163
144
repoPath, err := securejoin.SecureJoin(h.c.Repo.ScanPath, didSlashRepo)
164
145
if err != nil {
165
-
return err
146
+
return fmt.Errorf("failed to construct absolute repo path: %w", err)
166
147
}
167
148
168
149
gr, err := git.Open(repoPath, record.Source.Branch)
169
150
if err != nil {
170
-
return err
151
+
return fmt.Errorf("failed to open git repository: %w", err)
171
152
}
172
153
173
154
workflowDir, err := gr.FileTree(ctx, workflow.WorkflowDir)
174
155
if err != nil {
175
-
return err
156
+
return fmt.Errorf("failed to open workflow directory: %w", err)
176
157
}
177
158
178
159
var pipeline workflow.RawPipeline
···
215
196
cp := compiler.Compile(compiler.Parse(pipeline))
216
197
eventJson, err := json.Marshal(cp)
217
198
if err != nil {
218
-
return err
199
+
return fmt.Errorf("failed to marshal pipeline event: %w", err)
219
200
}
220
201
221
202
// do not run empty pipelines
-53
knotserver/middleware.go
-53
knotserver/middleware.go
···
1
-
package knotserver
2
-
3
-
import (
4
-
"crypto/hmac"
5
-
"crypto/sha256"
6
-
"encoding/hex"
7
-
"net/http"
8
-
"time"
9
-
)
10
-
11
-
func (h *Handle) VerifySignature(next http.Handler) http.Handler {
12
-
if h.c.Server.Dev {
13
-
return next
14
-
}
15
-
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
16
-
signature := r.Header.Get("X-Signature")
17
-
if signature == "" || !h.verifyHMAC(signature, r) {
18
-
writeError(w, "signature verification failed", http.StatusForbidden)
19
-
return
20
-
}
21
-
next.ServeHTTP(w, r)
22
-
})
23
-
}
24
-
25
-
func (h *Handle) verifyHMAC(signature string, r *http.Request) bool {
26
-
secret := h.c.Server.Secret
27
-
timestamp := r.Header.Get("X-Timestamp")
28
-
if timestamp == "" {
29
-
return false
30
-
}
31
-
32
-
// Verify that the timestamp is not older than a minute
33
-
reqTime, err := time.Parse(time.RFC3339, timestamp)
34
-
if err != nil {
35
-
return false
36
-
}
37
-
if time.Since(reqTime) > time.Minute {
38
-
return false
39
-
}
40
-
41
-
message := r.Method + r.URL.Path + timestamp
42
-
43
-
mac := hmac.New(sha256.New, []byte(secret))
44
-
mac.Write([]byte(message))
45
-
expectedMAC := mac.Sum(nil)
46
-
47
-
signatureBytes, err := hex.DecodeString(signature)
48
-
if err != nil {
49
-
return false
50
-
}
51
-
52
-
return hmac.Equal(signatureBytes, expectedMAC)
53
-
}
+138
-1176
knotserver/routes.go
+138
-1176
knotserver/routes.go
···
1
1
package knotserver
2
2
3
3
import (
4
-
"compress/gzip"
5
4
"context"
6
-
"crypto/sha256"
7
-
"encoding/json"
8
-
"errors"
9
5
"fmt"
10
-
"log"
6
+
"log/slog"
11
7
"net/http"
12
-
"net/url"
13
-
"os"
14
-
"path/filepath"
15
-
"strconv"
16
-
"strings"
17
-
"sync"
18
-
"time"
8
+
"runtime/debug"
19
9
20
-
securejoin "github.com/cyphar/filepath-securejoin"
21
-
"github.com/gliderlabs/ssh"
22
10
"github.com/go-chi/chi/v5"
23
-
"github.com/go-git/go-git/v5/plumbing"
24
-
"github.com/go-git/go-git/v5/plumbing/object"
25
-
"tangled.sh/tangled.sh/core/hook"
11
+
"tangled.sh/tangled.sh/core/idresolver"
12
+
"tangled.sh/tangled.sh/core/jetstream"
13
+
"tangled.sh/tangled.sh/core/knotserver/config"
26
14
"tangled.sh/tangled.sh/core/knotserver/db"
27
-
"tangled.sh/tangled.sh/core/knotserver/git"
28
-
"tangled.sh/tangled.sh/core/patchutil"
15
+
"tangled.sh/tangled.sh/core/knotserver/xrpc"
16
+
tlog "tangled.sh/tangled.sh/core/log"
17
+
"tangled.sh/tangled.sh/core/notifier"
29
18
"tangled.sh/tangled.sh/core/rbac"
30
-
"tangled.sh/tangled.sh/core/types"
19
+
"tangled.sh/tangled.sh/core/xrpc/serviceauth"
31
20
)
32
21
33
-
func (h *Handle) Index(w http.ResponseWriter, r *http.Request) {
34
-
w.Write([]byte("This is a knot server. More info at https://tangled.sh"))
22
+
type Handle struct {
23
+
c *config.Config
24
+
db *db.DB
25
+
jc *jetstream.JetstreamClient
26
+
e *rbac.Enforcer
27
+
l *slog.Logger
28
+
n *notifier.Notifier
29
+
resolver *idresolver.Resolver
35
30
}
36
31
37
-
func (h *Handle) Capabilities(w http.ResponseWriter, r *http.Request) {
38
-
w.Header().Set("Content-Type", "application/json")
32
+
func Setup(ctx context.Context, c *config.Config, db *db.DB, e *rbac.Enforcer, jc *jetstream.JetstreamClient, l *slog.Logger, n *notifier.Notifier) (http.Handler, error) {
33
+
r := chi.NewRouter()
39
34
40
-
capabilities := map[string]any{
41
-
"pull_requests": map[string]any{
42
-
"format_patch": true,
43
-
"patch_submissions": true,
44
-
"branch_submissions": true,
45
-
"fork_submissions": true,
46
-
},
35
+
h := Handle{
36
+
c: c,
37
+
db: db,
38
+
e: e,
39
+
l: l,
40
+
jc: jc,
41
+
n: n,
42
+
resolver: idresolver.DefaultResolver(),
47
43
}
48
44
49
-
jsonData, err := json.Marshal(capabilities)
45
+
err := e.AddKnot(rbac.ThisServer)
50
46
if err != nil {
51
-
http.Error(w, "Failed to serialize JSON", http.StatusInternalServerError)
52
-
return
47
+
return nil, fmt.Errorf("failed to setup enforcer: %w", err)
53
48
}
54
49
55
-
w.Write(jsonData)
56
-
}
57
-
58
-
func (h *Handle) RepoIndex(w http.ResponseWriter, r *http.Request) {
59
-
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
60
-
l := h.l.With("path", path, "handler", "RepoIndex")
61
-
ref := chi.URLParam(r, "ref")
62
-
ref, _ = url.PathUnescape(ref)
63
-
64
-
gr, err := git.Open(path, ref)
65
-
if err != nil {
66
-
plain, err2 := git.PlainOpen(path)
67
-
if err2 != nil {
68
-
l.Error("opening repo", "error", err2.Error())
69
-
notFound(w)
70
-
return
71
-
}
72
-
branches, _ := plain.Branches()
73
-
74
-
log.Println(err)
75
-
76
-
if errors.Is(err, plumbing.ErrReferenceNotFound) {
77
-
resp := types.RepoIndexResponse{
78
-
IsEmpty: true,
79
-
Branches: branches,
80
-
}
81
-
writeJSON(w, resp)
82
-
return
83
-
} else {
84
-
l.Error("opening repo", "error", err.Error())
85
-
notFound(w)
86
-
return
87
-
}
50
+
// configure owner
51
+
if err = h.configureOwner(); err != nil {
52
+
return nil, err
88
53
}
89
-
90
-
var (
91
-
commits []*object.Commit
92
-
total int
93
-
branches []types.Branch
94
-
files []types.NiceTree
95
-
tags []object.Tag
96
-
)
97
-
98
-
var wg sync.WaitGroup
99
-
errorsCh := make(chan error, 5)
100
-
101
-
wg.Add(1)
102
-
go func() {
103
-
defer wg.Done()
104
-
cs, err := gr.Commits(0, 60)
105
-
if err != nil {
106
-
errorsCh <- fmt.Errorf("commits: %w", err)
107
-
return
108
-
}
109
-
commits = cs
110
-
}()
111
-
112
-
wg.Add(1)
113
-
go func() {
114
-
defer wg.Done()
115
-
t, err := gr.TotalCommits()
116
-
if err != nil {
117
-
errorsCh <- fmt.Errorf("calculating total: %w", err)
118
-
return
119
-
}
120
-
total = t
121
-
}()
54
+
h.l.Info("owner set", "did", h.c.Server.Owner)
55
+
h.jc.AddDid(h.c.Server.Owner)
122
56
123
-
wg.Add(1)
124
-
go func() {
125
-
defer wg.Done()
126
-
bs, err := gr.Branches()
127
-
if err != nil {
128
-
errorsCh <- fmt.Errorf("fetching branches: %w", err)
129
-
return
130
-
}
131
-
branches = bs
132
-
}()
133
-
134
-
wg.Add(1)
135
-
go func() {
136
-
defer wg.Done()
137
-
ts, err := gr.Tags()
138
-
if err != nil {
139
-
errorsCh <- fmt.Errorf("fetching tags: %w", err)
140
-
return
141
-
}
142
-
tags = ts
143
-
}()
144
-
145
-
wg.Add(1)
146
-
go func() {
147
-
defer wg.Done()
148
-
fs, err := gr.FileTree(r.Context(), "")
149
-
if err != nil {
150
-
errorsCh <- fmt.Errorf("fetching filetree: %w", err)
151
-
return
152
-
}
153
-
files = fs
154
-
}()
155
-
156
-
wg.Wait()
157
-
close(errorsCh)
158
-
159
-
// show any errors
160
-
for err := range errorsCh {
161
-
l.Error("loading repo", "error", err.Error())
162
-
writeError(w, err.Error(), http.StatusInternalServerError)
163
-
return
164
-
}
165
-
166
-
rtags := []*types.TagReference{}
167
-
for _, tag := range tags {
168
-
var target *object.Tag
169
-
if tag.Target != plumbing.ZeroHash {
170
-
target = &tag
171
-
}
172
-
tr := types.TagReference{
173
-
Tag: target,
174
-
}
175
-
176
-
tr.Reference = types.Reference{
177
-
Name: tag.Name,
178
-
Hash: tag.Hash.String(),
179
-
}
180
-
181
-
if tag.Message != "" {
182
-
tr.Message = tag.Message
183
-
}
184
-
185
-
rtags = append(rtags, &tr)
186
-
}
187
-
188
-
var readmeContent string
189
-
var readmeFile string
190
-
for _, readme := range h.c.Repo.Readme {
191
-
content, _ := gr.FileContent(readme)
192
-
if len(content) > 0 {
193
-
readmeContent = string(content)
194
-
readmeFile = readme
195
-
}
196
-
}
197
-
198
-
if ref == "" {
199
-
mainBranch, err := gr.FindMainBranch()
200
-
if err != nil {
201
-
writeError(w, err.Error(), http.StatusInternalServerError)
202
-
l.Error("finding main branch", "error", err.Error())
203
-
return
204
-
}
205
-
ref = mainBranch
206
-
}
207
-
208
-
resp := types.RepoIndexResponse{
209
-
IsEmpty: false,
210
-
Ref: ref,
211
-
Commits: commits,
212
-
Description: getDescription(path),
213
-
Readme: readmeContent,
214
-
ReadmeFileName: readmeFile,
215
-
Files: files,
216
-
Branches: branches,
217
-
Tags: rtags,
218
-
TotalCommits: total,
219
-
}
220
-
221
-
writeJSON(w, resp)
222
-
return
223
-
}
224
-
225
-
func (h *Handle) RepoTree(w http.ResponseWriter, r *http.Request) {
226
-
treePath := chi.URLParam(r, "*")
227
-
ref := chi.URLParam(r, "ref")
228
-
ref, _ = url.PathUnescape(ref)
229
-
230
-
l := h.l.With("handler", "RepoTree", "ref", ref, "treePath", treePath)
231
-
232
-
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
233
-
gr, err := git.Open(path, ref)
57
+
// configure known-dids in jetstream consumer
58
+
dids, err := h.db.GetAllDids()
234
59
if err != nil {
235
-
notFound(w)
236
-
return
60
+
return nil, fmt.Errorf("failed to get all dids: %w", err)
237
61
}
238
-
239
-
files, err := gr.FileTree(r.Context(), treePath)
240
-
if err != nil {
241
-
writeError(w, err.Error(), http.StatusInternalServerError)
242
-
l.Error("file tree", "error", err.Error())
243
-
return
62
+
for _, d := range dids {
63
+
jc.AddDid(d)
244
64
}
245
65
246
-
resp := types.RepoTreeResponse{
247
-
Ref: ref,
248
-
Parent: treePath,
249
-
Description: getDescription(path),
250
-
DotDot: filepath.Dir(treePath),
251
-
Files: files,
252
-
}
253
-
254
-
writeJSON(w, resp)
255
-
return
256
-
}
257
-
258
-
func (h *Handle) BlobRaw(w http.ResponseWriter, r *http.Request) {
259
-
treePath := chi.URLParam(r, "*")
260
-
ref := chi.URLParam(r, "ref")
261
-
ref, _ = url.PathUnescape(ref)
262
-
263
-
l := h.l.With("handler", "BlobRaw", "ref", ref, "treePath", treePath)
264
-
265
-
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
266
-
gr, err := git.Open(path, ref)
66
+
err = h.jc.StartJetstream(ctx, h.processMessages)
267
67
if err != nil {
268
-
notFound(w)
269
-
return
68
+
return nil, fmt.Errorf("failed to start jetstream: %w", err)
270
69
}
271
70
272
-
contents, err := gr.RawContent(treePath)
273
-
if err != nil {
274
-
writeError(w, err.Error(), http.StatusBadRequest)
275
-
l.Error("file content", "error", err.Error())
276
-
return
277
-
}
71
+
r.Get("/", h.Index)
72
+
r.Get("/capabilities", h.Capabilities)
73
+
r.Get("/version", h.Version)
74
+
r.Get("/owner", func(w http.ResponseWriter, r *http.Request) {
75
+
w.Write([]byte(h.c.Server.Owner))
76
+
})
77
+
r.Route("/{did}", func(r chi.Router) {
78
+
// Repo routes
79
+
r.Route("/{name}", func(r chi.Router) {
278
80
279
-
mimeType := http.DetectContentType(contents)
81
+
r.Route("/languages", func(r chi.Router) {
82
+
r.Get("/", h.RepoLanguages)
83
+
r.Get("/{ref}", h.RepoLanguages)
84
+
})
280
85
281
-
// exception for svg
282
-
if filepath.Ext(treePath) == ".svg" {
283
-
mimeType = "image/svg+xml"
284
-
}
86
+
r.Get("/", h.RepoIndex)
87
+
r.Get("/info/refs", h.InfoRefs)
88
+
r.Post("/git-upload-pack", h.UploadPack)
89
+
r.Post("/git-receive-pack", h.ReceivePack)
90
+
r.Get("/compare/{rev1}/{rev2}", h.Compare) // git diff-tree compare of two objects
285
91
286
-
contentHash := sha256.Sum256(contents)
287
-
eTag := fmt.Sprintf("\"%x\"", contentHash)
92
+
r.Route("/tree/{ref}", func(r chi.Router) {
93
+
r.Get("/", h.RepoIndex)
94
+
r.Get("/*", h.RepoTree)
95
+
})
288
96
289
-
// allow image, video, and text/plain files to be served directly
290
-
switch {
291
-
case strings.HasPrefix(mimeType, "image/"), strings.HasPrefix(mimeType, "video/"):
292
-
if clientETag := r.Header.Get("If-None-Match"); clientETag == eTag {
293
-
w.WriteHeader(http.StatusNotModified)
294
-
return
295
-
}
296
-
w.Header().Set("ETag", eTag)
97
+
r.Route("/blob/{ref}", func(r chi.Router) {
98
+
r.Get("/*", h.Blob)
99
+
})
297
100
298
-
case strings.HasPrefix(mimeType, "text/plain"):
299
-
w.Header().Set("Cache-Control", "public, no-cache")
300
-
301
-
default:
302
-
l.Error("attempted to serve disallowed file type", "mimetype", mimeType)
303
-
writeError(w, "only image, video, and text files can be accessed directly", http.StatusForbidden)
304
-
return
305
-
}
306
-
307
-
w.Header().Set("Content-Type", mimeType)
308
-
w.Write(contents)
309
-
}
310
-
311
-
func (h *Handle) Blob(w http.ResponseWriter, r *http.Request) {
312
-
treePath := chi.URLParam(r, "*")
313
-
ref := chi.URLParam(r, "ref")
314
-
ref, _ = url.PathUnescape(ref)
315
-
316
-
l := h.l.With("handler", "Blob", "ref", ref, "treePath", treePath)
317
-
318
-
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
319
-
gr, err := git.Open(path, ref)
320
-
if err != nil {
321
-
notFound(w)
322
-
return
323
-
}
324
-
325
-
var isBinaryFile bool = false
326
-
contents, err := gr.FileContent(treePath)
327
-
if errors.Is(err, git.ErrBinaryFile) {
328
-
isBinaryFile = true
329
-
} else if errors.Is(err, object.ErrFileNotFound) {
330
-
notFound(w)
331
-
return
332
-
} else if err != nil {
333
-
writeError(w, err.Error(), http.StatusInternalServerError)
334
-
return
335
-
}
336
-
337
-
bytes := []byte(contents)
338
-
// safe := string(sanitize(bytes))
339
-
sizeHint := len(bytes)
340
-
341
-
resp := types.RepoBlobResponse{
342
-
Ref: ref,
343
-
Contents: string(bytes),
344
-
Path: treePath,
345
-
IsBinary: isBinaryFile,
346
-
SizeHint: uint64(sizeHint),
347
-
}
348
-
349
-
h.showFile(resp, w, l)
350
-
}
351
-
352
-
func (h *Handle) Archive(w http.ResponseWriter, r *http.Request) {
353
-
name := chi.URLParam(r, "name")
354
-
file := chi.URLParam(r, "file")
355
-
356
-
l := h.l.With("handler", "Archive", "name", name, "file", file)
357
-
358
-
// TODO: extend this to add more files compression (e.g.: xz)
359
-
if !strings.HasSuffix(file, ".tar.gz") {
360
-
notFound(w)
361
-
return
362
-
}
363
-
364
-
ref := strings.TrimSuffix(file, ".tar.gz")
365
-
366
-
unescapedRef, err := url.PathUnescape(ref)
367
-
if err != nil {
368
-
notFound(w)
369
-
return
370
-
}
371
-
372
-
safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(unescapedRef).Short(), "/", "-")
373
-
374
-
// This allows the browser to use a proper name for the file when
375
-
// downloading
376
-
filename := fmt.Sprintf("%s-%s.tar.gz", name, safeRefFilename)
377
-
setContentDisposition(w, filename)
378
-
setGZipMIME(w)
379
-
380
-
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
381
-
gr, err := git.Open(path, unescapedRef)
382
-
if err != nil {
383
-
notFound(w)
384
-
return
385
-
}
386
-
387
-
gw := gzip.NewWriter(w)
388
-
defer gw.Close()
389
-
390
-
prefix := fmt.Sprintf("%s-%s", name, safeRefFilename)
391
-
err = gr.WriteTar(gw, prefix)
392
-
if err != nil {
393
-
// once we start writing to the body we can't report error anymore
394
-
// so we are only left with printing the error.
395
-
l.Error("writing tar file", "error", err.Error())
396
-
return
397
-
}
398
-
399
-
err = gw.Flush()
400
-
if err != nil {
401
-
// once we start writing to the body we can't report error anymore
402
-
// so we are only left with printing the error.
403
-
l.Error("flushing?", "error", err.Error())
404
-
return
405
-
}
406
-
}
407
-
408
-
func (h *Handle) Log(w http.ResponseWriter, r *http.Request) {
409
-
ref := chi.URLParam(r, "ref")
410
-
ref, _ = url.PathUnescape(ref)
411
-
412
-
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
413
-
414
-
l := h.l.With("handler", "Log", "ref", ref, "path", path)
415
-
416
-
gr, err := git.Open(path, ref)
417
-
if err != nil {
418
-
notFound(w)
419
-
return
420
-
}
421
-
422
-
// Get page parameters
423
-
page := 1
424
-
pageSize := 30
425
-
426
-
if pageParam := r.URL.Query().Get("page"); pageParam != "" {
427
-
if p, err := strconv.Atoi(pageParam); err == nil && p > 0 {
428
-
page = p
429
-
}
430
-
}
431
-
432
-
if pageSizeParam := r.URL.Query().Get("per_page"); pageSizeParam != "" {
433
-
if ps, err := strconv.Atoi(pageSizeParam); err == nil && ps > 0 {
434
-
pageSize = ps
435
-
}
436
-
}
437
-
438
-
// convert to offset/limit
439
-
offset := (page - 1) * pageSize
440
-
limit := pageSize
441
-
442
-
commits, err := gr.Commits(offset, limit)
443
-
if err != nil {
444
-
writeError(w, err.Error(), http.StatusInternalServerError)
445
-
l.Error("fetching commits", "error", err.Error())
446
-
return
447
-
}
448
-
449
-
total := len(commits)
450
-
451
-
resp := types.RepoLogResponse{
452
-
Commits: commits,
453
-
Ref: ref,
454
-
Description: getDescription(path),
455
-
Log: true,
456
-
Total: total,
457
-
Page: page,
458
-
PerPage: pageSize,
459
-
}
460
-
461
-
writeJSON(w, resp)
462
-
return
463
-
}
464
-
465
-
func (h *Handle) Diff(w http.ResponseWriter, r *http.Request) {
466
-
ref := chi.URLParam(r, "ref")
467
-
ref, _ = url.PathUnescape(ref)
468
-
469
-
l := h.l.With("handler", "Diff", "ref", ref)
470
-
471
-
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
472
-
gr, err := git.Open(path, ref)
473
-
if err != nil {
474
-
notFound(w)
475
-
return
476
-
}
477
-
478
-
diff, err := gr.Diff()
479
-
if err != nil {
480
-
writeError(w, err.Error(), http.StatusInternalServerError)
481
-
l.Error("getting diff", "error", err.Error())
482
-
return
483
-
}
484
-
485
-
resp := types.RepoCommitResponse{
486
-
Ref: ref,
487
-
Diff: diff,
488
-
}
489
-
490
-
writeJSON(w, resp)
491
-
return
492
-
}
493
-
494
-
func (h *Handle) Tags(w http.ResponseWriter, r *http.Request) {
495
-
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
496
-
l := h.l.With("handler", "Refs")
497
-
498
-
gr, err := git.Open(path, "")
499
-
if err != nil {
500
-
notFound(w)
501
-
return
502
-
}
503
-
504
-
tags, err := gr.Tags()
505
-
if err != nil {
506
-
// Non-fatal, we *should* have at least one branch to show.
507
-
l.Warn("getting tags", "error", err.Error())
508
-
}
509
-
510
-
rtags := []*types.TagReference{}
511
-
for _, tag := range tags {
512
-
var target *object.Tag
513
-
if tag.Target != plumbing.ZeroHash {
514
-
target = &tag
515
-
}
516
-
tr := types.TagReference{
517
-
Tag: target,
518
-
}
519
-
520
-
tr.Reference = types.Reference{
521
-
Name: tag.Name,
522
-
Hash: tag.Hash.String(),
523
-
}
524
-
525
-
if tag.Message != "" {
526
-
tr.Message = tag.Message
527
-
}
528
-
529
-
rtags = append(rtags, &tr)
530
-
}
531
-
532
-
resp := types.RepoTagsResponse{
533
-
Tags: rtags,
534
-
}
535
-
536
-
writeJSON(w, resp)
537
-
return
538
-
}
539
-
540
-
func (h *Handle) Branches(w http.ResponseWriter, r *http.Request) {
541
-
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
542
-
543
-
gr, err := git.PlainOpen(path)
544
-
if err != nil {
545
-
notFound(w)
546
-
return
547
-
}
101
+
r.Route("/raw/{ref}", func(r chi.Router) {
102
+
r.Get("/*", h.BlobRaw)
103
+
})
548
104
549
-
branches, _ := gr.Branches()
550
-
551
-
resp := types.RepoBranchesResponse{
552
-
Branches: branches,
553
-
}
554
-
555
-
writeJSON(w, resp)
556
-
return
557
-
}
558
-
559
-
func (h *Handle) Branch(w http.ResponseWriter, r *http.Request) {
560
-
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
561
-
branchName := chi.URLParam(r, "branch")
562
-
branchName, _ = url.PathUnescape(branchName)
563
-
564
-
l := h.l.With("handler", "Branch")
565
-
566
-
gr, err := git.PlainOpen(path)
567
-
if err != nil {
568
-
notFound(w)
569
-
return
570
-
}
571
-
572
-
ref, err := gr.Branch(branchName)
573
-
if err != nil {
574
-
l.Error("getting branch", "error", err.Error())
575
-
writeError(w, err.Error(), http.StatusInternalServerError)
576
-
return
577
-
}
105
+
r.Get("/log/{ref}", h.Log)
106
+
r.Get("/archive/{file}", h.Archive)
107
+
r.Get("/commit/{ref}", h.Diff)
108
+
r.Get("/tags", h.Tags)
109
+
r.Route("/branches", func(r chi.Router) {
110
+
r.Get("/", h.Branches)
111
+
r.Get("/{branch}", h.Branch)
112
+
r.Get("/default", h.DefaultBranch)
113
+
})
114
+
})
115
+
})
578
116
579
-
commit, err := gr.Commit(ref.Hash())
580
-
if err != nil {
581
-
l.Error("getting commit object", "error", err.Error())
582
-
writeError(w, err.Error(), http.StatusInternalServerError)
583
-
return
584
-
}
117
+
// xrpc apis
118
+
r.Mount("/xrpc", h.XrpcRouter())
585
119
586
-
defaultBranch, err := gr.FindMainBranch()
587
-
isDefault := false
588
-
if err != nil {
589
-
l.Error("getting default branch", "error", err.Error())
590
-
// do not quit though
591
-
} else if defaultBranch == branchName {
592
-
isDefault = true
593
-
}
120
+
// Socket that streams git oplogs
121
+
r.Get("/events", h.Events)
594
122
595
-
resp := types.RepoBranchResponse{
596
-
Branch: types.Branch{
597
-
Reference: types.Reference{
598
-
Name: ref.Name().Short(),
599
-
Hash: ref.Hash().String(),
600
-
},
601
-
Commit: commit,
602
-
IsDefault: isDefault,
603
-
},
604
-
}
123
+
// All public keys on the knot.
124
+
r.Get("/keys", h.Keys)
605
125
606
-
writeJSON(w, resp)
607
-
return
126
+
return r, nil
608
127
}
609
128
610
-
func (h *Handle) Keys(w http.ResponseWriter, r *http.Request) {
611
-
l := h.l.With("handler", "Keys")
612
-
613
-
switch r.Method {
614
-
case http.MethodGet:
615
-
keys, err := h.db.GetAllPublicKeys()
616
-
if err != nil {
617
-
writeError(w, err.Error(), http.StatusInternalServerError)
618
-
l.Error("getting public keys", "error", err.Error())
619
-
return
620
-
}
621
-
622
-
data := make([]map[string]any, 0)
623
-
for _, key := range keys {
624
-
j := key.JSON()
625
-
data = append(data, j)
626
-
}
627
-
writeJSON(w, data)
628
-
return
629
-
630
-
case http.MethodPut:
631
-
pk := db.PublicKey{}
632
-
if err := json.NewDecoder(r.Body).Decode(&pk); err != nil {
633
-
writeError(w, "invalid request body", http.StatusBadRequest)
634
-
return
635
-
}
129
+
func (h *Handle) XrpcRouter() http.Handler {
130
+
logger := tlog.New("knots")
636
131
637
-
_, _, _, _, err := ssh.ParseAuthorizedKey([]byte(pk.Key))
638
-
if err != nil {
639
-
writeError(w, "invalid pubkey", http.StatusBadRequest)
640
-
}
132
+
serviceAuth := serviceauth.NewServiceAuth(h.l, h.resolver, h.c.Server.Did().String())
641
133
642
-
if err := h.db.AddPublicKey(pk); err != nil {
643
-
writeError(w, err.Error(), http.StatusInternalServerError)
644
-
l.Error("adding public key", "error", err.Error())
645
-
return
646
-
}
647
-
648
-
w.WriteHeader(http.StatusNoContent)
649
-
return
134
+
xrpc := &xrpc.Xrpc{
135
+
Config: h.c,
136
+
Db: h.db,
137
+
Ingester: h.jc,
138
+
Enforcer: h.e,
139
+
Logger: logger,
140
+
Notifier: h.n,
141
+
Resolver: h.resolver,
142
+
ServiceAuth: serviceAuth,
650
143
}
144
+
return xrpc.Router()
651
145
}
652
146
653
-
func (h *Handle) RepoForkAheadBehind(w http.ResponseWriter, r *http.Request) {
654
-
l := h.l.With("handler", "RepoForkAheadBehind")
147
+
// version is set during build time.
148
+
var version string
655
149
656
-
data := struct {
657
-
Did string `json:"did"`
658
-
Source string `json:"source"`
659
-
Name string `json:"name,omitempty"`
660
-
HiddenRef string `json:"hiddenref"`
661
-
}{}
662
-
663
-
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
664
-
writeError(w, "invalid request body", http.StatusBadRequest)
665
-
return
666
-
}
667
-
668
-
did := data.Did
669
-
source := data.Source
670
-
671
-
if did == "" || source == "" {
672
-
l.Error("invalid request body, empty did or name")
673
-
w.WriteHeader(http.StatusBadRequest)
674
-
return
675
-
}
676
-
677
-
var name string
678
-
if data.Name != "" {
679
-
name = data.Name
680
-
} else {
681
-
name = filepath.Base(source)
682
-
}
683
-
684
-
branch := chi.URLParam(r, "branch")
685
-
branch, _ = url.PathUnescape(branch)
686
-
687
-
relativeRepoPath := filepath.Join(did, name)
688
-
repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath)
689
-
690
-
gr, err := git.PlainOpen(repoPath)
691
-
if err != nil {
692
-
log.Println(err)
693
-
notFound(w)
694
-
return
695
-
}
696
-
697
-
forkCommit, err := gr.ResolveRevision(branch)
698
-
if err != nil {
699
-
l.Error("error resolving ref revision", "msg", err.Error())
700
-
writeError(w, fmt.Sprintf("error resolving revision %s", branch), http.StatusBadRequest)
701
-
return
702
-
}
703
-
704
-
sourceCommit, err := gr.ResolveRevision(data.HiddenRef)
705
-
if err != nil {
706
-
l.Error("error resolving hidden ref revision", "msg", err.Error())
707
-
writeError(w, fmt.Sprintf("error resolving revision %s", data.HiddenRef), http.StatusBadRequest)
708
-
return
709
-
}
710
-
711
-
status := types.UpToDate
712
-
if forkCommit.Hash.String() != sourceCommit.Hash.String() {
713
-
isAncestor, err := forkCommit.IsAncestor(sourceCommit)
714
-
if err != nil {
715
-
log.Printf("error resolving whether %s is ancestor of %s: %s", branch, data.HiddenRef, err)
150
+
func (h *Handle) Version(w http.ResponseWriter, r *http.Request) {
151
+
if version == "" {
152
+
info, ok := debug.ReadBuildInfo()
153
+
if !ok {
154
+
http.Error(w, "failed to read build info", http.StatusInternalServerError)
716
155
return
717
156
}
718
157
719
-
if isAncestor {
720
-
status = types.FastForwardable
721
-
} else {
722
-
status = types.Conflict
158
+
var modVer string
159
+
for _, mod := range info.Deps {
160
+
if mod.Path == "tangled.sh/tangled.sh/knotserver" {
161
+
version = mod.Version
162
+
break
163
+
}
723
164
}
724
-
}
725
165
726
-
w.Header().Set("Content-Type", "application/json")
727
-
json.NewEncoder(w).Encode(types.AncestorCheckResponse{Status: status})
728
-
}
729
-
730
-
func (h *Handle) RepoLanguages(w http.ResponseWriter, r *http.Request) {
731
-
repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
732
-
ref := chi.URLParam(r, "ref")
733
-
ref, _ = url.PathUnescape(ref)
734
-
735
-
l := h.l.With("handler", "RepoLanguages")
736
-
737
-
gr, err := git.Open(repoPath, ref)
738
-
if err != nil {
739
-
l.Error("opening repo", "error", err.Error())
740
-
notFound(w)
741
-
return
742
-
}
743
-
744
-
ctx, cancel := context.WithTimeout(r.Context(), 1*time.Second)
745
-
defer cancel()
746
-
747
-
sizes, err := gr.AnalyzeLanguages(ctx)
748
-
if err != nil {
749
-
l.Error("failed to analyze languages", "error", err.Error())
750
-
writeError(w, err.Error(), http.StatusNoContent)
751
-
return
752
-
}
753
-
754
-
resp := types.RepoLanguageResponse{Languages: sizes}
755
-
756
-
writeJSON(w, resp)
757
-
}
758
-
759
-
func (h *Handle) RepoForkSync(w http.ResponseWriter, r *http.Request) {
760
-
l := h.l.With("handler", "RepoForkSync")
761
-
762
-
data := struct {
763
-
Did string `json:"did"`
764
-
Source string `json:"source"`
765
-
Name string `json:"name,omitempty"`
766
-
}{}
767
-
768
-
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
769
-
writeError(w, "invalid request body", http.StatusBadRequest)
770
-
return
771
-
}
772
-
773
-
did := data.Did
774
-
source := data.Source
775
-
776
-
if did == "" || source == "" {
777
-
l.Error("invalid request body, empty did or name")
778
-
w.WriteHeader(http.StatusBadRequest)
779
-
return
780
-
}
781
-
782
-
var name string
783
-
if data.Name != "" {
784
-
name = data.Name
785
-
} else {
786
-
name = filepath.Base(source)
787
-
}
788
-
789
-
branch := chi.URLParam(r, "*")
790
-
branch, _ = url.PathUnescape(branch)
791
-
792
-
relativeRepoPath := filepath.Join(did, name)
793
-
repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath)
794
-
795
-
gr, err := git.Open(repoPath, branch)
796
-
if err != nil {
797
-
log.Println(err)
798
-
notFound(w)
799
-
return
800
-
}
801
-
802
-
err = gr.Sync()
803
-
if err != nil {
804
-
l.Error("error syncing repo fork", "error", err.Error())
805
-
writeError(w, err.Error(), http.StatusInternalServerError)
806
-
return
807
-
}
808
-
809
-
w.WriteHeader(http.StatusNoContent)
810
-
}
811
-
812
-
func (h *Handle) RepoFork(w http.ResponseWriter, r *http.Request) {
813
-
l := h.l.With("handler", "RepoFork")
814
-
815
-
data := struct {
816
-
Did string `json:"did"`
817
-
Source string `json:"source"`
818
-
Name string `json:"name,omitempty"`
819
-
}{}
820
-
821
-
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
822
-
writeError(w, "invalid request body", http.StatusBadRequest)
823
-
return
824
-
}
825
-
826
-
did := data.Did
827
-
source := data.Source
828
-
829
-
if did == "" || source == "" {
830
-
l.Error("invalid request body, empty did or name")
831
-
w.WriteHeader(http.StatusBadRequest)
832
-
return
833
-
}
834
-
835
-
var name string
836
-
if data.Name != "" {
837
-
name = data.Name
838
-
} else {
839
-
name = filepath.Base(source)
840
-
}
841
-
842
-
relativeRepoPath := filepath.Join(did, name)
843
-
repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath)
844
-
845
-
err := git.Fork(repoPath, source)
846
-
if err != nil {
847
-
l.Error("forking repo", "error", err.Error())
848
-
writeError(w, err.Error(), http.StatusInternalServerError)
849
-
return
850
-
}
851
-
852
-
// add perms for this user to access the repo
853
-
err = h.e.AddRepo(did, rbac.ThisServer, relativeRepoPath)
854
-
if err != nil {
855
-
l.Error("adding repo permissions", "error", err.Error())
856
-
writeError(w, err.Error(), http.StatusInternalServerError)
857
-
return
858
-
}
859
-
860
-
hook.SetupRepo(
861
-
hook.Config(
862
-
hook.WithScanPath(h.c.Repo.ScanPath),
863
-
hook.WithInternalApi(h.c.Server.InternalListenAddr),
864
-
),
865
-
repoPath,
866
-
)
867
-
868
-
w.WriteHeader(http.StatusNoContent)
869
-
}
870
-
871
-
func (h *Handle) RemoveRepo(w http.ResponseWriter, r *http.Request) {
872
-
l := h.l.With("handler", "RemoveRepo")
873
-
874
-
data := struct {
875
-
Did string `json:"did"`
876
-
Name string `json:"name"`
877
-
}{}
878
-
879
-
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
880
-
writeError(w, "invalid request body", http.StatusBadRequest)
881
-
return
882
-
}
883
-
884
-
did := data.Did
885
-
name := data.Name
886
-
887
-
if did == "" || name == "" {
888
-
l.Error("invalid request body, empty did or name")
889
-
w.WriteHeader(http.StatusBadRequest)
890
-
return
891
-
}
892
-
893
-
relativeRepoPath := filepath.Join(did, name)
894
-
repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath)
895
-
err := os.RemoveAll(repoPath)
896
-
if err != nil {
897
-
l.Error("removing repo", "error", err.Error())
898
-
writeError(w, err.Error(), http.StatusInternalServerError)
899
-
return
900
-
}
901
-
902
-
w.WriteHeader(http.StatusNoContent)
903
-
904
-
}
905
-
func (h *Handle) Merge(w http.ResponseWriter, r *http.Request) {
906
-
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
907
-
908
-
data := types.MergeRequest{}
909
-
910
-
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
911
-
writeError(w, err.Error(), http.StatusBadRequest)
912
-
h.l.Error("git: failed to unmarshal json patch", "handler", "Merge", "error", err)
913
-
return
914
-
}
915
-
916
-
mo := &git.MergeOptions{
917
-
AuthorName: data.AuthorName,
918
-
AuthorEmail: data.AuthorEmail,
919
-
CommitBody: data.CommitBody,
920
-
CommitMessage: data.CommitMessage,
921
-
}
922
-
923
-
patch := data.Patch
924
-
branch := data.Branch
925
-
gr, err := git.Open(path, branch)
926
-
if err != nil {
927
-
notFound(w)
928
-
return
929
-
}
930
-
931
-
mo.FormatPatch = patchutil.IsFormatPatch(patch)
932
-
933
-
if err := gr.MergeWithOptions([]byte(patch), branch, mo); err != nil {
934
-
var mergeErr *git.ErrMerge
935
-
if errors.As(err, &mergeErr) {
936
-
conflicts := make([]types.ConflictInfo, len(mergeErr.Conflicts))
937
-
for i, conflict := range mergeErr.Conflicts {
938
-
conflicts[i] = types.ConflictInfo{
939
-
Filename: conflict.Filename,
940
-
Reason: conflict.Reason,
941
-
}
942
-
}
943
-
response := types.MergeCheckResponse{
944
-
IsConflicted: true,
945
-
Conflicts: conflicts,
946
-
Message: mergeErr.Message,
947
-
}
948
-
writeConflict(w, response)
949
-
h.l.Error("git: merge conflict", "handler", "Merge", "error", mergeErr)
950
-
} else {
951
-
writeError(w, err.Error(), http.StatusBadRequest)
952
-
h.l.Error("git: failed to merge", "handler", "Merge", "error", err.Error())
166
+
if modVer == "" {
167
+
version = "unknown"
953
168
}
954
-
return
955
169
}
956
170
957
-
w.WriteHeader(http.StatusOK)
171
+
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
172
+
fmt.Fprintf(w, "knotserver/%s", version)
958
173
}
959
174
960
-
func (h *Handle) MergeCheck(w http.ResponseWriter, r *http.Request) {
961
-
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
962
-
963
-
var data struct {
964
-
Patch string `json:"patch"`
965
-
Branch string `json:"branch"`
966
-
}
175
+
func (h *Handle) configureOwner() error {
176
+
cfgOwner := h.c.Server.Owner
967
177
968
-
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
969
-
writeError(w, err.Error(), http.StatusBadRequest)
970
-
h.l.Error("git: failed to unmarshal json patch", "handler", "MergeCheck", "error", err)
971
-
return
972
-
}
178
+
rbacDomain := "thisserver"
973
179
974
-
patch := data.Patch
975
-
branch := data.Branch
976
-
gr, err := git.Open(path, branch)
180
+
existing, err := h.e.GetKnotUsersByRole("server:owner", rbacDomain)
977
181
if err != nil {
978
-
notFound(w)
979
-
return
182
+
return err
980
183
}
981
184
982
-
err = gr.MergeCheck([]byte(patch), branch)
983
-
if err == nil {
984
-
response := types.MergeCheckResponse{
985
-
IsConflicted: false,
986
-
}
987
-
writeJSON(w, response)
988
-
return
989
-
}
185
+
switch len(existing) {
186
+
case 0:
187
+
// no owner configured, continue
188
+
case 1:
189
+
// find existing owner
190
+
existingOwner := existing[0]
990
191
991
-
var mergeErr *git.ErrMerge
992
-
if errors.As(err, &mergeErr) {
993
-
conflicts := make([]types.ConflictInfo, len(mergeErr.Conflicts))
994
-
for i, conflict := range mergeErr.Conflicts {
995
-
conflicts[i] = types.ConflictInfo{
996
-
Filename: conflict.Filename,
997
-
Reason: conflict.Reason,
998
-
}
999
-
}
1000
-
response := types.MergeCheckResponse{
1001
-
IsConflicted: true,
1002
-
Conflicts: conflicts,
1003
-
Message: mergeErr.Message,
192
+
// no ownership change, this is okay
193
+
if existingOwner == h.c.Server.Owner {
194
+
break
1004
195
}
1005
-
writeConflict(w, response)
1006
-
h.l.Error("git: merge conflict", "handler", "MergeCheck", "error", mergeErr.Error())
1007
-
return
1008
-
}
1009
-
writeError(w, err.Error(), http.StatusInternalServerError)
1010
-
h.l.Error("git: failed to check merge", "handler", "MergeCheck", "error", err.Error())
1011
-
}
1012
196
1013
-
func (h *Handle) Compare(w http.ResponseWriter, r *http.Request) {
1014
-
rev1 := chi.URLParam(r, "rev1")
1015
-
rev1, _ = url.PathUnescape(rev1)
1016
-
1017
-
rev2 := chi.URLParam(r, "rev2")
1018
-
rev2, _ = url.PathUnescape(rev2)
1019
-
1020
-
l := h.l.With("handler", "Compare", "r1", rev1, "r2", rev2)
1021
-
1022
-
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
1023
-
gr, err := git.PlainOpen(path)
1024
-
if err != nil {
1025
-
notFound(w)
1026
-
return
1027
-
}
1028
-
1029
-
commit1, err := gr.ResolveRevision(rev1)
1030
-
if err != nil {
1031
-
l.Error("error resolving revision 1", "msg", err.Error())
1032
-
writeError(w, fmt.Sprintf("error resolving revision %s", rev1), http.StatusBadRequest)
1033
-
return
1034
-
}
1035
-
1036
-
commit2, err := gr.ResolveRevision(rev2)
1037
-
if err != nil {
1038
-
l.Error("error resolving revision 2", "msg", err.Error())
1039
-
writeError(w, fmt.Sprintf("error resolving revision %s", rev2), http.StatusBadRequest)
1040
-
return
1041
-
}
1042
-
1043
-
rawPatch, formatPatch, err := gr.FormatPatch(commit1, commit2)
1044
-
if err != nil {
1045
-
l.Error("error comparing revisions", "msg", err.Error())
1046
-
writeError(w, "error comparing revisions", http.StatusBadRequest)
1047
-
return
1048
-
}
1049
-
1050
-
writeJSON(w, types.RepoFormatPatchResponse{
1051
-
Rev1: commit1.Hash.String(),
1052
-
Rev2: commit2.Hash.String(),
1053
-
FormatPatch: formatPatch,
1054
-
Patch: rawPatch,
1055
-
})
1056
-
return
1057
-
}
1058
-
1059
-
func (h *Handle) NewHiddenRef(w http.ResponseWriter, r *http.Request) {
1060
-
l := h.l.With("handler", "NewHiddenRef")
1061
-
1062
-
forkRef := chi.URLParam(r, "forkRef")
1063
-
forkRef, _ = url.PathUnescape(forkRef)
1064
-
1065
-
remoteRef := chi.URLParam(r, "remoteRef")
1066
-
remoteRef, _ = url.PathUnescape(remoteRef)
1067
-
1068
-
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
1069
-
gr, err := git.PlainOpen(path)
1070
-
if err != nil {
1071
-
notFound(w)
1072
-
return
1073
-
}
1074
-
1075
-
err = gr.TrackHiddenRemoteRef(forkRef, remoteRef)
1076
-
if err != nil {
1077
-
l.Error("error tracking hidden remote ref", "msg", err.Error())
1078
-
writeError(w, "error tracking hidden remote ref", http.StatusBadRequest)
1079
-
return
1080
-
}
1081
-
1082
-
w.WriteHeader(http.StatusNoContent)
1083
-
return
1084
-
}
1085
-
1086
-
func (h *Handle) AddMember(w http.ResponseWriter, r *http.Request) {
1087
-
l := h.l.With("handler", "AddMember")
1088
-
1089
-
data := struct {
1090
-
Did string `json:"did"`
1091
-
}{}
1092
-
1093
-
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
1094
-
writeError(w, "invalid request body", http.StatusBadRequest)
1095
-
return
1096
-
}
1097
-
1098
-
did := data.Did
1099
-
1100
-
if err := h.db.AddDid(did); err != nil {
1101
-
l.Error("adding did", "error", err.Error())
1102
-
writeError(w, err.Error(), http.StatusInternalServerError)
1103
-
return
1104
-
}
1105
-
h.jc.AddDid(did)
1106
-
1107
-
if err := h.e.AddKnotMember(rbac.ThisServer, did); err != nil {
1108
-
l.Error("adding member", "error", err.Error())
1109
-
writeError(w, err.Error(), http.StatusInternalServerError)
1110
-
return
1111
-
}
1112
-
1113
-
if err := h.fetchAndAddKeys(r.Context(), did); err != nil {
1114
-
l.Error("fetching and adding keys", "error", err.Error())
1115
-
writeError(w, err.Error(), http.StatusInternalServerError)
1116
-
return
1117
-
}
1118
-
1119
-
w.WriteHeader(http.StatusNoContent)
1120
-
}
1121
-
1122
-
func (h *Handle) AddRepoCollaborator(w http.ResponseWriter, r *http.Request) {
1123
-
l := h.l.With("handler", "AddRepoCollaborator")
1124
-
1125
-
data := struct {
1126
-
Did string `json:"did"`
1127
-
}{}
1128
-
1129
-
ownerDid := chi.URLParam(r, "did")
1130
-
repo := chi.URLParam(r, "name")
1131
-
1132
-
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
1133
-
writeError(w, "invalid request body", http.StatusBadRequest)
1134
-
return
1135
-
}
1136
-
1137
-
if err := h.db.AddDid(data.Did); err != nil {
1138
-
l.Error("adding did", "error", err.Error())
1139
-
writeError(w, err.Error(), http.StatusInternalServerError)
1140
-
return
1141
-
}
1142
-
h.jc.AddDid(data.Did)
1143
-
1144
-
repoName, _ := securejoin.SecureJoin(ownerDid, repo)
1145
-
if err := h.e.AddCollaborator(data.Did, rbac.ThisServer, repoName); err != nil {
1146
-
l.Error("adding repo collaborator", "error", err.Error())
1147
-
writeError(w, err.Error(), http.StatusInternalServerError)
1148
-
return
1149
-
}
1150
-
1151
-
if err := h.fetchAndAddKeys(r.Context(), data.Did); err != nil {
1152
-
l.Error("fetching and adding keys", "error", err.Error())
1153
-
writeError(w, err.Error(), http.StatusInternalServerError)
1154
-
return
1155
-
}
1156
-
1157
-
w.WriteHeader(http.StatusNoContent)
1158
-
}
1159
-
1160
-
func (h *Handle) DefaultBranch(w http.ResponseWriter, r *http.Request) {
1161
-
l := h.l.With("handler", "DefaultBranch")
1162
-
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
1163
-
1164
-
gr, err := git.Open(path, "")
1165
-
if err != nil {
1166
-
notFound(w)
1167
-
return
1168
-
}
1169
-
1170
-
branch, err := gr.FindMainBranch()
1171
-
if err != nil {
1172
-
writeError(w, err.Error(), http.StatusInternalServerError)
1173
-
l.Error("getting default branch", "error", err.Error())
1174
-
return
1175
-
}
1176
-
1177
-
writeJSON(w, types.RepoDefaultBranchResponse{
1178
-
Branch: branch,
1179
-
})
1180
-
}
1181
-
1182
-
func (h *Handle) SetDefaultBranch(w http.ResponseWriter, r *http.Request) {
1183
-
l := h.l.With("handler", "SetDefaultBranch")
1184
-
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
1185
-
1186
-
data := struct {
1187
-
Branch string `json:"branch"`
1188
-
}{}
1189
-
1190
-
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
1191
-
writeError(w, err.Error(), http.StatusBadRequest)
1192
-
return
1193
-
}
1194
-
1195
-
gr, err := git.PlainOpen(path)
1196
-
if err != nil {
1197
-
notFound(w)
1198
-
return
1199
-
}
1200
-
1201
-
err = gr.SetDefaultBranch(data.Branch)
1202
-
if err != nil {
1203
-
writeError(w, err.Error(), http.StatusInternalServerError)
1204
-
l.Error("setting default branch", "error", err.Error())
1205
-
return
1206
-
}
1207
-
1208
-
w.WriteHeader(http.StatusNoContent)
1209
-
}
1210
-
1211
-
func (h *Handle) Health(w http.ResponseWriter, r *http.Request) {
1212
-
w.Write([]byte("ok"))
1213
-
}
1214
-
1215
-
func validateRepoName(name string) error {
1216
-
// check for path traversal attempts
1217
-
if name == "." || name == ".." ||
1218
-
strings.Contains(name, "/") || strings.Contains(name, "\\") {
1219
-
return fmt.Errorf("Repository name contains invalid path characters")
1220
-
}
1221
-
1222
-
// check for sequences that could be used for traversal when normalized
1223
-
if strings.Contains(name, "./") || strings.Contains(name, "../") ||
1224
-
strings.HasPrefix(name, ".") || strings.HasSuffix(name, ".") {
1225
-
return fmt.Errorf("Repository name contains invalid path sequence")
1226
-
}
1227
-
1228
-
// then continue with character validation
1229
-
for _, char := range name {
1230
-
if !((char >= 'a' && char <= 'z') ||
1231
-
(char >= 'A' && char <= 'Z') ||
1232
-
(char >= '0' && char <= '9') ||
1233
-
char == '-' || char == '_' || char == '.') {
1234
-
return fmt.Errorf("Repository name can only contain alphanumeric characters, periods, hyphens, and underscores")
197
+
// remove existing owner
198
+
err = h.e.RemoveKnotOwner(rbacDomain, existingOwner)
199
+
if err != nil {
200
+
return nil
1235
201
}
1236
-
}
1237
-
1238
-
// additional check to prevent multiple sequential dots
1239
-
if strings.Contains(name, "..") {
1240
-
return fmt.Errorf("Repository name cannot contain sequential dots")
202
+
default:
203
+
return fmt.Errorf("more than one owner in DB, try deleting %q and starting over", h.c.Server.DBPath)
1241
204
}
1242
205
1243
-
// if all checks pass
1244
-
return nil
206
+
return h.e.AddKnotOwner(rbacDomain, cfgOwner)
1245
207
}
+8
rbac/rbac.go
+8
rbac/rbac.go
···
277
277
return e.isInviteAllowed(user, intoSpindle(domain))
278
278
}
279
279
280
+
func (e *Enforcer) IsRepoCreateAllowed(user, domain string) (bool, error) {
281
+
return e.E.Enforce(user, domain, domain, "repo:create")
282
+
}
283
+
284
+
func (e *Enforcer) IsRepoDeleteAllowed(user, domain string) (bool, error) {
285
+
return e.E.Enforce(user, domain, domain, "repo:delete")
286
+
}
287
+
280
288
func (e *Enforcer) IsPushAllowed(user, domain, repo string) (bool, error) {
281
289
return e.E.Enforce(user, domain, repo, "repo:push")
282
290
}