clone

an offline map explorer where each world, from a game map to a city's open data, ships as one .atlas file

feat/atlas-next-horizon
atlas / tools / depcheck / netconfine.go
netconfine.go 3.9K
1package main23import (4 "go/ast"5 "go/token"6 "go/types"7 "strconv"8 "strings"910 "golang.org/x/tools/go/analysis"11 "golang.org/x/tools/go/analysis/passes/inspect"12 "golang.org/x/tools/go/ast/inspector"13)1415// netConfine is the pipeline-side face of the offline invariant: a bundle16// serves with zero network, forever, because only one package ever reaches17// outward (issue #5 §2, §9). Fetching is crawling — even the national18// enricher's evidence is captured in generate/crawl and travels in the19// archive.20var netConfine = &analysis.Analyzer{21 Name: "netconfine",22 Doc: `confine outbound HTTP to generate/crawl (issue #5 §9)2324Two checks. At import level, the format and pipeline lanes may not import25net/http at all outside generate/crawl. At use level, the outbound half of26net/http — the client constructors and package-level convenience calls — is27reported anywhere outside generate/crawl, so the app and the workbench may28serve HTTP without being able to fetch it.`,29 Requires: []*analysis.Analyzer{inspect.Analyzer},30 Run: runNetConfine,31}3233// crawlPackage is the one lane member allowed to touch the network.34const crawlPackage = "internal/generate/crawl"3536// outbound names the net/http and net identifiers that reach out. The server37// half of net/http (Handler, ServeMux, Server, Error, StatusOK…) is absent by38// design: the application is an http.Handler and must stay able to say so.39var outbound = map[string]map[string]bool{40 "net/http": {41 "Get": true, "Head": true, "Post": true, "PostForm": true,42 "Client": true, "Transport": true,43 "DefaultClient": true, "DefaultTransport": true,44 "NewRequest": true, "NewRequestWithContext": true,45 "ProxyFromEnvironment": true, "ReadResponse": true,46 },47 "net": {48 "Dial": true, "DialTimeout": true, "Dialer": true,49 },50}5152func runNetConfine(pass *analysis.Pass) (any, error) {53 if testBinary(pass) {54 return nil, nil55 }56 fromRel, ok := rel(packagePath(pass))57 if !ok {58 return nil, nil59 }60 from := laneOf(fromRel)61 if from == LaneOutside || from == LaneTests || under(fromRel, crawlPackage) {62 return nil, nil63 }6465 r := newReporter(pass)6667 // Import level: in the lanes that produce and read bundles, the networking68 // stack has no business being linked at all.69 if from == LaneFormat || from == LaneGenerate || from == LaneEnrich {70 for _, f := range pass.Files {71 for _, spec := range f.Imports {72 path, err := strconv.Unquote(spec.Path.Value)73 if err != nil || !isHTTPStack(path) {74 continue75 }76 r.reportf(importPos(spec), contractf(77 string(from)+" must not import "+quote(path)+"; outbound HTTP lives in "+crawlPackage,78 "9",79 "offline purity is a format invariant: capture is the only network-touching step, so a policy change replays over archived captures instead of recrawling, and no payload can ever carry a runtime URL",80 ))81 }82 }83 }8485 // Use level: the hosts may serve HTTP; they may not fetch it.86 insp, _ := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)87 if insp == nil {88 return nil, nil89 }90 insp.Preorder([]ast.Node{(*ast.SelectorExpr)(nil)}, func(n ast.Node) {91 node := n.(*ast.SelectorExpr)92 if isTestFile(pass, node.Pos()) {93 return // httptest wiring is not an outbound call94 }95 ident, ok := node.X.(*ast.Ident)96 if !ok {97 return98 }99 pkgName, ok := pass.TypesInfo.Uses[ident].(*types.PkgName)100 if !ok {101 return102 }103 path := pkgName.Imported().Path()104 if !outbound[path][node.Sel.Name] {105 return106 }107 r.reportf(node.Pos(), contractf(108 "outbound HTTP ("+path+"."+node.Sel.Name+") is confined to "+crawlPackage,109 "9",110 "the application serves the /data plane from bytes it already has; anything that fetches belongs to capture, where politeness, content addressing and the archive live",111 ))112 })113 return nil, nil114}115116func isHTTPStack(path string) bool {117 return path == "net/http" || strings.HasPrefix(path, "net/http/")118}119120func isTestFile(pass *analysis.Pass, pos token.Pos) bool {121 return strings.HasSuffix(pass.Fset.Position(pos).Filename, "_test.go")122}