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 {
4040 components.path = ""
4141 components.query = nil
4242 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 }
4349 guard let url = components.url else { throw InvalidURL.notAURL(trimmed) }
4450 self.baseURL = url
4551 }
gitbay/Push/PushTarget.swift +6 −1
@@ -10,8 +10,13 @@ nonisolated struct PushTarget: Equatable, Sendable {
1010 /// or when the path names nothing the app can route to.
1111 init?(payload: PushPayload, accounts: [Account]) {
1212 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 }
1318 let match = accounts.first {
14 $0.username == payload.user && $0.instance.baseURL == payload.instance
19 $0.username == payload.user && $0.instance.baseURL == instance.baseURL
1520 }
1621 guard let match else { return nil }
1722 self.accountID = match.id
gitbayTests/PushTargetTests.swift +19
@@ -46,3 +46,22 @@ private func payload(_ instance: String, _ user: String, _ path: String) throws
4646 #expect(PushTarget(payload: try payload("https://gitbay.org", "cmc", "krz/gitbay/wiki/Home"),
4747 accounts: accounts) == nil)
4848}
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}