Fix url parsing
This commit is contained in:
parent
2db08cde89
commit
6c0f54a688
3 changed files with 165 additions and 0 deletions
|
|
@ -20,6 +20,7 @@ swift_library(
|
||||||
"//submodules/OverlayStatusController:OverlayStatusController",
|
"//submodules/OverlayStatusController:OverlayStatusController",
|
||||||
"//submodules/UrlWhitelist:UrlWhitelist",
|
"//submodules/UrlWhitelist:UrlWhitelist",
|
||||||
"//submodules/TelegramUI/Components/AlertComponent",
|
"//submodules/TelegramUI/Components/AlertComponent",
|
||||||
|
"//submodules/UrlHandling",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import OverlayStatusController
|
||||||
import UrlWhitelist
|
import UrlWhitelist
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
import AlertComponent
|
import AlertComponent
|
||||||
|
import UrlHandling
|
||||||
|
|
||||||
public func openUserGeneratedUrl(context: AccountContext, peerId: PeerId?, url: String, concealed: Bool, skipUrlAuth: Bool = false, skipConcealedAlert: Bool = false, forceDark: Bool = false, present: @escaping (ViewController) -> Void, openResolved: @escaping (ResolvedUrl) -> Void, progress: Promise<Bool>? = nil, alertDisplayUpdated: ((ViewController?) -> Void)? = nil) -> Disposable {
|
public func openUserGeneratedUrl(context: AccountContext, peerId: PeerId?, url: String, concealed: Bool, skipUrlAuth: Bool = false, skipConcealedAlert: Bool = false, forceDark: Bool = false, present: @escaping (ViewController) -> Void, openResolved: @escaping (ResolvedUrl) -> Void, progress: Promise<Bool>? = nil, alertDisplayUpdated: ((ViewController?) -> Void)? = nil) -> Disposable {
|
||||||
var concealed = concealed
|
var concealed = concealed
|
||||||
|
|
@ -83,14 +84,23 @@ public func openUserGeneratedUrl(context: AccountContext, peerId: PeerId?, url:
|
||||||
let (parsedString, parsedConcealed) = parseUrl(url: url, wasConcealed: concealed)
|
let (parsedString, parsedConcealed) = parseUrl(url: url, wasConcealed: concealed)
|
||||||
concealed = parsedConcealed
|
concealed = parsedConcealed
|
||||||
|
|
||||||
|
if let parsedUrl = parseInternalUrl(sharedContext: context.sharedContext, context: context, query: url) {
|
||||||
|
if case .proxy = parsedUrl {
|
||||||
|
concealed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if concealed && !skipConcealedAlert {
|
if concealed && !skipConcealedAlert {
|
||||||
var rawDisplayUrl: String = parsedString
|
var rawDisplayUrl: String = parsedString
|
||||||
let maxLength = 180
|
let maxLength = 180
|
||||||
if rawDisplayUrl.count > maxLength {
|
if rawDisplayUrl.count > maxLength {
|
||||||
rawDisplayUrl = String(rawDisplayUrl[..<rawDisplayUrl.index(rawDisplayUrl.startIndex, offsetBy: maxLength - 2)]) + "..."
|
rawDisplayUrl = String(rawDisplayUrl[..<rawDisplayUrl.index(rawDisplayUrl.startIndex, offsetBy: maxLength - 2)]) + "..."
|
||||||
}
|
}
|
||||||
|
|
||||||
var displayUrl = rawDisplayUrl
|
var displayUrl = rawDisplayUrl
|
||||||
displayUrl = displayUrl.replacingOccurrences(of: "\u{202e}", with: "")
|
displayUrl = displayUrl.replacingOccurrences(of: "\u{202e}", with: "")
|
||||||
|
displayUrl = (try? punycodedFullURLString(displayUrl)) ?? displayUrl
|
||||||
|
|
||||||
let disposable = MetaDisposable()
|
let disposable = MetaDisposable()
|
||||||
|
|
||||||
let alertController = textAlertController(context: context, forceTheme: forceDark ? presentationData.theme : nil, title: nil, text: presentationData.strings.Generic_OpenHiddenLinkAlert(displayUrl).string, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_No, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Yes, action: {
|
let alertController = textAlertController(context: context, forceTheme: forceDark ? presentationData.theme : nil, title: nil, text: presentationData.strings.Generic_OpenHiddenLinkAlert(displayUrl).string, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_No, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Yes, action: {
|
||||||
|
|
@ -108,3 +118,153 @@ public func openUserGeneratedUrl(context: AccountContext, peerId: PeerId?, url:
|
||||||
return openImpl()
|
return openImpl()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private enum PunycodeError: Error {
|
||||||
|
case emptyInput
|
||||||
|
case overflow
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct Punycode {
|
||||||
|
private static let base = 36
|
||||||
|
private static let tmin = 1
|
||||||
|
private static let tmax = 26
|
||||||
|
private static let skew = 38
|
||||||
|
private static let damp = 700
|
||||||
|
private static let initialBias = 72
|
||||||
|
private static let initialN: UInt32 = 128
|
||||||
|
private static let delimiter: Character = "-"
|
||||||
|
|
||||||
|
/// Encodes a single DNS label to punycode WITHOUT "xn--" prefix.
|
||||||
|
static func encodeLabel(_ input: String) throws -> String {
|
||||||
|
guard !input.isEmpty else { throw PunycodeError.emptyInput }
|
||||||
|
|
||||||
|
let scalars: [UInt32] = input.unicodeScalars.map { $0.value }
|
||||||
|
var output = ""
|
||||||
|
|
||||||
|
var b = 0
|
||||||
|
for cp in scalars where cp < 0x80 {
|
||||||
|
output.unicodeScalars.append(UnicodeScalar(cp)!)
|
||||||
|
b += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
var h = b
|
||||||
|
if b > 0 && h < scalars.count { output.append(delimiter) }
|
||||||
|
|
||||||
|
var n = initialN
|
||||||
|
var delta: UInt32 = 0
|
||||||
|
var bias = initialBias
|
||||||
|
|
||||||
|
func checkedAdd(_ a: UInt32, _ b: UInt32) throws -> UInt32 {
|
||||||
|
let (res, overflow) = a.addingReportingOverflow(b)
|
||||||
|
if overflow { throw PunycodeError.overflow }
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
func checkedMul(_ a: UInt32, _ b: UInt32) throws -> UInt32 {
|
||||||
|
let (res, overflow) = a.multipliedReportingOverflow(by: b)
|
||||||
|
if overflow { throw PunycodeError.overflow }
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
while h < scalars.count {
|
||||||
|
var m: UInt32 = UInt32.max
|
||||||
|
for cp in scalars where cp >= n {
|
||||||
|
if cp < m { m = cp }
|
||||||
|
}
|
||||||
|
|
||||||
|
let hm1 = UInt32(h + 1)
|
||||||
|
delta = try checkedAdd(delta, try checkedMul(m - n, hm1))
|
||||||
|
n = m
|
||||||
|
|
||||||
|
for cp in scalars {
|
||||||
|
if cp < n {
|
||||||
|
delta = try checkedAdd(delta, 1)
|
||||||
|
} else if cp == n {
|
||||||
|
var q = delta
|
||||||
|
var k = base
|
||||||
|
|
||||||
|
while true {
|
||||||
|
let t: UInt32
|
||||||
|
if k <= bias { t = UInt32(tmin) }
|
||||||
|
else if k >= bias + tmax { t = UInt32(tmax) }
|
||||||
|
else { t = UInt32(k - bias) }
|
||||||
|
|
||||||
|
if q < t { break }
|
||||||
|
|
||||||
|
let digit = Int(t + (q - t) % UInt32(base - Int(t)))
|
||||||
|
output.append(encodeDigit(digit))
|
||||||
|
q = (q - t) / UInt32(base - Int(t))
|
||||||
|
k += base
|
||||||
|
}
|
||||||
|
|
||||||
|
output.append(encodeDigit(Int(q)))
|
||||||
|
bias = adapt(delta: delta, numPoints: h + 1, firstTime: h == b)
|
||||||
|
delta = 0
|
||||||
|
h += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delta = try checkedAdd(delta, 1)
|
||||||
|
n = try checkedAdd(n, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encodes a domain name label-by-label; prefixes "xn--" only when needed.
|
||||||
|
static func encodeDomain(_ domain: String) throws -> String {
|
||||||
|
let normalized = domain.precomposedStringWithCanonicalMapping
|
||||||
|
|
||||||
|
return try normalized
|
||||||
|
.split(separator: ".", omittingEmptySubsequences: false)
|
||||||
|
.map { piece -> String in
|
||||||
|
let label = String(piece)
|
||||||
|
if label.isEmpty { return label } // keep empty labels as-is (rare)
|
||||||
|
if label.unicodeScalars.allSatisfy({ $0.isASCII }) { return label }
|
||||||
|
return "xn--" + (try encodeLabel(label))
|
||||||
|
}
|
||||||
|
.joined(separator: ".")
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func encodeDigit(_ d: Int) -> Character {
|
||||||
|
precondition((0..<36).contains(d))
|
||||||
|
if d < 26 { return Character(UnicodeScalar(UInt8(97 + d))) } // a-z
|
||||||
|
else { return Character(UnicodeScalar(UInt8(48 + (d - 26)))) } // 0-9
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func adapt(delta: UInt32, numPoints: Int, firstTime: Bool) -> Int {
|
||||||
|
var d = Int(delta)
|
||||||
|
d = firstTime ? d / damp : d / 2
|
||||||
|
d += d / numPoints
|
||||||
|
|
||||||
|
var k = 0
|
||||||
|
while d > ((base - tmin) * tmax) / 2 {
|
||||||
|
d /= (base - tmin)
|
||||||
|
k += base
|
||||||
|
}
|
||||||
|
|
||||||
|
return k + (base - tmin + 1) * d / (d + skew)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum URLPunycodeError: Error {
|
||||||
|
case invalidURL
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts a full URL string to an ASCII-safe form:
|
||||||
|
/// - punycodes the host (IDN) using RFC 3492 punycode
|
||||||
|
/// - leaves scheme/port/user/pass intact
|
||||||
|
/// - lets URLComponents percent-encode path/query/fragment as needed
|
||||||
|
private func punycodedFullURLString(_ input: String) throws -> String {
|
||||||
|
guard var comps = URLComponents(string: input) else {
|
||||||
|
throw URLPunycodeError.invalidURL
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there is a host, encode it. If it's a relative URL, host may be nil.
|
||||||
|
if let host = comps.host, !host.isEmpty {
|
||||||
|
comps.host = try Punycode.encodeDomain(host)
|
||||||
|
}
|
||||||
|
|
||||||
|
// URLComponents.string can be nil for some malformed component combinations
|
||||||
|
if let out = comps.string { return out }
|
||||||
|
throw URLPunycodeError.invalidURL
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,10 @@ public func doesUrlMatchText(url: String, text: String, fullText: String) -> Boo
|
||||||
if fullText.range(of: "\u{202e}") != nil {
|
if fullText.range(of: "\u{202e}") != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
let allowed = CharacterSet(charactersIn: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~:/?#[]@!$&'()*+,;=%")
|
||||||
|
if !url.unicodeScalars.allSatisfy({ allowed.contains($0) }) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if url == text {
|
if url == text {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue