-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathvalidations.go
More file actions
254 lines (202 loc) · 6.63 KB
/
validations.go
File metadata and controls
254 lines (202 loc) · 6.63 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
package publiccode
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"image"
"image/png"
"math/rand"
"net/url"
"os"
"path"
"path/filepath"
"regexp"
"runtime"
"slices"
"strings"
"github.com/alranel/go-vcsurl/v2"
"github.com/italia/publiccode-parser-go/v5/data"
netutil "github.com/italia/publiccode-parser-go/v5/internal"
)
func init() {
image.RegisterFormat("png", "png", png.Decode, png.DecodeConfig)
}
var errMissingURLScheme = errors.New("missing URL scheme")
var oembedSchemes []*regexp.Regexp
func init() {
var schemes []string
if err := json.Unmarshal(data.OembedSchemes, &schemes); err != nil {
panic("failed to parse oEmbed schemes: " + err.Error()) //nolint:forbidigo,lll // embedded at compile time, a failure here is a programming error
}
for _, scheme := range schemes {
pattern := "^" + regexp.QuoteMeta(scheme) + "$"
pattern = strings.ReplaceAll(pattern, `\*`, `.*`)
oembedSchemes = append(oembedSchemes, regexp.MustCompile(pattern))
}
}
func getBasicAuth(domain Domain) string {
if len(domain.BasicAuth) > 0 {
auth := domain.BasicAuth[rand.Intn(len(domain.BasicAuth))] //nolint:gosec,lll // G404: not security-sensitive, picks from pre-configured list
return "Basic " + base64.StdEncoding.EncodeToString([]byte(auth))
}
return ""
}
func isHostInDomain(domain Domain, u string) bool {
if len(domain.UseTokenFor) == 0 {
return false
}
urlP, err := url.Parse(u)
if err != nil {
return false
}
return slices.Contains(domain.UseTokenFor, urlP.Host)
}
func getHeaderFromDomain(domain Domain, url string) map[string]string {
if !isHostInDomain(domain, url) {
return nil
}
// Set BasicAuth header
headers := make(map[string]string)
headers["Authorization"] = getBasicAuth(domain)
return headers
}
// isReachable checks whether the URL resource is reachable.
// An URL resource is reachable if it returns HTTP 200.
func (p *Parser) isReachable(u url.URL) (bool, error) {
// Don't check if we are running in WASM because we'd most likely
// fail due to CORS errors.
if runtime.GOARCH == "wasm" {
return true, nil
}
if u.Scheme == "" {
return false, errMissingURLScheme
}
_, err := p.httpclient.GetURL(u.String(), getHeaderFromDomain(p.domain, u.String()))
if err != nil {
return false, fmt.Errorf("HTTP GET failed for %s: %w", u.String(), err)
}
return true, nil
}
// toAbsoluteURL turns the passed string into an URL, trying to resolve
// code hosting URLs to their raw URL.
//
// It supports relative paths and turns them into remote URLs or file:// URLs
// depending on the value of baseURL.
func toAbsoluteURL(file string, baseURL *url.URL, network bool) *url.URL {
// Check if file is an absolute URL
if uri, err := url.ParseRequestURI(file); err == nil {
if !network {
return nil
}
// this uses the network to detect the git branch
if raw := vcsurl.GetRawFile(uri); raw != nil {
return raw
}
return uri
}
// We always pass the computed base URL here, with a fallback to the cwd,
// so baseURL won't be nil. It'd be a programming mistake in case it is, so
// it'll be good to crash here.
u := *baseURL
u.Path = path.Join(u.Path, file)
return &u
}
// fileExists returns true if the file resource exists.
func (p *Parser) fileExists(u url.URL, network bool) (bool, error) {
// Don't check if we are running in WASM because there's no stat(2) there
if runtime.GOARCH == "wasm" {
return true, nil
}
// If we have an absolute local path, perform validation on it, otherwise do it
// on the remote URL if any. If none are available, validation is skipped.
if u.Scheme == "file" {
_, err := os.Stat(u.Path) //nolint:gosec // G703: path is from a validated file:// URL
if err != nil {
err = fmt.Errorf("no such file: %s", netutil.DisplayURL(&u)) //nolint:err113 // dynamic message with path context
}
return err == nil, err
}
if network {
reachable, err := p.isReachable(u)
return reachable, err
}
return true, nil
}
// isImageFile check whether the string is a valid image. It also checks if the file exists.
// It returns true if it is an image or false if it's not and an error, if any.
func (p *Parser) isImageFile(u url.URL, network bool) (bool, error) {
validExt := []string{".jpg", ".png"}
ext := strings.ToLower(filepath.Ext(u.Path))
if !slices.Contains(validExt, ext) {
return false, fmt.Errorf("invalid file extension for: %s", netutil.DisplayURL(&u)) //nolint:err113,lll // dynamic message with path context
}
return p.fileExists(u, network)
}
// validLogo returns true if the file path in value is a valid logo.
// It also checks if the file exists.
func (p *Parser) validLogo(u url.URL, network bool) (bool, error) {
validExt := []string{".svg", ".svgz", ".png"}
ext := strings.ToLower(filepath.Ext(u.Path))
// Check for valid extension.
if !slices.Contains(validExt, ext) {
return false, fmt.Errorf("invalid file extension for: %s", netutil.DisplayURL(&u)) //nolint:err113,lll // dynamic message with path context
}
if exists, err := p.fileExists(u, network); !exists {
return false, err
}
var localPath string
// Remote. Create a temp dir, download and check the file. Remove the temp dir.
if u.Scheme != "file" {
var err error
if !network {
return true, nil
}
localPath, err = netutil.DownloadTmpFile(p.httpclient, &u, getHeaderFromDomain(p.domain, u.String()))
if err != nil {
return false, fmt.Errorf("downloading %s: %w", u.String(), err)
}
defer func() {
if localPath == "" {
return
}
if err := os.Remove(localPath); err != nil { //nolint:gosec // G703: path from validated download
fmt.Fprintf(os.Stderr, "failed to remove %s: %v\n", localPath, err)
}
dir := path.Dir(localPath)
if err = os.Remove(dir); err != nil { //nolint:gosec // G703: path from validated download
fmt.Fprintf(os.Stderr, "failed to remove %s: %v\n", dir, err)
}
}()
} else {
localPath = u.Path
}
if ext == ".png" {
f, err := os.Open(localPath) //nolint:gosec // G703: path from validated download
if err != nil {
return false, fmt.Errorf("opening %s: %w", localPath, err)
}
defer f.Close()
if _, _, err := image.DecodeConfig(f); err != nil {
return false, fmt.Errorf("%w", err)
}
}
return true, nil
}
func matchesOembedScheme(link string) bool {
for _, re := range oembedSchemes {
if re.MatchString(link) {
return true
}
}
return false
}
// checkOEmbedURL returns whether the link is from a valid oEmbed provider.
// Reference: https://oembed.com/providers.json
func (p *Parser) checkOEmbedURL(url *url.URL) error {
link := url.String()
if !matchesOembedScheme(link) {
return fmt.Errorf("invalid oEmbed link: %s", link) //nolint:err113 // dynamic message with URL context
}
return nil
}