File: utils.go

package info (click to toggle)
distrobuilder 3.2-2
  • links: PTS, VCS
  • area: main
  • in suites: trixie
  • size: 1,384 kB
  • sloc: sh: 204; makefile: 75
file content (333 lines) | stat: -rw-r--r-- 7,846 bytes parent folder | download | duplicates (3)
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
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
package sources

import (
	"bufio"
	"bytes"
	"context"
	"errors"
	"fmt"
	"hash"
	"io"
	"net/http"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"slices"
	"strings"

	incus "github.com/lxc/incus/v6/shared/util"
)

// downloadChecksum downloads or opens URL, and matches fname against the
// checksums inside of the downloaded or opened file.
func downloadChecksum(ctx context.Context, client *http.Client, targetDir string, URL string, fname string, hashFunc hash.Hash, hashLen int) ([]string, error) {
	var (
		tempFile *os.File
		err      error
	)

	// do not re-download checksum file if it's already present
	fi, err := os.Stat(filepath.Join(targetDir, URL))
	if err == nil && !fi.IsDir() {
		tempFile, err = os.Open(filepath.Join(targetDir, URL))
		if err != nil {
			return nil, err
		}

		defer os.Remove(tempFile.Name())
	} else {
		tempFile, err = os.CreateTemp(targetDir, "hash.")
		if err != nil {
			return nil, err
		}

		defer os.Remove(tempFile.Name())

		done := make(chan struct{})
		defer close(done)

		_, err = incus.DownloadFileHash(ctx, client, "distrobuilder", nil, nil, "", URL, "", hashFunc, tempFile)
		// ignore hash mismatch
		if err != nil && !strings.HasPrefix(err.Error(), "Hash mismatch") {
			return nil, err
		}
	}

	_, err = tempFile.Seek(0, 0)
	if err != nil {
		return nil, fmt.Errorf("Failed setting offset in file %q: %w", tempFile.Name(), err)
	}

	checksum := getChecksum(filepath.Base(fname), hashLen, tempFile)
	if checksum != nil {
		return checksum, nil
	}

	return nil, errors.New("Could not find checksum")
}

func getChecksum(fname string, hashLen int, r io.Reader) []string {
	scanner := bufio.NewScanner(r)

	var matches []string
	var result []string

	regex := regexp.MustCompile("[[:xdigit:]]+")

	for scanner.Scan() {
		if !strings.Contains(scanner.Text(), fname) {
			continue
		}

		for _, s := range strings.Split(scanner.Text(), " ") {
			if !regex.MatchString(s) {
				continue
			}

			if hashLen == 0 || hashLen == len(strings.TrimSpace(s)) {
				matches = append(matches, scanner.Text())
			}
		}
	}

	// Check common checksum file (pattern: "<hash> <filename>") with the exact filename
	for _, m := range matches {
		fields := strings.Split(m, " ")

		if strings.TrimSpace(fields[len(fields)-1]) == fname {
			result = append(result, strings.TrimSpace(fields[0]))
		}
	}

	if len(result) > 0 {
		return result
	}

	// Check common checksum file (pattern: "<hash> <filename>") which contains the filename
	for _, m := range matches {
		fields := strings.Split(m, " ")

		if strings.Contains(strings.TrimSpace(fields[len(fields)-1]), fname) {
			result = append(result, strings.TrimSpace(fields[0]))
		}
	}

	if len(result) > 0 {
		return result
	}

	// Special case: CentOS
	for _, m := range matches {
		for _, s := range strings.Split(m, " ") {
			if !regex.MatchString(s) {
				continue
			}

			if hashLen == 0 || hashLen == len(strings.TrimSpace(s)) {
				result = append(result, s)
			}
		}
	}

	if len(result) > 0 {
		return result
	}

	return nil
}

func gpgCommandContext(ctx context.Context, gpgDir string, args ...string) (cmd *exec.Cmd) {
	cmd = exec.CommandContext(ctx, "gpg", append([]string{"--homedir", gpgDir}, args...)...)
	cmd.Env = append(os.Environ(), "LANG=C.UTF-8")
	return
}

func showFingerprint(ctx context.Context, gpgDir string, publicKey string) (fingerprint string, err error) {
	cmd := gpgCommandContext(ctx, gpgDir, "--show-keys")
	cmd.Stdin = strings.NewReader(publicKey)
	var stdout bytes.Buffer
	cmd.Stdout = &stdout
	cmd.Stderr = &stdout
	err = cmd.Run()
	if err != nil {
		err = fmt.Errorf("%s", stdout.String())
		return
	}

	lines := strings.Split(strings.TrimSpace(stdout.String()), "\n")
	for _, line := range lines {
		line = strings.TrimSpace(line)
		if notFingerprint(line) {
			continue
		}

		fingerprint = line
		return
	}

	if fingerprint == "" {
		err = fmt.Errorf("failed to get fingerprint from public key: %s, %v", publicKey, lines)
		return
	}

	return
}

