@synopackageland/cli 0.34.0 → 0.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,26 +2,436 @@
2
2
  package main
3
3
 
4
4
  import (
5
+ "bytes"
5
6
  "encoding/json"
6
7
  "fmt"
7
8
  "go/ast"
8
9
  "go/parser"
9
10
  "go/token"
11
+ "io"
10
12
  "os"
13
+ "os/exec"
11
14
  "path/filepath"
15
+ "regexp"
16
+ "sort"
12
17
  "strconv"
13
18
  "strings"
14
19
  )
15
20
 
16
21
  type input struct {
17
22
  Dir string
23
+ Roots []string
24
+ Arches []string
18
25
  Permissions map[string]string
19
26
  SDK map[string]map[string]string
20
27
  }
21
28
 
22
29
  type result struct {
23
- Problems []string `json:"problems"`
24
- EntryGate bool `json:"entryGate"`
30
+ Problems []string `json:"problems"`
31
+ EntryGate bool `json:"entryGate"`
32
+ HTTPRoutes []httpRoute `json:"httpRoutes"`
33
+ }
34
+
35
+ type httpRoute struct {
36
+ Prefix string `json:"prefix"`
37
+ Method string `json:"method"`
38
+ }
39
+
40
+ const sdkModule = "github.com/synopackageland/syno-pkg-land-sdk"
41
+
42
+ // Resolve target build constraints without running any application code.
43
+ func shippedFiles(in input) (map[string]string, error) {
44
+ files := map[string]string{}
45
+ if _, err := os.Stat(filepath.Join(in.Dir, "go.mod")); os.IsNotExist(err) {
46
+ matches, err := filepath.Glob(filepath.Join(in.Dir, "*.go"))
47
+ for _, path := range matches {
48
+ if !strings.HasSuffix(path, "_test.go") {
49
+ files[path] = "fixture"
50
+ }
51
+ }
52
+ return files, err
53
+ }
54
+ roots, arches := in.Roots, in.Arches
55
+ if len(roots) == 0 {
56
+ roots = []string{"."}
57
+ }
58
+ if len(arches) == 0 {
59
+ arches = []string{"amd64", "arm64", "arm"}
60
+ }
61
+ for _, arch := range arches {
62
+ cmd := exec.Command("go", append([]string{"list", "-e", "-mod=readonly", "-deps", "-json"}, roots...)...)
63
+ cmd.Dir = in.Dir
64
+ cmd.Env = append(os.Environ(), "GOOS=linux", "GOARCH="+arch, "GOARM=7", "CGO_ENABLED=0", "GOWORK=off", "GOFLAGS=")
65
+ raw, err := cmd.Output()
66
+ if err != nil {
67
+ return nil, fmt.Errorf("cannot resolve shipped Go import graph")
68
+ }
69
+ d := json.NewDecoder(bytes.NewReader(raw))
70
+ for {
71
+ var p struct {
72
+ Dir, ImportPath string
73
+ GoFiles []string
74
+ Standard bool
75
+ DepOnly bool
76
+ Error *struct{ Err string }
77
+ Module *struct {
78
+ Main bool
79
+ Replace *struct{ Dir string }
80
+ }
81
+ }
82
+ err := d.Decode(&p)
83
+ if err == io.EOF {
84
+ break
85
+ }
86
+ if err != nil {
87
+ return nil, err
88
+ }
89
+ if !p.DepOnly && p.Error != nil {
90
+ return nil, fmt.Errorf("cannot resolve a configured shipped Go root")
91
+ }
92
+ if p.Standard || p.ImportPath == sdkModule || strings.HasPrefix(p.ImportPath, sdkModule+"/") {
93
+ continue
94
+ }
95
+ if p.Module == nil || !p.Module.Main && p.Module.Replace == nil {
96
+ continue
97
+ }
98
+ for _, name := range p.GoFiles {
99
+ files[filepath.Join(p.Dir, name)] = p.ImportPath
100
+ }
101
+ }
102
+ }
103
+ if len(files) == 0 {
104
+ return nil, fmt.Errorf("shipped Go roots contain no auditable source")
105
+ }
106
+ return files, nil
107
+ }
108
+
109
+ func stringLiteral(expr ast.Expr) string {
110
+ if v, ok := expr.(*ast.BasicLit); ok && v.Kind == token.STRING {
111
+ s, _ := strconv.Unquote(v.Value)
112
+ return s
113
+ }
114
+ return ""
115
+ }
116
+
117
+ type source struct {
118
+ file *ast.File
119
+ path, pkg string
120
+ imports map[string]string
121
+ }
122
+
123
+ // Follow the handler argument through local declarations and factories. A
124
+ // boundary never exempts an entire directory, file, or any listener primitive.
125
+ func boundaryHandlers(sources []source) map[ast.Node]bool {
126
+ decls := map[string][]ast.Node{}
127
+ owners := map[ast.Node]source{}
128
+ for _, s := range sources {
129
+ ast.Inspect(s.file, func(n ast.Node) bool {
130
+ if n != nil {
131
+ owners[n] = s
132
+ }
133
+ return true
134
+ })
135
+ for _, decl := range s.file.Decls {
136
+ switch d := decl.(type) {
137
+ case *ast.FuncDecl:
138
+ key := s.pkg + "." + d.Name.Name
139
+ if d.Recv != nil {
140
+ t := d.Recv.List[0].Type
141
+ if v, ok := t.(*ast.StarExpr); ok {
142
+ t = v.X
143
+ }
144
+ if v, ok := t.(*ast.Ident); ok {
145
+ key = s.pkg + "." + v.Name
146
+ }
147
+ }
148
+ decls[key] = append(decls[key], d)
149
+ case *ast.GenDecl:
150
+ for _, spec := range d.Specs {
151
+ switch v := spec.(type) {
152
+ case *ast.TypeSpec:
153
+ decls[s.pkg+"."+v.Name.Name] = append(decls[s.pkg+"."+v.Name.Name], v)
154
+ case *ast.ValueSpec:
155
+ for _, name := range v.Names {
156
+ decls[s.pkg+"."+name.Name] = append(decls[s.pkg+"."+name.Name], v)
157
+ }
158
+ }
159
+ }
160
+ }
161
+ }
162
+ }
163
+ safe, visited := map[ast.Node]bool{}, map[ast.Node]bool{}
164
+ var mark func(ast.Node)
165
+ mark = func(root ast.Node) {
166
+ if root == nil || visited[root] {
167
+ return
168
+ }
169
+ visited[root] = true
170
+ s := owners[root]
171
+ ast.Inspect(root, func(n ast.Node) bool {
172
+ if n == nil {
173
+ return false
174
+ }
175
+ safe[n] = true
176
+ key := ""
177
+ switch v := n.(type) {
178
+ case *ast.Ident:
179
+ key = s.pkg + "." + v.Name
180
+ if v.Obj != nil {
181
+ // A parameter or receiver carries a value built elsewhere.
182
+ // Following its type would admit every method of that type,
183
+ // such as an App Config whose method value is the handler.
184
+ if _, param := v.Obj.Decl.(*ast.Field); param {
185
+ return true
186
+ }
187
+ if d, ok := v.Obj.Decl.(ast.Node); ok {
188
+ mark(d)
189
+ }
190
+ }
191
+ case *ast.SelectorExpr:
192
+ if id, ok := v.X.(*ast.Ident); ok {
193
+ key = s.imports[id.Name] + "." + v.Sel.Name
194
+ }
195
+ }
196
+ for _, decl := range decls[key] {
197
+ mark(decl)
198
+ }
199
+ return true
200
+ })
201
+ }
202
+ for _, s := range sources {
203
+ ast.Inspect(s.file, func(n ast.Node) bool {
204
+ call, ok := n.(*ast.CallExpr)
205
+ if ok && len(call.Args) == 2 {
206
+ if sel, ok := call.Fun.(*ast.SelectorExpr); ok {
207
+ if id, ok := sel.X.(*ast.Ident); ok && s.imports[id.Name] == sdkModule+"/api" && sel.Sel.Name == "HandleIntegration" {
208
+ mark(call.Args[1])
209
+ }
210
+ }
211
+ }
212
+ if !ok || len(call.Args) != 3 {
213
+ return true
214
+ }
215
+ sel, ok := call.Fun.(*ast.SelectorExpr)
216
+ if !ok {
217
+ return true
218
+ }
219
+ id, ok := sel.X.(*ast.Ident)
220
+ if ok && (s.imports[id.Name] == sdkModule+"/api" && sel.Sel.Name == "HandleHTTP" ||
221
+ s.imports[id.Name] == sdkModule+"/ipc" && (sel.Sel.Name == "ServeUnix" || sel.Sel.Name == "StartUnix")) {
222
+ mark(call.Args[2])
223
+ }
224
+ return true
225
+ })
226
+ }
227
+ return safe
228
+ }
229
+
230
+ // A handler admitted to a private/raw boundary cannot also be installed as
231
+ // the CGI root. SDK server construction ends this search because it gates
232
+ // its registered routes before invoking their handlers.
233
+ func reusesBoundary(expr ast.Node, owner source, sources []source, safe map[ast.Node]bool, seen map[ast.Node]bool) bool {
234
+ if expr == nil || seen[expr] {
235
+ return false
236
+ }
237
+ seen[expr] = true
238
+ imports := owner.imports
239
+ if call, ok := expr.(*ast.CallExpr); ok {
240
+ if sel, ok := call.Fun.(*ast.SelectorExpr); ok {
241
+ if id, ok := sel.X.(*ast.Ident); ok && imports[id.Name] == sdkModule+"/api" {
242
+ switch sel.Sel.Name {
243
+ case "NewServerWithPermissions":
244
+ return false
245
+ case "WithRequestDeadline", "WithDescription":
246
+ return len(call.Args) > 0 && reusesBoundary(call.Args[0], owner, sources, safe, seen)
247
+ }
248
+ }
249
+ }
250
+ }
251
+ found := false
252
+ ast.Inspect(expr, func(n ast.Node) bool {
253
+ if n == nil || found {
254
+ return false
255
+ }
256
+ if n != expr {
257
+ if _, ok := n.(*ast.CallExpr); ok {
258
+ found = reusesBoundary(n, owner, sources, safe, seen)
259
+ return false
260
+ }
261
+ }
262
+ targetPackage, targetName := "", ""
263
+ if id, ok := n.(*ast.Ident); ok {
264
+ if id.Obj != nil {
265
+ if decl, ok := id.Obj.Decl.(ast.Node); ok {
266
+ found = safe[decl] || reusesBoundary(decl, owner, sources, safe, seen)
267
+ }
268
+ } else {
269
+ targetPackage, targetName = owner.pkg, id.Name
270
+ }
271
+ }
272
+ if selector, ok := n.(*ast.SelectorExpr); ok {
273
+ if pkg, ok := selector.X.(*ast.Ident); ok {
274
+ targetPackage, targetName = imports[pkg.Name], selector.Sel.Name
275
+ }
276
+ }
277
+ if targetPackage != "" {
278
+ for _, candidate := range sources {
279
+ if candidate.pkg != targetPackage {
280
+ continue
281
+ }
282
+ for _, decl := range candidate.file.Decls {
283
+ var target ast.Node
284
+ switch d := decl.(type) {
285
+ case *ast.FuncDecl:
286
+ if d.Recv == nil && d.Name.Name == targetName {
287
+ target = d
288
+ }
289
+ case *ast.GenDecl:
290
+ for _, spec := range d.Specs {
291
+ switch v := spec.(type) {
292
+ case *ast.TypeSpec:
293
+ if v.Name.Name == targetName {
294
+ target = v
295
+ }
296
+ case *ast.ValueSpec:
297
+ for _, name := range v.Names {
298
+ if name.Name == targetName {
299
+ target = v
300
+ }
301
+ }
302
+ }
303
+ }
304
+ }
305
+ if target != nil && (safe[target] || reusesBoundary(target, candidate, sources, safe, seen)) {
306
+ found = true
307
+ }
308
+ }
309
+ }
310
+ }
311
+ return !found
312
+ })
313
+ return found
314
+ }
315
+
316
+ // Resolve declared receiver types without executing the App or requiring a
317
+ // successful type check (the audit also runs while projects are being edited).
318
+ func receiverType(expr ast.Node, src source, sources []source, seen map[ast.Node]bool) string {
319
+ if expr == nil || seen[expr] {
320
+ return ""
321
+ }
322
+ seen[expr] = true
323
+ follow := func(n ast.Node) string { return receiverType(n, src, sources, seen) }
324
+ find := func(pkg, name string) (ast.Node, source) {
325
+ for _, s := range sources {
326
+ if s.pkg != pkg {
327
+ continue
328
+ }
329
+ for _, decl := range s.file.Decls {
330
+ if fn, ok := decl.(*ast.FuncDecl); ok && fn.Recv == nil && fn.Name.Name == name {
331
+ return fn, s
332
+ }
333
+ if gen, ok := decl.(*ast.GenDecl); ok {
334
+ for _, spec := range gen.Specs {
335
+ if typ, ok := spec.(*ast.TypeSpec); ok && typ.Name.Name == name {
336
+ return typ, s
337
+ }
338
+ if value, ok := spec.(*ast.ValueSpec); ok {
339
+ for _, id := range value.Names {
340
+ if id.Name == name {
341
+ return id, s
342
+ }
343
+ }
344
+ }
345
+ }
346
+ }
347
+ }
348
+ }
349
+ return nil, src
350
+ }
351
+ switch v := expr.(type) {
352
+ case *ast.ParenExpr:
353
+ return follow(v.X)
354
+ case *ast.StarExpr:
355
+ return follow(v.X)
356
+ case *ast.UnaryExpr:
357
+ return follow(v.X)
358
+ case *ast.CompositeLit:
359
+ return follow(v.Type)
360
+ case *ast.SelectorExpr:
361
+ if id, ok := v.X.(*ast.Ident); ok && src.imports[id.Name] != "" {
362
+ return src.imports[id.Name] + "." + v.Sel.Name
363
+ }
364
+ typ := follow(v.X)
365
+ if dot := strings.LastIndex(typ, "."); dot > 0 {
366
+ decl, owner := find(typ[:dot], typ[dot+1:])
367
+ if d, ok := decl.(*ast.TypeSpec); ok {
368
+ if st, ok := d.Type.(*ast.StructType); ok {
369
+ for _, field := range st.Fields.List {
370
+ for _, name := range field.Names {
371
+ if name.Name == v.Sel.Name {
372
+ return receiverType(field.Type, owner, sources, seen)
373
+ }
374
+ }
375
+ }
376
+ }
377
+ }
378
+ }
379
+ case *ast.Ident:
380
+ if v.Obj != nil {
381
+ switch d := v.Obj.Decl.(type) {
382
+ case *ast.Field:
383
+ return follow(d.Type)
384
+ case *ast.ValueSpec:
385
+ if d.Type != nil {
386
+ return follow(d.Type)
387
+ }
388
+ for i, name := range d.Names {
389
+ if name.Name == v.Name && i < len(d.Values) {
390
+ return follow(d.Values[i])
391
+ }
392
+ }
393
+ case *ast.AssignStmt:
394
+ for i, lhs := range d.Lhs {
395
+ if id, ok := lhs.(*ast.Ident); ok && id.Name == v.Name && i < len(d.Rhs) {
396
+ return follow(d.Rhs[i])
397
+ }
398
+ }
399
+ case *ast.TypeSpec:
400
+ if _, ok := d.Type.(*ast.StructType); ok {
401
+ return src.pkg + "." + d.Name.Name
402
+ }
403
+ return follow(d.Type)
404
+ }
405
+ } else {
406
+ decl, owner := find(src.pkg, v.Name)
407
+ return receiverType(decl, owner, sources, seen)
408
+ }
409
+ case *ast.TypeSpec:
410
+ if _, ok := v.Type.(*ast.StructType); ok {
411
+ return src.pkg + "." + v.Name.Name
412
+ }
413
+ return follow(v.Type)
414
+ case *ast.FuncDecl:
415
+ if v.Type.Results != nil && len(v.Type.Results.List) > 0 {
416
+ return follow(v.Type.Results.List[0].Type)
417
+ }
418
+ case *ast.CallExpr:
419
+ if id, ok := v.Fun.(*ast.Ident); ok && id.Name == "new" && len(v.Args) == 1 {
420
+ return follow(v.Args[0])
421
+ }
422
+ pkg, name := src.pkg, ""
423
+ switch fun := v.Fun.(type) {
424
+ case *ast.Ident:
425
+ name = fun.Name
426
+ case *ast.SelectorExpr:
427
+ if id, ok := fun.X.(*ast.Ident); ok {
428
+ pkg, name = src.imports[id.Name], fun.Sel.Name
429
+ }
430
+ }
431
+ decl, owner := find(pkg, name)
432
+ return receiverType(decl, owner, sources, seen)
433
+ }
434
+ return ""
25
435
  }
