Commit c8d3f71d5d
c8d3f71d5d091165999561373ef7809dcc240b9a
parent: 93e0dbdbc7
Verified · cmc
cmc <hello@cleberg.net> · 2026-09-20 21:57 UTC
push: normalise the instance origin before matching an account
Comparing PushPayload.instance against Account.instance.baseURL
directly broke on a trailing slash or an explicit default port,
either of which an operator's unnormalised site_url config can
produce. Route the payload's instance through GitbayInstance's own
constructor before comparing, and close the gap in that constructor
itself: it stripped path/query/fragment but left an explicit :443
or :80 in place, so two instances typed with and without the default
port compared unequal.
Ref krz/gitbay#89
Layout: unified · split
gitbay/Networking/GitbayInstance.swift
+6
| @@ -40,6 +40,12 @@ nonisolated struct GitbayInstance: Sendable, Hashable, Codable { |
| 40 | components.path = "" |
40 | components.path = "" |
| 41 | components.query = nil |
41 | components.query = nil |
| 42 | components.fragment = nil |
42 | components.fragment = nil |
| |
43 | // An explicit default port is the same origin as no port at all — |
| |
44 | // URLComponents does not fold this on its own, and two instances |
| |
45 | // typed with and without it must normalise to one baseURL. |
| |
46 | if let port = components.port, (scheme == "https" && port == 443) || (scheme == "http" && port == 80) { |
| |
47 | components.port = nil |
| |
48 | } |
| 43 | guard let url = components.url else { throw InvalidURL.notAURL(trimmed) } |
49 | guard let url = components.url else { throw InvalidURL.notAURL(trimmed) } |
| 44 | self.baseURL = url |
50 | self.baseURL = url |
| 45 | } |
51 | } |
gitbay/Push/PushTarget.swift
+6 −1
| @@ -10,8 +10,13 @@ nonisolated struct PushTarget: Equatable, Sendable { |
| 10 | /// or when the path names nothing the app can route to. |
10 | /// or when the path names nothing the app can route to. |
| 11 | init?(payload: PushPayload, accounts: [Account]) { |
11 | init?(payload: PushPayload, accounts: [Account]) { |
| 12 | guard let destination = notificationDestination(forPath: payload.path) else { return nil } |
12 | guard let destination = notificationDestination(forPath: payload.path) else { return nil } |
| |
13 | // Run the payload's instance through the same normalisation an |
| |
14 | // account's did (trailing slash, default port) rather than |
| |
15 | // comparing raw URLs, which a server's unnormalised config can |
| |
16 | // make unequal even when they name the same instance. |
| |
17 | guard let instance = try? GitbayInstance(url: payload.instance.absoluteString) else { return nil } |
| 13 | let match = accounts.first { |
18 | let match = accounts.first { |
| 14 | $0.username == payload.user && $0.instance.baseURL == payload.instance |
19 | $0.username == payload.user && $0.instance.baseURL == instance.baseURL |
| 15 | } |
20 | } |
| 16 | guard let match else { return nil } |
21 | guard let match else { return nil } |
| 17 | self.accountID = match.id |
22 | self.accountID = match.id |
gitbayTests/PushTargetTests.swift
+19
| @@ -46,3 +46,22 @@ private func payload(_ instance: String, _ user: String, _ path: String) throws |
| 46 | #expect(PushTarget(payload: try payload("https://gitbay.org", "cmc", "krz/gitbay/wiki/Home"), |
46 | #expect(PushTarget(payload: try payload("https://gitbay.org", "cmc", "krz/gitbay/wiki/Home"), |
| 47 | accounts: accounts) == nil) |
47 | accounts: accounts) == nil) |
| 48 | } |
48 | } |
| |
49 | |
| |
50 | // A server's unnormalised site_url must not desync a payload from the |
| |
51 | // account it belongs to: both sides go through GitbayInstance's own |
| |
52 | // normalisation before comparing. |
| |
53 | @Test func targetMatchesAnInstanceWithATrailingSlash() throws { |
| |
54 | let accounts = [try account("gitbay.org", "cmc")] |
| |
55 | let target = try #require(PushTarget( |
| |
56 | payload: try payload("https://gitbay.org/", "cmc", "a/b/issues/1"), |
| |
57 | accounts: accounts)) |
| |
58 | #expect(target.accountID == accounts[0].id) |
| |
59 | } |
| |
60 | |
| |
61 | @Test func targetMatchesAnInstanceWithAnExplicitDefaultPort() throws { |
| |
62 | let accounts = [try account("gitbay.org", "cmc")] |
| |
63 | let target = try #require(PushTarget( |
| |
64 | payload: try payload("https://gitbay.org:443", "cmc", "a/b/issues/1"), |
| |
65 | accounts: accounts)) |
| |
66 | #expect(target.accountID == accounts[0].id) |
| |
67 | } |