-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathUpdateCheck.swift
More file actions
170 lines (145 loc) · 7.19 KB
/
Copy pathUpdateCheck.swift
File metadata and controls
170 lines (145 loc) · 7.19 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
import Foundation
/// Kijkt of er een nieuwere versie op GitHub staat. En verder niets.
///
/// Dat "verder niets" is hier geen bescheidenheid maar een ontwerpeis. Deze app heeft een
/// wachtwoordloze root-route — de sudoers-regel voor `pmset` — en daarmee is elk kanaal
/// waarlangs iets van buiten binnenkomt de moeite van het aanvallen waard. Dit bestand
/// houdt zich daarom aan één regel: uit het antwoord van GitHub komen twee dingen, een
/// versienummer en een URL, en die worden alleen getóónd. Er wordt niets gedownload,
/// niets weggeschreven, niets uitgevoerd. Er is geen pad van een HTTP-antwoord naar code
/// die draait.
///
/// `verify.sh` controleert dat met een grep op `Process`, `pmset`, `SleepFlag` en
/// schrijvende bestandsaanroepen in dit bestand, zodat de regel afdwingbaar is en niet
/// alleen opgeschreven.
@MainActor
final class UpdateCheck: ObservableObject {
static let shared = UpdateCheck()
enum Toestand: Equatable {
/// Nog niet gekeken, of niet te zeggen — bijvoorbeeld omdat deze build geen
/// versienummer draagt (uit een ZIP gebouwd, zonder git).
case onbekend
case actueel
case beschikbaar(versie: Version, notities: URL?)
}
@Published private(set) var toestand: Toestand = .onbekend
@Published private(set) var bezig = false
/// De versie van deze build, uit de Info.plist die `build.sh` stempelt.
///
/// `nil` als daar `0.0.0` staat: dat is wat `build.sh` invult wanneer er geen git-tag
/// te vinden was. Zonder eigen versie is vergelijken zinloos, en dan zegt de app dat
/// ook in plaats van te doen alsof.
let huidige: Version?
/// De volledige `git describe` van deze build, puur om te tonen. `onbekend` als de
/// bron geen git-gegevens had.
let bron: String
private static let endpoint = URL(string:
"https://api.github.com/repos/peter46jan/dopamine-code/releases/latest")!
/// Hooguit één keer per etmaal. Anoniem mag je 60 verzoeken per uur per IP doen, dus
/// dit zit ruim binnen de marge — ook als je de app tien keer per dag herstart.
private static let interval: TimeInterval = 24 * 60 * 60
private var timer: Timer?
private init() {
let kort = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String
let ruw = Version(kort ?? "")
huidige = (ruw?.onderdelen == [0, 0, 0]) ? nil : ruw
bron = Bundle.main.object(forInfoDictionaryKey: "DCSourceVersion") as? String ?? "onbekend"
}
// MARK: - Aansturing
/// Bij het starten van de app. Kijkt meteen als er lang genoeg niet gekeken is, en
/// daarna elke zes uur opnieuw — waarbij het etmaal-slot het echte werk doet.
func start() {
controleerIndienNodig()
timer = Timer.scheduledTimer(withTimeInterval: 6 * 60 * 60, repeats: true) { [weak self] _ in
Task { @MainActor in self?.controleerIndienNodig() }
}
}
func controleerIndienNodig() {
guard Prefs.updateCheckEnabled else { return }
if let laatst = Prefs.updateLastCheck, Date().timeIntervalSince(laatst) < Self.interval {
return
}
haalOp(handmatig: false)
}
/// De knop in Instellingen. Negeert het etmaal-slot, maar niet de schakelaar: staat de
/// controle uit, dan is één druk op de knop een expliciete vraag en dus toegestaan.
func controleerNu() {
haalOp(handmatig: true)
}
// MARK: - Het verzoek
private func haalOp(handmatig: Bool) {
guard !bezig else { return }
bezig = true
var verzoek = URLRequest(url: Self.endpoint)
verzoek.timeoutInterval = 15
verzoek.cachePolicy = .reloadIgnoringLocalAndRemoteCacheData
verzoek.setValue("application/vnd.github+json", forHTTPHeaderField: "Accept")
// GitHub weigert verzoeken zonder User-Agent. Alleen de app en zijn versie; geen
// machinenaam, geen gebruikersnaam, niets wat naar één Mac te herleiden is.
verzoek.setValue("DopamineCode/\(huidige.map(String.init(describing:)) ?? "onbekend")",
forHTTPHeaderField: "User-Agent")
URLSession.shared.dataTask(with: verzoek) { [weak self] data, antwoord, fout in
Task { @MainActor in
self?.verwerk(data: data, antwoord: antwoord, fout: fout, handmatig: handmatig)
}
}.resume()
}
private func verwerk(data: Data?, antwoord: URLResponse?, fout: Error?, handmatig: Bool) {
bezig = false
Prefs.updateLastCheck = Date()
// Een mislukte updatecontrole is geen probleem van de gebruiker. Het gaat naar het
// logboek en verder gebeurt er niets: geen melding, geen rode tekst in het menu,
// geen herhaalpoging die het netwerk bezig houdt. Over zes uur is er een nieuwe kans.
if let fout {
EventLog.shared.info("Updatecontrole mislukte: \(fout.localizedDescription)")
return
}
guard let http = antwoord as? HTTPURLResponse else { return }
guard http.statusCode == 200 else {
EventLog.shared.info("Updatecontrole gaf status \(http.statusCode).")
return
}
guard let data,
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
else {
EventLog.shared.info("Updatecontrole gaf een antwoord dat geen JSON-object was.")
return
}
guard let tag = json["tag_name"] as? String, let laatste = Version(tag) else {
EventLog.shared.info("Updatecontrole: tag_name is geen versienummer.")
return
}
let notities = veiligeNotitieURL(json["html_url"])
guard let huidige else {
// Deze build weet niet welke versie hij is. Dan is "er is 1.3.0" het enige
// eerlijke antwoord — of dat nieuwer is dan wat hier draait, valt niet te zeggen.
toestand = .beschikbaar(versie: laatste, notities: notities)
return
}
if laatste > huidige {
toestand = .beschikbaar(versie: laatste, notities: notities)
if handmatig || Prefs.updateLastSeenVersion != laatste.description {
EventLog.shared.info("Versie \(laatste) beschikbaar; deze build is \(huidige).")
}
Prefs.updateLastSeenVersion = laatste.description
} else {
toestand = .actueel
}
}
/// Laat alleen een `https`-URL op github.com door.
///
/// De URL komt uit een HTTP-antwoord en eindigt in `NSWorkspace.open`. Zonder deze
/// controle zou een vervalst of gekaapt antwoord de app een willekeurige pagina laten
/// openen — en "mijn wakkerhoud-app opende uit zichzelf een site" is precies het begin
/// van een geloofwaardige phishing-stap. Voldoet de URL niet, dan verdwijnt alleen de
/// knop; het versienummer blijft gewoon zichtbaar.
private func veiligeNotitieURL(_ ruw: Any?) -> URL? {
guard let tekst = ruw as? String,
let url = URL(string: tekst),
url.scheme?.lowercased() == "https",
let host = url.host?.lowercased(),
host == "github.com" || host == "www.github.com"
else { return nil }
return url
}
}