Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: correctly handle url containing hash but without search #593

Merged
merged 3 commits into from
Jan 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 17 additions & 63 deletions xcode/Ext-Safari/Functions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,29 +77,12 @@ func openSaveLocation() -> Bool {
}

func validateUrl(_ urlString: String) -> Bool {
var urlChecked = urlString
// if the url is already encoded, decode it
if isEncoded(urlChecked) {
if let decodedUrl = urlChecked.removingPercentEncoding {
urlChecked = decodedUrl
} else {
logger?.error("\(#function, privacy: .public) - failed at (1), couldn't decode url, \(urlString, privacy: .public)")
return false
}
}
// encode all urls strings
if let encodedUrl = urlChecked.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) {
urlChecked = encodedUrl
} else {
logger?.error("\(#function, privacy: .public) - failed at (2), couldn't percent encode url, \(urlString, privacy: .public)")
return false
}
guard
let parts = getUrlProps(urlChecked),
let parts = jsLikeURL(urlString),
let ptcl = parts["protocol"],
let path = parts["pathname"]
else {
logger?.error("\(#function, privacy: .public) - failed at (3) for \(urlString, privacy: .public)")
logger?.error("\(#function, privacy: .public) - Invalid URL: \(urlString, privacy: .public)")
return false
}
if
Expand Down Expand Up @@ -129,7 +112,10 @@ func isVersionNewer(_ oldVersion: String, _ newVersion: String) -> Bool {
}

func isEncoded(_ str: String) -> Bool {
return str.removingPercentEncoding != str
if let decoded = str.removingPercentEncoding {
return decoded != str
}
return false
}

