]>
Commit | Line | Data |
---|---|---|
600a2351 DK |
1 | #!/usr/bin/env sh |
2 | ||
3 | #This file name is "dns_freedns.sh" | |
4 | #So, here must be a method dns_freedns_add() | |
5 | #Which will be called by acme.sh to add the txt record to your api system. | |
6 | #returns 0 means success, otherwise error. | |
7 | # | |
8 | #Author: David Kerr | |
9 | #Report Bugs here: https://github.com/dkerr64/acme.sh | |
10 | # | |
11 | ######## Public functions ##################### | |
12 | ||
13 | # Export FreeDNS userid and password in folowing variables... | |
14 | # FREEDNS_User=username | |
15 | # FREEDNS_Password=password | |
16 | # login cookie is saved in acme account config file so userid / pw | |
17 | # need to be set only when changed. | |
18 | ||
19 | #Usage: dns_freedns_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" | |
20 | dns_freedns_add() { | |
21 | fulldomain="$1" | |
22 | txtvalue="$2" | |
23 | ||
24 | _info "Add TXT record using FreeDNS" | |
25 | _debug "fulldomain: $fulldomain" | |
26 | _debug "txtvalue: $txtvalue" | |
27 | ||
28 | if [ -z "$FREEDNS_User" ] || [ -z "$FREEDNS_Password" ]; then | |
29 | FREEDNS_User="" | |
30 | FREEDNS_Password="" | |
31 | if [ -z "$FREEDNS_COOKIE" ]; then | |
32 | _err "You did not specify the FreeDNS username and password yet." | |
33 | _err "Please export as FREEDNS_User / FREEDNS_Password and try again." | |
34 | return 1 | |
35 | fi | |
36 | using_cached_cookies="true" | |
37 | else | |
38 | FREEDNS_COOKIE="$(_freedns_login "$FREEDNS_User" "$FREEDNS_Password")" | |
39 | if [ -z "$FREEDNS_COOKIE" ]; then | |
40 | return 1 | |
41 | fi | |
42 | using_cached_cookies="false" | |
43 | fi | |
44 | ||
45 | _debug "FreeDNS login cookies: $FREEDNS_COOKIE (cached = $using_cached_cookies)" | |
46 | ||
47 | _saveaccountconf FREEDNS_COOKIE "$FREEDNS_COOKIE" | |
48 | ||
49 | # split our full domain name into two parts... | |
50 | i="$(echo "$fulldomain" | tr '.' ' ' | wc -w)" | |
51 | i="$(_math "$i" - 1)" | |
52 | top_domain="$(echo "$fulldomain" | cut -d. -f "$i"-100)" | |
53 | i="$(_math "$i" - 1)" | |
54 | sub_domain="$(echo "$fulldomain" | cut -d. -f -"$i")" | |
55 | ||
56 | # Sometimes FreeDNS does not reurn the subdomain page but rather | |
57 | # returns a page regarding becoming a premium member. This usually | |
58 | # happens after a period of inactivity. Immediately trying again | |
59 | # returns the correct subdomain page. So, we will try twice to | |
60 | # load the page and obtain our domain ID | |
61 | attempts=2 | |
62 | while [ "$attempts" -gt "0" ]; do | |
63 | attempts="$(_math "$attempts" - 1)" | |
64 | ||
65 | htmlpage="$(_freedns_retrieve_subdomain_page "$FREEDNS_COOKIE")" | |
66 | if [ "$?" != "0" ]; then | |
67 | if [ "$using_cached_cookies" = "true" ]; then | |
68 | _err "Has your FreeDNS username and password channged? If so..." | |
69 | _err "Please export as FREEDNS_User / FREEDNS_Password and try again." | |
70 | fi | |
71 | return 1 | |
72 | fi | |
73 | ||
74 | # Now convert the tables in the HTML to CSV. This litte gem from | |
75 | # http://stackoverflow.com/questions/1403087/how-can-i-convert-an-html-table-to-csv | |
76 | subdomain_csv="$(echo "$htmlpage" \ | |
77 | | grep -i -e '</\?TABLE\|</\?TD\|</\?TR\|</\?TH' \ | |
78 | | sed 's/^[\ \t]*//g' \ | |
79 | | tr -d '\n' \ | |
80 | | sed 's/<\/TR[^>]*>/\n/Ig' \ | |
81 | | sed 's/<\/\?\(TABLE\|TR\)[^>]*>//Ig' \ | |
82 | | sed 's/^<T[DH][^>]*>\|<\/\?T[DH][^>]*>$//Ig' \ | |
83 | | sed 's/<\/T[DH][^>]*><T[DH][^>]*>/,/Ig' \ | |
84 | | grep 'edit.php?' \ | |
85 | | grep "$top_domain")" | |
86 | # The above beauty ends with striping out rows that do not have an | |
87 | # href to edit.php and do not have the top domain we are looking for. | |
88 | # So all we should be left with is CSV of table of subdomains we are | |
89 | # interested in. | |
90 | ||
91 | # Now we have to read through this table and extract the data we need | |
92 | lines="$(echo "$subdomain_csv" | wc -l)" | |
93 | nl=' | |
94 | ' | |
95 | i=0 | |
96 | found=0 | |
97 | while [ "$i" -lt "$lines" ]; do | |
98 | i="$(_math "$i" + 1)" | |
99 | line="$(echo "$subdomain_csv" | cut -d "$nl" -f "$i")" | |
100 | tmp="$(echo "$line" | cut -d ',' -f 1)" | |
101 | if [ $found = 0 ] && _startswith "$tmp" "<td>$top_domain"; then | |
102 | # this line will contain DNSdomainid for the top_domain | |
103 | DNSdomainid="$(echo "$line" | cut -d ',' -f 2 | sed 's/^.*domain_id=//;s/>.*//')" | |
104 | found=1 | |
105 | else | |
106 | # lines contain DNS records for all subdomains | |
107 | DNSname="$(echo "$line" | cut -d ',' -f 2 | sed 's/^[^>]*>//;s/<\/a>.*//')" | |
108 | DNStype="$(echo "$line" | cut -d ',' -f 3)" | |
109 | if [ "$DNSname" = "$fulldomain" ] && [ "$DNStype" = "TXT" ]; then | |
110 | DNSdataid="$(echo "$line" | cut -d ',' -f 2 | sed 's/^.*data_id=//;s/>.*//')" | |
111 | # Now get current value for the TXT record. This method may | |
112 | # not produce accurate results as the value field is truncated | |
113 | # on this webpage. To get full value we would need to load | |
114 | # another page. However we don't really need this so long as | |
291c97dc | 115 | # there is only one TXT record for the acme challenge subdomain. |
600a2351 DK |
116 | DNSvalue="$(echo "$line" | cut -d ',' -f 4 | sed 's/^[^"]*"//;s/".*//;s/<\/td>.*//')" |
117 | if [ $found != 0 ]; then | |
118 | break | |
119 | # we are breaking out of the loop at the first match of DNS name | |
120 | # and DNS type (if we are past finding the domainid). This assumes | |
121 | # that there is only ever one TXT record for the LetsEncrypt/acme | |
122 | # challenge subdomain. This seems to be a reasonable assumption | |
123 | # as the acme client deletes the TXT record on successful validation. | |
124 | fi | |
125 | else | |
126 | DNSname="" | |
127 | DNStype="" | |
128 | fi | |
129 | fi | |
130 | done | |
131 | ||
132 | _debug "DNSname: $DNSname DNStype: $DNStype DNSdomainid: $DNSdomainid DNSdataid: $DNSdataid" | |
133 | _debug "DNSvalue: $DNSvalue" | |
134 | ||
135 | if [ -z "$DNSdomainid" ]; then | |
136 | # If domain ID is empty then something went wrong (top level | |
137 | # domain not found at FreeDNS). | |
138 | if [ "$attempts" = "0" ]; then | |
139 | # exhausted maximum retry attempts | |
140 | _debug "$htmlpage" | |
141 | _debug "$subdomain_csv" | |
142 | _err "Domain $top_domain not found at FreeDNS" | |
143 | return 1 | |
144 | fi | |
145 | else | |
146 | # break out of the 'retry' loop... we have found our domain ID | |
147 | break | |
148 | fi | |
149 | _info "Domain $top_domain not found at FreeDNS" | |
150 | _info "Retry loading subdomain page ($attempts attempts remaining)" | |
151 | done | |
152 | ||
153 | if [ -z "$DNSdataid" ]; then | |
154 | # If data ID is empty then specific subdomain does not exist yet, need | |
155 | # to create it this should always be the case as the acme client | |
156 | # deletes the entry after domain is validated. | |
157 | _freedns_add_txt_record "$FREEDNS_COOKIE" "$DNSdomainid" "$sub_domain" "$txtvalue" | |
158 | return $? | |
159 | else | |
160 | if [ "$txtvalue" = "$DNSvalue" ]; then | |
161 | # if value in TXT record matches value requested then DNS record | |
162 | # does not need to be updated. But... | |
163 | # Testing value match fails. Website is truncating the value field. | |
164 | # So for now we will always go down the else path. Though in theory | |
165 | # should never come here anyway as the acme client deletes | |
166 | # the TXT record on successful validation, so we should not even | |
167 | # have found a TXT record !! | |
168 | _info "No update necessary for $fulldomain at FreeDNS" | |
169 | return 0 | |
170 | else | |
171 | # Delete the old TXT record (with the wrong value) | |
172 | _freedns_delete_txt_record "$FREEDNS_COOKIE" "$DNSdataid" | |
173 | if [ "$?" = "0" ]; then | |
174 | # And add in new TXT record with the value provided | |
175 | _freedns_add_txt_record "$FREEDNS_COOKIE" "$DNSdomainid" "$sub_domain" "$txtvalue" | |
176 | fi | |
177 | return $? | |
178 | fi | |
179 | fi | |
180 | return 0 | |
181 | } | |
182 | ||
183 | #Usage: fulldomain txtvalue | |
184 | #Remove the txt record after validation. | |
185 | dns_freedns_rm() { | |
186 | fulldomain="$1" | |
187 | txtvalue="$2" | |
188 | ||
189 | _info "Delete TXT record using FreeDNS" | |
190 | _debug "fulldomain: $fulldomain" | |
191 | _debug "txtvalue: $txtvalue" | |
192 | ||
193 | # Need to read cookie from conf file again in case new value set | |
194 | # during login to FreeDNS when TXT record was created. | |
195 | # acme.sh does not have a _readaccountconf() fuction | |
196 | FREEDNS_COOKIE="$(_read_conf "$ACCOUNT_CONF_PATH" "FREEDNS_COOKIE")" | |
197 | _debug "FreeDNS login cookies: $FREEDNS_COOKIE" | |
198 | ||
199 | # Sometimes FreeDNS does not reurn the subdomain page but rather | |
200 | # returns a page regarding becoming a premium member. This usually | |
201 | # happens after a period of inactivity. Immediately trying again | |
202 | # returns the correct subdomain page. So, we will try twice to | |
203 | # load the page and obtain our TXT record. | |
204 | attempts=2 | |
205 | while [ "$attempts" -gt "0" ]; do | |
206 | attempts="$(_math "$attempts" - 1)" | |
207 | ||
208 | htmlpage="$(_freedns_retrieve_subdomain_page "$FREEDNS_COOKIE")" | |
209 | if [ "$?" != "0" ]; then | |
210 | return 1 | |
211 | fi | |
212 | ||
213 | # Now convert the tables in the HTML to CSV. This litte gem from | |
214 | # http://stackoverflow.com/questions/1403087/how-can-i-convert-an-html-table-to-csv | |
215 | subdomain_csv="$(echo "$htmlpage" \ | |
216 | | grep -i -e '</\?TABLE\|</\?TD\|</\?TR\|</\?TH' \ | |
217 | | sed 's/^[\ \t]*//g' \ | |
218 | | tr -d '\n' \ | |
219 | | sed 's/<\/TR[^>]*>/\n/Ig' \ | |
220 | | sed 's/<\/\?\(TABLE\|TR\)[^>]*>//Ig' \ | |
221 | | sed 's/^<T[DH][^>]*>\|<\/\?T[DH][^>]*>$//Ig' \ | |
222 | | sed 's/<\/T[DH][^>]*><T[DH][^>]*>/,/Ig' \ | |
223 | | grep 'edit.php?' \ | |
224 | | grep "$fulldomain")" | |
225 | # The above beauty ends with striping out rows that do not have an | |
226 | # href to edit.php and do not have the domain name we are looking for. | |
227 | # So all we should be left with is CSV of table of subdomains we are | |
228 | # interested in. | |
229 | ||
230 | # Now we have to read through this table and extract the data we need | |
231 | lines="$(echo "$subdomain_csv" | wc -l)" | |
232 | nl=' | |
233 | ' | |
234 | i=0 | |
235 | found=0 | |
236 | while [ "$i" -lt "$lines" ]; do | |
237 | i="$(_math "$i" + 1)" | |
238 | line="$(echo "$subdomain_csv" | cut -d "$nl" -f "$i")" | |
239 | DNSname="$(echo "$line" | cut -d ',' -f 2 | sed 's/^[^>]*>//;s/<\/a>.*//')" | |
240 | DNStype="$(echo "$line" | cut -d ',' -f 3)" | |
241 | if [ "$DNSname" = "$fulldomain" ] && [ "$DNStype" = "TXT" ]; then | |
242 | DNSdataid="$(echo "$line" | cut -d ',' -f 2 | sed 's/^.*data_id=//;s/>.*//')" | |
243 | DNSvalue="$(echo "$line" | cut -d ',' -f 4 | sed 's/^[^"]*"//;s/".*//;s/<\/td>.*//')" | |
244 | _debug "DNSvalue: $DNSvalue" | |
245 | # if [ "$DNSvalue" = "$txtvalue" ]; then | |
246 | # Testing value match fails. Website is truncating the value | |
247 | # field. So for now we will assume that there is only one TXT | |
248 | # field for the sub domain and just delete it. Currently this | |
249 | # is a safe assumption. | |
250 | _freedns_delete_txt_record "$FREEDNS_COOKIE" "$DNSdataid" | |
251 | return $? | |
252 | # fi | |
253 | fi | |
254 | done | |
255 | done | |
256 | ||
257 | # If we get this far we did not find a match (after two attempts) | |
258 | # Not necessarily an error, but log anyway. | |
259 | _debug2 "$subdomain_csv" | |
260 | _info "Cannot delete TXT record for $fulldomain/$txtvalue. Does not exist at FreeDNS" | |
261 | return 0 | |
262 | } | |
263 | ||
264 | #################### Private functions below ################################## | |
265 | ||
266 | # usage: _freedns_login username password | |
267 | # print string "cookie=value" etc. | |
268 | # returns 0 success | |
269 | _freedns_login() { | |
87f5ec5b | 270 | export _H1="Accept-Language:en-US" |
600a2351 DK |
271 | username="$1" |
272 | password="$2" | |
273 | url="https://freedns.afraid.org/zc.php?step=2" | |
274 | ||
275 | _debug "Login to FreeDNS as user $username" | |
276 | ||
277 | htmlpage="$(_post "username=$(printf '%s' "$username" | _url_encode)&password=$(printf '%s' "$password" | _url_encode)&submit=Login&action=auth" "$url")" | |
278 | ||
279 | if [ "$?" != "0" ]; then | |
280 | _err "FreeDNS login failed for user $username bad RC from _post" | |
281 | return 1 | |
282 | fi | |
283 | ||
284 | cookies="$(grep -i '^Set-Cookie.*dns_cookie.*$' "$HTTP_HEADER" | _head_n 1 | tr -d "\r\n" | cut -d " " -f 2)" | |
285 | ||
286 | # if cookies is not empty then logon successful | |
287 | if [ -z "$cookies" ]; then | |
288 | _debug "$htmlpage" | |
289 | _err "FreeDNS login failed for user $username. Check $HTTP_HEADER file" | |
290 | return 1 | |
291 | fi | |
292 | ||
293 | printf "%s" "$cookies" | |
294 | return 0 | |
295 | } | |
296 | ||
297 | # usage _freedns_retrieve_subdomain_page login_cookies | |
298 | # echo page retrieved (html) | |
299 | # returns 0 success | |
300 | _freedns_retrieve_subdomain_page() { | |
301 | export _H1="Cookie:$1" | |
87f5ec5b | 302 | export _H2="Accept-Language:en-US" |
600a2351 DK |
303 | url="https://freedns.afraid.org/subdomain/" |
304 | ||
305 | _debug "Retrieve subdmoain page from FreeDNS" | |
306 | ||
307 | htmlpage="$(_get "$url")" | |
308 | ||
309 | if [ "$?" != "0" ]; then | |
310 | _err "FreeDNS retrieve subdomins failed bad RC from _get" | |
311 | return 1 | |
f78b656f | 312 | elif [ -z "$htmlpage" ]; then |
600a2351 DK |
313 | _err "FreeDNS returned empty subdomain page" |
314 | return 1 | |
315 | fi | |
316 | ||
317 | _debug2 "$htmlpage" | |
318 | ||
319 | printf "%s" "$htmlpage" | |
320 | return 0 | |
321 | } | |
322 | ||
323 | # usage _freedns_add_txt_record login_cookies domain_id subdomain value | |
324 | # returns 0 success | |
325 | _freedns_add_txt_record() { | |
326 | export _H1="Cookie:$1" | |
87f5ec5b | 327 | export _H2="Accept-Language:en-US" |
600a2351 DK |
328 | domain_id="$2" |
329 | subdomain="$3" | |
330 | value="$(printf '%s' "$4" | _url_encode)" | |
331 | url="http://freedns.afraid.org/subdomain/save.php?step=2" | |
332 | ||
333 | htmlpage="$(_post "type=TXT&domain_id=$domain_id&subdomain=$subdomain&address=%22$value%22&send=Save%21" "$url")" | |
334 | ||
335 | if [ "$?" != "0" ]; then | |
336 | _err "FreeDNS failed to add TXT record for $subdomain bad RC from _post" | |
337 | return 1 | |
f78b656f | 338 | elif ! grep "200 OK" "$HTTP_HEADER" >/dev/null; then |
600a2351 DK |
339 | _debug "$htmlpage" |
340 | _err "FreeDNS failed to add TXT record for $subdomain. Check $HTTP_HEADER file" | |
341 | return 1 | |
f78b656f DK |
342 | elif _contains "$htmlpage" "security code was incorrect"; then |
343 | _debug "$htmlpage" | |
344 | _err "FreeDNS failed to add TXT record for $subdomain as FreeDNS requested seurity code" | |
345 | _err "Note that you cannot use automatic DNS validation for FreeDNS public domains" | |
346 | return 1 | |
600a2351 | 347 | fi |
f78b656f DK |
348 | |
349 | _debug2 "$htmlpage" | |
600a2351 DK |
350 | _info "Added acme challenge TXT record for $fulldomain at FreeDNS" |
351 | return 0 | |
352 | } | |
353 | ||
354 | # usage _freedns_delete_txt_record login_cookies data_id | |
355 | # returns 0 success | |
356 | _freedns_delete_txt_record() { | |
357 | export _H1="Cookie:$1" | |
87f5ec5b | 358 | export _H2="Accept-Language:en-US" |
600a2351 DK |
359 | data_id="$2" |
360 | url="https://freedns.afraid.org/subdomain/delete2.php" | |
361 | ||
362 | htmlheader="$(_get "$url?data_id%5B%5D=$data_id&submit=delete+selected" "onlyheader")" | |
363 | ||
364 | if [ "$?" != "0" ]; then | |
365 | _err "FreeDNS failed to delete TXT record for $data_id bad RC from _get" | |
366 | return 1 | |
f78b656f | 367 | elif ! _contains "$htmlheader" "200 OK"; then |
600a2351 DK |
368 | _debug "$htmlheader" |
369 | _err "FreeDNS failed to delete TXT record $data_id" | |
370 | return 1 | |
371 | fi | |
372 | ||
373 | _info "Deleted acme challenge TXT record for $fulldomain at FreeDNS" | |
374 | return 0 | |
375 | } |