@marcel91200 / caddy Mirror · read-only
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 11, 01:00 UTC

master
go 209 lines 7.17 KB
Raw
1 // Copyright 2015 Matthew Holt and The Caddy Authors
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14
15 package caddyfile
16
17 import (
18 "fmt"
19 "io"
20 "os"
21 "path/filepath"
22
23 "github.com/caddyserver/caddy/v2"
24 )
25
26 type importedFile struct {
27 path string
28 content []byte
29 info os.FileInfo
30 }
31
32 // discoverImportedFileContents returns files reached by imports which the parser
33 // actually executes, in discovery order and excluding rootFile. Driving the real
34 // parser is important here: import behavior depends on declaration order, snippet
35 // expansion, import arguments, block substitutions, environment expansion, and
36 // whether an import appears in active configuration. Reimplementing those rules
37 // in a separate token scanner caused FormatImports to disagree with adaptation.
38 //
39 // The parser calls importObserver after opening and reading each imported file,
40 // but before inserting its tokens. The observer retains those exact bytes for
41 // formatting so the file is not reopened later. Each physical file is retained
42 // once, but repeated imports still expand with their own arguments. Only imports
43 // of an active physical ancestor are skipped, preventing symlink cycles.
44 // os.FileInfo values are retained to verify that each canonical pathname still
45 // identifies the file whose contents were parsed.
46 func discoverImportedFileContents(rootFile string, rootInput []byte, rootInfo os.FileInfo) ([]importedFile, error) {
47 // Compare file identity rather than path spelling: lexical paths do not catch
48 // imports such as dir/link/dir/link/Caddyfile through a symlink cycle.
49 seen := []os.FileInfo{rootInfo}
50 var files []importedFile
51
52 // allTokens performs environment replacement in-place, so preserve the bytes
53 // which FormatImports will subsequently pass to the formatter.
54 inputCopy := append([]byte(nil), rootInput...)
55 tokens, err := allTokens(rootFile, inputCopy)
56 if err != nil {
57 return nil, err
58 }
59 rootAncestry := &importAncestor{info: rootInfo}
60 for i := range tokens {
61 tokens[i].importAncestry = rootAncestry
62 }
63 // Construct the parser exactly as parse() does. The import graph remains
64 // responsible for logical snippet/import cycles; the observer below adds
65 // physical-file identity handling for symlink aliases.
66 p := parser{
67 Dispenser: NewDispenser(tokens),
68 importGraph: importGraph{
69 nodes: make(map[string]struct{}),
70 edges: make(adjacency),
71 },
72 }
73 p.importObserver = func(path string, info os.FileInfo, content []byte, ancestry *importAncestor) (bool, error) {
74 for ancestor := ancestry; ancestor != nil; ancestor = ancestor.parent {
75 if os.SameFile(ancestor.info, info) {
76 return true, nil
77 }
78 }
79 // Deduplicate output without suppressing argument-dependent descendants
80 // or imports resolved relative to a different lexical alias.
81 for _, previous := range seen {
82 if os.SameFile(previous, info) {
83 return false, nil
84 }
85 }
86
87 // Store one stable, absolute spelling for callers, but verify it still
88 // resolves to the descriptor which supplied content before retaining it.
89 canonical, err := canonicalPath(path)
90 if err != nil {
91 return false, err
92 }
93 currentInfo, err := os.Stat(canonical)
94 if err != nil {
95 return false, err
96 }
97 if !os.SameFile(info, currentInfo) {
98 return false, fmt.Errorf("imported file changed while being read")
99 }
100
101 seen = append(seen, info)
102 files = append(files, importedFile{
103 path: canonical,
104 // The parser owns its read buffer; keep an independent copy for the
105 // formatting pass and avoid a second pathname-based read.
106 content: append([]byte(nil), content...),
107 info: info,
108 })
109 return false, nil
110 }
111
112 if _, err := p.parseAll(); err != nil {
113 return nil, err
114 }
115 return files, nil
116 }
117
118 // canonicalPath returns an absolute path with symlinks resolved. Canonical paths
119 // make the FormattedFile API consistent for the root and every imported file;
120 // physical deduplication still uses os.SameFile because spelling alone is not a
121 // portable file-identity test.
122 func canonicalPath(path string) (string, error) {
123 abs, err := caddy.FastAbs(path)
124 if err != nil {
125 return "", err
126 }
127 return filepath.EvalSymlinks(abs)
128 }
129
130 // FormattedFile is a file path paired with its formatted contents.
131 type FormattedFile struct {
132 Path string
133 Content []byte
134 }
135
136 // FormatImports reads filename, formats it (honoring opts), then discovers all
137 // files reached by active import directives and formats each one independently
138 // at nesting-0 baseline. WrapUnbracedSite is always disabled for imported files
139 // because they are fragments, not standalone Caddyfiles.
140 //
141 // All returned paths are absolute, symlink-resolved paths. The root file is
142 // always first, followed by imported files in parser discovery order.
143 func FormatImports(filename string, opts FormatOptions) ([]FormattedFile, error) {
144 // Keep the lexical absolute path for parser-relative import resolution, while
145 // using the canonical path for opening and returning the root file. This
146 // matches adaptation when the supplied Caddyfile itself is reached by symlink.
147 rootLexicalPath, err := caddy.FastAbs(filename)
148 if err != nil {
149 return nil, err
150 }
151 rootPath, err := canonicalPath(filename)
152 if err != nil {
153 return nil, err
154 }
155 // Reading and statting one open descriptor binds rootBytes and rootInfo to the
156 // same physical file. Separate ReadFile and Stat calls would permit a pathname
157 // replacement between the two operations.
158 root, err := os.Open(rootPath)
159 if err != nil {
160 return nil, err
161 }
162 rootInfo, err := root.Stat()
163 if err != nil {
164 root.Close()
165 return nil, err
166 }
167 rootBytes, err := io.ReadAll(root)
168 closeErr := root.Close()
169 if err != nil {
170 return nil, err
171 }
172 if closeErr != nil {
173 return nil, closeErr
174 }
175
176 discovered, err := discoverImportedFileContents(rootLexicalPath, rootBytes, rootInfo)
177 if err != nil {
178 return nil, err
179 }
180
181 results := make([]FormattedFile, 0, 1+len(discovered))
182 results = append(results, FormattedFile{Path: rootPath, Content: FormatWithOptions(rootBytes, opts)})
183 for _, file := range discovered {
184 results = append(results, FormattedFile{
185 Path: file.path,
186 Content: FormatWithOptions(file.content, FormatOptions{}),
187 })
188 }
189
190 // Formatting can take long enough for a pathname to be replaced. Before
191 // returning writable paths to the caller, ensure every one still identifies
192 // the physical file whose bytes were parsed and formatted. The command layer
193 // performs another check immediately before atomic replacement.
194 for i, result := range results {
195 currentInfo, err := os.Stat(result.Path)
196 if err != nil {
197 return nil, err
198 }
199 originalInfo := rootInfo
200 if i > 0 {
201 originalInfo = discovered[i-1].info
202 }
203 if !os.SameFile(originalInfo, currentInfo) {
204 return nil, fmt.Errorf("file changed while formatting: %s", result.Path)
205 }
206 }
207
208 return results, nil
209 }