RedditWebAuthHelper.swift 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. import Foundation
  2. enum RedditWebAuthHelper {
  3. static let redditHomeURL = URL(string: "https://www.reddit.com")!
  4. static let redditLoginURL = URL(string: "https://www.reddit.com/login/")!
  5. static let safariUserAgent =
  6. "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.4 Safari/605.1.15"
  7. private static let oauthHosts = [
  8. "accounts.google.com",
  9. "appleid.apple.com",
  10. "idmsa.apple.com",
  11. "gsa.apple.com",
  12. "login.microsoftonline.com",
  13. "login.live.com",
  14. "account.live.com",
  15. "login.microsoft.com",
  16. "www.facebook.com",
  17. "m.facebook.com",
  18. "facebook.com",
  19. "auth0.com",
  20. ]
  21. private static let authFlowPathPrefixes = [
  22. "/login",
  23. "/register",
  24. "/password",
  25. "/account/login",
  26. "/account/register",
  27. "/account/signup",
  28. ]
  29. static func isOAuthProviderURL(_ url: URL) -> Bool {
  30. guard let host = url.host?.lowercased() else { return false }
  31. if oauthHosts.contains(where: { host == $0 || host.hasSuffix(".\($0)") }) {
  32. return true
  33. }
  34. if host == "google.com" || host.hasSuffix(".google.com") {
  35. let path = url.path.lowercased()
  36. return path.contains("oauth")
  37. || path.contains("signin")
  38. || path.contains("gsi")
  39. }
  40. if host == "apple.com" || host.hasSuffix(".apple.com") {
  41. let path = url.path.lowercased()
  42. return path.contains("auth")
  43. || path.contains("signin")
  44. || path.contains("sso")
  45. }
  46. if host == "facebook.com" || host.hasSuffix(".facebook.com") {
  47. let path = url.path.lowercased()
  48. return path.contains("oauth") || path.contains("login") || path.contains("dialog")
  49. }
  50. if host.hasSuffix(".auth0.com") {
  51. return true
  52. }
  53. return false
  54. }
  55. static func isRedditHomeURL(_ url: URL) -> Bool {
  56. guard isRedditURL(url) else { return false }
  57. let host = url.host?.lowercased() ?? ""
  58. let isMainSite = host == "reddit.com"
  59. || host == "www.reddit.com"
  60. || host == "new.reddit.com"
  61. || host == "old.reddit.com"
  62. guard isMainSite else { return false }
  63. let path = url.path.lowercased()
  64. return path.isEmpty || path == "/" || path == "/home"
  65. }
  66. static func isRedditURL(_ url: URL) -> Bool {
  67. guard let host = url.host?.lowercased() else { return false }
  68. let redditHosts = [
  69. "reddit.com",
  70. "www.reddit.com",
  71. "old.reddit.com",
  72. "new.reddit.com",
  73. "accounts.reddit.com",
  74. "oauth.reddit.com",
  75. "reddit.app.link",
  76. "redd.it",
  77. ]
  78. return redditHosts.contains(where: { host == $0 || host.hasSuffix(".\($0)") })
  79. }
  80. static func isAuthFlowURL(_ url: URL) -> Bool {
  81. guard isRedditURL(url) else { return false }
  82. let path = url.path.lowercased()
  83. return authFlowPathPrefixes.contains { prefix in
  84. path == prefix || path.hasPrefix("\(prefix)/")
  85. }
  86. }
  87. static func isCredentialLoginPage(_ url: URL) -> Bool {
  88. guard isRedditURL(url) else { return false }
  89. let path = url.path.lowercased()
  90. guard path == "/login" || path.hasPrefix("/login/") else { return false }
  91. let host = url.host?.lowercased() ?? ""
  92. return host == "www.reddit.com" || host == "reddit.com" || host == "new.reddit.com"
  93. }
  94. static func isAccountsRedditHost(_ url: URL) -> Bool {
  95. url.host?.lowercased() == "accounts.reddit.com"
  96. }
  97. /// Auth subframes to accounts.reddit.com are required on the modern login page.
  98. /// Only redirect subframe auth loads when the main frame is not already on login.
  99. static func shouldRedirectAuthSubframe(mainFrameURL: URL?) -> Bool {
  100. guard let mainFrameURL else { return true }
  101. if isCredentialLoginPage(mainFrameURL) { return false }
  102. return !isAuthFlowURL(mainFrameURL)
  103. }
  104. static func looksLikeLoggedInURL(_ url: URL) -> Bool {
  105. guard isRedditURL(url) else { return false }
  106. if isAuthFlowURL(url) { return false }
  107. let path = url.path.lowercased()
  108. if url.host?.lowercased() == "accounts.reddit.com" {
  109. if path.contains("/oauth") || path.contains("/authorize") || path.contains("/login") {
  110. return false
  111. }
  112. return path.isEmpty || path == "/" || path.hasPrefix("/api/")
  113. }
  114. return true
  115. }
  116. /// Returns true when the OAuth completion panel can close and hand off to the main WebView.
  117. static func shouldCompleteOAuthSignIn(for url: URL) -> Bool {
  118. guard isRedditURL(url) else { return false }
  119. if isAuthFlowURL(url) { return false }
  120. let host = url.host?.lowercased() ?? ""
  121. let path = url.path.lowercased()
  122. if host == "accounts.reddit.com" {
  123. if path.contains("/oauth")
  124. || path.contains("/authorize")
  125. || path.contains("/login")
  126. || path.hasPrefix("/api/") {
  127. return false
  128. }
  129. return path.isEmpty || path == "/"
  130. }
  131. if host == "www.reddit.com"
  132. || host == "reddit.com"
  133. || host == "new.reddit.com"
  134. || host == "old.reddit.com" {
  135. return true
  136. }
  137. return looksLikeLoggedInURL(url)
  138. }
  139. static func oauthCallbackHost(for url: URL) -> String {
  140. if let host = parsedRedirectHost(from: url) {
  141. return host
  142. }
  143. return "accounts.reddit.com"
  144. }
  145. private static func parsedRedirectHost(from url: URL) -> String? {
  146. guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
  147. return nil
  148. }
  149. let redirectValue = components.queryItems?.first(where: {
  150. $0.name == "redirect_uri" || $0.name == "redirect_url" || $0.name == "redirect"
  151. })?.value
  152. guard let redirectValue else { return nil }
  153. let decoded = redirectValue.removingPercentEncoding ?? redirectValue
  154. if let host = hostFromRedirectString(decoded) {
  155. return host
  156. }
  157. if !decoded.contains("://"), let host = hostFromRedirectString("https://\(decoded)") {
  158. return host
  159. }
  160. return nil
  161. }
  162. private static func hostFromRedirectString(_ value: String) -> String? {
  163. guard let redirectURL = URL(string: value),
  164. let host = redirectURL.host?.lowercased(),
  165. isRedditURL(redirectURL) else {
  166. return nil
  167. }
  168. return host
  169. }
  170. }