RedditWebAuthHelper.swift 5.5 KB

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