// parser
Expand Down Expand Up @@ -888,25 +874,9 @@ func getRemoteFileContents(_ url: String) -> String? {
urlChecked = urlChecked.replacingOccurrences(of: "http:", with: "https:")
logger?.info("\(#function, privacy: .public) - \(url, privacy: .public) is using insecure http, attempt to fetch remote content with https")
}
// if the url is already encoded, decode it
if isEncoded(urlChecked) {
if let decodedUrl = urlChecked.removingPercentEncoding {
urlChecked = decodedUrl
} else {
logger?.error("\(#function, privacy: .public) - failed at (1), couldn't decode url, \(url, privacy: .public)")
return nil
}
}
// encode all urls strings
if let encodedUrl = urlChecked.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) {
urlChecked = encodedUrl
} else {
logger?.error("\(#function, privacy: .public) - failed at (2), couldn't percent encode url, \(url, privacy: .public)")
return nil
}
// convert url string to url
guard let solidURL = URL(string: urlChecked) else {
logger?.error("\(#function, privacy: .public) - failed at (3), couldn't convert string to url, \(url, privacy: .public)")
guard let solidURL = fixedURL(string: urlChecked) else {
logger?.error("\(#function, privacy: .public) - failed at (1), invalid URL: \(url, privacy: .public)")
return nil
}
var contents = ""
Expand All @@ -917,19 +887,25 @@ func getRemoteFileContents(_ url: String) -> String? {
if let r = response as? HTTPURLResponse, data != nil, error == nil {
if r.statusCode == 200 {
contents = String(data: data!, encoding: .utf8) ?? ""
} else {
logger?.error("\(#function, privacy: .public) - http statusCode (\(r.statusCode, privacy: .public)): \(url, privacy: .public)")
}
}
if let error = error {
logger?.error("\(#function, privacy: .public) - task error: \(error.localizedDescription, privacy: .public) (\(url, privacy: .public))")
}
semaphore.signal()
}
task?.resume()
// wait 30 seconds before timing out
if semaphore.wait(timeout: .now() + 30) == .timedOut {
task?.cancel()
logger?.error("\(#function, privacy: .public) - 30 seconds timeout: \(url, privacy: .public)")
}

// if made it to this point and contents still an empty string, something went wrong with the request
if contents.isEmpty {
logger?.error("\(#function, privacy: .public) - failed at (4), contents empty, \(url, privacy: .public)")
logger?.error("\(#function, privacy: .public) - failed at (2), contents empty: \(url, privacy: .public)")
return nil
}
logger?.info("\(#function, privacy: .public) - completed for \(url, privacy: .public)")
Expand Down Expand Up @@ -1032,28 +1008,6 @@ func checkDefaultDirectories() -> Bool {
}

// matching
func getUrlProps(_ url: String) -> [String: String]? {
guard
let parts = URLComponents(string: url),
let ptcl = parts.scheme,
let host = parts.host
else {
logger?.error("\(#function, privacy: .public) - failed to parse url")
return nil
}
var search = ""
if let query = parts.query {
search = "?" + query
}
return [
"protocol": "\(ptcl):",
"host": host,
"pathname": parts.path,
"search": search,
"href": url
]
}

func stringToRegex(_ stringPattern: String) -> NSRegularExpression? {
let pattern = #"[\.|\?|\^|\$|\+|\{|\}|\[|\]|\||\\(|\)|\/]"#
var patternReplace = "^\(stringPattern.replacingOccurrences(of: pattern, with: #"\\$0"#, options: .regularExpression))$"
Expand All @@ -1066,9 +1020,9 @@ func stringToRegex(_ stringPattern: String) -> NSRegularExpression? {

func match(_ url: String, _ matchPattern: String) -> Bool {
guard
let parts = getUrlProps(url),
let parts = jsLikeURL(url),
let ptcl = parts["protocol"],
let host = parts["host"],
let host = parts["hostname"],
var path = parts["pathname"]
else {
logger?.error("\(#function, privacy: .public) - invalid url \(url, privacy: .public)")
Expand Down
96 changes: 96 additions & 0 deletions xcode/Shared/UrlPolyfill.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import Foundation

extension CharacterSet {
// https://developer.apple.com/documentation/foundation/characterset#2902136
public static let urlAllowed_ = CharacterSet(charactersIn: "#")
.union(.urlFragmentAllowed)
.union(.urlHostAllowed)
.union(.urlPasswordAllowed)
.union(.urlPathAllowed)
.union(.urlQueryAllowed)
.union(.urlUserAllowed)
}

// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURI
func encodeURI(_ uri: String) -> String {
// https://developer.apple.com/documentation/foundation/characterset#2902136
// var urlAllowed = CharacterSet(charactersIn: "#")
// urlAllowed.formUnion(.urlFragmentAllowed)
// urlAllowed.formUnion(.urlHostAllowed)
// urlAllowed.formUnion(.urlPasswordAllowed)
// urlAllowed.formUnion(.urlPathAllowed)
// urlAllowed.formUnion(.urlQueryAllowed)
// urlAllowed.formUnion(.urlUserAllowed)
return uri.addingPercentEncoding(withAllowedCharacters: .urlAllowed_) ?? uri
}

func fixedURL(string urlString: String) -> URL? {
let rawUrlString = urlString.removingPercentEncoding ?? urlString
var url: URL?
if #available(macOS 14.0, iOS 17.0, *) {
url = URL(string: rawUrlString, encodingInvalidCharacters: true)
} else {
url = URL(string: encodeURI(rawUrlString))
}
return url
}

// https://developer.mozilla.org/en-US/docs/Web/API/URL
func jsLikeURL(_ urlString: String, baseString: String? = nil) -> [String: String]? {
var _url: URL?
if let baseString = baseString {
guard let baseUrl = fixedURL(string: baseString) else {
return nil
}
_url = URL(string: urlString, relativeTo: baseUrl)
} else {
_url = fixedURL(string: urlString)
}
guard let url = _url else {
return nil
}

guard let scheme = url.scheme else {
return nil
}
var port = (url.port == nil) ? "" : String(url.port!)
if (scheme == "http" && port == "80") { port = "" }
if (scheme == "https" && port == "443") { port = "" }
if #available(macOS 13.0, iOS 16.0, *) {
let hostname = url.host(percentEncoded: true) ?? ""
let host = (port == "") ? hostname : "\(hostname):\(port)"
let query = url.query(percentEncoded: true) ?? ""
let fragment = url.fragment(percentEncoded: true) ?? ""
return [
"hash": fragment == "" ? "" : "#\(fragment)",
"host": host,
"hostname": hostname,
// "href": url.absoluteString,
"origin": "\(scheme)://\(host)",
"password": url.password(percentEncoded: true) ?? "",
"pathname": url.path(percentEncoded: true),
"port": port,
"protocol": "\(scheme):",
"search": query == "" ? "" : "?\(query)",
"username": url.user(percentEncoded: true) ?? ""
]
} else {
let hostname = url.host ?? ""
let host = (port == "") ? hostname : "\(hostname):\(port)"
let query = url.query ?? ""
let fragment = url.fragment ?? ""
return [
"hash": fragment == "" ? "" : "#\(fragment)",
"host": host,
"hostname": hostname,
// "href": url.absoluteString,
"origin": "\(scheme)://\(host)",
"password": url.password ?? "",
"pathname": url.path.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? url.path,
"port": port,
"protocol": "\(scheme):",
"search": query == "" ? "" : "?\(query)",
"username": url.user?.addingPercentEncoding(withAllowedCharacters: .urlUserAllowed) ?? ""
]
}
}
Loading
Loading