26
436
 
27
437
  func main() {
@@ -30,7 +440,7 @@ func main() {
30
440
  fmt.Fprintln(os.Stderr, err)
31
441
  os.Exit(1)
32
442
  }
33
- out := result{Problems: []string{}}
443
+ out := result{Problems: []string{}, HTTPRoutes: []httpRoute{}}
34
444
  sdk := map[string]bool{}
35
445
  for _, group := range in.SDK {
36
446
  for method := range group {
@@ -38,50 +448,55 @@ func main() {
38
448
  }
39
449
  }
40
450
  set := token.NewFileSet()
41
- err := filepath.WalkDir(in.Dir, func(path string, d os.DirEntry, err error) error {
451
+ files, err := shippedFiles(in)
452
+ if err != nil {
453
+ fmt.Fprintln(os.Stderr, err)
454
+ os.Exit(1)
455
+ }
456
+ var sources []source
457
+ for path, pkg := range files {
458
+ file, err := parser.ParseFile(set, path, nil, 0)
42
459
  if err != nil {
43
- return err
460
+ fmt.Fprintln(os.Stderr, "invalid shipped Go source")
461
+ os.Exit(1)
44
462
  }
45
- if d.IsDir() {
46
- if path != in.Dir && (strings.HasPrefix(d.Name(), ".") || d.Name() == "vendor" || d.Name() == "node_modules") {
47
- return filepath.SkipDir
48
- }
49
- return nil
50
- }
51
- if d.Type()&os.ModeSymlink != 0 {
52
- target, err := os.Stat(path)
53
- if err == nil && target.IsDir() {
54
- shown, _ := filepath.Rel(in.Dir, path)
55
- out.Problems = append(out.Problems, filepath.ToSlash(shown)+":0: Go source directories cannot be symlinks")
56
- return nil
463
+ imports := map[string]string{}
464
+ for _, spec := range file.Imports {
465
+ module, _ := strconv.Unquote(spec.Path.Value)
466
+ alias := filepath.Base(module)
467
+ if spec.Name != nil {
468
+ alias = spec.Name.Name
57
469
  }
470
+ imports[alias] = module
58
471
  }
59
- if !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
60
- return nil
61
- }
472
+ sources = append(sources, source{file, path, pkg, imports})
473
+ }
474
+ safe := boundaryHandlers(sources)
475
+ for _, src := range sources {
476
+ path, file, imports := src.path, src.file, src.imports
62
477
  shown, _ := filepath.Rel(in.Dir, path)
63
478
  problem := func(pos token.Pos, message string) {
64
479
  out.Problems = append(out.Problems, fmt.Sprintf("%s:%d: %s", filepath.ToSlash(shown), set.Position(pos).Line, message))
65
480
  }
66
- if d.Type()&os.ModeSymlink != 0 {
481
+ d, err := os.Lstat(path)
482
+ if err != nil || d.Mode()&os.ModeSymlink != 0 {
67
483
  problem(token.NoPos, "Go source symlinks cannot be audited")
68
- return nil
484
+ continue
69
485
  }
70
- file, err := parser.ParseFile(set, path, nil, 0)
71
- if err != nil {
72
- return fmt.Errorf("%s: invalid Go source", shown)
486
+ for dir := filepath.Dir(path); dir != filepath.Dir(dir); dir = filepath.Dir(dir) {
487
+ info, err := os.Lstat(dir)
488
+ if err == nil && info.Mode()&os.ModeSymlink != 0 {
489
+ problem(token.NoPos, "Go source directories cannot be symlinks")
490
+ break
491
+ }
492
+ if dir == in.Dir {
493
+ break
494
+ }
73
495
  }
74
- imports := map[string]string{}
75
496
  for _, spec := range file.Imports {
76
- module, _ := strconv.Unquote(spec.Path.Value)
77
- alias := filepath.Base(module)
78
- if spec.Name != nil {
79
- alias = spec.Name.Name
80
- }
81
- if alias == "." {
497
+ if spec.Name != nil && spec.Name.Name == "." {
82
498
  problem(spec.Pos(), "dot imports cannot be audited")
83
499
  }
84
- imports[alias] = module
85
500
  }
86
501
  constants := map[string]string{}
87
502
  direct := map[*ast.SelectorExpr]bool{}
@@ -108,25 +523,47 @@ func main() {
108
523
  }
109
524
  }
110
525
  ast.Inspect(file, func(n ast.Node) bool {
111
- if fn, ok := n.(*ast.FuncDecl); ok && fn.Recv != nil && fn.Name.Name == "ServeHTTP" {
112
- problem(fn.Pos(), "custom ServeHTTP bypasses the SDK registry; use api.HandleRequest, HandleResponse or HandleStream")
526
+ if fn, ok := n.(*ast.FuncDecl); ok && fn.Recv != nil && fn.Name.Name == "ServeHTTP" && !safe[n] {
527
+ problem(fn.Pos(), "unregistered ServeHTTP; use api.HandleHTTP, api.HandleIntegration or ipc.ServeUnix")
113
528
  }
114
529
  selector, ok := n.(*ast.SelectorExpr)
115
530
  if !ok {
116
531
  return true
117
532
  }
533
+ if selector.Sel.Name == "ServeHTTP" && !safe[n] {
534
+ problem(selector.Pos(), "unregistered ServeHTTP dispatch")
535
+ }
536
+ receiver := receiverType(selector.X, src, sources, map[ast.Node]bool{})
537
+ if receiver == "net.ListenConfig" && (selector.Sel.Name == "Listen" || selector.Sel.Name == "ListenPacket") ||
538
+ receiver == "net/http.Server" && (selector.Sel.Name == "Serve" || selector.Sel.Name == "ServeTLS" ||
539
+ selector.Sel.Name == "ListenAndServe" || selector.Sel.Name == "ListenAndServeTLS") {
540
+ problem(selector.Pos(), "custom listener outside SDK boundaries: "+selector.Sel.Name)
541
+ }
118
542
  pkg, ok := selector.X.(*ast.Ident)
119
543
  if !ok {
120
544
  return true
121
545
  }
122
546
  module := imports[pkg.Name]
123
547
  name := selector.Sel.Name
548
+ if module == "net" && (name == "Listen" || name == "ListenTCP" || name == "ListenUnix" ||
549
+ name == "ListenPacket" || name == "ListenUDP" || name == "ListenIP" || name == "ListenUnixgram" ||
550
+ name == "FileListener" || name == "FilePacketConn") ||
551
+ module == "crypto/tls" && name == "Listen" ||
552
+ (module == "syscall" || module == "golang.org/x/sys/unix") && (name == "Bind" || name == "Listen" || name == "Socket") {
553
+ problem(selector.Pos(), "custom listener outside SDK boundaries: "+name)
554
+ }
555
+ if module == sdkModule+"/ipc" && !direct[selector] {
556
+ switch name {
557
+ case "ServeUnix", "StartUnix", "ServePeerUnix", "DialUnix", "DialPeerUnix", "ReserveLoopback", "ProbeLoopback":
558
+ problem(selector.Pos(), "IPC constructor aliases cannot be audited")
559
+ }
560
+ }
124
561
  if module == "github.com/synopackageland/syno-pkg-land-sdk/idgateway" && (name == "NewFront" || name == "Build" || name == "Run") {
125
562
  problem(selector.Pos(), "gateway listeners must use identity_gateway in the manifest; upstream_auth_paths never exempts SDK RPC or custom HTTP handlers")
126
563
  }
127
564
  if module == "github.com/synopackageland/syno-pkg-land-sdk/api" {
128
565
  switch name {
129
- case "Handle", "HandleRequest", "HandleResponse", "HandleStream", "HandleRequestBatch":
566
+ case "Handle", "HandleRequest", "HandleResponse", "HandleStream", "HandleRequestBatch", "HandleHTTP", "HandleIntegration":
130
567
  if !direct[selector] {
131
568
  problem(selector.Pos(), "route constructor aliases cannot be audited; register methods directly")
132
569
  }
@@ -134,8 +571,20 @@ func main() {
134
571
  }
135
572
  if module == "net/http" {
136
573
  switch name {
137
- case "Handle", "HandleFunc", "HandlerFunc", "NewServeMux", "Server", "Serve", "ServeTLS", "ListenAndServe", "ListenAndServeTLS":
574
+ case "Handle", "HandleFunc", "Serve", "ServeTLS", "ListenAndServe", "ListenAndServeTLS":
138
575
  problem(selector.Pos(), "HTTP handler outside the SDK registry: "+name)
576
+ case "HandlerFunc", "NewServeMux", "ServeMux", "FileServer", "FileServerFS", "StripPrefix", "TimeoutHandler", "NotFoundHandler", "RedirectHandler":
577
+ if !safe[n] {
578
+ problem(selector.Pos(), "HTTP handler outside the SDK registry: "+name)
579
+ }
580
+ }
581
+ }
582
+ if module == "net/http/httputil" && (name == "ReverseProxy" || name == "NewSingleHostReverseProxy") && !safe[n] {
583
+ problem(selector.Pos(), "HTTP proxy outside the SDK registry")
584
+ }
585
+ if module == "net/http/cgi" && name == "Serve" {
586
+ if !direct[selector] {
587
+ problem(selector.Pos(), "CGI constructor aliases cannot be audited")
139
588
  }
140
589
  }
141
590
  return true
@@ -149,11 +598,34 @@ func main() {
149
598
  if !ok {
150
599
  return true
151
600
  }
601
+ // An in-process administrator call names one fixed route that
602
+ // already requires DSM administrators; the SDK verifies the account.
603
+ if selector.Sel.Name == "InvokeAsAdministrator" {
604
+ method := ""
605
+ if len(call.Args) == 4 {
606
+ switch arg := call.Args[2].(type) {
607
+ case *ast.BasicLit:
608
+ if arg.Kind == token.STRING {
609
+ method, _ = strconv.Unquote(arg.Value)
610
+ }
611
+ case *ast.Ident:
612
+ method = constants[arg.Name]
613
+ }
614
+ }
615
+ if method == "" {
616
+ problem(call.Pos(), "dynamic InvokeAsAdministrator method cannot be audited; name a literal method")
617
+ } else if in.Permissions[method] != "administrators" {
618
+ problem(call.Pos(), "InvokeAsAdministrator requires a method declared administrators in permissions.json: "+method)
619
+ }
620
+ }
152
621
  pkg, ok := selector.X.(*ast.Ident)
153
622
  if !ok {
154
623
  return true
155
624
  }
156
625
  module := imports[pkg.Name]
626
+ if module == "net/http/cgi" && selector.Sel.Name == "Serve" && len(call.Args) > 0 && reusesBoundary(call.Args[0], src, sources, safe, map[ast.Node]bool{}) {
627
+ problem(call.Pos(), "private or raw HTTP handler reused outside its SDK boundary")
628
+ }
157
629
  if module == "github.com/synopackageland/syno-pkg-land-sdk/dsm" && selector.Sel.Name == "NewEntryGate" {
158
630
  out.EntryGate = true
159
631
  }
@@ -161,13 +633,17 @@ func main() {
161
633
  return true
162
634
  }
163
635
  switch selector.Sel.Name {
164
- case "Handle", "HandleRequest", "HandleResponse", "HandleStream", "HandleRequestBatch":
636
+ case "Handle", "HandleRequest", "HandleResponse", "HandleStream", "HandleRequestBatch", "HandleHTTP", "HandleIntegration":
165
637
  default:
166
638
  return true
167
639
  }
168
640
  method := ""
169
- if len(call.Args) > 0 {
170
- switch arg := call.Args[0].(type) {
641
+ index := 0
642
+ if selector.Sel.Name == "HandleHTTP" {
643
+ index = 1
644
+ }
645
+ if len(call.Args) > index {
646
+ switch arg := call.Args[index].(type) {
171
647
  case *ast.BasicLit:
172
648
  if arg.Kind == token.STRING {
173
649
  method, _ = strconv.Unquote(arg.Value)
@@ -181,14 +657,42 @@ func main() {
181
657
  } else if _, declared := in.Permissions[method]; !declared && !sdk[method] {
182
658
  problem(call.Pos(), "registered method missing from permissions.json: "+method)
183
659
  }
660
+ // Only HandleIntegration skips the DSM session; the permission
661
+ // value and the constructor must agree in both directions.
662
+ if (selector.Sel.Name == "HandleIntegration") != (in.Permissions[method] == "integration") {
663
+ problem(call.Pos(), "the integration permission requires api.HandleIntegration, and api.HandleIntegration requires the integration permission")
664
+ }
665
+ if selector.Sel.Name == "HandleIntegration" && method != "" {
666
+ out.HTTPRoutes = append(out.HTTPRoutes, httpRoute{"?method=" + method, method})
667
+ }
668
+ if selector.Sel.Name == "HandleHTTP" {
669
+ prefix := ""
670
+ if len(call.Args) > 0 {
671
+ prefix = stringLiteral(call.Args[0])
672
+ }
673
+ if !regexp.MustCompile(`^/([A-Za-z0-9_-]+/)+$`).MatchString(prefix) {
674
+ problem(call.Pos(), "HTTP prefix must be a fixed literal full-segment path ending in /")
675
+ }
676
+ for _, reserved := range []string{"/_sso/", "/_spl/", "/api/", "/api.cgi/", "/rpc/", "/webman/", "/cgi/"} {
677
+ if strings.HasPrefix(prefix, reserved) || strings.HasPrefix(reserved, prefix) {
678
+ problem(call.Pos(), "HTTP prefix overlaps a reserved path")
679
+ }
680
+ }
681
+ if sdk[method] || in.Permissions[method] != "authenticated" && in.Permissions[method] != "administrators" {
682
+ problem(call.Pos(), "HTTP routes require an explicit App authenticated or administrators permission")
683
+ }
684
+ for _, other := range out.HTTPRoutes {
685
+ if strings.HasPrefix(prefix, other.Prefix) || strings.HasPrefix(other.Prefix, prefix) {
686
+ problem(call.Pos(), "HTTP prefixes must not overlap")
687
+ }
688
+ }
689
+ out.HTTPRoutes = append(out.HTTPRoutes, httpRoute{prefix, method})
690
+ }
184
691
  return true
185
692
  })
186
- return nil
187
- })
188
- if err != nil {
189
- fmt.Fprintln(os.Stderr, err)
190
- os.Exit(1)
191
693
  }
694
+ sort.Strings(out.Problems)
695
+ sort.Slice(out.HTTPRoutes, func(i, j int) bool { return out.HTTPRoutes[i].Prefix < out.HTTPRoutes[j].Prefix })
192
696
  if err := json.NewEncoder(os.Stdout).Encode(out); err != nil {
193
697
  os.Exit(1)
194
698
  }