Various fixes
This commit is contained in:
parent
0b912f26de
commit
45cc8468bb
7 changed files with 167 additions and 163 deletions
|
|
@ -16359,3 +16359,12 @@ Error: %8$@";
|
|||
"ChatList.ClearSearchHistory.Confirm" = "Clear";
|
||||
|
||||
"ChatList.RemoveFolderConfirmationTitle" = "Remove %@?";
|
||||
|
||||
"SocksProxySetup.QrCode.TgLink" = "tg:// link";
|
||||
"SocksProxySetup.QrCode.TMeLink" = "t.me link";
|
||||
|
||||
"WebBrowser.OpenLinksInfo" = "Open links inside Telegram instead of your default browser for more privacy.";
|
||||
"WebBrowser.Exceptions.OpenInApp" = "OPEN IN-APP";
|
||||
"WebBrowser.Exceptions.DontOpenInApp" = "DON'T OPEN IN-APP";
|
||||
"WebBrowser.Exceptions.InAppInfo" = "These sites will still be opened in-app.";
|
||||
"WebBrowser.Exceptions.DeleteAll" = "Delete All Exceptions";
|
||||
|
|
|
|||
|
|
@ -224,13 +224,12 @@ private final class SheetContent: CombinedComponent {
|
|||
dividerColor: theme.rootController.navigationBar.segmentedDividerColor
|
||||
)
|
||||
|
||||
//TODO:localize
|
||||
let segmentControl = segmentControl.update(
|
||||
component: SegmentControlComponent(
|
||||
theme: theme,
|
||||
items: [
|
||||
SegmentControlComponent.Item(id: AnyHashable(false), title: "tg:// link"),
|
||||
SegmentControlComponent.Item(id: AnyHashable(true), title: "t.me link")
|
||||
SegmentControlComponent.Item(id: AnyHashable(false), title: strings.SocksProxySetup_QrCode_TgLink),
|
||||
SegmentControlComponent.Item(id: AnyHashable(true), title: strings.SocksProxySetup_QrCode_TMeLink)
|
||||
],
|
||||
selectedId: AnyHashable(state.selectedProxyExternalLink),
|
||||
action: { id in
|
||||
|
|
|
|||
|
|
@ -337,14 +337,14 @@ private func webBrowserSettingsControllerEntries(context: AccountContext, presen
|
|||
index += 1
|
||||
}
|
||||
|
||||
entries.append(.browserInfo(presentationData.theme, "Open links inside Telegram instead of your default browser for more privacy."))
|
||||
entries.append(.browserInfo(presentationData.theme, presentationData.strings.WebBrowser_OpenLinksInfo))
|
||||
|
||||
entries.append(.clearCookies(presentationData.theme, presentationData.strings.WebBrowser_ClearCookies))
|
||||
entries.append(.clearCookiesInfo(presentationData.theme, presentationData.strings.WebBrowser_ClearCookies_Info))
|
||||
|
||||
//TODO:localize
|
||||
if accountSettings.openExternalBrowser {
|
||||
entries.append(.neverHeader(presentationData.theme, "OPEN IN-APP"))
|
||||
entries.append(.neverHeader(presentationData.theme, presentationData.strings.WebBrowser_Exceptions_OpenInApp))
|
||||
entries.append(.neverAdd(presentationData.theme, presentationData.strings.WebBrowser_Exceptions_AddException))
|
||||
|
||||
var exceptionIndex: Int32 = 0
|
||||
|
|
@ -352,13 +352,13 @@ private func webBrowserSettingsControllerEntries(context: AccountContext, presen
|
|||
entries.append(.neverException(exceptionIndex, presentationData.theme, exception))
|
||||
exceptionIndex += 1
|
||||
}
|
||||
entries.append(.neverExceptionsInfo(presentationData.theme, "These sites will still be opened in-app."))
|
||||
entries.append(.neverExceptionsInfo(presentationData.theme, presentationData.strings.WebBrowser_Exceptions_InAppInfo))
|
||||
|
||||
if !accountSettings.inAppExceptions.isEmpty {
|
||||
entries.append(.neverExceptionsClear(presentationData.theme, "Delete All Exceptions"))
|
||||
entries.append(.neverExceptionsClear(presentationData.theme, presentationData.strings.WebBrowser_Exceptions_DeleteAll))
|
||||
}
|
||||
} else {
|
||||
entries.append(.alwaysHeader(presentationData.theme, "DON'T OPEN IN-APP"))
|
||||
entries.append(.alwaysHeader(presentationData.theme, presentationData.strings.WebBrowser_Exceptions_DontOpenInApp))
|
||||
entries.append(.alwaysAdd(presentationData.theme, presentationData.strings.WebBrowser_Exceptions_AddException))
|
||||
|
||||
var exceptionIndex: Int32 = 0
|
||||
|
|
@ -369,7 +369,7 @@ private func webBrowserSettingsControllerEntries(context: AccountContext, presen
|
|||
entries.append(.alwaysExceptionsInfo(presentationData.theme, presentationData.strings.WebBrowser_Exceptions_Info))
|
||||
|
||||
if !accountSettings.externalExceptions.isEmpty {
|
||||
entries.append(.alwaysExceptionsClear(presentationData.theme, "Delete All Exceptions"))
|
||||
entries.append(.alwaysExceptionsClear(presentationData.theme, presentationData.strings.WebBrowser_Exceptions_DeleteAll))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -557,9 +557,7 @@ final class ComposePollScreenComponent: Component {
|
|||
mappedKind = .poll(multipleAnswers: self.effectiveIsMultiAnswer && mappedOptions.count > 1)
|
||||
}
|
||||
|
||||
if self.isQuiz && mappedOptions.count < 2 {
|
||||
return .optionsNeeded
|
||||
} else if !self.isQuiz && mappedOptions.count < 1 {
|
||||
if mappedOptions.count < 1 {
|
||||
return .optionsNeeded
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -943,7 +943,7 @@ func editingItems(data: PeerInfoScreenData?, boostStatus: ChannelBoostStatus?, s
|
|||
} else {
|
||||
programTitleValue = .text(presentationData.strings.PeerInfo_ItemAffiliateProgram_ValueOff)
|
||||
}
|
||||
items[.peerDataSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemAffiliateProgram, label: programTitleValue, additionalBadgeLabel: presentationData.strings.Settings_New, text: presentationData.strings.PeerInfo_ItemAffiliateProgram_Title, icon: PresentationResourcesSettings.affiliateProgram, action: {
|
||||
items[.peerDataSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemAffiliateProgram, label: programTitleValue, text: presentationData.strings.PeerInfo_ItemAffiliateProgram_Title, icon: PresentationResourcesSettings.affiliateProgram, action: {
|
||||
interaction.editingOpenAffiliateProgram()
|
||||
}))
|
||||
}
|
||||
|
|
@ -1153,24 +1153,6 @@ func editingItems(data: PeerInfoScreenData?, boostStatus: ChannelBoostStatus?, s
|
|||
var boostIcon: UIImage?
|
||||
if let approximateBoostLevel = channel.approximateBoostLevel, approximateBoostLevel < 1 {
|
||||
boostIcon = generateDisclosureActionBoostLevelBadgeImage(text: presentationData.strings.Channel_Info_BoostLevelPlusBadge("1").string)
|
||||
} else {
|
||||
/*let labelText = NSAttributedString(string: presentationData.strings.Settings_New, font: Font.medium(11.0), textColor: presentationData.theme.list.itemCheckColors.foregroundColor)
|
||||
let labelBounds = labelText.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: [.usesLineFragmentOrigin], context: nil)
|
||||
let labelSize = CGSize(width: ceil(labelBounds.width), height: ceil(labelBounds.height))
|
||||
let badgeSize = CGSize(width: labelSize.width + 8.0, height: labelSize.height + 2.0 + 1.0)
|
||||
boostIcon = generateImage(badgeSize, rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
let rect = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height - UIScreenPixel * 2.0))
|
||||
|
||||
context.addPath(UIBezierPath(roundedRect: rect, cornerRadius: 5.0).cgPath)
|
||||
context.setFillColor(presentationData.theme.list.itemCheckColors.fillColor.cgColor)
|
||||
context.fillPath()
|
||||
|
||||
UIGraphicsPushContext(context)
|
||||
labelText.draw(at: CGPoint(x: 4.0, y: 1.0 + UIScreenPixel))
|
||||
UIGraphicsPopContext()
|
||||
})*/
|
||||
}
|
||||
items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemPeerColor, label: .image(colorImage, colorImage.size), additionalBadgeIcon: boostIcon, text: presentationData.strings.Channel_Info_AppearanceItem, icon: PresentationResourcesSettings.chatAppearance, action: {
|
||||
interaction.editingOpenNameColorSetup()
|
||||
|
|
@ -1226,7 +1208,7 @@ func editingItems(data: PeerInfoScreenData?, boostStatus: ChannelBoostStatus?, s
|
|||
labelString = NSAttributedString(string: presentationData.strings.PeerInfo_AllowChannelMessages_Off, font: labelFont, textColor: labelColor)
|
||||
}
|
||||
|
||||
items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemPostSuggestionsSettings, label: .attributedText(labelString), additionalBadgeLabel: presentationData.strings.Settings_New, text: presentationData.strings.PeerInfo_AllowChannelMessages, icon: PresentationResourcesSettings.channelMessages, action: {
|
||||
items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemPostSuggestionsSettings, label: .attributedText(labelString), text: presentationData.strings.PeerInfo_AllowChannelMessages, icon: PresentationResourcesSettings.channelMessages, action: {
|
||||
interaction.editingOpenPostSuggestionsSetup()
|
||||
}))
|
||||
|
||||
|
|
@ -1461,23 +1443,6 @@ func editingItems(data: PeerInfoScreenData?, boostStatus: ChannelBoostStatus?, s
|
|||
boostIcon = generateDisclosureActionBoostLevelBadgeImage(text: presentationData.strings.Channel_Info_BoostLevelPlusBadge("1").string)
|
||||
} else {
|
||||
boostIcon = nil
|
||||
/*let labelText = NSAttributedString(string: presentationData.strings.Settings_New, font: Font.medium(11.0), textColor: presentationData.theme.list.itemCheckColors.foregroundColor)
|
||||
let labelBounds = labelText.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: [.usesLineFragmentOrigin], context: nil)
|
||||
let labelSize = CGSize(width: ceil(labelBounds.width), height: ceil(labelBounds.height))
|
||||
let badgeSize = CGSize(width: labelSize.width + 8.0, height: labelSize.height + 2.0 + 1.0)
|
||||
boostIcon = generateImage(badgeSize, rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
let rect = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height - UIScreenPixel * 2.0))
|
||||
|
||||
context.addPath(UIBezierPath(roundedRect: rect, cornerRadius: 5.0).cgPath)
|
||||
context.setFillColor(presentationData.theme.list.itemCheckColors.fillColor.cgColor)
|
||||
context.fillPath()
|
||||
|
||||
UIGraphicsPushContext(context)
|
||||
labelText.draw(at: CGPoint(x: 4.0, y: 1.0 + UIScreenPixel))
|
||||
UIGraphicsPopContext()
|
||||
})*/
|
||||
}
|
||||
items[.peerDataSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemAppearance, label: .image(colorImage, colorImage.size), additionalBadgeIcon: boostIcon, text: presentationData.strings.Channel_Info_AppearanceItem, icon: PresentationResourcesSettings.chatAppearance, action: {
|
||||
interaction.editingOpenNameColorSetup()
|
||||
|
|
|
|||
|
|
@ -1262,7 +1262,9 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
|||
case "web_app_open_tg_link":
|
||||
if let json = json, let path = json["path_full"] as? String {
|
||||
let forceRequest = json["force_request"] as? Bool ?? false
|
||||
controller.openUrl("https://t.me\(path)", false, forceRequest, {})
|
||||
if let url = makeWebAppTelegramLink(pathFull: path) {
|
||||
controller.openUrl(url, false, forceRequest, {})
|
||||
}
|
||||
}
|
||||
case "web_app_open_invoice":
|
||||
if let json = json, let slug = json["slug"] as? String {
|
||||
|
|
@ -4349,116 +4351,3 @@ private struct WebAppConfiguration {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func isAllowedBotMediaUrl(_ urlString: String) -> Bool {
|
||||
guard let escaped = urlString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed),
|
||||
let url = URL(string: escaped) else {
|
||||
return false
|
||||
}
|
||||
guard url.scheme?.lowercased() == "https" else {
|
||||
return false
|
||||
}
|
||||
if url.user != nil || url.password != nil {
|
||||
return false
|
||||
}
|
||||
guard var host = url.host?.lowercased(), !host.isEmpty else {
|
||||
return false
|
||||
}
|
||||
if host.hasPrefix("[") && host.hasSuffix("]") {
|
||||
host = String(host.dropFirst().dropLast())
|
||||
}
|
||||
|
||||
// Strict canonical dotted-decimal IPv4 (4 octets, no leading zeros, each 0-255).
|
||||
// Do NOT use inet_pton here: Darwin's inet_pton accepts "0177.0.0.1" as
|
||||
// decimal 177.0.0.1, but getaddrinfo (used by URLSession) interprets the
|
||||
// same string as octal 127.0.0.1 — the divergence is a loopback bypass.
|
||||
if let v4Bytes = parseCanonicalIPv4(host) {
|
||||
return isPublicIPv4(v4Bytes)
|
||||
}
|
||||
|
||||
// IPv6 only — host must contain ":" so we don't accidentally hand a
|
||||
// numeric-looking hostname to inet_pton.
|
||||
if host.contains(":") {
|
||||
var v6 = in6_addr()
|
||||
if host.withCString({ inet_pton(AF_INET6, $0, &v6) }) == 1 {
|
||||
let bytes = withUnsafeBytes(of: &v6) { ptr -> [UInt8] in
|
||||
return Array(ptr)
|
||||
}
|
||||
return isPublicIPv6(bytes)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Strict DNS-name validation. Anything that doesn't look like a real
|
||||
// FQDN is rejected — this catches non-canonical numeric IP forms
|
||||
// (decimal-32 like "2130706433", octal like "0177.0.0.1", hex like
|
||||
// "0x7f.0.0.1", short forms like "127.1") that the OS resolver may
|
||||
// still treat as 127.0.0.1 even when inet_pton would accept them as
|
||||
// a different value or reject outright.
|
||||
let labels = host.split(separator: ".", omittingEmptySubsequences: false)
|
||||
guard labels.count >= 2 else { return false }
|
||||
for label in labels {
|
||||
guard !label.isEmpty, label.count <= 63 else { return false }
|
||||
if label.first == "-" || label.last == "-" { return false }
|
||||
for ch in label {
|
||||
guard ch.isASCII else { return false }
|
||||
if !(ch.isLetter || ch.isNumber || ch == "-") { return false }
|
||||
}
|
||||
}
|
||||
guard let tld = labels.last, tld.count >= 2, tld.contains(where: { $0.isLetter }) else {
|
||||
return false
|
||||
}
|
||||
|
||||
if host == "localhost" || host.hasSuffix(".localhost") || host.hasSuffix(".local") {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private func parseCanonicalIPv4(_ host: String) -> [UInt8]? {
|
||||
let parts = host.split(separator: ".", omittingEmptySubsequences: false)
|
||||
guard parts.count == 4 else { return nil }
|
||||
var bytes: [UInt8] = []
|
||||
bytes.reserveCapacity(4)
|
||||
for part in parts {
|
||||
guard !part.isEmpty, part.count <= 3 else { return nil }
|
||||
if part.count > 1 && part.first == "0" { return nil } // no leading zeros (octal-spoof)
|
||||
guard part.allSatisfy({ $0.isASCII && $0.isNumber }) else { return nil }
|
||||
guard let value = UInt8(part) else { return nil } // also caps at 255
|
||||
bytes.append(value)
|
||||
}
|
||||
return bytes
|
||||
}
|
||||
|
||||
private func isPublicIPv4(_ bytes: [UInt8]) -> Bool {
|
||||
guard bytes.count == 4 else { return false }
|
||||
let a = bytes[0]
|
||||
let b = bytes[1]
|
||||
if a == 0 { return false } // 0.0.0.0/8
|
||||
if a == 10 { return false } // 10.0.0.0/8
|
||||
if a == 127 { return false } // 127.0.0.0/8 loopback
|
||||
if a == 169 && b == 254 { return false } // 169.254.0.0/16 link-local
|
||||
if a == 172 && (b & 0xf0) == 16 { return false } // 172.16.0.0/12
|
||||
if a == 192 && b == 168 { return false } // 192.168.0.0/16
|
||||
if a == 100 && (b & 0xc0) == 64 { return false } // 100.64.0.0/10 CGNAT
|
||||
if a >= 224 { return false } // multicast + reserved + 255.255.255.255
|
||||
return true
|
||||
}
|
||||
|
||||
private func isPublicIPv6(_ bytes: [UInt8]) -> Bool {
|
||||
guard bytes.count == 16 else { return false }
|
||||
if bytes.allSatisfy({ $0 == 0 }) { return false } // ::
|
||||
let loopback: [UInt8] = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1]
|
||||
if bytes == loopback { return false } // ::1
|
||||
if bytes[0] == 0xff { return false } // ff00::/8 multicast
|
||||
if bytes[0] == 0xfe && (bytes[1] & 0xc0) == 0x80 { return false } // fe80::/10 link-local
|
||||
if (bytes[0] & 0xfe) == 0xfc { return false } // fc00::/7 unique-local
|
||||
let v4MappedPrefix: [UInt8] = [0,0,0,0,0,0,0,0,0,0,0xff,0xff]
|
||||
if Array(bytes.prefix(12)) == v4MappedPrefix { // ::ffff:a.b.c.d
|
||||
return isPublicIPv4(Array(bytes.suffix(4)))
|
||||
}
|
||||
if Array(bytes.prefix(12)).allSatisfy({ $0 == 0 }) { // ::a.b.c.d (deprecated)
|
||||
return isPublicIPv4(Array(bytes.suffix(4)))
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
|
|
|||
144
submodules/WebUI/Sources/WebAppUtils.swift
Normal file
144
submodules/WebUI/Sources/WebAppUtils.swift
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
import Foundation
|
||||
|
||||
func makeWebAppTelegramLink(pathFull: String) -> String? {
|
||||
guard pathFull.hasPrefix("/"), !pathFull.hasPrefix("//") else {
|
||||
return nil
|
||||
}
|
||||
if pathFull.rangeOfCharacter(from: .whitespacesAndNewlines) != nil {
|
||||
return nil
|
||||
}
|
||||
if pathFull.unicodeScalars.contains(where: { $0.value < 0x20 || $0.value == 0x7f }) {
|
||||
return nil
|
||||
}
|
||||
if pathFull.contains("#") {
|
||||
return nil
|
||||
}
|
||||
|
||||
let urlString = "https://t.me\(pathFull)"
|
||||
guard let url = URL(string: urlString) else {
|
||||
return nil
|
||||
}
|
||||
guard url.scheme?.lowercased() == "https" else {
|
||||
return nil
|
||||
}
|
||||
guard url.host?.lowercased() == "t.me" else {
|
||||
return nil
|
||||
}
|
||||
guard url.user == nil, url.password == nil, url.fragment == nil else {
|
||||
return nil
|
||||
}
|
||||
return url.absoluteString
|
||||
}
|
||||
|
||||
func isAllowedBotMediaUrl(_ urlString: String) -> Bool {
|
||||
guard let escaped = urlString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed),
|
||||
let url = URL(string: escaped) else {
|
||||
return false
|
||||
}
|
||||
guard url.scheme?.lowercased() == "https" else {
|
||||
return false
|
||||
}
|
||||
if url.user != nil || url.password != nil {
|
||||
return false
|
||||
}
|
||||
guard var host = url.host?.lowercased(), !host.isEmpty else {
|
||||
return false
|
||||
}
|
||||
if host.hasPrefix("[") && host.hasSuffix("]") {
|
||||
host = String(host.dropFirst().dropLast())
|
||||
}
|
||||
|
||||
// Strict canonical dotted-decimal IPv4 (4 octets, no leading zeros, each 0-255).
|
||||
// Do NOT use inet_pton here: Darwin's inet_pton accepts "0177.0.0.1" as
|
||||
// decimal 177.0.0.1, but getaddrinfo (used by URLSession) interprets the
|
||||
// same string as octal 127.0.0.1 — the divergence is a loopback bypass.
|
||||
if let v4Bytes = parseCanonicalIPv4(host) {
|
||||
return isPublicIPv4(v4Bytes)
|
||||
}
|
||||
|
||||
// IPv6 only — host must contain ":" so we don't accidentally hand a
|
||||
// numeric-looking hostname to inet_pton.
|
||||
if host.contains(":") {
|
||||
var v6 = in6_addr()
|
||||
if host.withCString({ inet_pton(AF_INET6, $0, &v6) }) == 1 {
|
||||
let bytes = withUnsafeBytes(of: &v6) { ptr -> [UInt8] in
|
||||
return Array(ptr)
|
||||
}
|
||||
return isPublicIPv6(bytes)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Strict DNS-name validation. Anything that doesn't look like a real
|
||||
// FQDN is rejected — this catches non-canonical numeric IP forms
|
||||
// (decimal-32 like "2130706433", octal like "0177.0.0.1", hex like
|
||||
// "0x7f.0.0.1", short forms like "127.1") that the OS resolver may
|
||||
// still treat as 127.0.0.1 even when inet_pton would accept them as
|
||||
// a different value or reject outright.
|
||||
let labels = host.split(separator: ".", omittingEmptySubsequences: false)
|
||||
guard labels.count >= 2 else { return false }
|
||||
for label in labels {
|
||||
guard !label.isEmpty, label.count <= 63 else { return false }
|
||||
if label.first == "-" || label.last == "-" { return false }
|
||||
for ch in label {
|
||||
guard ch.isASCII else { return false }
|
||||
if !(ch.isLetter || ch.isNumber || ch == "-") { return false }
|
||||
}
|
||||
}
|
||||
guard let tld = labels.last, tld.count >= 2, tld.contains(where: { $0.isLetter }) else {
|
||||
return false
|
||||
}
|
||||
|
||||
if host == "localhost" || host.hasSuffix(".localhost") || host.hasSuffix(".local") {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private func parseCanonicalIPv4(_ host: String) -> [UInt8]? {
|
||||
let parts = host.split(separator: ".", omittingEmptySubsequences: false)
|
||||
guard parts.count == 4 else { return nil }
|
||||
var bytes: [UInt8] = []
|
||||
bytes.reserveCapacity(4)
|
||||
for part in parts {
|
||||
guard !part.isEmpty, part.count <= 3 else { return nil }
|
||||
if part.count > 1 && part.first == "0" { return nil } // no leading zeros (octal-spoof)
|
||||
guard part.allSatisfy({ $0.isASCII && $0.isNumber }) else { return nil }
|
||||
guard let value = UInt8(part) else { return nil } // also caps at 255
|
||||
bytes.append(value)
|
||||
}
|
||||
return bytes
|
||||
}
|
||||
|
||||
private func isPublicIPv4(_ bytes: [UInt8]) -> Bool {
|
||||
guard bytes.count == 4 else { return false }
|
||||
let a = bytes[0]
|
||||
let b = bytes[1]
|
||||
if a == 0 { return false } // 0.0.0.0/8
|
||||
if a == 10 { return false } // 10.0.0.0/8
|
||||
if a == 127 { return false } // 127.0.0.0/8 loopback
|
||||
if a == 169 && b == 254 { return false } // 169.254.0.0/16 link-local
|
||||
if a == 172 && (b & 0xf0) == 16 { return false } // 172.16.0.0/12
|
||||
if a == 192 && b == 168 { return false } // 192.168.0.0/16
|
||||
if a == 100 && (b & 0xc0) == 64 { return false } // 100.64.0.0/10 CGNAT
|
||||
if a >= 224 { return false } // multicast + reserved + 255.255.255.255
|
||||
return true
|
||||
}
|
||||
|
||||
private func isPublicIPv6(_ bytes: [UInt8]) -> Bool {
|
||||
guard bytes.count == 16 else { return false }
|
||||
if bytes.allSatisfy({ $0 == 0 }) { return false } // ::
|
||||
let loopback: [UInt8] = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1]
|
||||
if bytes == loopback { return false } // ::1
|
||||
if bytes[0] == 0xff { return false } // ff00::/8 multicast
|
||||
if bytes[0] == 0xfe && (bytes[1] & 0xc0) == 0x80 { return false } // fe80::/10 link-local
|
||||
if (bytes[0] & 0xfe) == 0xfc { return false } // fc00::/7 unique-local
|
||||
let v4MappedPrefix: [UInt8] = [0,0,0,0,0,0,0,0,0,0,0xff,0xff]
|
||||
if Array(bytes.prefix(12)) == v4MappedPrefix { // ::ffff:a.b.c.d
|
||||
return isPublicIPv4(Array(bytes.suffix(4)))
|
||||
}
|
||||
if Array(bytes.prefix(12)).allSatisfy({ $0 == 0 }) { // ::a.b.c.d (deprecated)
|
||||
return isPublicIPv4(Array(bytes.suffix(4)))
|
||||
}
|
||||
return true
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue