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
import 'dart:convert';
import 'dart:io';
/// The app's version, "major.minor.patch": `version` in pubspec.yaml, which
/// the build passes (scripts/build-*.ps1: --dart-define=APP_VERSION=1.1.0).
/// "0.0.0" is a development build.
const appVersion = String.fromEnvironment('APP_VERSION', defaultValue: '0.0.0');
/// The GitHub repository, "owner/name", for the source link and the update
/// check. This build is the FoxyVPN fork, so its updates and source are
/// the fork's, not the upstream project's.
const githubRepo = 'amir123499/firefox-vpn-client';
/// A newer release on GitHub.
class Release {
const Release(this.version, this.url);
final String version;
final String url;
}
/// Whether "major.minor.patch" [a] is newer than [b].
bool newerVersion(String a, String b) {
List<int> parts(String v) => [
for (final p in v.replaceFirst(RegExp('^[vV]'), '').split('.'))
int.tryParse(p) ?? 0,
];
final x = parts(a), y = parts(b);
for (var i = 0; i < 3; i++) {
final xi = i < x.length ? x[i] : 0, yi = i < y.length ? y[i] : 0;
if (xi != yi) return xi > yi;
}
return false;
}
/// A release server used instead of GitHub, to try updates with a local
/// test release: it answers `<server>/releases/latest` as GitHub's API does,
/// and the core downloads from it too (core/internal/update, ServerEnv).
String get updateServer =>
Platform.environment['FIREFOX_VPN_UPDATE_SERVER'] ?? '';
/// Reads the latest release of [repo] from GitHub; null when [appVersion] is
/// the newest, or when there is no release yet. [proxy] ("host:port"), when
/// set, carries the request (the VPN's own proxy, for networks where GitHub
/// is blocked); it falls back to a direct connection.
Future<Release?> checkForUpdate({
String? proxy,
String repo = githubRepo,
}) async {
final server = updateServer;
final client = HttpClient()
..connectionTimeout = const Duration(seconds: 15)
..findProxy = (_) =>
proxy == null || server.isNotEmpty ? 'DIRECT' : 'PROXY $proxy; DIRECT';
try {
final api = server.isEmpty
? 'https://api.github.com/repos/$repo/releases/latest'
: '$server/releases/latest';
final req = await client
.getUrl(Uri.parse(api))
.timeout(const Duration(seconds: 20));
req.headers
..set(HttpHeaders.acceptHeader, 'application/vnd.github+json')
..set(HttpHeaders.userAgentHeader, 'firefox-vpn/$appVersion');
final res = await req.close().timeout(const Duration(seconds: 20));
final body = await res.transform(utf8.decoder).join();
// GitHub's answer for a repository without releases.
if (res.statusCode == 404) return null;
if (res.statusCode != 200) throw HttpException('HTTP ${res.statusCode}');
final j = jsonDecode(body);
final tag = j is Map<String, Object?> ? j['tag_name'] as String? ?? '' : '';
final url = j is Map<String, Object?> ? j['html_url'] as String? ?? '' : '';
final version = tag.replaceFirst(RegExp('^[vV]'), '');
return newerVersion(version, appVersion) ? Release(version, url) : null;
} finally {
client.close(force: true);
}
}