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.

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