-
Notifications
You must be signed in to change notification settings - Fork 16
/
Copy pathNonceRetrieval.swift
96 lines (78 loc) · 3.38 KB
/
NonceRetrieval.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
import Foundation
import WordPressShared
enum NonceRetrievalMethod {
case newPostScrap
case ajaxNonceRequest
func retrieveNonce(username: String, password: Secret<String>, loginURL: URL, adminURL: URL, using urlSession: URLSession) async -> String? {
guard let webpageThatContainsNonce = buildURL(base: adminURL) else { return nil }
// First, make a request to the URL to grab REST API nonce. The HTTP request is very likely to pass, because
// when this method is called, user should have already authenticated and their site's cookies are already in
// the `urlSession.
if let found = await nonce(from: webpageThatContainsNonce, using: urlSession) {
return found
}
// If the above request failed, then make a login request, which redirects to the webpage that contains
// REST API nonce.
let loginThenRedirect = HTTPRequestBuilder(url: loginURL)
.method(.post)
.body(form: [
"log": username,
"pwd": password.secretValue,
"rememberme": "true",
"redirect_to": webpageThatContainsNonce.absoluteString
])
return await nonce(from: loginThenRedirect, using: urlSession)
}
private func buildURL(base: URL) -> URL? {
switch self {
case .newPostScrap:
return URL(string: "post-new.php", relativeTo: base)
case .ajaxNonceRequest:
return URL(string: "admin-ajax.php?action=rest-nonce", relativeTo: base)
}
}
private func retrieveNonce(from html: String) -> String? {
switch self {
case .newPostScrap:
return scrapNonceFromNewPost(html: html)
case .ajaxNonceRequest:
return readNonceFromAjaxAction(html: html)
}
}
private func scrapNonceFromNewPost(html: String) -> String? {
guard let regex = try? NSRegularExpression(pattern: "apiFetch.createNonceMiddleware\\(\\s*['\"](?<nonce>\\w+)['\"]\\s*\\)", options: []),
let match = regex.firstMatch(in: html, options: [], range: NSRange(location: 0, length: html.count)) else {
return nil
}
let nsrange = match.range(withName: "nonce")
let nonce = Range(nsrange, in: html)
.map({ html[$0] })
.map( String.init )
return nonce
}
private func readNonceFromAjaxAction(html: String) -> String? {
guard !html.isEmpty,
html.allSatisfy({ $0.isNumber || $0.isLetter })
else {
return nil
}
return html
}
}
private extension NonceRetrievalMethod {
func nonce(from url: URL, using urlSession: URLSession) async -> String? {
await nonce(from: HTTPRequestBuilder(url: url), using: urlSession)
}
func nonce(from builder: HTTPRequestBuilder, using urlSession: URLSession) async -> String? {
guard let request = try? builder.build() else { return nil }
guard let (data, response) = try? await urlSession.data(for: request),
let httpResponse = response as? HTTPURLResponse
else {
return nil
}
guard 200...299 ~= httpResponse.statusCode, let content = HTTPAPIResponse(response: httpResponse, body: data).bodyText else {
return nil
}
return retrieveNonce(from: content)
}
}