Skip to content

Commit 8f64b5b

Browse files
committed
add OS and architecture overrides; refactor asset handling and regex patterns
1 parent c52940f commit 8f64b5b

9 files changed

Lines changed: 216 additions & 67 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ curl https://i.jpillora.com/<query>! | bash
7474
* `type=homebrew` is **not** working at the moment – see [Homebrew](#homebrew)
7575
* `?insecure=1` Force `curl`/`wget` to skip certificate checks
7676
* `?as=` Force the binary to be named as this parameter value
77+
* `?os=` Explicit set OS (ignore system OS)
78+
* `?arch=` Explicit set architecture (ignore system arch)
7779

7880
## Security
7981

handler/handler.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ type Query struct {
3030
User, Program, Release string
3131
AsProgram, Select string
3232
MoveToPath, Search, Insecure bool
33-
SudoMove bool // deprecated: not used, now automatically detected
33+
SudoMove bool // deprecated: not used, now automatically detected
34+
OS, Arch string // override OS and Arch
3435
}
3536

3637
type Result struct {
@@ -94,6 +95,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
9495
Insecure: r.URL.Query().Get("insecure") == "1",
9596
AsProgram: r.URL.Query().Get("as"),
9697
Select: r.URL.Query().Get("select"),
98+
OS: r.URL.Query().Get("os"),
99+
Arch: r.URL.Query().Get("arch"),
97100
}
98101
// set query from route
99102
path := strings.TrimPrefix(r.URL.Path, "/")

handler/handler_execute.go

Lines changed: 79 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -111,11 +111,16 @@ func (h *Handler) getAssetsNoCache(q Query) (string, Assets, error) {
111111
if l := len(sumIndex); l > 0 {
112112
log.Printf("fetched %d asset shasums", l)
113113
}
114-
index := map[string]Asset{}
114+
115+
var (
116+
candidates = map[string]Asset{}
117+
index = map[string]Asset{}
118+
foundLinuxAMD64 = false
119+
)
115120
for _, ga := range ghas {
116121
url := ga.BrowserDownloadURL
117-
//only binary containers are supported
118-
//TODO deb,rpm etc
122+
// only binary containers are supported
123+
// TODO deb,rpm etc
119124
fext := getFileExt(url)
120125
if fext == "" && ga.Size > 1024*1024 {
121126
fext = ".bin" // +1MB binary
@@ -127,21 +132,38 @@ func (h *Handler) getAssetsNoCache(q Query) (string, Assets, error) {
127132
log.Printf("fetched asset has unsupported file type: %s (ext '%s')", ga.Name, fext)
128133
continue
129134
}
130-
//match
135+
// match
131136
os := getOS(ga.Name)
132137
arch := getArch(ga.Name)
133-
//windows not supported yet
138+
// windows not supported yet
134139
if os == "windows" {
135140
log.Printf("fetched asset is for windows: %s", ga.Name)
136-
//TODO: powershell
141+
// TODO: powershell
137142
// EG: iwr https://deno.land/x/install/install.ps1 -useb | iex
138143
continue
139144
}
140-
//unknown os, cant use
145+
146+
// stop guessing for linux/amd64 assets when the exact match is found
147+
if os == "linux" && arch == "amd64" {
148+
foundLinuxAMD64 = true
149+
}
150+
assumedLinuxAsset := false
151+
// unknown arch/os, the asset will be regarded as linux/amd64 if no other assets match
141152
if os == "" {
142-
log.Printf("fetched asset has unknown os: %s", ga.Name)
143-
continue
153+
assumedLinuxAsset = true
154+
if arch == "" || arch == "amd64" {
155+
if foundLinuxAMD64 {
156+
continue
157+
}
158+
}
144159
}
160+
if arch == "" {
161+
arch = "amd64"
162+
if os == "linux" {
163+
assumedLinuxAsset = true
164+
}
165+
}
166+
145167
// user selecting a particular asset?
146168
if q.Select != "" && !strings.Contains(ga.Name, q.Select) {
147169
log.Printf("select excludes asset: %s", ga.Name)
@@ -155,10 +177,26 @@ func (h *Handler) getAssetsNoCache(q Query) (string, Assets, error) {
155177
Type: fext,
156178
SHA256: sumIndex[ga.Name],
157179
}
158-
//there can only be 1 file for each OS/Arch
180+
159181
key := asset.Key()
160-
other, exists := index[key]
161-
if exists {
182+
// "linux/", "/amd64" will all be assumed as "linux/amd64"
183+
if assumedLinuxAsset {
184+
// "linux/" always win.
185+
if key == "linux/" {
186+
delete(candidates, "/amd64")
187+
foundLinuxAMD64 = true
188+
189+
// If key "linux/" exist,
190+
// assets like "unknown-os-i386" would be ignored (stop guessing OS)
191+
} else if _, exists := candidates["linux/"]; exists {
192+
continue
193+
}
194+
candidates[key] = asset
195+
continue
196+
}
197+
198+
// there can only be 1 file for each OS/Arch
199+
if other, exists := index[key]; exists {
162200
gnu := func(s string) bool { return strings.Contains(s, "gnu") }
163201
musl := func(s string) bool { return strings.Contains(s, "musl") }
164202
g2m := gnu(other.Name) && !musl(other.Name) && !gnu(asset.Name) && musl(asset.Name)
@@ -169,6 +207,18 @@ func (h *Handler) getAssetsNoCache(q Query) (string, Assets, error) {
169207
}
170208
index[key] = asset
171209
}
210+
211+
for _, cAsset := range candidates {
212+
// "/loong64" will be assumed to be "linux/loong64"
213+
if cAsset.OS == "" {
214+
cAsset.OS = "linux"
215+
}
216+
indexKey := cAsset.Key()
217+
// and will only be selected if the exact match failed
218+
if _, exists := index[indexKey]; !exists {
219+
index[indexKey] = cAsset
220+
}
221+
}
172222
if len(index) == 0 {
173223
return release, nil, errors.New("no downloads found for this release")
174224
}
@@ -199,10 +249,12 @@ func (as ghAssets) getSumIndex() (map[string]string, error) {
199249
}
200250
resp, err := httpGet(url)
201251
if err != nil {
202-
fmt.Println(err)
203252
return nil, err
204253
}
205254
defer resp.Body.Close()
255+
if resp.StatusCode != 200 {
256+
return nil, fmt.Errorf("sum file request returned status: %s", resp.Status)
257+
}
206258
// take each line and insert into the index
207259
index := map[string]string{}
208260
s := bufio.NewScanner(resp.Body)
@@ -248,18 +300,18 @@ type ghRelease struct {
248300
ID int `json:"id"`
249301
Login string `json:"login"`
250302
} `json:"author"`
251-
Body string `json:"body"`
252-
CreatedAt string `json:"created_at"`
253-
Draft bool `json:"draft"`
254-
HTMLURL string `json:"html_url"`
255-
ID int `json:"id"`
256-
Name interface{} `json:"name"`
257-
Prerelease bool `json:"prerelease"`
258-
PublishedAt string `json:"published_at"`
259-
TagName string `json:"tag_name"`
260-
TarballURL string `json:"tarball_url"`
261-
TargetCommitish string `json:"target_commitish"`
262-
UploadURL string `json:"upload_url"`
263-
URL string `json:"url"`
264-
ZipballURL string `json:"zipball_url"`
303+
Body string `json:"body"`
304+
CreatedAt string `json:"created_at"`
305+
Draft bool `json:"draft"`
306+
HTMLURL string `json:"html_url"`
307+
ID int `json:"id"`
308+
Name any `json:"name"`
309+
Prerelease bool `json:"prerelease"`
310+
PublishedAt string `json:"published_at"`
311+
TagName string `json:"tag_name"`
312+
TarballURL string `json:"tarball_url"`
313+
TargetCommitish string `json:"target_commitish"`
314+
UploadURL string `json:"upload_url"`
315+
URL string `json:"url"`
316+
ZipballURL string `json:"zipball_url"`
265317
}

handler/patterns.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package handler
2+
3+
import (
4+
"regexp"
5+
)
6+
7+
// os patterns
8+
var (
9+
// '\b' ([^a-zA-Z0-9_]) is not ideal for matching the boundary
10+
// for example: gitleaks_8.24.0_darwin_x64.tar.gz
11+
// Since the RE2 does not support lookaheads & lookbehinds, we use the following workaround:
12+
// (?:[^a-zA-Z0-9]|^) to match the beginning of the substring,
13+
// and (?:[^a-zA-Z0-9]|$) to match the end of the substring.
14+
// Then we use the regexp.FindStringSubmatch to extract the first capturing group.
15+
16+
// for OS detection, it is prefered to do a prefix match,
17+
// so that example_macos_x64.tar.gz can also be matched.
18+
19+
osReDarwin = regexp.MustCompile(`(?:[^a-zA-Z0-9]|^)(darwin|mac|osx)`)
20+
osReDragonfly = regexp.MustCompile(`(?:[^a-zA-Z0-9]|^)(dragonfly)`)
21+
osReWindows = regexp.MustCompile(`(?:[^a-zA-Z0-9]|^)(win)`)
22+
23+
// It is only necessary to match both the beginning and end of the substring,
24+
// if the regexp is meant to match the whole string.
25+
26+
osReMisc = regexp.MustCompile(`(?:[^a-zA-Z0-9]|^)` +
27+
`(aix|android|illumos|ios|linux|(free|net|open)bsd|plan9|solaris)` +
28+
`(?:[^a-zA-Z0-9]|$)`)
29+
)
30+
31+
// architecture patterns
32+
var (
33+
// for architecture detection, it is prefered to do a suffix match,
34+
// so that example_i686.tar.gz can also be matched.
35+
36+
archReAmd64 = regexp.MustCompile(`(amd64|x86_64)(?:[^a-zA-Z0-9]|$)`)
37+
archRe386 = regexp.MustCompile(`(386|686)(?:[^a-zA-Z0-9]|$)`)
38+
archReArm64 = regexp.MustCompile(`(arm64|aarch64)(?:[^a-zA-Z0-9]|$)`)
39+
archReArm = regexp.MustCompile(`(arm(v[567]|32)?[eh]?[fl]?)(?:[^a-zA-Z0-9]|$)`)
40+
archReLoong64 = regexp.MustCompile(`(loong64|loongarch64)(?:[^a-zA-Z0-9]|$)`)
41+
archRePPC64 = regexp.MustCompile(`(ppc64|powerpc64)(?:[^a-zA-Z0-9]|$)`)
42+
archRePPC64LE = regexp.MustCompile(`(ppc64le|powerpc64le)(?:[^a-zA-Z0-9]|$)`)
43+
archReRiscv64 = regexp.MustCompile(`(riscv64)`) // also match riscv64gc
44+
archReMisc = regexp.MustCompile(`(?:[^a-zA-Z0-9]|^)` +
45+
`(mips|mips64|mips64le|mipsle|s390x|wasm)` +
46+
`(?:[^a-zA-Z0-9]|$)`)
47+
48+
fuzzArchAmd64 = regexp.MustCompile(`(x?64(bit)?)\b`)
49+
fuzzArch386 = regexp.MustCompile(`(x?32(bit)?|x86)\b`)
50+
)
51+
52+
var (
53+
checksumRe = regexp.MustCompile(`(checksums|sha256sums)`)
54+
fileExtRe = regexp.MustCompile(`(\.tar)?(\.[a-z][a-z0-9]+)$`)
55+
searchGithubRe = regexp.MustCompile(`https:\/\/github\.com\/(\w+)\/(\w+)`)
56+
)

handler/search.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,8 @@ import (
44
"errors"
55
"fmt"
66
"net/url"
7-
"regexp"
87
)
98

10-
var searchGithubRe = regexp.MustCompile(`https:\/\/github\.com\/(\w+)\/(\w+)`)
11-
129
func imFeelingLuck(phrase string) (user, project string, err error) {
1310
phrase += " site:github.com"
1411
// try dgg

handler/strings.go

Lines changed: 40 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,57 @@
11
package handler
22

33
import (
4-
"regexp"
54
"strings"
65
)
76

8-
var (
9-
archRe = regexp.MustCompile(`(arm64|arm|386|686|amd64|x86_64|aarch64|32|64)`)
10-
fileExtRe = regexp.MustCompile(`(\.tar)?(\.[a-z][a-z0-9]+)$`)
11-
posixOSRe = regexp.MustCompile(`(darwin|linux|(net|free|open)bsd|mac|osx|windows|win)`)
12-
checksumRe = regexp.MustCompile(`(checksums|sha256sums)`)
13-
)
14-
157
func getOS(s string) string {
168
s = strings.ToLower(s)
17-
o := posixOSRe.FindString(s)
18-
if o == "mac" || o == "osx" {
19-
o = "darwin"
9+
switch {
10+
case osReDarwin.MatchString(s):
11+
return "darwin"
12+
case osReDragonfly.MatchString(s):
13+
return "dragonfly"
14+
case osReWindows.MatchString(s):
15+
return "windows"
16+
case osReMisc.MatchString(s):
17+
// return the first capturing group (contains only the alphanumeric characters)
18+
return osReMisc.FindStringSubmatch(s)[1]
19+
default:
20+
return ""
2021
}
21-
if o == "win" {
22-
o = "windows"
23-
}
24-
return o
2522
}
2623

2724
func getArch(s string) string {
2825
s = strings.ToLower(s)
29-
a := archRe.FindString(s)
30-
//arch modifications
31-
if a == "64" || a == "x86_64" || a == "" {
32-
a = "amd64" //default
33-
} else if a == "32" || a == "686" {
34-
a = "386"
35-
} else if a == "aarch64" {
36-
a = "arm64"
26+
switch {
27+
case archReLoong64.MatchString(s):
28+
return "loong64"
29+
case archRePPC64.MatchString(s):
30+
return "ppc64"
31+
case archRePPC64LE.MatchString(s):
32+
return "ppc64le"
33+
case archReRiscv64.MatchString(s):
34+
return "riscv64"
35+
case archReArm64.MatchString(s):
36+
return "arm64"
37+
case archReAmd64.MatchString(s):
38+
return "amd64"
39+
case archReArm.MatchString(s):
40+
return "arm"
41+
case archRe386.MatchString(s):
42+
return "386"
43+
case archReMisc.MatchString(s):
44+
return archReMisc.FindStringSubmatch(s)[1]
45+
46+
// fuzz match 'x?64(bit)?'
47+
case fuzzArchAmd64.MatchString(s):
48+
return "amd64"
49+
// fuzz match 'x?32(bit)?'
50+
case fuzzArch386.MatchString(s):
51+
return "386"
52+
default:
53+
return ""
3754
}
38-
return a
3955
}
4056

4157
func getFileExt(s string) string {

scripts/install.sh.tmpl

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,22 @@ function install {
5757
GET="$GET -H 'Authorization: $AUTH'"
5858
fi
5959
#find OS #TODO BSDs and other posixs
60-
case `uname -s` in
61-
Darwin) OS="darwin";;
62-
Linux) OS="linux";;
63-
*) fail "unknown os: $(uname -s)";;
64-
esac
60+
OS="{{ .OS }}"
61+
if [ -n "$OS" ]; then
62+
echo "Override OS: $OS"
63+
else
64+
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
65+
case "$OS" in
66+
darwin) OS="darwin";;
67+
linux) OS="linux";;
68+
*) fail "unknown os: $(uname -s)";;
69+
esac
70+
fi
6571
#find ARCH
66-
if uname -m | grep -E '(arm|arch)64' > /dev/null; then
72+
ARCH="{{ .Arch }}"
73+
if [ -n "$ARCH" ]; then
74+
echo "Override architecture: $ARCH"
75+
elif uname -m | grep -E '(arm|arch)64' > /dev/null; then
6776
ARCH="arm64"
6877
{{ if not .M1Asset }}
6978
# no m1 assets. if on mac arm64, rosetta allows fallback to amd64
@@ -127,6 +136,10 @@ function install {
127136
which tar > /dev/null || fail "tar is not installed"
128137
which gzip > /dev/null || fail "gzip is not installed"
129138
bash -c "$GET $URL" | tar zxf - || fail "download failed"
139+
elif [[ $FTYPE = ".tar.xz" ]] || [[ $FTYPE = ".txz" ]]; then
140+
which tar > /dev/null || fail "tar is not installed"
141+
which xz > /dev/null || fail "xz is not installed"
142+
bash -c "$GET $URL" | tar Jxf - || fail "download failed"
130143
elif [[ $FTYPE = ".zip" ]]; then
131144
which unzip > /dev/null || fail "unzip is not installed"
132145
bash -c "$GET $URL" > tmp.zip || fail "download failed"

wrangler.jsonc

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"name": "installer-worker",
3+
"main": "build/worker.mjs",
4+
"compatibility_date": "2024-09-14",
5+
"build": {
6+
"command": "make build"
7+
},
8+
"observability": {
9+
"logs": {
10+
"enabled": true,
11+
"head_sampling_rate": 1,
12+
"invocation_logs": true,
13+
"persist": true
14+
}
15+
}
16+
}

0 commit comments

Comments
 (0)