You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

412 lines
17 KiB

  1. #!/usr/bin/env sh
  2. # This script has been created at June 2020, based on knowledge base of wedos.com provider.
  3. # It is intended to allow DNS-01 challenges for acme.sh using wedos's WAPI using XML.
  4. # See WIKI page how to use it https://github.com/acmesh-official/acme.sh/wiki/dnsapi#117-use-wedos-dns-api
  5. # Author Michal Tuma <mxtuma@gmail.com>
  6. # For issues, please perform the action with --debug switch and report to https://github.com/acmesh-official/acme.sh/issues/3166
  7. # MAIN WAPI ENDPOINT
  8. WEDOS_WAPI_ENDPOINT="https://api.wedos.com/wapi/xml"
  9. # WHEN SET TO ANYTHINK, THEN GENERATED XML WAPI REQUEST ADD TESTING SWITCH
  10. TESTING_STAGE=
  11. ######## Public functions #####################
  12. # Main implemented function for acme.sh.
  13. # Function manages provided user informations, parse requested domain and subdomain name and create new TXT row with provided value.
  14. # WEDOS WAPI Requests usage:
  15. # - dns-domains-list : to retrieve a list of valid managed domains and check input $fulldomain
  16. # - dns-row-add : to add new TXT row to a $fulldomain with $txtvalue set
  17. # - dns-domain-commit : to commit added dns row
  18. # Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
  19. dns_wedos_add() {
  20. fulldomain=$1
  21. txtvalue=$2
  22. WEDOS_Username="${WEDOS_Username:-$(_readaccountconf_mutable WEDOS_Username)}"
  23. WEDOS_Wapipass="${WEDOS_Wapipass:-$(_readaccountconf_mutable WEDOS_Wapipass)}"
  24. WEDOS_Authtoken="${WEDOS_Authtoken:-$(_readaccountconf_mutable WEDOS_Authtoken)}"
  25. if [ "${WEDOS_Authtoken}" ]; then
  26. _debug "WEDOS Authtoken was already saved, using saved one"
  27. _saveaccountconf_mutable WEDOS_Authtoken "${WEDOS_Authtoken}"
  28. else
  29. if [ -z "${WEDOS_Username}" ] || [ -z "${WEDOS_Wapipass}" ]; then
  30. WEDOS_Username=""
  31. WEDOS_Wapipass=""
  32. _err "You didn't specify a WEDOS's username and wapi key yet."
  33. _err "Please type: export WEDOS_Username=<your user name to login to wedos web account>"
  34. _err "And: export WEDOS_Wapipass=<your WAPI passwords you setup using wedos web pages>"
  35. _err "After you export those variables, run the script again, the values will be saved for future"
  36. return 1
  37. fi
  38. #build WEDOS_Authtoken
  39. _debug "WEDOS Authtoken were not saved yet, building"
  40. WEDOS_Authtoken=$(printf '%s' "${WEDOS_Wapipass}" | _digest "sha1" "true" | head -c 40)
  41. _debug "WEDOS_Authtoken step 1, WAPI PASS sha1 sum: '${WEDOS_Authtoken}'"
  42. WEDOS_Authtoken="${WEDOS_Username}${WEDOS_Authtoken}"
  43. _debug "WEDOS_Authtoken step 2, username concat with token without hours: '${WEDOS_Authtoken}'"
  44. #save details
  45. _saveaccountconf_mutable WEDOS_Username "${WEDOS_Username}"
  46. _saveaccountconf_mutable WEDOS_Wapipass "${WEDOS_Wapipass}"
  47. _saveaccountconf_mutable WEDOS_Authtoken "${WEDOS_Authtoken}"
  48. fi
  49. if ! _get_root "${fulldomain}"; then
  50. _err "WEDOS Account do not contain primary domain to fullfill add of ${fulldomain}!"
  51. return 1
  52. fi
  53. _debug _sub_domain "${_sub_domain}"
  54. _debug _domain "${_domain}"
  55. if _wapi_row_add "${_domain}" "${_sub_domain}" "${txtvalue}" "300"; then
  56. _info "WEDOS WAPI: dns record added and dns changes were commited"
  57. return 0
  58. else
  59. _err "FAILED TO ADD DNS RECORD OR COMMIT DNS CHANGES"
  60. return 1
  61. fi
  62. }
  63. # Main implemented function for acme.sh
  64. # This function verify provided domain if is managed by stored account, try to find TXT row for the domain and removes it if it is found.
  65. # WEDOS WAPI Requests used:
  66. # - dns-domains-list : to verify requested $fulldomain is managed and to parse what is subdomain from it
  67. # - dns-rows-list : to verify if provided $txtvalue exists as TXT entry
  68. # - dns-row-delete : to request deletion of TXT value
  69. # - dns-domain-commit : to commit deletion
  70. # Usage: rm _acme_challenge.www.domain.org "e89fhwie73869yhe993e27d4hi"
  71. dns_wedos_rm() {
  72. fulldomain=$1
  73. txtvalue=$2
  74. WEDOS_Username="${WEDOS_Username:-$(_readaccountconf_mutable WEDOS_Username)}"
  75. WEDOS_Wapipass="${WEDOS_Wapipass:-$(_readaccountconf_mutable WEDOS_Wapipass)}"
  76. WEDOS_Authtoken="${WEDOS_Authtoken:-$(_readaccountconf_mutable WEDOS_Authtoken)}"
  77. if [ "${WEDOS_Authtoken}" ]; then
  78. _debug "WEDOS Authtoken was already saved, using saved one"
  79. _saveaccountconf_mutable WEDOS_Authtoken "${WEDOS_Authtoken}"
  80. else
  81. if [ -z "${WEDOS_Username}" ] || [ -z "${WEDOS_Wapipass}" ]; then
  82. WEDOS_Username=""
  83. WEDOS_Wapipass=""
  84. _err "You didn't specify a WEDOS's username and wapi key yet."
  85. _err "Please type: export WEDOS_Username=<your user name to login to wedos web account>"
  86. _err "And: export WEDOS_Wapipass=<your WAPI passwords you setup using wedos web pages>"
  87. _err "After you export those variables, run the script again, the values will be saved for future"
  88. return 1
  89. fi
  90. #build WEDOS_Authtoken
  91. _debug "WEDOS Authtoken were not saved yet, building"
  92. WEDOS_Authtoken=$(printf '%s' "${WEDOS_Wapipass}" | sha1sum | head -c 40)
  93. _debug "WEDOS_Authtoken step 1, WAPI PASS sha1 sum: '${WEDOS_Authtoken}'"
  94. WEDOS_Authtoken="${WEDOS_Username}${WEDOS_Authtoken}"
  95. _debug "WEDOS_Authtoken step 2, username concat with token without hours: '${WEDOS_Authtoken}'"
  96. #save details
  97. _saveaccountconf_mutable WEDOS_Username "${WEDOS_Username}"
  98. _saveaccountconf_mutable WEDOS_Wapipass "${WEDOS_Wapipass}"
  99. _saveaccountconf_mutable WEDOS_Authtoken "${WEDOS_Authtoken}"
  100. fi
  101. if ! _get_root "${fulldomain}"; then
  102. _err "WEDOS Account do not contain primary domain to fullfill add of ${fulldomain}!"
  103. return 1
  104. fi
  105. _debug _sub_domain "${_sub_domain}"
  106. _debug _domain "${_domain}"
  107. if _wapi_find_row "${_domain}" "${_sub_domain}" "${txtvalue}"; then
  108. _info "WEDOS WAPI: dns record found with id '${_row_id}'"
  109. if _wapi_delete_row "${_domain}" "${_row_id}"; then
  110. _info "WEDOS WAPI: dns row were deleted and changes commited!"
  111. return 0
  112. fi
  113. fi
  114. _err "Requested dns row were not found or was imposible to delete it, do it manually"
  115. _err "Delete: ${fulldomain}"
  116. _err "Value: ${txtvalue}"
  117. return 1
  118. }
  119. #################### Private functions below ##################################
  120. # Function _wapi_post(), only takes data, prepares auth token and provide result
  121. # $1 - WAPI command string, like 'dns-domains-list'
  122. # $2 - WAPI data for given command, is not required
  123. # returns WAPI response if request were successfully delivered to WAPI endpoint
  124. _wapi_post() {
  125. command=$1
  126. data=$2
  127. _debug "Command : ${command}"
  128. _debug "Data : ${data}"
  129. if [ -z "${command}" ]; then
  130. _err "No command were provided, implamantation error!"
  131. return 1
  132. fi
  133. # Prepare authentification token
  134. hour=$(TZ='Europe/Prague' date +%H)
  135. token=$(printf '%s' "${WEDOS_Authtoken}${hour}" | _digest "sha1" "true" | head -c 40)
  136. _debug "Authentification token is '${token}'"
  137. # Build xml request
  138. request="request=<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
  139. <request>\
  140. <user>${WEDOS_Username}</user>\
  141. <auth>${token}</auth>\
  142. <command>${command}</command>"
  143. if [ -z "${data}" ]; then
  144. echo "" 1>/dev/null
  145. else
  146. request="${request}${data}"
  147. fi
  148. if [ -z "$TESTING_STAGE" ]; then
  149. echo "" 1>/dev/null
  150. else
  151. request="${request}\
  152. <test>1</test>"
  153. fi
  154. request="${request}\
  155. </request>"
  156. _debug "Request to WAPI is: ${request}"
  157. if ! response="$(_post "${request}" "$WEDOS_WAPI_ENDPOINT")"; then
  158. _err "Error contacting WEDOS WAPI with command ${command}"
  159. return 1
  160. fi
  161. _debug "Response : ${response}"
  162. _contains "${response}" "<code>1000</code>"
  163. return "$?"
  164. }
  165. # _get_root() function, for provided full domain, like _acme_challenge.www.example.com verify if WEDOS contains a primary active domain and found what is subdomain
  166. # $1 - full domain to verify, ie _acme_challenge.www.example.com
  167. # builds ${_domain} found at WEDOS, like example.com and ${_sub_domain} from provided full domain, like _acme_challenge.www
  168. _get_root() {
  169. domain=$1
  170. if [ -z "${domain}" ]; then
  171. _err "Function _get_root was called without argument, implementation error!"
  172. return 1
  173. fi
  174. _debug "Get root for domain: ${domain}"
  175. _debug "Getting list of domains using WAPI ..."
  176. if ! _wapi_post "dns-domains-list"; then
  177. _err "Error on WAPI request for list of domains, response : ${response}"
  178. return 1
  179. else
  180. _debug "DNS list were successfully retrieved, response : ${response}"
  181. fi
  182. # In for each cycle, try parse the response to find primary active domains
  183. # - in order to success with MacOSX, always take care if all sed works with --posix when testing the all sed commmands
  184. # For cycle description:
  185. # 1st tr -d '\011\012\015' = remove all newlines and tab characters - whole XML became single line
  186. # 2nd sed "s/^.*<data>[ ]*//g" = remove all the xml data from the beggining of the XML - XML now start with the content of <data> element
  187. # 3rd sed "s/<\/data>.*$//g" = remove all the data after the data xml element - XML now contains only the content of data xml element
  188. # 4th sed "s/>[ ]*<\([^\/]\)/><\1/g" = remove all spaces between XML tag and XML start tag - XML now contains content of data xml element and is without spaces between end and start xml tags
  189. # 5th sed "s/<domain>//g" = remove all domain xml start tags - XML now contains only <name>...</name><type>...</type><status>...</status> </domain>(next xml domain)
  190. # 6th sed "s/[ ]*<\/domain>/\t/g" = replace all "spaces</domain>" by tab - now we are preparing to create multiple lines
  191. # 7th th '\011' '\n' = replace all tabs from previous sed (Mac OS change) - now we create multiple lines each should contain only <name>...</name><type>...</type><status>...</status>
  192. # 8th sed -n "/<name>\([a-zA-Z0-9_.-][a-zA-Z0-9_.-]*\)<\/name><type>primary<\/type><status>active<\/status>/p" = remove all non primary or non active domains lines
  193. # 9th sed "s/<name>\([a-zA-Z0-9_.-][a-zA-Z0-9_.-]*\)<\/name><type>primary<\/type><status>active<\/status>/\1/g" = substitute for domain names only
  194. for xml_domain in $(echo "${response}" | tr -d '\011\012\015' | sed "s/^.*<data>[ ]*//g" | sed "s/<\/data>.*$//g" | sed "s/>[ ]*<\([^\/]\)/><\1/g" | sed "s/<domain>//g" | sed "s/[ ]*<\/domain>/\t/g" | tr '\011' '\n' | sed -n "/<name>\([a-zA-Z0-9_.-][a-zA-Z0-9_.-]*\)<\/name><type>primary<\/type><status>active<\/status>/p" | sed "s/<name>\([a-zA-Z0-9_.-][a-zA-Z0-9_.-]*\)<\/name><type>primary<\/type><status>active<\/status>/\1/g"); do
  195. _debug "Found primary active domain: ${xml_domain}"
  196. if _endswith "${domain}" "${xml_domain}"; then
  197. length_difference=$(_math "${#domain} - ${#xml_domain}")
  198. possible_subdomain=$(echo "${domain}" | cut -c -"${length_difference}")
  199. if _endswith "${possible_subdomain}" "."; then
  200. length_difference=$(_math "${length_difference} - 1")
  201. _domain=${xml_domain}
  202. _sub_domain=$(echo "${possible_subdomain}" | cut -c -"${length_difference}")
  203. _info "Domain '${_domain}' was found at WEDOS account as primary, and subdomain is '${_sub_domain}'!"
  204. return 0
  205. fi
  206. fi
  207. _debug " ... found domain does not match required!"
  208. done
  209. return 1
  210. }
  211. # for provided domain, it commites all performed changes
  212. _wapi_dns_commit() {
  213. domain=$1
  214. if [ -z "${domain}" ]; then
  215. _err "Invalid request to commit dns changes, domain is empty, implementation error!"
  216. return 1
  217. fi
  218. data=" <data>\
  219. <name>${domain}</name>\
  220. </data>"
  221. if ! _wapi_post "dns-domain-commit" "${data}"; then
  222. _err "Error on WAPI request to commit DNS changes, response : ${response}"
  223. _err "PLEASE USE WEB ACCESS TO CHECK IF CHANGES ARE REQUIRED TO COMMIT OR ROLLBACKED IMMEDIATELLY!"
  224. return 1
  225. else
  226. _debug "DNS CHANGES COMMITED, response : ${response}"
  227. _info "WEDOS DNS WAPI: Changes were commited to domain '${domain}'"
  228. fi
  229. return 0
  230. }
  231. # add one TXT dns row to a specified fomain
  232. _wapi_row_add() {
  233. domain=$1
  234. sub_domain=$2
  235. value=$3
  236. ttl=$4
  237. if [ -z "${domain}" ] || [ -z "${sub_domain}" ] || [ -z "${value}" ] || [ -z "${ttl}" ]; then
  238. _err "Invalid request to add record, domain: '${domain}', sub_domain: '${sub_domain}', value: '${value}' and ttl: '${ttl}', on of required input were not provided, implementation error!"
  239. return 1
  240. fi
  241. # Prepare data for request to WAPI
  242. data=" <data>\
  243. <domain>${domain}</domain>\
  244. <name>${sub_domain}</name>\
  245. <ttl>${ttl}</ttl>\
  246. <type>TXT</type>\
  247. <rdata>${value}</rdata>\
  248. <auth_comment>Created using WAPI from acme.sh</auth_comment>\
  249. </data>"
  250. _debug "Adding row using WAPI ..."
  251. if ! _wapi_post "dns-row-add" "${data}"; then
  252. _err "Error on WAPI request to add new TXT row, response : ${response}"
  253. return 1
  254. else
  255. _debug "ROW ADDED, response : ${response}"
  256. _info "WEDOS DNS WAPI: Row to domain '${domain}' with name '${sub_domain}' were successfully added with value '${value}' and ttl set to ${ttl}"
  257. fi
  258. # Now we have to commit
  259. _wapi_dns_commit "${domain}"
  260. return "$?"
  261. }
  262. _wapi_find_row() {
  263. domain=$1
  264. sub_domain=$2
  265. value=$3
  266. if [ -z "${domain}" ] || [ -z "${sub_domain}" ] || [ -z "${value}" ]; then
  267. _err "Invalud request to finad a row, domain: '${domain}', sub_domain: '${sub_domain}' and value: '${value}', one of required input were not provided, implementation error!"
  268. return 1
  269. fi
  270. data=" <data>\
  271. <domain>${domain}</domain>\
  272. </data>"
  273. _debug "Searching rows using WAPI ..."
  274. if ! _wapi_post "dns-rows-list" "${data}"; then
  275. _err "Error on WAPI request to list domain rows, response : ${response}"
  276. return 1
  277. fi
  278. _debug "Domain rows found, response : ${response}"
  279. # Prepare sub domain regex which will be later used for search domain row
  280. # from _acme_challenge.sub it should be _acme_challenge\.sub
  281. sub_domain_regex=$(echo "${sub_domain}" | sed "s/\./\\\\./g")
  282. _debug "Subdomain regex '${sub_domain_regex}'"
  283. # In for each cycle loops over the domains rows, description:
  284. # - when testing use sed --posix to test if compatible with MacOSX = avoid replacement to \n and using + as repeater one or more (instead use the same patern without repeater and then the same with *)
  285. # 1st tr -d '\011\012\015' = delete all newlines and tab characters - XML became a single line
  286. # 2nd sed "s/^.*<data>[ ]*//g" = remove all from the beggining to the start of the content of the data xml element - XML is without unusefull beginning
  287. # 3rd sed "s/[ ]*<\/data>.*$//g" = remove the end of the xml starting with xml end tag data - XML contains only the content of data xml element and is trimmed
  288. # 4th sed "s/>[ ]*<\([^\/]\)/><\1/g" = remove all spaces between XML tag and XML start tag - XML now contains content of data xml element and is without spaces between end and start xml tags
  289. # 5th sed "s/<row>//g" = remove all row xml start tags - XML now contains rows xml element content and its end tag
  290. # 6th sed "s/[ ]*<\/row>/\t/g" = replace all "spaces</row>" by tab - now we are preparing to create multiple lines
  291. # 7th tr '\011' '\n' = replace all tabs with new lines (Mac OS X hint) - we create multiple lines each should contain only single row xml content
  292. # 8th sed -n "/<name>${sub_domain_regex}<\/name>.*<rdtype>TXT<\/rdtype>/p" = remove all non TXT and non name matching row lines - now we have only xml lines with TXT rows matching requested values
  293. # 9th sed "s/^<ID>\([0-9][0-9]*\)<\/ID>.*<rdata>\(.*\)<\/rdata>.*$/\1-\2/" = replace the whole lines to ID-value pairs
  294. # -- now there are only lines with ID-value but value might contain spaces (BAD FOR FOREACH LOOP) or special characters (BAD FOR REGEX MATCHING)
  295. # 10th grep "${value}" = match only a line containg searched value
  296. # 11th sed "s/^\([0-9][0-9]*\).*$/\1/" = get only ID from the row
  297. for xml_row in $(echo "${response}" | tr -d '\011\012\015' | sed "s/^.*<data>[ ]*//g" | sed "s/[ ]*<\/data>.*$//g" | sed "s/>[ ]*<\([^\/]\)/><\1/g" | sed "s/<row>//g" | sed "s/[ ]*<\/row>/\t/g" | tr '\011' '\n' | sed -n "/<name>${sub_domain_regex}<\/name>.*<rdtype>TXT<\/rdtype>/p" | sed "s/^<ID>\([0-9][0-9]*\)<\/ID>.*<rdata>\(.*\)<\/rdata>.*$/\1-\2/" | grep "${value}" | sed "s/^\([0-9][0-9]*\).*$/\1/"); do
  298. _row_id="${xml_row}"
  299. _info "WEDOS API: Found DNS row id ${_row_id} for domain ${domain}"
  300. return 0
  301. done
  302. _info "WEDOS API: No TXT row found for domain '${domain}' with name '${sub_domain}' and value '${value}'"
  303. return 1
  304. }
  305. _wapi_delete_row() {
  306. domain=$1
  307. row_id=$2
  308. if [ -z "${domain}" ] || [ -z "${row_id}" ]; then
  309. _err "Invalid request to delete domain dns row, domain: '${domain}' and row_id: '${row_id}', one of required input were not provided, implementation error!"
  310. return 1
  311. fi
  312. data=" <data>\
  313. <domain>${domain}</domain>
  314. <row_id>${row_id}</row_id>
  315. </data>"
  316. _debug "Deleting dns row using WAPI ..."
  317. if ! _wapi_post "dns-row-delete" "${data}"; then
  318. _err "Error on WAPI request to delete dns row, response: ${response}"
  319. return 1
  320. fi
  321. _debug "DNS row were deleted, response: ${response}"
  322. _info "WEDOS API: Required dns domain row with row_id '${row_id}' were correctly deleted at domain '${domain}'"
  323. # Now we have to commit changes
  324. _wapi_dns_commit "${domain}"
  325. return "$?"
  326. }