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
package routing
import (
"bufio"
"bytes"
"context"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"regexp"
"time"
"github.com/armanlife/firefox-vpn-client/core/internal/securefile"
)
// AppListURL is where a region's phone apps that should connect directly
// come from: hiddify/Android-GFW-Apps, one Android package name per line.
// The lists are GPL-3.0, so they are downloaded, not shipped.
const AppListURL = "https://raw.githubusercontent.com/hiddify/Android-GFW-Apps/master/direct_%s"
// appListRegions are the regions that have a list.
var appListRegions = map[string]bool{RegionIran: true, "cn": true, "ru": true}
// maxAppList bounds a downloaded app list.
const maxAppList = 1 << 20
var packageName = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z0-9_]+)+$`)
// HasAppList reports whether region has a list of apps.
func HasAppList(region string) bool { return appListRegions[region] }
// AppListFile is the name of region's app list in the routing folder.
func AppListFile(region string) string { return "apps-direct-" + region + ".txt" }
// AppListDue reports whether region has a list that is missing from dir
// or older than maxAge.
func AppListDue(dir, region string, maxAge time.Duration) bool {
if !HasAppList(region) {
return false
}
info, err := os.Stat(filepath.Join(dir, AppListFile(region)))
return err != nil || time.Since(info.ModTime()) >= maxAge
}
// DownloadAppList fetches region's app list with client into dir. The
// file is replaced only by a complete list of package names.
func DownloadAppList(ctx context.Context, client *http.Client, dir, region string) error {
if !HasAppList(region) {
return nil
}
if err := securefile.EnsurePrivateDir(dir); err != nil {
return err
}
url := fmt.Sprintf(AppListURL, region)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return err
}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("HTTP %d from %s", resp.StatusCode, url)
}
data, err := io.ReadAll(io.LimitReader(resp.Body, maxAppList+1))
if err != nil {
return err
}
if len(data) > maxAppList {
return fmt.Errorf("app list larger than %d KiB", maxAppList>>10)
}
apps := parseAppList(data)
if len(apps) == 0 {
return fmt.Errorf("no package names in %s", url)
}
return securefile.WritePrivate(filepath.Join(dir, AppListFile(region)), data)
}
// ReadAppList returns the package names of region's list in dir; none
// when it is missing.
func ReadAppList(dir, region string) []string {
if !HasAppList(region) {
return nil
}
data, err := os.ReadFile(filepath.Join(dir, AppListFile(region)))
if err != nil {
return nil
}
return parseAppList(data)
}
// parseAppList keeps the lines that are package names.
func parseAppList(data []byte) []string {
var apps []string
scanner := bufio.NewScanner(bytes.NewReader(data))
for scanner.Scan() {
if line := bytes.TrimSpace(scanner.Bytes()); packageName.Match(line) {
apps = append(apps, string(line))
}
}
return apps
}