]>
Commit | Line | Data |
---|---|---|
e90f3b84 | 1 | #!/usr/bin/env sh |
2 | ||
83b1a98d | 3 | WIKI="https://github.com/Neilpang/acme.sh/wiki/How-to-use-Azure-DNS" |
4 | ||
e90f3b84 | 5 | ######## Public functions ##################### |
6 | ||
3fdbbafc | 7 | # Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" |
e90f3b84 | 8 | # Used to add txt record |
9 | # | |
10 | # Ref: https://docs.microsoft.com/en-us/rest/api/dns/recordsets/createorupdate | |
11 | # | |
91607bb2 | 12 | dns_azure_add() { |
13 | fulldomain=$1 | |
14 | txtvalue=$2 | |
15 | ||
16 | AZUREDNS_SUBSCRIPTIONID="${AZUREDNS_SUBSCRIPTIONID:-$(_readaccountconf_mutable AZUREDNS_SUBSCRIPTIONID)}" | |
17 | AZUREDNS_TENANTID="${AZUREDNS_TENANTID:-$(_readaccountconf_mutable AZUREDNS_TENANTID)}" | |
18 | AZUREDNS_APPID="${AZUREDNS_APPID:-$(_readaccountconf_mutable AZUREDNS_APPID)}" | |
19 | AZUREDNS_CLIENTSECRET="${AZUREDNS_CLIENTSECRET:-$(_readaccountconf_mutable AZUREDNS_CLIENTSECRET)}" | |
dd171ca4 | 20 | |
91607bb2 | 21 | if [ -z "$AZUREDNS_SUBSCRIPTIONID" ]; then |
22 | AZUREDNS_SUBSCRIPTIONID="" | |
23 | AZUREDNS_TENANTID="" | |
24 | AZUREDNS_APPID="" | |
25 | AZUREDNS_CLIENTSECRET="" | |
26 | _err "You didn't specify the Azure Subscription ID " | |
27 | return 1 | |
28 | fi | |
e90f3b84 | 29 | |
dd171ca4 | 30 | if [ -z "$AZUREDNS_TENANTID" ]; then |
91607bb2 | 31 | AZUREDNS_SUBSCRIPTIONID="" |
32 | AZUREDNS_TENANTID="" | |
33 | AZUREDNS_APPID="" | |
e4b24d20 | 34 | AZUREDNS_CLIENTSECRET="" |
72fe7396 | 35 | _err "You didn't specify the Azure Tenant ID " |
e4b24d20 | 36 | return 1 |
91607bb2 | 37 | fi |
38 | ||
dd171ca4 | 39 | if [ -z "$AZUREDNS_APPID" ]; then |
91607bb2 | 40 | AZUREDNS_SUBSCRIPTIONID="" |
41 | AZUREDNS_TENANTID="" | |
42 | AZUREDNS_APPID="" | |
43 | AZUREDNS_CLIENTSECRET="" | |
44 | _err "You didn't specify the Azure App ID" | |
45 | return 1 | |
46 | fi | |
47 | ||
48 | if [ -z "$AZUREDNS_CLIENTSECRET" ]; then | |
49 | AZUREDNS_SUBSCRIPTIONID="" | |
50 | AZUREDNS_TENANTID="" | |
51 | AZUREDNS_APPID="" | |
52 | AZUREDNS_CLIENTSECRET="" | |
53 | _err "You didn't specify the Azure Client Secret" | |
54 | return 1 | |
55 | fi | |
56 | #save account details to account conf file. | |
57 | _saveaccountconf_mutable AZUREDNS_SUBSCRIPTIONID "$AZUREDNS_SUBSCRIPTIONID" | |
58 | _saveaccountconf_mutable AZUREDNS_TENANTID "$AZUREDNS_TENANTID" | |
59 | _saveaccountconf_mutable AZUREDNS_APPID "$AZUREDNS_APPID" | |
60 | _saveaccountconf_mutable AZUREDNS_CLIENTSECRET "$AZUREDNS_CLIENTSECRET" | |
e90f3b84 | 61 | |
91607bb2 | 62 | accesstoken=$(_azure_getaccess_token "$AZUREDNS_TENANTID" "$AZUREDNS_APPID" "$AZUREDNS_CLIENTSECRET") |
3fdbbafc | 63 | |
dd171ca4 | 64 | if ! _get_root "$fulldomain" "$AZUREDNS_SUBSCRIPTIONID" "$accesstoken"; then |
65 | _err "invalid domain" | |
66 | return 1 | |
91607bb2 | 67 | fi |
68 | _debug _domain_id "$_domain_id" | |
69 | _debug _sub_domain "$_sub_domain" | |
70 | _debug _domain "$_domain" | |
71 | ||
dd171ca4 | 72 | acmeRecordURI="https://management.azure.com$(printf '%s' "$_domain_id" | sed 's/\\//g')/TXT/$_sub_domain?api-version=2017-09-01" |
91607bb2 | 73 | _debug "$acmeRecordURI" |
83b1a98d | 74 | # Get existing TXT record |
75 | _azure_rest GET "$acmeRecordURI" "" "$accesstoken" | |
76 | values="{\"value\":[\"$txtvalue\"]}" | |
77 | timestamp="$(_time)" | |
78 | if [ "$_code" = "200" ]; then | |
9e3c931b | 79 | vlist="$(echo "$response" | _egrep_o "\"value\"\\s*:\\s*\\[\\s*\"[^\"]*\"\\s*]" | cut -d : -f 2 | tr -d "[]\"")" |
83b1a98d | 80 | _debug "existing TXT found" |
81 | _debug "$vlist" | |
9e3c931b | 82 | existingts="$(echo "$response" | _egrep_o "\"acmetscheck\"\\s*:\\s*\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d "\"")" |
83b1a98d | 83 | if [ -z "$existingts" ]; then |
84 | # the record was not created by acme.sh. Copy the exisiting entires | |
85 | existingts=$timestamp | |
86 | fi | |
87 | _diff="$(_math "$timestamp - $existingts")" | |
88 | _debug "existing txt age: $_diff" | |
89 | # only use recently added records and discard if older than 2 hours because they are probably orphaned | |
90 | if [ "$_diff" -lt 7200 ]; then | |
91 | _debug "existing txt value: $vlist" | |
92 | for v in $vlist; do | |
93 | values="$values ,{\"value\":[\"$v\"]}" | |
94 | done | |
95 | fi | |
96 | fi | |
97 | # Add the txtvalue TXT Record | |
98 | body="{\"properties\":{\"metadata\":{\"acmetscheck\":\"$timestamp\"},\"TTL\":10, \"TXTRecords\":[$values]}}" | |
91607bb2 | 99 | _azure_rest PUT "$acmeRecordURI" "$body" "$accesstoken" |
100 | if [ "$_code" = "200" ] || [ "$_code" = '201' ]; then | |
83b1a98d | 101 | _info "validation value added" |
224e0c29 | 102 | return 0 |
91607bb2 | 103 | else |
83b1a98d | 104 | _err "error adding validation value ($_code)" |
e90f3b84 | 105 | return 1 |
dd171ca4 | 106 | fi |
e90f3b84 | 107 | } |
108 | ||
109 | # Usage: fulldomain txtvalue | |
110 | # Used to remove the txt record after validation | |
111 | # | |
112 | # Ref: https://docs.microsoft.com/en-us/rest/api/dns/recordsets/delete | |
113 | # | |
91607bb2 | 114 | dns_azure_rm() { |
115 | fulldomain=$1 | |
116 | txtvalue=$2 | |
117 | ||
118 | AZUREDNS_SUBSCRIPTIONID="${AZUREDNS_SUBSCRIPTIONID:-$(_readaccountconf_mutable AZUREDNS_SUBSCRIPTIONID)}" | |
119 | AZUREDNS_TENANTID="${AZUREDNS_TENANTID:-$(_readaccountconf_mutable AZUREDNS_TENANTID)}" | |
120 | AZUREDNS_APPID="${AZUREDNS_APPID:-$(_readaccountconf_mutable AZUREDNS_APPID)}" | |
121 | AZUREDNS_CLIENTSECRET="${AZUREDNS_CLIENTSECRET:-$(_readaccountconf_mutable AZUREDNS_CLIENTSECRET)}" | |
122 | ||
123 | if [ -z "$AZUREDNS_SUBSCRIPTIONID" ]; then | |
124 | AZUREDNS_SUBSCRIPTIONID="" | |
125 | AZUREDNS_TENANTID="" | |
126 | AZUREDNS_APPID="" | |
127 | AZUREDNS_CLIENTSECRET="" | |
128 | _err "You didn't specify the Azure Subscription ID " | |
129 | return 1 | |
130 | fi | |
131 | ||
dd171ca4 | 132 | if [ -z "$AZUREDNS_TENANTID" ]; then |
91607bb2 | 133 | AZUREDNS_SUBSCRIPTIONID="" |
134 | AZUREDNS_TENANTID="" | |
135 | AZUREDNS_APPID="" | |
136 | AZUREDNS_CLIENTSECRET="" | |
137 | _err "You didn't specify the Azure Tenant ID " | |
138 | return 1 | |
139 | fi | |
140 | ||
8c887574 | 141 | if [ -z "$AZUREDNS_APPID" ]; then |
91607bb2 | 142 | AZUREDNS_SUBSCRIPTIONID="" |
143 | AZUREDNS_TENANTID="" | |
144 | AZUREDNS_APPID="" | |
145 | AZUREDNS_CLIENTSECRET="" | |
146 | _err "You didn't specify the Azure App ID" | |
147 | return 1 | |
148 | fi | |
149 | ||
150 | if [ -z "$AZUREDNS_CLIENTSECRET" ]; then | |
151 | AZUREDNS_SUBSCRIPTIONID="" | |
152 | AZUREDNS_TENANTID="" | |
153 | AZUREDNS_APPID="" | |
154 | AZUREDNS_CLIENTSECRET="" | |
72fe7396 | 155 | _err "You didn't specify the Azure Client Secret" |
91607bb2 | 156 | return 1 |
157 | fi | |
e90f3b84 | 158 | |
91607bb2 | 159 | accesstoken=$(_azure_getaccess_token "$AZUREDNS_TENANTID" "$AZUREDNS_APPID" "$AZUREDNS_CLIENTSECRET") |
3fdbbafc | 160 | |
dd171ca4 | 161 | if ! _get_root "$fulldomain" "$AZUREDNS_SUBSCRIPTIONID" "$accesstoken"; then |
162 | _err "invalid domain" | |
163 | return 1 | |
91607bb2 | 164 | fi |
165 | _debug _domain_id "$_domain_id" | |
166 | _debug _sub_domain "$_sub_domain" | |
167 | _debug _domain "$_domain" | |
168 | ||
dd171ca4 | 169 | acmeRecordURI="https://management.azure.com$(printf '%s' "$_domain_id" | sed 's/\\//g')/TXT/$_sub_domain?api-version=2017-09-01" |
91607bb2 | 170 | _debug "$acmeRecordURI" |
83b1a98d | 171 | # Get existing TXT record |
172 | _azure_rest GET "$acmeRecordURI" "" "$accesstoken" | |
173 | timestamp="$(_time)" | |
174 | if [ "$_code" = "200" ]; then | |
9e3c931b | 175 | vlist="$(echo "$response" | _egrep_o "\"value\"\\s*:\\s*\\[\\s*\"[^\"]*\"\\s*]" | cut -d : -f 2 | tr -d "[]\"" | grep -v "$txtvalue")" |
83b1a98d | 176 | values="" |
177 | comma="" | |
178 | for v in $vlist; do | |
179 | values="$values$comma{\"value\":[\"$v\"]}" | |
180 | comma="," | |
181 | done | |
182 | if [ -z "$values" ]; then | |
183 | # No values left remove record | |
184 | _debug "removing validation record completely $acmeRecordURI" | |
185 | _azure_rest DELETE "$acmeRecordURI" "" "$accesstoken" | |
186 | if [ "$_code" = "200" ] || [ "$_code" = '204' ]; then | |
187 | _info "validation record removed" | |
188 | else | |
189 | _err "error removing validation record ($_code)" | |
190 | return 1 | |
191 | fi | |
192 | else | |
193 | # Remove only txtvalue from the TXT Record | |
194 | body="{\"properties\":{\"metadata\":{\"acmetscheck\":\"$timestamp\"},\"TTL\":10, \"TXTRecords\":[$values]}}" | |
195 | _azure_rest PUT "$acmeRecordURI" "$body" "$accesstoken" | |
196 | if [ "$_code" = "200" ] || [ "$_code" = '201' ]; then | |
197 | _info "validation value removed" | |
224e0c29 | 198 | return 0 |
83b1a98d | 199 | else |
200 | _err "error removing validation value ($_code)" | |
201 | return 1 | |
202 | fi | |
203 | fi | |
91607bb2 | 204 | fi |
e90f3b84 | 205 | } |
206 | ||
207 | ################### Private functions below ################################## | |
208 | ||
209 | _azure_rest() { | |
91607bb2 | 210 | m=$1 |
211 | ep="$2" | |
212 | data="$3" | |
213 | accesstoken="$4" | |
214 | ||
83b1a98d | 215 | MAX_REQUEST_RETRY_TIMES=5 |
216 | _request_retry_times=0 | |
217 | while [ "${_request_retry_times}" -lt "$MAX_REQUEST_RETRY_TIMES" ]; do | |
218 | _debug3 _request_retry_times "$_request_retry_times" | |
219 | export _H1="authorization: Bearer $accesstoken" | |
220 | export _H2="accept: application/json" | |
221 | export _H3="Content-Type: application/json" | |
222 | # clear headers from previous request to avoid getting wrong http code on timeouts | |
223 | :>"$HTTP_HEADER" | |
224 | _debug "$ep" | |
225 | if [ "$m" != "GET" ]; then | |
226 | _secure_debug2 "data $data" | |
227 | response="$(_post "$data" "$ep" "" "$m")" | |
228 | else | |
229 | response="$(_get "$ep")" | |
230 | fi | |
224e0c29 | 231 | _ret="$?" |
83b1a98d | 232 | _secure_debug2 "response $response" |
9e3c931b | 233 | _code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\\r\\n")" |
83b1a98d | 234 | _debug "http response code $_code" |
235 | if [ "$_code" = "401" ]; then | |
236 | # we have an invalid access token set to expired | |
237 | _saveaccountconf_mutable AZUREDNS_TOKENVALIDTO "0" | |
238 | _err "access denied make sure your Azure settings are correct. See $WIKI" | |
239 | return 1 | |
240 | fi | |
241 | # See https://docs.microsoft.com/en-us/azure/architecture/best-practices/retry-service-specific#general-rest-and-retry-guidelines for retryable HTTP codes | |
224e0c29 | 242 | if [ "$_ret" != "0" ] || [ -z "$_code" ] || [ "$_code" = "408" ] || [ "$_code" = "500" ] || [ "$_code" = "503" ] || [ "$_code" = "504" ]; then |
83b1a98d | 243 | _request_retry_times="$(_math "$_request_retry_times" + 1)" |
244 | _info "REST call error $_code retrying $ep in $_request_retry_times s" | |
245 | _sleep "$_request_retry_times" | |
246 | continue | |
247 | fi | |
248 | break | |
249 | done | |
250 | if [ "$_request_retry_times" = "$MAX_REQUEST_RETRY_TIMES" ]; then | |
251 | _err "Error Azure REST called was retried $MAX_REQUEST_RETRY_TIMES times." | |
252 | _err "Calling $ep failed." | |
dd171ca4 | 253 | return 1 |
91607bb2 | 254 | fi |
83b1a98d | 255 | response="$(echo "$response" | _normalizeJson)" |
91607bb2 | 256 | return 0 |
e90f3b84 | 257 | } |
258 | ||
259 | ## Ref: https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-protocols-oauth-service-to-service#request-an-access-token | |
260 | _azure_getaccess_token() { | |
83b1a98d | 261 | tenantID=$1 |
91607bb2 | 262 | clientID=$2 |
263 | clientSecret=$3 | |
264 | ||
83b1a98d | 265 | accesstoken="${AZUREDNS_BEARERTOKEN:-$(_readaccountconf_mutable AZUREDNS_BEARERTOKEN)}" |
266 | expires_on="${AZUREDNS_TOKENVALIDTO:-$(_readaccountconf_mutable AZUREDNS_TOKENVALIDTO)}" | |
267 | ||
268 | # can we reuse the bearer token? | |
269 | if [ -n "$accesstoken" ] && [ -n "$expires_on" ]; then | |
270 | if [ "$(_time)" -lt "$expires_on" ]; then | |
271 | # brearer token is still valid - reuse it | |
272 | _debug "reusing bearer token" | |
273 | printf "%s" "$accesstoken" | |
274 | return 0 | |
275 | else | |
276 | _debug "bearer token expired" | |
277 | fi | |
278 | fi | |
279 | _debug "getting new bearer token" | |
280 | ||
91607bb2 | 281 | export _H1="accept: application/json" |
282 | export _H2="Content-Type: application/x-www-form-urlencoded" | |
283 | ||
dd171ca4 | 284 | body="resource=$(printf "%s" 'https://management.core.windows.net/' | _url_encode)&client_id=$(printf "%s" "$clientID" | _url_encode)&client_secret=$(printf "%s" "$clientSecret" | _url_encode)&grant_type=client_credentials" |
83b1a98d | 285 | _secure_debug2 "data $body" |
286 | response="$(_post "$body" "https://login.microsoftonline.com/$tenantID/oauth2/token" "" "POST")" | |
224e0c29 | 287 | _ret="$?" |
83b1a98d | 288 | _secure_debug2 "response $response" |
289 | response="$(echo "$response" | _normalizeJson)" | |
dd171ca4 | 290 | accesstoken=$(echo "$response" | _egrep_o "\"access_token\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \") |
83b1a98d | 291 | expires_on=$(echo "$response" | _egrep_o "\"expires_on\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \") |
91607bb2 | 292 | |
8c887574 | 293 | if [ -z "$accesstoken" ]; then |
83b1a98d | 294 | _err "no acccess token received. Check your Azure settings see $WIKI" |
dd171ca4 | 295 | return 1 |
91607bb2 | 296 | fi |
224e0c29 | 297 | if [ "$_ret" != "0" ]; then |
91607bb2 | 298 | _err "error $response" |
299 | return 1 | |
300 | fi | |
83b1a98d | 301 | _saveaccountconf_mutable AZUREDNS_BEARERTOKEN "$accesstoken" |
302 | _saveaccountconf_mutable AZUREDNS_TOKENVALIDTO "$expires_on" | |
91607bb2 | 303 | printf "%s" "$accesstoken" |
304 | return 0 | |
e90f3b84 | 305 | } |
306 | ||
307 | _get_root() { | |
91607bb2 | 308 | domain=$1 |
309 | subscriptionId=$2 | |
310 | accesstoken=$3 | |
9e3c931b | 311 | i=1 |
91607bb2 | 312 | p=1 |
313 | ||
314 | ## Ref: https://docs.microsoft.com/en-us/rest/api/dns/zones/list | |
315 | ## returns up to 100 zones in one response therefore handling more results is not not implemented | |
316 | ## (ZoneListResult with continuation token for the next page of results) | |
317 | ## Per https://docs.microsoft.com/en-us/azure/azure-subscription-service-limits#dns-limits you are limited to 100 Zone/subscriptions anyways | |
318 | ## | |
12956679 | 319 | _azure_rest GET "https://management.azure.com/subscriptions/$subscriptionId/providers/Microsoft.Network/dnszones?\$top=500&api-version=2017-09-01" "" "$accesstoken" |
971a85a6 | 320 | # Find matching domain name in Json response |
91607bb2 | 321 | while true; do |
322 | h=$(printf "%s" "$domain" | cut -d . -f $i-100) | |
323 | _debug2 "Checking domain: $h" | |
324 | if [ -z "$h" ]; then | |
325 | #not valid | |
326 | _err "Invalid domain" | |
327 | return 1 | |
328 | fi | |
e90f3b84 | 329 | |
330 | if _contains "$response" "\"name\":\"$h\"" >/dev/null; then | |
971a85a6 | 331 | _domain_id=$(echo "$response" | _egrep_o "\\{\"id\":\"[^\"]*\\/$h\"" | head -n 1 | cut -d : -f 2 | tr -d \") |
e90f3b84 | 332 | if [ "$_domain_id" ]; then |
9e3c931b | 333 | if [ "$i" = 1 ]; then |
334 | #create the record at the domain apex (@) if only the domain name was provided as --domain-alias | |
335 | _sub_domain="@" | |
336 | else | |
337 | _sub_domain=$(echo "$domain" | cut -d . -f 1-$p) | |
338 | fi | |
e90f3b84 | 339 | _domain=$h |
340 | return 0 | |
341 | fi | |
342 | return 1 | |
91607bb2 | 343 | fi |
344 | p=$i | |
345 | i=$(_math "$i" + 1) | |
346 | done | |
347 | return 1 | |
e90f3b84 | 348 | } |