func notFingerprint(line string) bool {
	return len(line) != 40 ||
		strings.HasPrefix(line, "/") ||
		strings.HasPrefix(line, "-") ||
		strings.HasPrefix(line, "pub") ||
		strings.HasPrefix(line, "sub") ||
		strings.HasPrefix(line, "uid")
}

func listFingerprints(ctx context.Context, gpgDir string) (fingerprints []string, err error) {
	cmd := gpgCommandContext(ctx, gpgDir, "--list-keys")
	var buffer bytes.Buffer
	cmd.Stdout = &buffer
	err = cmd.Run()
	if err != nil {
		return
	}

	lines := strings.Split(buffer.String(), "\n")
	for _, line := range lines {
		line = strings.TrimSpace(line)
		if notFingerprint(line) {
			continue
		}

		fingerprints = append(fingerprints, line)
	}

	return
}

func importPublicKeys(ctx context.Context, gpgDir string, publicKeys []string) (err error) {
	fingerprints := make([]string, len(publicKeys))
	for i, f := range publicKeys {
		fingerprints[i], err = showFingerprint(ctx, gpgDir, f)
		if err != nil {
			return
		}

		cmd := gpgCommandContext(ctx, gpgDir, "--import")
		cmd.Stdin = strings.NewReader(f)

		var buffer bytes.Buffer
		cmd.Stderr = &buffer
		err = cmd.Run()
		if err != nil {
			err = fmt.Errorf("failed to run: %s: %s",
				strings.Join(cmd.Args, " "), strings.TrimSpace(buffer.String()))
			return
		}
	}

	importedFingerprints, err := listFingerprints(ctx, gpgDir)
	if err != nil {
		return
	}

	for i, fingerprint := range fingerprints {
		if !slices.Contains(importedFingerprints, fingerprint) {
			err = fmt.Errorf("fingerprint %s of publickey %s not imported", fingerprint, publicKeys[i])
			return
		}
	}
	return
}

func recvFingerprints(ctx context.Context, gpgDir string, keyserver string, fingerprints []string) (err error) {
	args := []string{}
	if keyserver != "" {
		args = append(args, "--keyserver", keyserver)
		httpProxy := getEnvHttpProxy()
		if httpProxy != "" {
			args = append(args, "--keyserver-options",
				fmt.Sprintf("http-proxy=%s", httpProxy))
		}
	}

	args = append(args, "--recv-keys")
	cmd := gpgCommandContext(ctx, gpgDir, append(args, fingerprints...)...)
	var buffer bytes.Buffer
	cmd.Stderr = &buffer

	err = cmd.Run()
	if err != nil {
		err = fmt.Errorf("Failed to run: %s: %s", strings.Join(cmd.Args, " "), strings.TrimSpace(buffer.String()))
		return
	}

	// Verify output
	var importedKeys []string
	var missingKeys []string
	lines := strings.Split(buffer.String(), "\n")

	for _, l := range lines {
		if strings.HasPrefix(l, "gpg: key ") && (strings.HasSuffix(l, " imported") || strings.HasSuffix(l, " not changed")) {
			key := strings.Split(l, " ")
			importedKeys = append(importedKeys, strings.Split(key[2], ":")[0])
		}
	}

	// Figure out which key(s) couldn't be imported
	if len(importedKeys) < len(fingerprints) {
		for _, j := range fingerprints {
			found := false
			for _, k := range importedKeys {
				if strings.HasSuffix(j, k) {
					found = true
				}
			}

			if !found {
				missingKeys = append(missingKeys, j)
			}
		}
		err = fmt.Errorf("Failed to import keys: %s", strings.Join(missingKeys, " "))
	}

	return
}

func recvGPGKeys(ctx context.Context, gpgDir string, keyserver string, keys []string) (bool, error) {
	var fingerprints []string
	var publicKeys []string

	for _, k := range keys {
		if strings.HasPrefix(strings.TrimSpace(k), "-----BEGIN PGP PUBLIC KEY BLOCK-----") {
			publicKeys = append(publicKeys, strings.TrimSpace(k))
		} else {
			fingerprints = append(fingerprints, strings.TrimSpace(k))
		}
	}

	err := importPublicKeys(ctx, gpgDir, publicKeys)
	if err != nil {
		return false, err
	}

	err = recvFingerprints(ctx, gpgDir, keyserver, fingerprints)
	if err != nil {
		return false, err
	}

	return true, nil
}

func getEnvHttpProxy() (httpProxy string) {
	for _, key := range []string{
		"http_proxy",
		"HTTP_PROXY", "https_proxy", "HTTPS_PROXY",
	} {
		if httpProxy = os.Getenv(key); httpProxy != "" {
			return
		}
	}

	return
}