瀏覽代碼

wip: added DNS challenge provider

0xJacky 2 年之前
父節點
當前提交
5b8fae10df
共有 100 個文件被更改,包括 4474 次插入894 次删除
  1. 7 0
      app.example.ini
  2. 13 0
      frontend/src/api/auto_cert.ts
  3. 1 0
      frontend/src/views/domain/cert/Cert.vue
  4. 21 218
      frontend/src/views/domain/cert/IssueCert.vue
  5. 68 0
      frontend/src/views/domain/cert/components/AutoCertStepOne.vue
  6. 70 0
      frontend/src/views/domain/cert/components/DNSChallenge.vue
  7. 246 0
      frontend/src/views/domain/cert/components/ObtainCert.vue
  8. 0 1
      frontend/src/views/domain/ngx_conf/directive/DirectiveEditor.vue
  9. 50 0
      frontend/src/views/preference/BasicSettings.vue
  10. 24 0
      frontend/src/views/preference/GitSettings.vue
  11. 24 0
      frontend/src/views/preference/NginxLogSettings.vue
  12. 41 0
      frontend/src/views/preference/OpenAISettings.vue
  13. 39 72
      frontend/src/views/preference/Preference.vue
  14. 28 0
      frontend/src/views/preference/typedef.ts
  15. 99 2
      go.mod
  16. 812 8
      go.sum
  17. 43 0
      lego-config.sh
  18. 1 13
      resources/development/nginx/sites-available/homework.jackyu.cn
  19. 4 4
      resources/development/nginx/sites-available/qi.jackyu.cn
  20. 88 59
      resources/development/nginx/ssl/qi.jackyu.cn/fullchain.cer
  21. 25 25
      resources/development/nginx/ssl/qi.jackyu.cn/private.key
  22. 22 22
      resources/development/nginx/ssl/qi.jackyu.cn_amstourship.jackyu.cn/fullchain.cer
  23. 25 25
      resources/development/nginx/ssl/qi.jackyu.cn_amstourship.jackyu.cn/private.key
  24. 322 304
      server/api/cert.go
  25. 38 37
      server/api/settings.go
  26. 94 94
      server/pkg/cert/auto_cert.go
  27. 55 10
      server/pkg/cert/cert.go
  28. 20 0
      server/pkg/cert/config/acmedns.toml
  29. 33 0
      server/pkg/cert/config/alidns.toml
  30. 24 0
      server/pkg/cert/config/allinkl.toml
  31. 22 0
      server/pkg/cert/config/arvancloud.toml
  32. 25 0
      server/pkg/cert/config/auroradns.toml
  33. 26 0
      server/pkg/cert/config/autodns.toml
  34. 28 0
      server/pkg/cert/config/azure.toml
  35. 22 0
      server/pkg/cert/config/bindman.toml
  36. 31 0
      server/pkg/cert/config/bluecat.toml
  37. 22 0
      server/pkg/cert/config/bunny.toml
  38. 25 0
      server/pkg/cert/config/checkdomain.toml
  39. 21 0
      server/pkg/cert/config/civo.toml
  40. 28 0
      server/pkg/cert/config/clouddns.toml
  41. 78 0
      server/pkg/cert/config/cloudflare.toml
  42. 25 0
      server/pkg/cert/config/cloudns.toml
  43. 24 0
      server/pkg/cert/config/cloudxns.toml
  44. 6 0
      server/pkg/cert/config/config.go
  45. 27 0
      server/pkg/cert/config/conoha.toml
  46. 24 0
      server/pkg/cert/config/constellix.toml
  47. 22 0
      server/pkg/cert/config/desec.toml
  48. 68 0
      server/pkg/cert/config/designate.toml
  49. 23 0
      server/pkg/cert/config/digitalocean.toml
  50. 22 0
      server/pkg/cert/config/dnshomede.toml
  51. 41 0
      server/pkg/cert/config/dnsimple.toml
  52. 25 0
      server/pkg/cert/config/dnsmadeeasy.toml
  53. 25 0
      server/pkg/cert/config/dnspod.toml
  54. 23 0
      server/pkg/cert/config/dode.toml
  55. 31 0
      server/pkg/cert/config/domeneshop.toml
  56. 22 0
      server/pkg/cert/config/dreamhost.toml
  57. 23 0
      server/pkg/cert/config/duckdns.toml
  58. 26 0
      server/pkg/cert/config/dyn.toml
  59. 22 0
      server/pkg/cert/config/dynu.toml
  60. 30 0
      server/pkg/cert/config/easydns.toml
  61. 63 0
      server/pkg/cert/config/edgedns.toml
  62. 22 0
      server/pkg/cert/config/epik.toml
  63. 112 0
      server/pkg/cert/config/exec.toml
  64. 27 0
      server/pkg/cert/config/exoscale.toml
  65. 23 0
      server/pkg/cert/config/freemyip.toml
  66. 22 0
      server/pkg/cert/config/gandi.toml
  67. 22 0
      server/pkg/cert/config/gandiv5.toml
  68. 30 0
      server/pkg/cert/config/gcloud.toml
  69. 22 0
      server/pkg/cert/config/gcore.toml
  70. 24 0
      server/pkg/cert/config/glesys.toml
  71. 24 0
      server/pkg/cert/config/godaddy.toml
  72. 22 0
      server/pkg/cert/config/googledomains.toml
  73. 22 0
      server/pkg/cert/config/hetzner.toml
  74. 25 0
      server/pkg/cert/config/hostingde.toml
  75. 23 0
      server/pkg/cert/config/hosttech.toml
  76. 61 0
      server/pkg/cert/config/httpreq.toml
  77. 48 0
      server/pkg/cert/config/hurricane.toml
  78. 49 0
      server/pkg/cert/config/hyperone.toml
  79. 25 0
      server/pkg/cert/config/ibmcloud.toml
  80. 26 0
      server/pkg/cert/config/iij.toml
  81. 25 0
      server/pkg/cert/config/iijdpf.toml
  82. 36 0
      server/pkg/cert/config/infoblox.toml
  83. 30 0
      server/pkg/cert/config/infomaniak.toml
  84. 24 0
      server/pkg/cert/config/internetbs.toml
  85. 32 0
      server/pkg/cert/config/inwx.toml
  86. 22 0
      server/pkg/cert/config/ionos.toml
  87. 24 0
      server/pkg/cert/config/iwantmyname.toml
  88. 59 0
      server/pkg/cert/config/joker.toml
  89. 22 0
      server/pkg/cert/config/liara.toml
  90. 59 0
      server/pkg/cert/config/lightsail.toml
  91. 23 0
      server/pkg/cert/config/linode.toml
  92. 28 0
      server/pkg/cert/config/liquidweb.toml
  93. 38 0
      server/pkg/cert/config/loopia.toml
  94. 24 0
      server/pkg/cert/config/luadns.toml
  95. 24 0
      server/pkg/cert/config/mydnsjp.toml
  96. 33 0
      server/pkg/cert/config/mythicbeasts.toml
  97. 32 0
      server/pkg/cert/config/namecheap.toml
  98. 26 0
      server/pkg/cert/config/namedotcom.toml
  99. 22 0
      server/pkg/cert/config/namesilo.toml
  100. 25 0
      server/pkg/cert/config/nearlyfreespeech.toml

+ 7 - 0
app.example.ini

@@ -20,3 +20,10 @@ Model =
 BaseUrl =
 Proxy =
 Token =
+
+[git]
+Url =
+AuthMethod =
+Username =
+Password =
+PrivateKeyFile =

+ 13 - 0
frontend/src/api/auto_cert.ts

@@ -0,0 +1,13 @@
+import http from '@/lib/http'
+
+const auto_cert = {
+    get_dns_providers() {
+        return http.get('/auto_cert/dns/providers')
+    },
+
+    get_dns_provider(code: string) {
+        return http.get('/auto_cert/dns/provider/' + code)
+    }
+}
+
+export default auto_cert

+ 1 - 0
frontend/src/views/domain/cert/Cert.vue

@@ -4,6 +4,7 @@ import IssueCert from '@/views/domain/cert/IssueCert.vue'
 import {computed, ref} from 'vue'
 import {useGettext} from 'vue3-gettext'
 import ChangeCert from '@/views/domain/cert/ChangeCert.vue'
+import DNSChallenge from '@/views/domain/cert/components/DNSChallenge.vue'
 
 const {$gettext} = useGettext()
 

+ 21 - 218
frontend/src/views/domain/cert/IssueCert.vue

@@ -1,23 +1,19 @@
 <script setup lang="ts">
 import {useGettext} from 'vue3-gettext'
-import {computed, inject, nextTick, ref, watch} from 'vue'
-import {message, Modal} from 'ant-design-vue'
-import domain from '@/api/domain'
-import websocket from '@/lib/websocket'
+import {computed, inject, nextTick, provide, ref, watch} from 'vue'
 import Template from '@/views/template/Template.vue'
-import template from '@/api/template'
+import ObtainCert from '@/views/domain/cert/components/ObtainCert.vue'
 
 const {$gettext, interpolate} = useGettext()
 
 const props = defineProps(['config_name', 'directivesMap', 'current_server_directives',
     'enabled', 'ngx_config'])
 
-const emit = defineEmits(['changeEnabled', 'callback', 'update:enabled'])
-
-const save_site_config: Function = inject('save_site_config')!
+const emit = defineEmits(['callback', 'update:enabled'])
 
 const issuing_cert = ref(false)
-const modalVisible = ref(false)
+
+const obtain_cert: any = ref()
 
 const enabled = computed({
     get() {
@@ -28,233 +24,40 @@ const enabled = computed({
     }
 })
 
-function confirm() {
-    Modal.confirm({
-        title: enabled.value ? $gettext('Do you want to disable auto-cert renewal?') :
-            $gettext('Do you want to enable auto-cert renewal?'),
-        content: enabled.value ? $gettext('We need to add the HTTPChallenge configuration to ' +
-                'this file and reload the Nginx. Are you sure you want to continue?') :
-            $gettext('We will remove the HTTPChallenge configuration from this file and ' +
-                'reload the Nginx configuration file. Are you sure you want to continue?'),
-        mask: false,
-        centered: true,
-        onOk() {
-            if (enabled.value) {
-                onchange(false)
-            } else {
-                onchange(true)
-            }
-        }
-    })
-}
-
-async function onchange(r: boolean) {
-    emit('changeEnabled', r)
-    change_auto_cert(r)
-    if (r) {
-        await template.get_block('letsencrypt.conf').then(r => {
-            props.ngx_config.servers.forEach(async (v: any) => {
-                v.locations = v.locations.filter((l: any) => l.path !== '/.well-known/acme-challenge')
-
-                v.locations.push(...r.locations)
-            })
-        })
-        // if ssl_certificate is empty, do not save, just use the config from last step.
-        if (!props.directivesMap['ssl_certificate']?.[0]) {
-            await save_site_config()
-        }
-        job()
-    } else {
-        await props.ngx_config.servers.forEach((v: any) => {
-            v.locations = v.locations.filter((l: any) => l.path !== '/.well-known/acme-challenge')
-        })
-        save_site_config()
-    }
-}
-
-function job() {
-    issuing_cert.value = true
-
-    if (no_server_name.value) {
-        message.error($gettext('server_name not found in directives'))
-        issuing_cert.value = false
-        return
-    }
-
-    const server_name = props.directivesMap['server_name'][0]
-
-    if (!props.directivesMap['ssl_certificate']) {
-        props.current_server_directives.splice(server_name.idx + 1, 0, {
-            directive: 'ssl_certificate',
-            params: ''
-        })
-    }
-
-    nextTick(() => {
-        if (!props.directivesMap['ssl_certificate_key']) {
-            const ssl_certificate = props.directivesMap['ssl_certificate'][0]
-            props.current_server_directives.splice(ssl_certificate.idx + 1, 0, {
-                directive: 'ssl_certificate_key',
-                params: ''
-            })
-        }
-    }).then(() => {
-        issue_cert(props.config_name, name.value, callback)
-    })
-}
-
-async function callback(ssl_certificate: string, ssl_certificate_key: string) {
-    props.directivesMap['ssl_certificate'][0]['params'] = ssl_certificate
-    props.directivesMap['ssl_certificate_key'][0]['params'] = ssl_certificate_key
-    save_site_config()
-}
-
-function change_auto_cert(r: boolean) {
-    if (r) {
-        domain.add_auto_cert(props.config_name, {domains: name.value.trim().split(' ')}).then(() => {
-            message.success(interpolate($gettext('Auto-renewal enabled for %{name}'), {name: name.value}))
-        }).catch(e => {
-            message.error(e.message ?? interpolate($gettext('Enable auto-renewal failed for %{name}'), {name: name.value}))
-        })
-    } else {
-        domain.remove_auto_cert(props.config_name).then(() => {
-            message.success(interpolate($gettext('Auto-renewal disabled for %{name}'), {name: name.value}))
-        }).catch(e => {
-            message.error(e.message ?? interpolate($gettext('Disable auto-renewal failed for %{name}'), {name: name.value}))
-        })
-    }
-}
-
-const logContainer = ref(null)
-
-function log(msg: string) {
-    const para = document.createElement('p')
-    para.appendChild(document.createTextNode($gettext(msg)));
-
-    (logContainer.value as any as Node).appendChild(para);
-
-    (logContainer.value as any as Element).scroll({top: 320, left: 0, behavior: 'smooth'})
-}
-
-const issue_cert = async (config_name: string, server_name: string, callback: Function) => {
-    progressStatus.value = 'active'
-    modalClosable.value = false
-    modalVisible.value = true
-    progressPercent.value = 0;
-    (logContainer.value as any as Element).innerHTML = ''
-
-    log($gettext('Getting the certificate, please wait...'))
-
-    const ws = websocket(`/api/domain/${config_name}/cert`, false)
-
-    ws.onopen = () => {
-        ws.send(JSON.stringify({
-            server_name: server_name.trim().split(' ')
-        }))
-    }
-
-    ws.onmessage = m => {
-        const r = JSON.parse(m.data)
-        log(r.message)
-
-        switch (r.status) {
-            case 'info':
-                progressPercent.value += 5
-                break
-            default:
-                modalClosable.value = true
-                issuing_cert.value = false
-
-                if (r.status === 'success' && r.ssl_certificate !== undefined && r.ssl_certificate_key !== undefined) {
-                    progressStatus.value = 'success'
-                    progressPercent.value = 100
-                    callback(r.ssl_certificate, r.ssl_certificate_key)
-                } else {
-                    progressStatus.value = 'exception'
-                }
-                break
-        }
+const no_server_name = computed(() => {
+    if (props.directivesMap['server_name'] === undefined) {
+        return true
     }
-}
 
-const no_server_name = computed(() => {
-    return props.directivesMap['server_name']?.length === 0
+    return props.directivesMap['server_name'].length === 0
 })
 
-const name = computed(() => {
-    return props.directivesMap['server_name'][0].params.trim()
-})
+provide('no_server_name', no_server_name)
+provide('props', props)
+provide('issuing_cert', issuing_cert)
 
-watch(no_server_name, () => {
-    emit('update:enabled', false)
-    onchange(false)
-})
+watch(no_server_name, () => emit('update:enabled', false))
+const update = ref(0)
 
-const progressStrokeColor = {
-    from: '#108ee9',
-    to: '#87d068'
+async function onchange() {
+    update.value++
+    await nextTick(() => {
+        obtain_cert.value.toggle(enabled.value)
+    })
 }
-
-const progressPercent = ref(0)
-
-const progressStatus = ref('active')
-
-const modalClosable = ref(false)
 </script>
 
 <template>
-    <a-modal
-        :title="$gettext('Obtaining certificate')"
-        v-model:visible="modalVisible"
-        :mask-closable="modalClosable"
-        :footer="null" :closable="modalClosable" force-render>
-        <a-progress
-            :stroke-color="progressStrokeColor"
-            :percent="progressPercent"
-            :status="progressStatus"
-        />
-
-        <div class="issue-cert-log-container" ref="logContainer"/>
-
-    </a-modal>
+    <obtain-cert ref="obtain_cert" :key="update"/>
     <div class="issue-cert">
         <a-form-item :label="$gettext('Encrypt website with Let\'s Encrypt')">
             <a-switch
                 :loading="issuing_cert"
                 :checked="enabled"
-                @change="confirm"
                 :disabled="no_server_name"
+                @change="onchange"
             />
-            <a-alert
-                v-if="no_server_name"
-                :message="$gettext('Warning')"
-                type="warning"
-                show-icon
-            >
-                <template slot="description">
-                    <span v-if="no_server_name" v-translate>
-                        server_name parameter is required
-                    </span>
-                </template>
-            </a-alert>
         </a-form-item>
-        <a-alert type="info" closable :message="$gettext('Note')">
-            <template #description>
-                <p v-translate>
-                    The server_name
-                    in the current configuration must be the domain name you need to get the certificate, support
-                    multiple domains.
-                </p>
-                <p v-translate>
-                    The certificate for the domain will be checked every hour,
-                    and will be renewed if it has been more than 1 week since it was last issued.
-                </p>
-                <p v-translate>
-                    Make sure you have configured a reverse proxy for .well-known
-                    directory to HTTPChallengePort before obtaining the certificate.
-                </p>
-            </template>
-        </a-alert>
     </div>
 </template>
 

+ 68 - 0
frontend/src/views/domain/cert/components/AutoCertStepOne.vue

@@ -0,0 +1,68 @@
+<script setup lang="ts">
+import {inject, ref, Ref} from 'vue'
+import {useGettext} from 'vue3-gettext'
+import DNSChallenge from '@/views/domain/cert/components/DNSChallenge.vue'
+
+const {$gettext} = useGettext()
+const no_server_name: Ref = inject('no_server_name')!
+const data: Ref = inject('data')!
+</script>
+
+<template>
+    <template v-if="no_server_name">
+        <a-alert
+            :message="$gettext('Warning')"
+            type="warning"
+            show-icon
+        >
+            <template #description>
+                    <span v-if="no_server_name">
+                        {{ $gettext('server_name parameter is required') }}
+                    </span>
+            </template>
+        </a-alert>
+        <br/>
+    </template>
+
+    <a-alert type="info" show-icon :message="$gettext('Note')">
+        <template #description>
+            <p v-translate>
+                The server_name
+                in the current configuration must be the domain name you need to get the certificate, support
+                multiple domains.
+            </p>
+            <p v-translate>
+                The certificate for the domain will be checked every hour,
+                and will be renewed if it has been more than 1 week since it was last issued.
+            </p>
+            <p v-if="data.challenge_method==='http01'" v-translate>
+                Make sure you have configured a reverse proxy for .well-known
+                directory to HTTPChallengePort before obtaining the certificate.
+            </p>
+            <p v-else-if="data.challenge_method==='dns01'" v-translate>
+                Please fill in the API authentication credentials provided by your DNS provider.
+                We will add a TXT record to the DNS records of your domain for ownership verification.
+                Once the verification is complete, the record will be removed.
+                Please note that the time configurations below are all in seconds.
+            </p>
+        </template>
+    </a-alert>
+    <br/>
+    <a-form layout="vertical">
+        <a-form-item :label="$gettext('Challenge Method')">
+            <a-select v-model:value="data.challenge_method">
+                <a-select-option value="http01">
+                    {{ $gettext('HTTP01') }}
+                </a-select-option>
+                <a-select-option value="dns01">
+                    {{ $gettext('DNS01') }}
+                </a-select-option>
+            </a-select>
+        </a-form-item>
+    </a-form>
+    <d-n-s-challenge v-if="data.challenge_method==='dns01'"/>
+</template>
+
+<style lang="less" scoped>
+
+</style>

+ 70 - 0
frontend/src/views/domain/cert/components/DNSChallenge.vue

@@ -0,0 +1,70 @@
+<script setup lang="ts">
+import {computed, inject, Ref, ref, watch} from 'vue'
+import auto_cert from '@/api/auto_cert'
+import {useGettext} from 'vue3-gettext'
+import {SelectProps} from 'ant-design-vue'
+
+const {$gettext} = useGettext()
+const providers: any = ref([])
+
+const data: any = inject('data')!
+
+auto_cert.get_dns_providers().then(r => {
+    providers.value = r
+})
+
+const provider_idx = ref()
+
+const current: any = computed(() => {
+    return providers.value?.[provider_idx.value]
+})
+
+watch(current, () => {
+    data.code = current.value.code
+    auto_cert.get_dns_provider(current.value.code).then(r => {
+        Object.assign(current.value, r)
+    })
+})
+
+const options = computed<SelectProps['options']>(() => {
+    let list: SelectProps['options'] = []
+
+    providers.value.forEach((v: any, k: number) => {
+        list!.push({
+            value: k,
+            label: v.name
+        })
+    })
+
+    return list
+})
+
+const filterOption = (input: string, option: any) => {
+    return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
+}
+</script>
+
+<template>
+    <a-form layout="vertical">
+        <a-form-item :label="$gettext('DNS Provider')">
+            <a-select v-model:value="provider_idx" show-search :options="options" :filter-option="filterOption"/>
+        </a-form-item>
+        <template v-if="current?.configuration?.credentials">
+            <h4>{{ $gettext('Credentials') }}</h4>
+            <a-form-item :label="k" v-for="(v,k) in current?.configuration?.credentials"
+                         :extra="v" :rules="[{ required: true }]">
+                <a-input v-model:value="data.configuration.credentials[k]"/>
+            </a-form-item>
+        </template>
+        <template v-if="current?.configuration?.additional">
+            <h4>{{ $gettext('Additional') }}</h4>
+            <a-form-item :label="k" v-for="(v,k) in current?.configuration?.additional" :extra="v">
+                <a-input v-model:value="data.configuration.additional[k]"/>
+            </a-form-item>
+        </template>
+    </a-form>
+</template>
+
+<style lang="less" scoped>
+
+</style>

+ 246 - 0
frontend/src/views/domain/cert/components/ObtainCert.vue

@@ -0,0 +1,246 @@
+<script setup lang="ts">
+import {useGettext} from 'vue3-gettext'
+import {computed, inject, nextTick, provide, reactive, Ref, ref, watch} from 'vue'
+import websocket from '@/lib/websocket'
+import {message, Modal} from 'ant-design-vue'
+import template from '@/api/template'
+import domain from '@/api/domain'
+import AutoCertStepOne from '@/views/domain/cert/components/AutoCertStepOne.vue'
+
+const {$gettext, interpolate} = useGettext()
+
+const modalVisible = ref(false)
+
+const step = ref(1)
+
+const progressStrokeColor = {
+    from: '#108ee9',
+    to: '#87d068'
+}
+
+const data: any = reactive({
+    challenge_method: 'http01',
+    code: '',
+    configuration: {
+        credentials: {},
+        additional: {}
+    }
+})
+const progressPercent = ref(0)
+const progressStatus = ref('active')
+const modalClosable = ref(true)
+provide('data', data)
+
+const logContainer = ref(null)
+
+const save_site_config: Function = inject('save_site_config')!
+const no_server_name: Ref = inject('no_server_name')!
+const props: any = inject('props')!
+const issuing_cert: Ref<boolean> = inject('issuing_cert')!
+
+async function callback(ssl_certificate: string, ssl_certificate_key: string) {
+    props.directivesMap['ssl_certificate'][0]['params'] = ssl_certificate
+    props.directivesMap['ssl_certificate_key'][0]['params'] = ssl_certificate_key
+    save_site_config()
+}
+
+function change_auto_cert(r: boolean) {
+    if (r) {
+        domain.add_auto_cert(props.config_name, {domains: name.value.trim().split(' ')}).then(() => {
+            message.success(interpolate($gettext('Auto-renewal enabled for %{name}'), {name: name.value}))
+        }).catch(e => {
+            message.error(e.message ?? interpolate($gettext('Enable auto-renewal failed for %{name}'), {name: name.value}))
+        })
+    } else {
+        domain.remove_auto_cert(props.config_name).then(() => {
+            message.success(interpolate($gettext('Auto-renewal disabled for %{name}'), {name: name.value}))
+        }).catch(e => {
+            message.error(e.message ?? interpolate($gettext('Disable auto-renewal failed for %{name}'), {name: name.value}))
+        })
+    }
+}
+
+async function onchange(r: boolean) {
+    change_auto_cert(r)
+    if (r) {
+        await template.get_block('letsencrypt.conf').then(r => {
+            props.ngx_config.servers.forEach(async (v: any) => {
+                v.locations = v.locations.filter((l: any) => l.path !== '/.well-known/acme-challenge')
+
+                v.locations.push(...r.locations)
+            })
+        })
+        // if ssl_certificate is empty, do not save, just use the config from last step.
+        if (!props.directivesMap['ssl_certificate']?.[0]) {
+            await save_site_config()
+        }
+        job()
+    } else {
+        await props.ngx_config.servers.forEach((v: any) => {
+            v.locations = v.locations.filter((l: any) => l.path !== '/.well-known/acme-challenge')
+        })
+        save_site_config()
+    }
+}
+
+function job() {
+    modalClosable.value = false
+    issuing_cert.value = true
+
+    if (no_server_name.value) {
+        message.error($gettext('server_name not found in directives'))
+        issuing_cert.value = false
+        return
+    }
+
+    const server_name = props.directivesMap['server_name'][0]
+
+    if (!props.directivesMap['ssl_certificate']) {
+        props.current_server_directives.splice(server_name.idx + 1, 0, {
+            directive: 'ssl_certificate',
+            params: ''
+        })
+    }
+
+    nextTick(() => {
+        if (!props.directivesMap['ssl_certificate_key']) {
+            const ssl_certificate = props.directivesMap['ssl_certificate'][0]
+            props.current_server_directives.splice(ssl_certificate.idx + 1, 0, {
+                directive: 'ssl_certificate_key',
+                params: ''
+            })
+        }
+    }).then(() => {
+        issue_cert(props.config_name, name.value, callback)
+    })
+}
+
+function log(msg: string) {
+    const para = document.createElement('p')
+    para.appendChild(document.createTextNode($gettext(msg)));
+
+    (logContainer.value as any as Node).appendChild(para);
+
+    (logContainer.value as any as Element).scroll({top: 320, left: 0, behavior: 'smooth'})
+}
+
+const issue_cert = async (config_name: string, server_name: string, callback: Function) => {
+    progressStatus.value = 'active'
+    modalClosable.value = false
+    modalVisible.value = true
+    progressPercent.value = 0;
+    (logContainer.value as any as Element).innerHTML = ''
+
+    log($gettext('Getting the certificate, please wait...'))
+
+    const ws = websocket(`/api/domain/${config_name}/cert`, false)
+
+    ws.onopen = () => {
+        ws.send(JSON.stringify({
+            server_name: server_name.trim().split(' '),
+            challenge_method: data.challenge_method,
+            config: {
+                ...data
+            }
+        }))
+    }
+
+    ws.onmessage = m => {
+        const r = JSON.parse(m.data)
+        log(r.message)
+
+        switch (r.status) {
+            case 'info':
+                progressPercent.value += 5
+                break
+            default:
+                modalClosable.value = true
+                issuing_cert.value = false
+
+                if (r.status === 'success' && r.ssl_certificate !== undefined && r.ssl_certificate_key !== undefined) {
+                    progressStatus.value = 'success'
+                    progressPercent.value = 100
+                    callback(r.ssl_certificate, r.ssl_certificate_key)
+                } else {
+                    progressStatus.value = 'exception'
+                }
+                break
+        }
+    }
+}
+
+const name = computed(() => {
+    return props.directivesMap['server_name'][0].params.trim()
+})
+
+
+function toggle(status: boolean) {
+    if (status) {
+        Modal.confirm({
+            title: $gettext('Do you want to disable auto-cert renewal?'),
+            content: $gettext('We will remove the HTTPChallenge configuration from ' +
+                'this file and reload the Nginx. Are you sure you want to continue?'),
+            mask: false,
+            centered: true,
+            onOk: () => onchange(false)
+        })
+    } else {
+        modalVisible.value = true
+        modalClosable.value = true
+    }
+}
+
+defineExpose({
+    toggle
+})
+
+const can_next = computed(() => {
+    if (step.value === 2) {
+        return false
+    } else {
+        if (data.challenge_method === 'http01') {
+            return true
+        } else if (data.challenge_method === 'dns01') {
+            return data?.code ?? false
+        }
+    }
+})
+
+function next() {
+    step.value++
+    onchange(true)
+}
+</script>
+
+<template>
+    <a-modal
+        :title="$gettext('Obtain certificate')"
+        v-model:visible="modalVisible"
+        :mask-closable="modalClosable"
+        :footer="null" :closable="modalClosable" force-render>
+        <template v-if="step===1">
+            <auto-cert-step-one/>
+        </template>
+        <template v-else-if="step===2">
+            <a-progress
+                :stroke-color="progressStrokeColor"
+                :percent="progressPercent"
+                :status="progressStatus"
+            />
+
+            <div class="issue-cert-log-container" ref="logContainer"/>
+        </template>
+        <div class="control-btn" v-if="can_next">
+            <a-button type="primary" @click="next">
+                {{ $gettext('Next') }}
+            </a-button>
+        </div>
+    </a-modal>
+</template>
+
+<style lang="less" scoped>
+.control-btn {
+    display: flex;
+    justify-content: flex-end;
+}
+</style>

+ 0 - 1
frontend/src/views/domain/ngx_conf/directive/DirectiveEditor.vue

@@ -31,7 +31,6 @@ function onSave(idx: number) {
         class="list-group"
         ghost-class="ghost"
         handle=".anticon-holder"
-        v-auto-animate
     >
         <template #item="{ element: directive, index }">
             <directive-editor-item @click="current_idx=index"

+ 50 - 0
frontend/src/views/preference/BasicSettings.vue

@@ -0,0 +1,50 @@
+<script setup lang="ts">
+import {useGettext} from 'vue3-gettext'
+import {inject} from 'vue'
+import {IData} from '@/views/preference/typedef'
+
+const {$gettext} = useGettext()
+const data: IData = inject('data')!
+const theme = inject('theme')
+</script>
+
+<template>
+    <a-form layout="vertical">
+        <a-form-item :label="$gettext('Theme')">
+            <a-select v-model:value="theme">
+                <a-select-option value="auto">
+                    {{ $gettext('Auto') }}
+                </a-select-option>
+                <a-select-option value="light">
+                    {{ $gettext('Light') }}
+                </a-select-option>
+                <a-select-option value="dark">
+                    {{ $gettext('Dark') }}
+                </a-select-option>
+            </a-select>
+        </a-form-item>
+        <a-form-item :label="$gettext('HTTP Port')">
+            <p>{{ data.server.http_port }}</p>
+        </a-form-item>
+        <a-form-item :label="$gettext('Run Mode')">
+            <p>{{ data.server.run_mode }}</p>
+        </a-form-item>
+        <a-form-item :label="$gettext('Jwt Secret')">
+            <p>{{ data.server.jwt_secret }}</p>
+        </a-form-item>
+        <a-form-item :label="$gettext('Terminal Start Command')">
+            <p>{{ data.server.start_cmd }}</p>
+        </a-form-item>
+        <a-form-item :label="$gettext('HTTP Challenge Port')">
+            <a-input-number v-model:value="data.server.http_challenge_port"/>
+        </a-form-item>
+        <a-form-item :label="$gettext('Github Proxy')">
+            <a-input v-model:value="data.server.github_proxy"
+                     :placeholder="$gettext('Chinese user: https://ghproxy.com/')"/>
+        </a-form-item>
+    </a-form>
+</template>
+
+<style lang="less" scoped>
+
+</style>

+ 24 - 0
frontend/src/views/preference/GitSettings.vue

@@ -0,0 +1,24 @@
+<script setup lang="ts">
+import {useGettext} from 'vue3-gettext'
+import {inject} from 'vue'
+import {IData} from '@/views/preference/typedef'
+
+const {$gettext} = useGettext()
+
+const data: IData = inject('data')!
+</script>
+
+<template>
+    <a-form layout="vertical">
+        <a-form-item :label="$gettext('Repo url')">
+            <a-input v-model:value="data.git.url"/>
+        </a-form-item>
+        <a-form-item :label="$gettext('Username')">
+            <a-input v-model:value="data.git.username"/>
+        </a-form-item>
+    </a-form>
+</template>
+
+<style lang="less" scoped>
+
+</style>

+ 24 - 0
frontend/src/views/preference/NginxLogSettings.vue

@@ -0,0 +1,24 @@
+<script setup lang="ts">
+import {useGettext} from 'vue3-gettext'
+import {inject} from 'vue'
+import {IData} from '@/views/preference/typedef'
+
+const {$gettext} = useGettext()
+
+const data: IData = inject('data')!
+</script>
+
+<template>
+    <a-form layout="vertical">
+        <a-form-item :label="$gettext('Nginx Access Log Path')">
+            <a-input v-model:value="data.nginx_log.access_log_path"/>
+        </a-form-item>
+        <a-form-item :label="$gettext('Nginx Error Log Path')">
+            <a-input v-model:value="data.nginx_log.error_log_path"/>
+        </a-form-item>
+    </a-form>
+</template>
+
+<style lang="less" scoped>
+
+</style>

+ 41 - 0
frontend/src/views/preference/OpenAISettings.vue

@@ -0,0 +1,41 @@
+<script setup lang="ts">
+import {useGettext} from 'vue3-gettext'
+import {IData} from '@/views/preference/typedef'
+import {inject} from 'vue'
+
+const {$gettext} = useGettext()
+
+const data: IData = inject('data')!
+</script>
+
+<template>
+    <a-form layout="vertical">
+        <a-form-item :label="$gettext('ChatGPT Model')">
+            <a-select v-model:value="data.openai.model">
+                <a-select-option value="gpt-4">
+                    {{ $gettext('GPT-4') }}
+                </a-select-option>
+                <a-select-option value="gpt-4-32k">
+                    {{ $gettext('GPT-4-32K') }}
+                </a-select-option>
+                <a-select-option value="gpt-3.5-turbo">
+                    {{ $gettext('GPT-3.5-Turbo') }}
+                </a-select-option>
+            </a-select>
+        </a-form-item>
+        <a-form-item :label="$gettext('API Base Url')">
+            <a-input v-model:value="data.openai.base_url"
+                     :placeholder="$gettext('Leave blank for the default: https://api.openai.com/')"/>
+        </a-form-item>
+        <a-form-item :label="$gettext('API Proxy')">
+            <a-input v-model:value="data.openai.proxy" placeholder="http://127.0.0.1:1087"/>
+        </a-form-item>
+        <a-form-item :label="$gettext('API Token')">
+            <a-input-password v-model:value="data.openai.token"/>
+        </a-form-item>
+    </a-form>
+</template>
+
+<style lang="less" scoped>
+
+</style>

+ 39 - 72
frontend/src/views/preference/Preference.vue

@@ -1,24 +1,29 @@
 <script setup lang="ts">
 import {useGettext} from 'vue3-gettext'
-import {reactive, ref} from 'vue'
+import {provide, reactive, ref} from 'vue'
 import FooterToolBar from '@/components/FooterToolbar/FooterToolBar.vue'
 import {useSettingsStore} from '@/pinia'
 import {dark_mode} from '@/lib/theme'
 import settings from '@/api/settings'
 import {message} from 'ant-design-vue'
+import BasicSettings from '@/views/preference/BasicSettings.vue'
+import OpenAISettings from '@/views/preference/OpenAISettings.vue'
+import NginxLogSettings from '@/views/preference/NginxLogSettings.vue'
+import GitSettings from '@/views/preference/GitSettings.vue'
+import {IData} from '@/views/preference/typedef'
 
 const {$gettext} = useGettext()
 
 const settingsStore = useSettingsStore()
 const theme = ref(settingsStore.theme)
-const data = ref({
+const data = ref<IData>({
     server: {
-        http_port: 9000,
+        http_port: '9000',
         run_mode: 'debug',
         jwt_secret: '',
         start_cmd: '',
         email: '',
-        http_challenge_port: 9180,
+        http_challenge_port: '9180',
         github_proxy: ''
     },
     nginx_log: {
@@ -30,6 +35,13 @@ const data = ref({
         base_url: '',
         proxy: '',
         token: ''
+    },
+    git: {
+        url: '',
+        auth_method: '',
+        username: '',
+        password: '',
+        private_key_file_path: ''
     }
 })
 
@@ -37,10 +49,12 @@ settings.get().then(r => {
     data.value = r
 })
 
-function save() {
+async function save() {
     settingsStore.set_theme(theme.value)
     settingsStore.set_preference_theme(theme.value)
-    dark_mode(theme.value === 'dark')
+    await dark_mode(theme.value === 'dark')
+    // fix type
+    data.value.server.http_challenge_port = data.value.server.http_challenge_port.toString()
     settings.save(data.value).then(r => {
         data.value = r
         message.success($gettext('Save successfully'))
@@ -48,77 +62,30 @@ function save() {
         message.error(e?.message ?? $gettext('Server error'))
     })
 }
+
+provide('data', data)
+provide('theme', theme)
+
+const activeKey = ref('1')
 </script>
 
 <template>
     <a-card :title="$gettext('Preference')">
         <div class="preference-container">
-            <a-form layout="vertical">
-                <h4>{{ $gettext('Basic') }}</h4>
-                <a-form-item :label="$gettext('HTTP Port')">
-                    <p>{{ data.server.http_port }}</p>
-                </a-form-item>
-                <a-form-item :label="$gettext('Run Mode')">
-                    <p>{{ data.server.run_mode }}</p>
-                </a-form-item>
-                <a-form-item :label="$gettext('Jwt Secret')">
-                    <p>{{ data.server.jwt_secret }}</p>
-                </a-form-item>
-                <a-form-item :label="$gettext('Terminal Start Command')">
-                    <p>{{ data.server.start_cmd }}</p>
-                </a-form-item>
-                <a-form-item :label="$gettext('HTTP Challenge Port')">
-                    <a-input-number v-model:value="data.server.http_challenge_port"/>
-                </a-form-item>
-                <a-form-item :label="$gettext('Github Proxy')">
-                    <a-input v-model:value="data.server.github_proxy"
-                             :placeholder="$gettext('Chinese user: https://ghproxy.com/')"/>
-                </a-form-item>
-                <a-form-item :label="$gettext('Theme')">
-                    <a-select v-model:value="theme">
-                        <a-select-option value="auto">
-                            {{ $gettext('Auto') }}
-                        </a-select-option>
-                        <a-select-option value="light">
-                            {{ $gettext('Light') }}
-                        </a-select-option>
-                        <a-select-option value="dark">
-                            {{ $gettext('Dark') }}
-                        </a-select-option>
-                    </a-select>
-                </a-form-item>
-                <h4>{{ $gettext('Nginx Log') }}</h4>
-                <a-form-item :label="$gettext('Nginx Access Log Path')">
-                    <a-input v-model:value="data.nginx_log.access_log_path"/>
-                </a-form-item>
-                <a-form-item :label="$gettext('Nginx Error Log Path')">
-                    <a-input v-model:value="data.nginx_log.error_log_path"/>
-                </a-form-item>
-                <h4>{{ $gettext('OpenAI') }}</h4>
-                <a-form-item :label="$gettext('ChatGPT Model')">
-                    <a-select v-model:value="data.openai.model">
-                        <a-select-option value="gpt-4">
-                            {{ $gettext('GPT-4') }}
-                        </a-select-option>
-                        <a-select-option value="gpt-4-32k">
-                            {{ $gettext('GPT-4-32K') }}
-                        </a-select-option>
-                        <a-select-option value="gpt-3.5-turbo">
-                            {{ $gettext('GPT-3.5-Turbo') }}
-                        </a-select-option>
-                    </a-select>
-                </a-form-item>
-                <a-form-item :label="$gettext('API Base Url')">
-                    <a-input v-model:value="data.openai.base_url"
-                             :placeholder="$gettext('Leave blank for the default: https://api.openai.com/')"/>
-                </a-form-item>
-                <a-form-item :label="$gettext('API Proxy')">
-                    <a-input v-model:value="data.openai.proxy" placeholder="http://127.0.0.1:1087"/>
-                </a-form-item>
-                <a-form-item :label="$gettext('API Token')">
-                    <a-input-password v-model:value="data.openai.token"/>
-                </a-form-item>
-            </a-form>
+            <a-tabs v-model:activeKey="activeKey">
+                <a-tab-pane :tab="$gettext('Basic')" key="1">
+                    <basic-settings/>
+                </a-tab-pane>
+                <a-tab-pane :tab="$gettext('Nginx Log')" key="2">
+                    <nginx-log-settings/>
+                </a-tab-pane>
+                <a-tab-pane :tab="$gettext('OpenAI')" key="3">
+                    <open-a-i-settings/>
+                </a-tab-pane>
+                <a-tab-pane :tab="$gettext('Git')" key="4">
+                    <git-settings/>
+                </a-tab-pane>
+            </a-tabs>
         </div>
         <footer-tool-bar>
             <a-button type="primary" @click="save">

+ 28 - 0
frontend/src/views/preference/typedef.ts

@@ -0,0 +1,28 @@
+export interface IData {
+    server: {
+        http_port: string
+        run_mode: string
+        jwt_secret: string
+        start_cmd: string
+        http_challenge_port: string
+        github_proxy: string,
+        email: string
+    },
+    nginx_log: {
+        access_log_path: string
+        error_log_path: string
+    },
+    openai: {
+        model: string
+        base_url: string
+        proxy: string
+        token: string
+    },
+    git: {
+        url: string
+        auth_method: string
+        username: string
+        password: string
+        private_key_file_path: string
+    }
+}

+ 99 - 2
go.mod

@@ -3,6 +3,7 @@ module github.com/0xJacky/Nginx-UI
 go 1.19
 
 require (
+	github.com/BurntSushi/toml v1.2.1
 	github.com/creack/pty v1.1.18
 	github.com/dustin/go-humanize v1.0.1
 	github.com/gin-contrib/static v0.0.1
@@ -33,47 +34,143 @@ require (
 )
 
 require (
-	github.com/BurntSushi/toml v1.2.1 // indirect
+	cloud.google.com/go v0.54.0 // indirect
+	github.com/Azure/azure-sdk-for-go v32.4.0+incompatible // indirect
+	github.com/Azure/go-autorest v14.2.0+incompatible // indirect
+	github.com/Azure/go-autorest/autorest v0.11.24 // indirect
+	github.com/Azure/go-autorest/autorest/adal v0.9.18 // indirect
+	github.com/Azure/go-autorest/autorest/azure/auth v0.5.11 // indirect
+	github.com/Azure/go-autorest/autorest/azure/cli v0.4.5 // indirect
+	github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
+	github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect
+	github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect
+	github.com/Azure/go-autorest/logger v0.2.1 // indirect
+	github.com/Azure/go-autorest/tracing v0.6.0 // indirect
+	github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 // indirect
 	github.com/StackExchange/wmi v1.2.1 // indirect
+	github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.1 // indirect
+	github.com/aliyun/alibaba-cloud-sdk-go v1.61.1755 // indirect
+	github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect
+	github.com/aws/aws-sdk-go v1.39.0 // indirect
+	github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
 	github.com/bytedance/sonic v1.8.7 // indirect
-	github.com/cavaliergopher/grab/v3 v3.0.1 // indirect
 	github.com/cenkalti/backoff/v4 v4.2.0 // indirect
 	github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
+	github.com/civo/civogo v0.3.11 // indirect
+	github.com/cloudflare/cloudflare-go v0.49.0 // indirect
+	github.com/cpu/goacmedns v0.1.1 // indirect
+	github.com/davecgh/go-spew v1.1.1 // indirect
+	github.com/deepmap/oapi-codegen v1.9.1 // indirect
+	github.com/dimchansky/utfbom v1.1.1 // indirect
+	github.com/dnsimple/dnsimple-go v0.71.1 // indirect
+	github.com/exoscale/egoscale v0.90.0 // indirect
+	github.com/fatih/structs v1.1.0 // indirect
+	github.com/fsnotify/fsnotify v1.5.4 // indirect
+	github.com/ghodss/yaml v1.0.0 // indirect
 	github.com/gin-contrib/sse v0.1.0 // indirect
+	github.com/go-errors/errors v1.0.1 // indirect
 	github.com/go-jose/go-jose/v3 v3.0.0 // indirect
 	github.com/go-ole/go-ole v1.2.6 // indirect
+	github.com/go-resty/resty/v2 v2.1.1-0.20191201195748-d7b97669fe48 // indirect
 	github.com/go-sql-driver/mysql v1.7.0 // indirect
 	github.com/goccy/go-json v0.10.2 // indirect
+	github.com/golang-jwt/jwt/v4 v4.2.0 // indirect
+	github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
+	github.com/golang/protobuf v1.5.2 // indirect
+	github.com/google/go-querystring v1.1.0 // indirect
+	github.com/googleapis/gax-go/v2 v2.0.5 // indirect
+	github.com/gophercloud/gophercloud v1.0.0 // indirect
+	github.com/gophercloud/utils v0.0.0-20210216074907-f6de111f2eae // indirect
+	github.com/hashicorp/errwrap v1.0.0 // indirect
+	github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
+	github.com/hashicorp/go-multierror v1.1.1 // indirect
+	github.com/hashicorp/go-retryablehttp v0.7.1 // indirect
+	github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df // indirect
+	github.com/infobloxopen/infoblox-go-client v1.1.1 // indirect
 	github.com/jinzhu/inflection v1.0.0 // indirect
 	github.com/jinzhu/now v1.1.5 // indirect
+	github.com/jmespath/go-jmespath v0.4.0 // indirect
 	github.com/jpillora/s3 v1.1.4 // indirect
 	github.com/json-iterator/go v1.1.12 // indirect
+	github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 // indirect
 	github.com/klauspost/cpuid/v2 v2.2.4 // indirect
+	github.com/kolo/xmlrpc v0.0.0-20200310150728-e0350524596b // indirect
+	github.com/labbsr0x/bindman-dns-webhook v1.0.2 // indirect
+	github.com/labbsr0x/goh v1.0.1 // indirect
 	github.com/leodido/go-urn v1.2.2 // indirect
+	github.com/linode/linodego v1.9.1 // indirect
+	github.com/liquidweb/go-lwApi v0.0.5 // indirect
+	github.com/liquidweb/liquidweb-cli v0.6.9 // indirect
+	github.com/liquidweb/liquidweb-go v1.6.3 // indirect
 	github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a // indirect
 	github.com/mattn/go-isatty v0.0.18 // indirect
 	github.com/mattn/go-sqlite3 v1.14.16 // indirect
 	github.com/miekg/dns v1.1.53 // indirect
+	github.com/mimuret/golang-iij-dpf v0.7.1 // indirect
+	github.com/mitchellh/go-homedir v1.1.0 // indirect
+	github.com/mitchellh/mapstructure v1.5.0 // indirect
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 	github.com/modern-go/reflect2 v1.0.2 // indirect
+	github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 // indirect
+	github.com/nrdcg/auroradns v1.1.0 // indirect
+	github.com/nrdcg/desec v0.6.0 // indirect
+	github.com/nrdcg/dnspod-go v0.4.0 // indirect
+	github.com/nrdcg/freemyip v0.2.0 // indirect
+	github.com/nrdcg/goinwx v0.8.1 // indirect
+	github.com/nrdcg/namesilo v0.2.1 // indirect
+	github.com/nrdcg/porkbun v0.1.1 // indirect
+	github.com/oracle/oci-go-sdk v24.3.0+incompatible // indirect
+	github.com/ovh/go-ovh v1.1.0 // indirect
+	github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
 	github.com/pelletier/go-toml/v2 v2.0.7 // indirect
+	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect
+	github.com/pquerna/otp v1.3.0 // indirect
 	github.com/robfig/cron/v3 v3.0.1 // indirect
+	github.com/sacloud/api-client-go v0.2.1 // indirect
+	github.com/sacloud/go-http v0.1.2 // indirect
+	github.com/sacloud/iaas-api-go v1.3.2 // indirect
+	github.com/sacloud/packages-go v0.0.5 // indirect
+	github.com/scaleway/scaleway-sdk-go v1.0.0-beta.9 // indirect
+	github.com/sirupsen/logrus v1.8.1 // indirect
+	github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 // indirect
+	github.com/softlayer/softlayer-go v1.0.6 // indirect
+	github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e // indirect
+	github.com/stretchr/objx v0.5.0 // indirect
+	github.com/stretchr/testify v1.8.2 // indirect
+	github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.490 // indirect
+	github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.490 // indirect
 	github.com/tklauser/go-sysconf v0.3.11 // indirect
 	github.com/tklauser/numcpus v0.6.0 // indirect
+	github.com/transip/gotransip/v6 v6.17.0 // indirect
 	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
 	github.com/ugorji/go/codec v1.2.11 // indirect
+	github.com/ultradns/ultradns-go-sdk v1.4.0-20221107152238-f3f1d1d // indirect
+	github.com/vinyldns/go-vinyldns v0.9.16 // indirect
+	github.com/vultr/govultr/v2 v2.17.2 // indirect
+	github.com/yandex-cloud/go-genproto v0.0.0-20220805142335-27b56ddae16f // indirect
+	github.com/yandex-cloud/go-sdk v0.0.0-20220805164847-cf028e604997 // indirect
 	github.com/yusufpapurcu/wmi v1.2.2 // indirect
+	go.opencensus.io v0.22.3 // indirect
+	go.uber.org/ratelimit v0.2.0 // indirect
 	golang.org/x/arch v0.3.0 // indirect
 	golang.org/x/mod v0.9.0 // indirect
 	golang.org/x/net v0.8.0 // indirect
+	golang.org/x/oauth2 v0.4.0 // indirect
 	golang.org/x/sync v0.1.0 // indirect
 	golang.org/x/sys v0.6.0 // indirect
 	golang.org/x/text v0.8.0 // indirect
+	golang.org/x/time v0.3.0 // indirect
 	golang.org/x/tools v0.7.0 // indirect
+	google.golang.org/api v0.20.0 // indirect
+	google.golang.org/appengine v1.6.7 // indirect
+	google.golang.org/genproto v0.0.0-20211021150943-2b146023228c // indirect
+	google.golang.org/grpc v1.41.0 // indirect
 	google.golang.org/protobuf v1.30.0 // indirect
 	gopkg.in/fsnotify.v1 v1.4.7 // indirect
+	gopkg.in/ns1/ns1-go.v2 v2.6.5 // indirect
 	gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
+	gopkg.in/yaml.v2 v2.4.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 	gorm.io/datatypes v1.1.1 // indirect
 	gorm.io/driver/mysql v1.4.7 // indirect

文件差異過大導致無法顯示
+ 812 - 8
go.sum


+ 43 - 0
lego-config.sh

@@ -0,0 +1,43 @@
+#!/bin/bash
+
+# Download go-acme/lego repository
+download_and_extract() {
+    local repo_url="https://github.com/go-acme/lego/archive/refs/heads/master.zip"
+    local target_dir="$1"
+
+    # Check if wget and unzip are installed
+    if ! command -v wget >/dev/null || ! command -v unzip >/dev/null; then
+        echo "Please ensure wget and unzip are installed."
+        exit 1
+    fi
+
+    # Download and extract the source code
+    wget -q -O lego-master.zip "$repo_url"
+    unzip -q lego-master.zip -d "$target_dir"
+    rm lego-master.zip
+}
+
+# Copy .toml files from providers to the specified directory
+copy_toml_files() {
+    local source_dir="$1/lego-master/providers"
+    local target_dir="server/pkg/cert/config"
+
+    # Remove the lego-master folder
+    if [ ! -d "$target_dir" ]; then
+        mkdir -p "$target_dir"
+    fi
+
+    # Copy .toml files
+    find "$source_dir" -type f -name "*.toml" -exec cp {} "$target_dir" \;
+}
+
+# Remove the lego-master folder
+remove_lego_master_folder() {
+  local folder="$1/lego-master"
+  rm -rf "$folder"
+}
+
+destination="./tmp"
+download_and_extract "$destination"
+copy_toml_files "$destination"
+remove_lego_master_folder "$destination"

+ 1 - 13
resources/development/nginx/sites-available/homework.jackyu.cn

@@ -4,25 +4,13 @@ server {
     server_name homework.jackyu.cn;
     # rewrite ^(.*)$  https://$host$1 permanent;
     return 307 https://$server_name$request_uri;
-    location /.well-known/acme-challenge {
-        proxy_set_header Host $host;
-        proxy_set_header X-Real_IP $remote_addr;
-        proxy_set_header X-Forwarded-For $remote_addr:$remote_port;
-        proxy_pass http://127.0.0.1:5002;
-    }
 }
 server {
     listen 443 ssl http2;
     listen [::]:443 ssl http2;
-    server_name homework.jackyu.cn;
     ssl_certificate /etc/nginx/ssl/homework.jackyu.cn/fullchain.cer;
     ssl_certificate_key /etc/nginx/ssl/homework.jackyu.cn/private.key;
     # rewrite ^(.*)$  https://$host$1 permanent;
     return 307 https://$server_name$request_uri;
-    location /.well-known/acme-challenge {
-        proxy_set_header Host $host;
-        proxy_set_header X-Real_IP $remote_addr;
-        proxy_set_header X-Forwarded-For $remote_addr:$remote_port;
-        proxy_pass http://127.0.0.1:5002;
-    }
+    server_name home.jackyu.cn;
 }

+ 4 - 4
resources/development/nginx/sites-available/qi.jackyu.cn

@@ -1,7 +1,7 @@
 server {
     listen 80;
     listen [::]:80;
-    server_name qi.jackyu.cn amstourship.jackyu.cn;
+    server_name qi.jackyu.cn;
     rewrite ^(.*)$ https://$host$1 permanent;
     location /.well-known/acme-challenge {
         proxy_set_header Host $host;
@@ -11,9 +11,9 @@ server {
     }
 }
 server {
-    server_name qi.jackyu.cn amstourship.jackyu.cn;
-    ssl_certificate /etc/nginx/ssl/qi.jackyu.cn_amstourship.jackyu.cn/fullchain.cer;
-    ssl_certificate_key /etc/nginx/ssl/qi.jackyu.cn_amstourship.jackyu.cn/private.key;
+    server_name qi.jackyu.cn;
+    ssl_certificate /etc/nginx/ssl/qi.jackyu.cn/fullchain.cer;
+    ssl_certificate_key /etc/nginx/ssl/qi.jackyu.cn/private.key;
     listen 443 ssl;
     listen [::]:443 ssl;
     location / {

+ 88 - 59
resources/development/nginx/ssl/qi.jackyu.cn/fullchain.cer

@@ -1,64 +1,93 @@
 -----BEGIN CERTIFICATE-----
-MIIFdjCCBF6gAwIBAgITAP/njBCTrCjMpiq9Xh8Xh4hU0DANBgkqhkiG9w0BAQsF
-ADBDMQswCQYDVQQGEwJVUzESMBAGA1UEChMJZ29vZCBndXlzMSAwHgYDVQQDExdD
-QSBpbnRlcm1lZGlhdGUgKFJTQSkgQTAeFw0yMzA0MTAwMTQ5MzVaFw0yMzA3MDkw
-MTQ5MzRaMBcxFTATBgNVBAMTDHFpLmphY2t5dS5jbjCCASIwDQYJKoZIhvcNAQEB
-BQADggEPADCCAQoCggEBANBt8w+53ef/mHfz8JRHIz/C2/5pEZP0VM4rW8NfbO26
-KnugGnfGtsRzCxKNHQ1SWamMzqgyEjLzcM0L9U7/yjWW3sSBCglVfg5BFrYiX5gY
-S2a7pUPh3/zpfEUHRV6PfpPgDWVFzPzAGF3rwJIeQoxwQho9ljfffYsf7llBBCKU
-DOHHiBgGh2cmf7eg6Wu+mzWI+dnOzS2mW0QX9+X7Nz5q5ph1bRikYHP2yJ6kix8t
-DSeNpwlKFYynv/FfHbCg+mTfQ8gWPCXU/Wr3TnK5VQtqr3FzCq2IdzfEYYjaq1Mn
-7zdQ7gE07v/Ovq53/hXdjdiaDflF+2uDQ12cPdt7t40CAwEAAaOCAo0wggKJMA4G
-A1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYD
-VR0TAQH/BAIwADAdBgNVHQ4EFgQU2C4AQmUi47BO+ZnPqn+5WYH0BaswKwYDVR0j
-BCQwIoAgYjWGqKXC1CgUyRtbS1bZxpMqaNdKnoY33nyaZdVEQ/IwcQYIKwYBBQUH
-AQEEZTBjMCIGCCsGAQUFBzABhhZodHRwOi8vMTI3LjAuMC4xOjQwMDIvMD0GCCsG
-AQUFBzAChjFodHRwOi8vMTI3LjAuMC4xOjQwMDEvYWlhL2lzc3Vlci82NjA1NDQw
-NDk4MzY5NzQxMBcGA1UdEQQQMA6CDHFpLmphY2t5dS5jbjAnBgNVHR8EIDAeMByg
-GqAYhhZodHRwOi8vZXhhbXBsZS5jb20vY3JsMEAGA1UdIAQ5MDcwCAYGZ4EMAQIB
-MCsGAyoDBDAkMCIGCCsGAQUFBwIBFhZodHRwOi8vZXhhbXBsZS5jb20vY3BzMIIB
-BQYKKwYBBAHWeQIEAgSB9gSB8wDxAHcAHRrTQca8iy14Qbrw6/itgVzVWTcaENF3
-tWnJP743pq8AAAGHaRGgTAAABAMASDBGAiEA6F91bdTgx9slRzhi3ep1LSgVSUzA
-iotTuU5cQJDIdyACIQCsrhyGncGIE8ljh5yQ3NI7cFnlD27zz166EV8MwWfu8wB2
-AHvdIE8nOCpGaR79qYHkjkttuDHev9tVJliMURfZl73iAAABh2kRokMAAAQDAEcw
-RQIgLfLzsUf8xwA8WH7F/lO/loAz7vUeglFn2nqhf3nzyN4CIQDo4llhw/V1eqYq
-pJ0IjezwIzreOdPBXza3J0PXSRTlNzANBgkqhkiG9w0BAQsFAAOCAQEAcMUXX5/w
-n7n12daDqe9UQczlngpkC10o7j4rVqkOZ9m+fmcz/PKt3mMfQZ92z4zRh60AUgpw
-SL0Gfvkz1nRJ2exMipuTkT8ftpGJD77+Wqb8pKNek/7T9LbDhv8QPWXcB9T8qTIR
-jw4gP4jF1HmAa2I05M5ofx+EF6WEYUahJduYs5CoQ8gpk4frchAvo8V6wNnALQHV
-xMW3lppLItGIrfu3o+m32LELI+Hz8nyvuguT9JRXKhtCYqadtWlVqPLisj8l1CjK
-NcZz0DtALqtQqBWfE0MeWkb94TDLu8BahQM4AtPHqzZ3DFkJGB2VGMAWtl0tt0DB
-Pf6fDkASgWZKNQ==
+MIIFHTCCBAWgAwIBAgISBOq798VWOHfbJedrr1a9p6XmMA0GCSqGSIb3DQEBCwUA
+MDIxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQD
+EwJSMzAeFw0yMzA0MTIxMjI4MDZaFw0yMzA3MTExMjI4MDVaMBcxFTATBgNVBAMT
+DHFpLmphY2t5dS5jbjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANgt
+SYgwXhNM1XIDWd+wH5N9wQPVd1dSDAVOK4NakfxPyP1ztbCA26ELPaF7+Bt2r2pA
+4HrTXqAmtjCw3BQHaUIHt1ioAQ2jfY8+khjsZByZcMzI2DGQxLm8qyukUjh9IRxI
+k/0cYkcEE5Rc94Zi7YjVyJIepDtBJQYHRJdOHDFj8oqjEBn318M8SwWIz05AfC5Z
+AoIgT+HrAHmWTw6lFbVOzoEddPIK1aqmEtmn4XeGgwMXfbo6TA8WJedqVrTn02/9
+ZEaaZ+CDx85LXv2ZV7PhSCh+0BJSj/NHjJE3pi3ZRPwA64tbeOCJmYofXnH4CaaE
+PJjgfeqAc7AbBLy0VMUCAwEAAaOCAkYwggJCMA4GA1UdDwEB/wQEAwIFoDAdBgNV
+HSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4E
+FgQU+DT3aItAosiiAoe4SFpHJs/rWUcwHwYDVR0jBBgwFoAUFC6zF7dYVsuuUAlA
+5h+vnYsUwsYwVQYIKwYBBQUHAQEESTBHMCEGCCsGAQUFBzABhhVodHRwOi8vcjMu
+by5sZW5jci5vcmcwIgYIKwYBBQUHMAKGFmh0dHA6Ly9yMy5pLmxlbmNyLm9yZy8w
+FwYDVR0RBBAwDoIMcWkuamFja3l1LmNuMEwGA1UdIARFMEMwCAYGZ4EMAQIBMDcG
+CysGAQQBgt8TAQEBMCgwJgYIKwYBBQUHAgEWGmh0dHA6Ly9jcHMubGV0c2VuY3J5
+cHQub3JnMIIBAwYKKwYBBAHWeQIEAgSB9ASB8QDvAHUAtz77JN+cTbp18jnFulj0
+bF38Qs96nzXEnh0JgSXttJkAAAGHdabr6AAABAMARjBEAiBzsclX1T8pV+HhHFd4
+1IdJ2l0gMrhkIpOvjwc/u7SoswIgAK/8W7m5jG3V/7ifXcpq0FUHMjDB7EgA3u19
+wWC7S6oAdgB6MoxU2LcttiDqOOBSHumEFnAyE4VNO9IrwTpXo1LrUgAAAYd1puvw
+AAAEAwBHMEUCICOOkrLw2k+IMKS5Y1Yyry3JOO/0H0GFBjZkq0bma+CnAiEAw4n8
+huXWs9r/d5Wkh98dDScGV3TcbPo3kPMM+n+pnlgwDQYJKoZIhvcNAQELBQADggEB
+AB9/AXndrva++VWh3J9pnV4ozDD58nc50X8PhNEWWvx5Gc7ut05xG/RBSAgoGtmg
+A0iCIRdVegwMnJXKQo7r3fJwInY+cah3VbS1ItbDBBEx0m3cpOm8FnyH+87FaZSd
+RvnmARYRhlPomzeSDHRL/zGC6fg1KygEY8CALwSsRdQIGQmMLFXWz2HUQXxNRmAQ
+/UatYLTaUf7AVWGeD2z+Of1wDNs+GcP3hMDfyrxCf/oFfp7iDzp/VZboX40NoXBq
+vvdS9kJ0d9Tm//clGTXlI+FZbbAUwGkL5zwY4TraWJFqcmX1LKlSjQwyr0rT40cg
+61jNhVHyxUKVg97lFHH/MXs=
 -----END CERTIFICATE-----
 
 -----BEGIN CERTIFICATE-----
-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
+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
+nLRbwHOoq7hHwg==
+-----END CERTIFICATE-----
+
+-----BEGIN CERTIFICATE-----
+MIIFYDCCBEigAwIBAgIQQAF3ITfU6UK47naqPGQKtzANBgkqhkiG9w0BAQsFADA/
+MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT
+DkRTVCBSb290IENBIFgzMB4XDTIxMDEyMDE5MTQwM1oXDTI0MDkzMDE4MTQwM1ow
+TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
+cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwggIiMA0GCSqGSIb3DQEB
+AQUAA4ICDwAwggIKAoICAQCt6CRz9BQ385ueK1coHIe+3LffOJCMbjzmV6B493XC
+ov71am72AE8o295ohmxEk7axY/0UEmu/H9LqMZshftEzPLpI9d1537O4/xLxIZpL
+wYqGcWlKZmZsj348cL+tKSIG8+TA5oCu4kuPt5l+lAOf00eXfJlII1PoOK5PCm+D
+LtFJV4yAdLbaL9A4jXsDcCEbdfIwPPqPrt3aY6vrFk/CjhFLfs8L6P+1dy70sntK
+4EwSJQxwjQMpoOFTJOwT2e4ZvxCzSow/iaNhUd6shweU9GNx7C7ib1uYgeGJXDR5
+bHbvO5BieebbpJovJsXQEOEO3tkQjhb7t/eo98flAgeYjzYIlefiN5YNNnWe+w5y
+sR2bvAP5SQXYgd0FtCrWQemsAXaVCg/Y39W9Eh81LygXbNKYwagJZHduRze6zqxZ
+Xmidf3LWicUGQSk+WT7dJvUkyRGnWqNMQB9GoZm1pzpRboY7nn1ypxIFeFntPlF4
+FQsDj43QLwWyPntKHEtzBRL8xurgUBN8Q5N0s8p0544fAQjQMNRbcTa0B7rBMDBc
+SLeCO5imfWCKoqMpgsy6vYMEG6KDA0Gh1gXxG8K28Kh8hjtGqEgqiNx2mna/H2ql
+PRmP6zjzZN7IKw0KKP/32+IVQtQi0Cdd4Xn+GOdwiK1O5tmLOsbdJ1Fu/7xk9TND
+TwIDAQABo4IBRjCCAUIwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYw
+SwYIKwYBBQUHAQEEPzA9MDsGCCsGAQUFBzAChi9odHRwOi8vYXBwcy5pZGVudHJ1
+c3QuY29tL3Jvb3RzL2RzdHJvb3RjYXgzLnA3YzAfBgNVHSMEGDAWgBTEp7Gkeyxx
++tvhS5B1/8QVYIWJEDBUBgNVHSAETTBLMAgGBmeBDAECATA/BgsrBgEEAYLfEwEB
+ATAwMC4GCCsGAQUFBwIBFiJodHRwOi8vY3BzLnJvb3QteDEubGV0c2VuY3J5cHQu
+b3JnMDwGA1UdHwQ1MDMwMaAvoC2GK2h0dHA6Ly9jcmwuaWRlbnRydXN0LmNvbS9E
+U1RST09UQ0FYM0NSTC5jcmwwHQYDVR0OBBYEFHm0WeZ7tuXkAXOACIjIGlj26Ztu
+MA0GCSqGSIb3DQEBCwUAA4IBAQAKcwBslm7/DlLQrt2M51oGrS+o44+/yQoDFVDC
+5WxCu2+b9LRPwkSICHXM6webFGJueN7sJ7o5XPWioW5WlHAQU7G75K/QosMrAdSW
+9MUgNTP52GE24HGNtLi1qoJFlcDyqSMo59ahy2cI2qBDLKobkx/J3vWraV0T9VuG
+WCLKTVXkcGdtwlfFRjlBz4pYg1htmf5X6DYO8A4jqv2Il9DjXA6USbW1FzXSLr9O
+he8Y4IWS6wY7bCkjCWDcRQJMEhg76fsO3txE+FiYruq9RUWhiF1myv4Q6W+CyBFC
+Dfvp7OOGAN6dEOM4+qR9sdjoSYKEBpsr6GtPAQw4dy753ec5
 -----END CERTIFICATE-----

+ 25 - 25
resources/development/nginx/ssl/qi.jackyu.cn/private.key

@@ -1,27 +1,27 @@
 -----BEGIN RSA PRIVATE KEY-----
-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
+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
 -----END RSA PRIVATE KEY-----

+ 22 - 22
resources/development/nginx/ssl/qi.jackyu.cn_amstourship.jackyu.cn/fullchain.cer

@@ -1,34 +1,34 @@
 -----BEGIN CERTIFICATE-----
-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
+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
-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
+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
-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
+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
-dHA6Ly9leGFtcGxlLmNvbS9jcHMwggEDBgorBgEEAdZ5AgQCBIH0BIHxAO8AdQAd
-GtNBxryLLXhBuvDr+K2BXNVZNxoQ0Xe1ack/vjemrwAAAYdpFxjPAAAEAwBGMEQC
-IClnqIozudKr21hQr5KnaXwSh+YZz/NVJVp+Rzz0urXGAiAamh8HeKIR73f4UzyR
-foE8M13U7ukO+WAYZzgVyO+5DgB2AHvdIE8nOCpGaR79qYHkjkttuDHev9tVJliM
-URfZl73iAAABh2kXGM8AAAQDAEcwRQIhAO0Vpl5twRA3b+do3hRHIR9cGjFG9py3
-mbleaKRyRfmsAiA2cGX38jZuxhKTb2i4BOWyJ9UlQiwhabMBLK57wKW3dzANBgkq
-hkiG9w0BAQsFAAOCAQEAGHXtZKcr0RNxaP3icV48Pc6LMTdtPIILbbbfzEGvqCQa
-JmI0hdX6E4QeLsSJyrX8JvVebAILYsqEZgnkx/EQg43PDlz6giD69PV/QBmS6FW9
-z7P62xGjrI696obeH2/jKUdgjSUhxPmluDZnTyizg8dCAOpKcReu2LIFNcE0+qiE
-9kfRi8FRjvauF/y8xhvJ+OxkvMDIEXKlobjffP8Q61Cf7fEGe2PiAoiTndRUqR72
-pTwHDmilZKLEKFIFbxmTZxLa9pMOeRDpwl5wqrXW43FoihoJpt15H72R14f2apaM
-bJDegluaH9Jq3ACB7JIivyn3J9XddLHlTGG+nqDA9Q==
+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
 -----END CERTIFICATE-----
 
 -----BEGIN CERTIFICATE-----

+ 25 - 25
resources/development/nginx/ssl/qi.jackyu.cn_amstourship.jackyu.cn/private.key

@@ -1,27 +1,27 @@
 -----BEGIN RSA PRIVATE KEY-----
-MIIEpAIBAAKCAQEAxFoZztRT0tXGrbppXd0KFYqwo8j8mXqhKBzw4V3wkqGjwvpw
-X4+Jt5UROE44QlXBn8qQE2+Lbcu1m39inG+q8ZB53EjCtEw7ZvWx0QH6u9fCQdEI
-AU+OFQ+yyOL/or9vRwJUBHx4RXg0Xrc4KYZBurIhPEcFHEJt3Of7+3RskB2pCJEX
-k3w1V6AIrDIFqGsOIln/63k0ynnE6kgDtphpILE8WpXZMm24zXGJtH6KGEPuKy8u
-ddLfs6Ya8njt6P5W3VpID+pL8Q4rvPVDLDwxLd/W8en96XozGV5f//MujGokmIrQ
-vY83a1uBWTYv7OAUnA434izNt+YvFhW/jiPMNQIDAQABAoIBABKyAYMP9Gs+r4wb
-E608PpmOewMdP+/dHTsUhlru9tj/SvhloA15PUOdjeYujGzEfYsZXl57YGsz2jbU
-Ci+a8kvN1cyDoQZJu94xxpx/87+u63AY1wpI6N5YgE1gT7bPQ2kfb6B9uXXj3Bio
-mVBa2CdOjpuOp43pRUyjSfrHOw79mPOlCyOmYF3xrJr0EeZxxh06KKqRZUUTQjA4
-V75gRkjwMwWFJWBe8OL+YPcSxunN37MrtFsxgGn6tDgTLJC3S5Q4AjbvUyVA4M1F
-5FiloWWTwO7MBKZmd0S77e/LTG8ysejXHyLy/n5arM7H6YjkANx9vBb3Tgs+3PdB
-8d+qHIECgYEAxehYduV7TN6RTSP3IJMi0aCCDAId/1xK/qh9iFX/VznZX7gIluM7
-1QWU1Zblbk1OeXaJTCwKf2hTIfkLhs8FnhHKcoReWBKEJzl76xwrlwwUKI+JXf1O
-vdQk3OwRVS1iKayWaBQnC2/fT+CzUBuQKH3tMGVvhMuTXJ2iqmL7fQcCgYEA/fzb
-d6AoIL81rzLeeFgV1X3k1tXoV/RlnWq2doG+81QynLqA/yERrJAY7k/89yyRSqcN
-SbfRvwCxkc3NDFt0R2uUKkPvUDw9oCidYU/711/xfGR4xT0bzMUX7O9mmUTPe8Lj
-A9FcTBdVqdy/lEwpc1Fi+YKCAJ0GV3h+hgFP2eMCgYEAl4SGTijtWHHB7rxNVKwx
-aSqoxgbM7pe7dvKKgVnNzvskOEtOkC7SvQmz5D7N0r/vB8E/5oiFVPwLQaRJNeHD
-nDxksX40ONucP+ebvunnWZJO0Krr8YCgy2bi/hziPEMtt1hCItbi+beh0abnwboI
-iWe3s2jFk3bEkJDDXA3CGvsCgYAbLsK2TNe+mXg/ZexVaOf9T1n8fCw/rZJqhI8+
-o6gyFqLMD1Ha/EkN9OYWK01hjs2F9dcAOsIh9QsMFcCKQAdz1VmoJFkqdygJSg99
-6LeFV2la7M3YHjApfDaRTxXl9W1XS4ZMVE3SfvTWFyAR2QzeoKk9FvCe7C9PkT/J
-S76mDQKBgQCTdNUdbwGsalySqB6EHlaNQnmlhW1yGTKIobQ/fbEYS/aCm1yhKkVD
-V9H10LXOeEQGZaLKdS4I8i46ldDpcvHsdBYpvaZ3w+sWzHanyiEcq6goXwloQ8a2
-j8Fqvj+G4S06gvNQ4Xkv8eHSZM1FGsqezTBFgNr+C7Kb0xNdQ9ISag==
+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
 -----END RSA PRIVATE KEY-----

+ 322 - 304
server/api/cert.go

@@ -1,350 +1,368 @@
 package api
 
 import (
-    "github.com/0xJacky/Nginx-UI/server/model"
-    "github.com/0xJacky/Nginx-UI/server/pkg/cert"
-    "github.com/0xJacky/Nginx-UI/server/pkg/nginx"
-    "github.com/gin-gonic/gin"
-    "github.com/gorilla/websocket"
-    "github.com/spf13/cast"
-    "log"
-    "net/http"
-    "os"
-    "path/filepath"
-    "strings"
+	"github.com/0xJacky/Nginx-UI/server/model"
+	"github.com/0xJacky/Nginx-UI/server/pkg/cert"
+	"github.com/0xJacky/Nginx-UI/server/pkg/cert/dns"
+	"github.com/0xJacky/Nginx-UI/server/pkg/nginx"
+	"github.com/gin-gonic/gin"
+	"github.com/gorilla/websocket"
+	"github.com/spf13/cast"
+	"log"
+	"net/http"
+	"os"
+	"path/filepath"
+	"strings"
 )
 
 const (
-    Success = "success"
-    Info    = "info"
-    Error   = "error"
+	Success = "success"
+	Info    = "info"
+	Error   = "error"
 )
 
 type IssueCertResponse struct {
-    Status            string `json:"status"`
-    Message           string `json:"message"`
-    SSLCertificate    string `json:"ssl_certificate,omitempty"`
-    SSLCertificateKey string `json:"ssl_certificate_key,omitempty"`
+	Status            string `json:"status"`
+	Message           string `json:"message"`
+	SSLCertificate    string `json:"ssl_certificate,omitempty"`
+	SSLCertificateKey string `json:"ssl_certificate_key,omitempty"`
 }
 
 func handleIssueCertLogChan(conn *websocket.Conn, logChan chan string) {
-    defer func() {
-        if err := recover(); err != nil {
-            log.Println("api.handleIssueCertLogChan recover", err)
-        }
-    }()
+	defer func() {
+		if err := recover(); err != nil {
+			log.Println("api.handleIssueCertLogChan recover", err)
+		}
+	}()
 
-    for logString := range logChan {
+	for logString := range logChan {
 
-        err := conn.WriteJSON(IssueCertResponse{
-            Status:  Info,
-            Message: logString,
-        })
+		err := conn.WriteJSON(IssueCertResponse{
+			Status:  Info,
+			Message: logString,
+		})
 
-        if err != nil {
-            log.Println("Error handleIssueCertLogChan", err)
-            return
-        }
+		if err != nil {
+			log.Println("Error handleIssueCertLogChan", err)
+			return
+		}
 
-    }
+	}
 }
 
 func IssueCert(c *gin.Context) {
-    var upGrader = websocket.Upgrader{
-        CheckOrigin: func(r *http.Request) bool {
-            return true
-        },
-    }
-
-    // upgrade http to websocket
-    ws, err := upGrader.Upgrade(c.Writer, c.Request, nil)
-    if err != nil {
-        log.Println(err)
-        return
-    }
-
-    defer func(ws *websocket.Conn) {
-        err := ws.Close()
-        if err != nil {
-            log.Println("defer websocket close err", err)
-        }
-    }(ws)
-
-    // read
-    var buffer struct {
-        ServerName []string `json:"server_name"`
-    }
-
-    err = ws.ReadJSON(&buffer)
-
-    if err != nil {
-        log.Println(err)
-        return
-    }
-
-    certModel, err := model.FirstOrCreateCert(c.Param("name"))
-
-    if err != nil {
-        log.Println(err)
-    }
-
-    logChan := make(chan string, 1)
-    errChan := make(chan error, 1)
-
-    go cert.IssueCert(buffer.ServerName, logChan, errChan)
-
-    go handleIssueCertLogChan(ws, logChan)
-
-    // block, until errChan closes
-    for err = range errChan {
-        errLog := &cert.AutoCertErrorLog{}
-        errLog.SetCertModel(&certModel)
-        errLog.Exit("issue cert", err)
-
-        err = ws.WriteJSON(IssueCertResponse{
-            Status:  Error,
-            Message: err.Error(),
-        })
-
-        if err != nil {
-            log.Println("Error WriteJSON", err)
-            return
-        }
-
-        return
-    }
-
-    certDirName := strings.Join(buffer.ServerName, "_")
-    sslCertificatePath := nginx.GetConfPath("ssl", certDirName, "fullchain.cer")
-    sslCertificateKeyPath := nginx.GetConfPath("ssl", certDirName, "private.key")
-
-    err = certModel.Updates(&model.Cert{
-        Domains:               buffer.ServerName,
-        SSLCertificatePath:    sslCertificatePath,
-        SSLCertificateKeyPath: sslCertificateKeyPath,
-    })
-
-    if err != nil {
-        log.Println(err)
-        err = ws.WriteJSON(IssueCertResponse{
-            Status:  Error,
-            Message: err.Error(),
-        })
-        return
-    }
-
-    certModel.ClearLog()
-
-    err = ws.WriteJSON(IssueCertResponse{
-        Status:            Success,
-        Message:           "Issued certificate successfully",
-        SSLCertificate:    sslCertificatePath,
-        SSLCertificateKey: sslCertificateKeyPath,
-    })
-
-    if err != nil {
-        log.Println(err)
-        return
-    }
+	var upGrader = websocket.Upgrader{
+		CheckOrigin: func(r *http.Request) bool {
+			return true
+		},
+	}
+
+	// upgrade http to websocket
+	ws, err := upGrader.Upgrade(c.Writer, c.Request, nil)
+	if err != nil {
+		log.Println(err)
+		return
+	}
+
+	defer func(ws *websocket.Conn) {
+		err := ws.Close()
+		if err != nil {
+			log.Println("defer websocket close err", err)
+		}
+	}(ws)
+
+	// read
+	buffer := &cert.ConfigPayload{}
+
+	err = ws.ReadJSON(buffer)
+
+	if err != nil {
+		log.Println(err)
+		return
+	}
+
+	certModel, err := model.FirstOrCreateCert(c.Param("name"))
+
+	if err != nil {
+		log.Println(err)
+	}
+
+	logChan := make(chan string, 1)
+	errChan := make(chan error, 1)
+
+	go cert.IssueCert(buffer, logChan, errChan)
+
+	go handleIssueCertLogChan(ws, logChan)
+
+	// block, until errChan closes
+	for err = range errChan {
+		errLog := &cert.AutoCertErrorLog{}
+		errLog.SetCertModel(&certModel)
+		errLog.Exit("issue cert", err)
+
+		err = ws.WriteJSON(IssueCertResponse{
+			Status:  Error,
+			Message: err.Error(),
+		})
+
+		if err != nil {
+			log.Println("Error WriteJSON", err)
+			return
+		}
+
+		return
+	}
+
+	certDirName := strings.Join(buffer.ServerName, "_")
+	sslCertificatePath := nginx.GetConfPath("ssl", certDirName, "fullchain.cer")
+	sslCertificateKeyPath := nginx.GetConfPath("ssl", certDirName, "private.key")
+
+	err = certModel.Updates(&model.Cert{
+		Domains:               buffer.ServerName,
+		SSLCertificatePath:    sslCertificatePath,
+		SSLCertificateKeyPath: sslCertificateKeyPath,
+	})
+
+	if err != nil {
+		log.Println(err)
+		err = ws.WriteJSON(IssueCertResponse{
+			Status:  Error,
+			Message: err.Error(),
+		})
+		return
+	}
+
+	certModel.ClearLog()
+
+	err = ws.WriteJSON(IssueCertResponse{
+		Status:            Success,
+		Message:           "Issued certificate successfully",
+		SSLCertificate:    sslCertificatePath,
+		SSLCertificateKey: sslCertificateKeyPath,
+	})
+
+	if err != nil {
+		log.Println(err)
+		return
+	}
 
 }
 
 func GetCertList(c *gin.Context) {
-    certList := model.GetCertList(c.Query("name"), c.Query("domain"))
+	certList := model.GetCertList(c.Query("name"), c.Query("domain"))
 
-    c.JSON(http.StatusOK, gin.H{
-        "data": certList,
-    })
+	c.JSON(http.StatusOK, gin.H{
+		"data": certList,
+	})
 }
 
 func getCert(c *gin.Context, certModel *model.Cert) {
-    type resp struct {
-        *model.Cert
-        SSLCertification    string           `json:"ssl_certification"`
-        SSLCertificationKey string           `json:"ssl_certification_key"`
-        CertificateInfo     *CertificateInfo `json:"certificate_info,omitempty"`
-    }
-
-    var sslCertificationBytes, sslCertificationKeyBytes []byte
-    var certificateInfo *CertificateInfo
-    if certModel.SSLCertificatePath != "" {
-        if _, err := os.Stat(certModel.SSLCertificatePath); err == nil {
-            sslCertificationBytes, _ = os.ReadFile(certModel.SSLCertificatePath)
-        }
-
-        pubKey, err := cert.GetCertInfo(certModel.SSLCertificatePath)
-
-        if err != nil {
-            ErrHandler(c, err)
-            return
-        }
-
-        certificateInfo = &CertificateInfo{
-            SubjectName: pubKey.Subject.CommonName,
-            IssuerName:  pubKey.Issuer.CommonName,
-            NotAfter:    pubKey.NotAfter,
-            NotBefore:   pubKey.NotBefore,
-        }
-    }
-
-    if certModel.SSLCertificateKeyPath != "" {
-        if _, err := os.Stat(certModel.SSLCertificateKeyPath); err == nil {
-            sslCertificationKeyBytes, _ = os.ReadFile(certModel.SSLCertificateKeyPath)
-        }
-    }
-
-    c.JSON(http.StatusOK, resp{
-        certModel,
-        string(sslCertificationBytes),
-        string(sslCertificationKeyBytes),
-        certificateInfo,
-    })
+	type resp struct {
+		*model.Cert
+		SSLCertification    string           `json:"ssl_certification"`
+		SSLCertificationKey string           `json:"ssl_certification_key"`
+		CertificateInfo     *CertificateInfo `json:"certificate_info,omitempty"`
+	}
+
+	var sslCertificationBytes, sslCertificationKeyBytes []byte
+	var certificateInfo *CertificateInfo
+	if certModel.SSLCertificatePath != "" {
+		if _, err := os.Stat(certModel.SSLCertificatePath); err == nil {
+			sslCertificationBytes, _ = os.ReadFile(certModel.SSLCertificatePath)
+		}
+
+		pubKey, err := cert.GetCertInfo(certModel.SSLCertificatePath)
+
+		if err != nil {
+			ErrHandler(c, err)
+			return
+		}
+
+		certificateInfo = &CertificateInfo{
+			SubjectName: pubKey.Subject.CommonName,
+			IssuerName:  pubKey.Issuer.CommonName,
+			NotAfter:    pubKey.NotAfter,
+			NotBefore:   pubKey.NotBefore,
+		}
+	}
+
+	if certModel.SSLCertificateKeyPath != "" {
+		if _, err := os.Stat(certModel.SSLCertificateKeyPath); err == nil {
+			sslCertificationKeyBytes, _ = os.ReadFile(certModel.SSLCertificateKeyPath)
+		}
+	}
+
+	c.JSON(http.StatusOK, resp{
+		certModel,
+		string(sslCertificationBytes),
+		string(sslCertificationKeyBytes),
+		certificateInfo,
+	})
 }
 
 func GetCert(c *gin.Context) {
-    certModel, err := model.FirstCertByID(cast.ToInt(c.Param("id")))
+	certModel, err := model.FirstCertByID(cast.ToInt(c.Param("id")))
 
-    if err != nil {
-        ErrHandler(c, err)
-        return
-    }
+	if err != nil {
+		ErrHandler(c, err)
+		return
+	}
 
-    getCert(c, &certModel)
+	getCert(c, &certModel)
 }
 
 func AddCert(c *gin.Context) {
-    var json struct {
-        Name                  string `json:"name"`
-        SSLCertificatePath    string `json:"ssl_certificate_path" binding:"required"`
-        SSLCertificateKeyPath string `json:"ssl_certificate_key_path" binding:"required"`
-        SSLCertification      string `json:"ssl_certification"`
-        SSLCertificationKey   string `json:"ssl_certification_key"`
-    }
-    if !BindAndValid(c, &json) {
-        return
-    }
-    certModel := &model.Cert{
-        Name:                  json.Name,
-        SSLCertificatePath:    json.SSLCertificatePath,
-        SSLCertificateKeyPath: json.SSLCertificateKeyPath,
-    }
-
-    err := certModel.Insert()
-
-    if err != nil {
-        ErrHandler(c, err)
-        return
-    }
-
-    err = os.MkdirAll(filepath.Dir(json.SSLCertificatePath), 0644)
-    if err != nil {
-        ErrHandler(c, err)
-        return
-    }
-
-    err = os.MkdirAll(filepath.Dir(json.SSLCertificateKeyPath), 0644)
-    if err != nil {
-        ErrHandler(c, err)
-        return
-    }
-
-    if json.SSLCertification != "" {
-        err = os.WriteFile(json.SSLCertificatePath, []byte(json.SSLCertification), 0644)
-        if err != nil {
-            ErrHandler(c, err)
-            return
-        }
-    }
-
-    if json.SSLCertificationKey != "" {
-        err = os.WriteFile(json.SSLCertificateKeyPath, []byte(json.SSLCertificationKey), 0644)
-        if err != nil {
-            ErrHandler(c, err)
-            return
-        }
-    }
-
-    getCert(c, certModel)
+	var json struct {
+		Name                  string `json:"name"`
+		SSLCertificatePath    string `json:"ssl_certificate_path" binding:"required"`
+		SSLCertificateKeyPath string `json:"ssl_certificate_key_path" binding:"required"`
+		SSLCertification      string `json:"ssl_certification"`
+		SSLCertificationKey   string `json:"ssl_certification_key"`
+	}
+	if !BindAndValid(c, &json) {
+		return
+	}
+	certModel := &model.Cert{
+		Name:                  json.Name,
+		SSLCertificatePath:    json.SSLCertificatePath,
+		SSLCertificateKeyPath: json.SSLCertificateKeyPath,
+	}
+
+	err := certModel.Insert()
+
+	if err != nil {
+		ErrHandler(c, err)
+		return
+	}
+
+	err = os.MkdirAll(filepath.Dir(json.SSLCertificatePath), 0644)
+	if err != nil {
+		ErrHandler(c, err)
+		return
+	}
+
+	err = os.MkdirAll(filepath.Dir(json.SSLCertificateKeyPath), 0644)
+	if err != nil {
+		ErrHandler(c, err)
+		return
+	}
+
+	if json.SSLCertification != "" {
+		err = os.WriteFile(json.SSLCertificatePath, []byte(json.SSLCertification), 0644)
+		if err != nil {
+			ErrHandler(c, err)
+			return
+		}
+	}
+
+	if json.SSLCertificationKey != "" {
+		err = os.WriteFile(json.SSLCertificateKeyPath, []byte(json.SSLCertificationKey), 0644)
+		if err != nil {
+			ErrHandler(c, err)
+			return
+		}
+	}
+
+	getCert(c, certModel)
 }
 
 func ModifyCert(c *gin.Context) {
-    id := cast.ToInt(c.Param("id"))
-    certModel, err := model.FirstCertByID(id)
-
-    var json struct {
-        Name                  string `json:"name"`
-        SSLCertificatePath    string `json:"ssl_certificate_path" binding:"required"`
-        SSLCertificateKeyPath string `json:"ssl_certificate_key_path" binding:"required"`
-        SSLCertification      string `json:"ssl_certification"`
-        SSLCertificationKey   string `json:"ssl_certification_key"`
-    }
-
-    if !BindAndValid(c, &json) {
-        return
-    }
-
-    if err != nil {
-        ErrHandler(c, err)
-        return
-    }
-
-    err = certModel.Updates(&model.Cert{
-        Name:                  json.Name,
-        SSLCertificatePath:    json.SSLCertificatePath,
-        SSLCertificateKeyPath: json.SSLCertificateKeyPath,
-    })
-
-    if err != nil {
-        ErrHandler(c, err)
-        return
-    }
-
-    err = os.MkdirAll(filepath.Dir(json.SSLCertificatePath), 0644)
-    if err != nil {
-        ErrHandler(c, err)
-        return
-    }
-
-    err = os.MkdirAll(filepath.Dir(json.SSLCertificateKeyPath), 0644)
-    if err != nil {
-        ErrHandler(c, err)
-        return
-    }
-
-    if json.SSLCertification != "" {
-        err = os.WriteFile(json.SSLCertificatePath, []byte(json.SSLCertification), 0644)
-        if err != nil {
-            ErrHandler(c, err)
-            return
-        }
-    }
-
-    if json.SSLCertificationKey != "" {
-        err = os.WriteFile(json.SSLCertificateKeyPath, []byte(json.SSLCertificationKey), 0644)
-        if err != nil {
-            ErrHandler(c, err)
-            return
-        }
-    }
-
-    GetCert(c)
+	id := cast.ToInt(c.Param("id"))
+	certModel, err := model.FirstCertByID(id)
+
+	var json struct {
+		Name                  string `json:"name"`
+		SSLCertificatePath    string `json:"ssl_certificate_path" binding:"required"`
+		SSLCertificateKeyPath string `json:"ssl_certificate_key_path" binding:"required"`
+		SSLCertification      string `json:"ssl_certification"`
+		SSLCertificationKey   string `json:"ssl_certification_key"`
+	}
+
+	if !BindAndValid(c, &json) {
+		return
+	}
+
+	if err != nil {
+		ErrHandler(c, err)
+		return
+	}
+
+	err = certModel.Updates(&model.Cert{
+		Name:                  json.Name,
+		SSLCertificatePath:    json.SSLCertificatePath,
+		SSLCertificateKeyPath: json.SSLCertificateKeyPath,
+	})
+
+	if err != nil {
+		ErrHandler(c, err)
+		return
+	}
+
+	err = os.MkdirAll(filepath.Dir(json.SSLCertificatePath), 0644)
+	if err != nil {
+		ErrHandler(c, err)
+		return
+	}
+
+	err = os.MkdirAll(filepath.Dir(json.SSLCertificateKeyPath), 0644)
+	if err != nil {
+		ErrHandler(c, err)
+		return
+	}
+
+	if json.SSLCertification != "" {
+		err = os.WriteFile(json.SSLCertificatePath, []byte(json.SSLCertification), 0644)
+		if err != nil {
+			ErrHandler(c, err)
+			return
+		}
+	}
+
+	if json.SSLCertificationKey != "" {
+		err = os.WriteFile(json.SSLCertificateKeyPath, []byte(json.SSLCertificationKey), 0644)
+		if err != nil {
+			ErrHandler(c, err)
+			return
+		}
+	}
+
+	GetCert(c)
 }
 
 func RemoveCert(c *gin.Context) {
-    id := cast.ToInt(c.Param("id"))
-    certModel, err := model.FirstCertByID(id)
+	id := cast.ToInt(c.Param("id"))
+	certModel, err := model.FirstCertByID(id)
 
-    if err != nil {
-        ErrHandler(c, err)
-        return
-    }
+	if err != nil {
+		ErrHandler(c, err)
+		return
+	}
 
-    err = certModel.Remove()
+	err = certModel.Remove()
 
-    if err != nil {
-        ErrHandler(c, err)
-        return
-    }
+	if err != nil {
+		ErrHandler(c, err)
+		return
+	}
 
-    c.JSON(http.StatusNoContent, nil)
+	c.JSON(http.StatusNoContent, nil)
+}
+
+func GetDNSProvidersList(c *gin.Context) {
+	c.JSON(http.StatusOK, dns.GetProvidersList())
+}
+
+func GetDNSProvider(c *gin.Context) {
+	code := c.Param("code")
+
+	provider, ok := dns.GetProvider(code)
+
+	if !ok {
+		c.JSON(http.StatusNotFound, gin.H{
+			"message": "provider not found",
+		})
+		return
+	}
+
+	c.JSON(http.StatusOK, provider)
 }

+ 38 - 37
server/api/settings.go

@@ -1,47 +1,48 @@
 package api
 
 import (
-    "github.com/0xJacky/Nginx-UI/server/settings"
-    "github.com/gin-gonic/gin"
-    "net/http"
+	"github.com/0xJacky/Nginx-UI/server/settings"
+	"github.com/gin-gonic/gin"
+	"net/http"
 )
 
 func GetSettings(c *gin.Context) {
-    c.JSON(http.StatusOK, gin.H{
-        "server":    settings.ServerSettings,
-        "nginx_log": settings.NginxLogSettings,
-        "openai":    settings.OpenAISettings,
-    })
+	c.JSON(http.StatusOK, gin.H{
+		"server":    settings.ServerSettings,
+		"nginx_log": settings.NginxLogSettings,
+		"openai":    settings.OpenAISettings,
+		"git":       settings.GitSettings,
+	})
 }
 
 func SaveSettings(c *gin.Context) {
-    var json struct {
-        Server   settings.Server   `json:"server"`
-        NginxLog settings.NginxLog `json:"nginx_log"`
-        Openai   settings.OpenAI   `json:"openai"`
-    }
-
-    if !BindAndValid(c, &json) {
-        return
-    }
-
-    settings.Conf.Section("server").Key("Email").SetValue(json.Server.Email)
-    settings.Conf.Section("server").Key("HTTPChallengePort").SetValue(json.Server.HTTPChallengePort)
-    settings.Conf.Section("server").Key("GithubProxy").SetValue(json.Server.GithubProxy)
-
-    settings.Conf.Section("nginx_log").Key("AccessLogPath").SetValue(json.NginxLog.AccessLogPath)
-    settings.Conf.Section("nginx_log").Key("ErrorLogPath").SetValue(json.NginxLog.ErrorLogPath)
-
-    settings.Conf.Section("openai").Key("Model").SetValue(json.Openai.Model)
-    settings.Conf.Section("openai").Key("BaseUrl").SetValue(json.Openai.BaseUrl)
-    settings.Conf.Section("openai").Key("Proxy").SetValue(json.Openai.Proxy)
-    settings.Conf.Section("openai").Key("Token").SetValue(json.Openai.Token)
-
-    err := settings.Save()
-    if err != nil {
-        ErrHandler(c, err)
-        return
-    }
-
-    GetSettings(c)
+	var json struct {
+		Server   settings.Server   `json:"server"`
+		NginxLog settings.NginxLog `json:"nginx_log"`
+		Openai   settings.OpenAI   `json:"openai"`
+	}
+
+	if !BindAndValid(c, &json) {
+		return
+	}
+
+	settings.Conf.Section("server").Key("Email").SetValue(json.Server.Email)
+	settings.Conf.Section("server").Key("HTTPChallengePort").SetValue(json.Server.HTTPChallengePort)
+	settings.Conf.Section("server").Key("GithubProxy").SetValue(json.Server.GithubProxy)
+
+	settings.Conf.Section("nginx_log").Key("AccessLogPath").SetValue(json.NginxLog.AccessLogPath)
+	settings.Conf.Section("nginx_log").Key("ErrorLogPath").SetValue(json.NginxLog.ErrorLogPath)
+
+	settings.Conf.Section("openai").Key("Model").SetValue(json.Openai.Model)
+	settings.Conf.Section("openai").Key("BaseUrl").SetValue(json.Openai.BaseUrl)
+	settings.Conf.Section("openai").Key("Proxy").SetValue(json.Openai.Proxy)
+	settings.Conf.Section("openai").Key("Token").SetValue(json.Openai.Token)
+
+	err := settings.Save()
+	if err != nil {
+		ErrHandler(c, err)
+		return
+	}
+
+	GetSettings(c)
 }

+ 94 - 94
server/pkg/cert/auto_cert.go

@@ -1,123 +1,123 @@
 package cert
 
 import (
-    "fmt"
-    "github.com/0xJacky/Nginx-UI/server/model"
-    "github.com/pkg/errors"
-    "log"
-    "time"
+	"fmt"
+	"github.com/0xJacky/Nginx-UI/server/model"
+	"github.com/pkg/errors"
+	"log"
+	"time"
 )
 
 func handleIssueCertLogChan(logChan chan string) {
-    defer func() {
-        if err := recover(); err != nil {
-            log.Println("[Auto Cert] handleIssueCertLogChan", err)
-        }
-    }()
-
-    for logString := range logChan {
-        log.Println("[Auto Cert] Info", logString)
-    }
+	defer func() {
+		if err := recover(); err != nil {
+			log.Println("[Auto Cert] handleIssueCertLogChan", err)
+		}
+	}()
+
+	for logString := range logChan {
+		log.Println("[Auto Cert] Info", logString)
+	}
 }
 
 type AutoCertErrorLog struct {
-    buffer []string
-    cert   *model.Cert
+	buffer []string
+	cert   *model.Cert
 }
 
 func (t *AutoCertErrorLog) SetCertModel(cert *model.Cert) {
-    t.cert = cert
+	t.cert = cert
 }
 
 func (t *AutoCertErrorLog) Push(text string, err error) {
-    t.buffer = append(t.buffer, text+" "+err.Error())
-    log.Println("[AutoCert Error]", text, err)
+	t.buffer = append(t.buffer, text+" "+err.Error())
+	log.Println("[AutoCert Error]", text, err)
 }
 
 func (t *AutoCertErrorLog) Exit(text string, err error) {
-    t.buffer = append(t.buffer, text+" "+err.Error())
-    log.Println("[AutoCert Error]", text, err)
+	t.buffer = append(t.buffer, text+" "+err.Error())
+	log.Println("[AutoCert Error]", text, err)
 
-    if t.cert == nil {
-        return
-    }
+	if t.cert == nil {
+		return
+	}
 
-    _ = t.cert.Updates(&model.Cert{
-        Log: t.ToString(),
-    })
+	_ = t.cert.Updates(&model.Cert{
+		Log: t.ToString(),
+	})
 }
 
 func (t *AutoCertErrorLog) ToString() (content string) {
 
-    for _, v := range t.buffer {
-        content += fmt.Sprintf("[AutoCert Error] %s\n", v)
-    }
+	for _, v := range t.buffer {
+		content += fmt.Sprintf("[AutoCert Error] %s\n", v)
+	}
 
-    return
+	return
 }
 
 func AutoObtain() {
-    defer func() {
-        if err := recover(); err != nil {
-            log.Println("[AutoCert] Recover", err)
-        }
-    }()
-    log.Println("[AutoCert] Start")
-    autoCertList := model.GetAutoCertList()
-    for _, certModel := range autoCertList {
-        confName := certModel.Filename
-
-        errLog := &AutoCertErrorLog{}
-        errLog.SetCertModel(certModel)
-
-        if len(certModel.Filename) == 0 {
-            errLog.Exit("", errors.New("filename is empty"))
-            continue
-        }
-
-        if len(certModel.Domains) == 0 {
-            errLog.Exit(confName, errors.New("domains list is empty, "+
-                "try to reopen auto-cert for this config:"+confName))
-            continue
-        }
-
-        if certModel.SSLCertificatePath != "" {
-            cert, err := GetCertInfo(certModel.SSLCertificatePath)
-            if err != nil {
-                errLog.Push("get cert info", err)
-                // Get certificate info error, ignore this domain
-                continue
-            }
-            // every week
-            if time.Now().Sub(cert.NotBefore).Hours()/24 < 7 {
-                continue
-            }
-        }
-        // after 1 mo, reissue certificate
-        logChan := make(chan string, 1)
-        errChan := make(chan error, 1)
-
-        // support SAN certification
-        go IssueCert(certModel.Domains, logChan, errChan)
-
-        go handleIssueCertLogChan(logChan)
-
-        // block, unless errChan closed
-        for err := range errChan {
-            errLog.Push("issue cert", err)
-        }
-
-        logStr := errLog.ToString()
-        if logStr != "" {
-            // store error log to db
-            _ = certModel.Updates(&model.Cert{
-                Log: errLog.ToString(),
-            })
-        } else {
-            certModel.ClearLog()
-        }
-
-        close(logChan)
-    }
-    log.Println("[AutoCert] End")
+	defer func() {
+		if err := recover(); err != nil {
+			log.Println("[AutoCert] Recover", err)
+		}
+	}()
+	log.Println("[AutoCert] Start")
+	autoCertList := model.GetAutoCertList()
+	for _, certModel := range autoCertList {
+		confName := certModel.Filename
+
+		errLog := &AutoCertErrorLog{}
+		errLog.SetCertModel(certModel)
+
+		if len(certModel.Filename) == 0 {
+			errLog.Exit("", errors.New("filename is empty"))
+			continue
+		}
+
+		if len(certModel.Domains) == 0 {
+			errLog.Exit(confName, errors.New("domains list is empty, "+
+				"try to reopen auto-cert for this config:"+confName))
+			continue
+		}
+
+		if certModel.SSLCertificatePath != "" {
+			cert, err := GetCertInfo(certModel.SSLCertificatePath)
+			if err != nil {
+				errLog.Push("get cert info", err)
+				// Get certificate info error, ignore this domain
+				continue
+			}
+			// every week
+			if time.Now().Sub(cert.NotBefore).Hours()/24 < 7 {
+				continue
+			}
+		}
+		// after 1 mo, reissue certificate
+		logChan := make(chan string, 1)
+		errChan := make(chan error, 1)
+
+		// support SAN certification
+		// go IssueCert(certModel.Domains, logChan, errChan)
+
+		go handleIssueCertLogChan(logChan)
+
+		// block, unless errChan closed
+		for err := range errChan {
+			errLog.Push("issue cert", err)
+		}
+
+		logStr := errLog.ToString()
+		if logStr != "" {
+			// store error log to db
+			_ = certModel.Updates(&model.Cert{
+				Log: errLog.ToString(),
+			})
+		} else {
+			certModel.ClearLog()
+		}
+
+		close(logChan)
+	}
+	log.Println("[AutoCert] End")
 }

+ 55 - 10
server/pkg/cert/cert.go

@@ -6,12 +6,14 @@ import (
 	"crypto/elliptic"
 	"crypto/rand"
 	"crypto/tls"
+	dns2 "github.com/0xJacky/Nginx-UI/server/pkg/cert/dns"
 	"github.com/0xJacky/Nginx-UI/server/pkg/nginx"
 	"github.com/0xJacky/Nginx-UI/server/settings"
 	"github.com/go-acme/lego/v4/certcrypto"
 	"github.com/go-acme/lego/v4/certificate"
 	"github.com/go-acme/lego/v4/challenge/http01"
 	"github.com/go-acme/lego/v4/lego"
+	"github.com/go-acme/lego/v4/providers/dns"
 	"github.com/go-acme/lego/v4/registration"
 	"github.com/pkg/errors"
 	"log"
@@ -21,11 +23,16 @@ import (
 	"strings"
 )
 
+const (
+	HTTP01 = "http01"
+	DNS01  = "dns01"
+)
+
 // MyUser You'll need a user or account type that implements acme.User
 type MyUser struct {
 	Email        string
 	Registration *registration.Resource
-	key          crypto.PrivateKey
+	Key          crypto.PrivateKey
 }
 
 func (u *MyUser) GetEmail() string {
@@ -35,16 +42,24 @@ func (u *MyUser) GetRegistration() *registration.Resource {
 	return u.Registration
 }
 func (u *MyUser) GetPrivateKey() crypto.PrivateKey {
-	return u.key
+	return u.Key
+}
+
+type ConfigPayload struct {
+	ServerName      []string    `json:"server_name"`
+	ChallengeMethod string      `json:"challenge_method"`
+	Config          dns2.Config `json:"config"`
 }
 
-func IssueCert(domain []string, logChan chan string, errChan chan error) {
+func IssueCert(payload *ConfigPayload, logChan chan string, errChan chan error) {
 	defer func() {
 		if err := recover(); err != nil {
 			log.Println("Issue Cert recover", err)
 		}
 	}()
 
+	domain := payload.ServerName
+
 	// Create a user. New accounts need an email and private key to start.
 	logChan <- "Generating private key for registering account"
 	privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
@@ -56,7 +71,7 @@ func IssueCert(domain []string, logChan chan string, errChan chan error) {
 	logChan <- "Preparing lego configurations"
 	myUser := MyUser{
 		Email: settings.ServerSettings.Email,
-		key:   privateKey,
+		Key:   privateKey,
 	}
 
 	config := lego.NewConfig(&myUser)
@@ -84,12 +99,42 @@ func IssueCert(domain []string, logChan chan string, errChan chan error) {
 		return
 	}
 
-	logChan <- "Using HTTP01 challenge provider"
-	err = client.Challenge.SetHTTP01Provider(
-		http01.NewProviderServer("",
-			settings.ServerSettings.HTTPChallengePort,
-		),
-	)
+	switch payload.ChallengeMethod {
+	case HTTP01:
+		logChan <- "Using HTTP01 challenge provider"
+		err = client.Challenge.SetHTTP01Provider(
+			http01.NewProviderServer("",
+				settings.ServerSettings.HTTPChallengePort,
+			),
+		)
+	case DNS01:
+		code := payload.Config.Code
+		pConfig, ok := dns2.GetProvider(code)
+
+		if !ok {
+			errChan <- errors.Wrap(err, "provider not found")
+		}
+		logChan <- "Setting environment variables"
+		if payload.Config.Configuration != nil {
+			err = pConfig.SetEnv(*payload.Config.Configuration)
+			if err != nil {
+				break
+			}
+			defer func() {
+				logChan <- "Cleaning environment variables"
+				pConfig.CleanEnv()
+			}()
+			provider, err := dns.NewDNSChallengeProviderByName(code)
+			if err != nil {
+				break
+			}
+			err = client.Challenge.SetDNS01Provider(provider)
+		} else {
+			errChan <- errors.Wrap(err, "environment configuration is empty")
+			return
+		}
+
+	}
 
 	if err != nil {
 		errChan <- errors.Wrap(err, "fail to challenge")

+ 20 - 0
server/pkg/cert/config/acmedns.toml

@@ -0,0 +1,20 @@
+Name = "Joohoi's ACME-DNS"
+Description = ''''''
+URL = "https://github.com/joohoi/acme-dns"
+Code = "acme-dns"
+Since = "v1.1.0"
+
+Example = '''
+ACME_DNS_API_BASE=http://10.0.0.8:4443 \
+ACME_DNS_STORAGE_PATH=/root/.lego-acme-dns-accounts.json \
+lego --email you@example.com --dns acme-dns --domains my.example.org run
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    ACME_DNS_API_BASE  = "The ACME-DNS API address"
+    ACME_DNS_STORAGE_PATH = "The ACME-DNS JSON account data file. A per-domain account will be registered/persisted to this file and used for TXT updates."
+
+[Links]
+  API = "https://github.com/joohoi/acme-dns#api"
+  GoClient = "https://github.com/cpu/goacmedns"

+ 33 - 0
server/pkg/cert/config/alidns.toml

@@ -0,0 +1,33 @@
+Name = "Alibaba Cloud DNS"
+Description = ''''''
+URL = "https://www.alibabacloud.com/product/dns"
+Code = "alidns"
+Since = "v1.1.0"
+
+Example = '''
+# Setup using instance RAM role
+ALICLOUD_RAM_ROLE=lego \
+lego --email you@example.com --dns alidns --domains my.example.org run
+
+# Or, using credentials
+ALICLOUD_ACCESS_KEY=abcdefghijklmnopqrstuvwx \
+ALICLOUD_SECRET_KEY=your-secret-key \
+ALICLOUD_SECURITY_TOKEN=your-sts-token \
+lego --email you@example.com --dns alidns --domains my.example.org run
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    ALICLOUD_RAM_ROLE = "Your instance RAM role (https://www.alibabacloud.com/help/doc-detail/54579.htm)"
+    ALICLOUD_ACCESS_KEY = "Access key ID"
+    ALICLOUD_SECRET_KEY = "Access Key secret"
+    ALICLOUD_SECURITY_TOKEN = "STS Security Token (optional)"
+  [Configuration.Additional]
+    ALICLOUD_POLLING_INTERVAL = "Time between DNS propagation check"
+    ALICLOUD_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    ALICLOUD_TTL = "The TTL of the TXT record used for the DNS challenge"
+    ALICLOUD_HTTP_TIMEOUT = "API request timeout"
+
+[Links]
+  API = "https://www.alibabacloud.com/help/doc-detail/42875.htm"
+  GoClient = "https://github.com/aliyun/alibaba-cloud-sdk-go"

+ 24 - 0
server/pkg/cert/config/allinkl.toml

@@ -0,0 +1,24 @@
+Name = "all-inkl"
+Description = ''''''
+URL = "https://all-inkl.com"
+Code = "allinkl"
+Since = "v4.5.0"
+
+Example = '''
+ALL_INKL_LOGIN=xxxxxxxxxxxxxxxxxxxxxxxxxx \
+ALL_INKL_PASSWORD=yyyyyyyyyyyyyyyyyyyyyyyyyy \
+lego --email you@example.com --dns allinkl --domains my.example.org run
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    ALL_INKL_LOGIN = "KAS login"
+    ALL_INKL_PASSWORD = "KAS password"
+  [Configuration.Additional]
+    ALL_INKL_POLLING_INTERVAL = "Time between DNS propagation check"
+    ALL_INKL_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    ALL_INKL_HTTP_TIMEOUT = "API request timeout"
+
+[Links]
+  API = "https://kasapi.kasserver.com/dokumentation/phpdoc/index.html"
+  Guide = "https://kasapi.kasserver.com/dokumentation/"

+ 22 - 0
server/pkg/cert/config/arvancloud.toml

@@ -0,0 +1,22 @@
+Name = "ArvanCloud"
+Description = ''''''
+URL = "https://arvancloud.ir"
+Code = "arvancloud"
+Since = "v3.8.0"
+
+Example = '''
+ARVANCLOUD_API_KEY="Apikey xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" \
+lego --email you@example.com --dns arvancloud --domains my.example.org run
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    ARVANCLOUD_API_KEY = "API key"
+  [Configuration.Additional]
+    ARVANCLOUD_POLLING_INTERVAL = "Time between DNS propagation check"
+    ARVANCLOUD_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    ARVANCLOUD_TTL = "The TTL of the TXT record used for the DNS challenge"
+    ARVANCLOUD_HTTP_TIMEOUT = "API request timeout"
+
+[Links]
+  API = "https://www.arvancloud.ir/docs/api/cdn/4.0"

+ 25 - 0
server/pkg/cert/config/auroradns.toml

@@ -0,0 +1,25 @@
+Name = "Aurora DNS"
+Description = ''''''
+URL = "https://www.pcextreme.com/dns-health-checks"
+Code = "auroradns"
+Since = "v0.4.0"
+
+Example = '''
+AURORA_API_KEY=xxxxx \
+AURORA_SECRET=yyyyyy \
+lego --email you@example.com --dns auroradns --domains my.example.org run
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    AURORA_API_KEY = "API key or username to used"
+    AURORA_SECRET = "Secret password to be used"
+  [Configuration.Additional]
+    AURORA_ENDPOINT = "API endpoint URL"
+    AURORA_POLLING_INTERVAL = "Time between DNS propagation check"
+    AURORA_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    AURORA_TTL = "The TTL of the TXT record used for the DNS challenge"
+
+[Links]
+  API = "https://libcloud.readthedocs.io/en/latest/dns/drivers/auroradns.html#api-docs"
+  GoClient = "https://github.com/nrdcg/auroradns"

+ 26 - 0
server/pkg/cert/config/autodns.toml

@@ -0,0 +1,26 @@
+Name = "Autodns"
+Description = ''''''
+URL = "https://www.internetx.com/domains/autodns/"
+Code = "autodns"
+Since = "v3.2.0"
+
+Example = '''
+AUTODNS_API_USER=username \
+AUTODNS_API_PASSWORD=supersecretpassword \
+lego --email you@example.com --dns autodns --domains my.example.org run
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    AUTODNS_API_USER = "Username"
+    AUTODNS_API_PASSWORD = "User Password"
+  [Configuration.Additional]
+    AUTODNS_ENDPOINT = "API endpoint URL, defaults to https://api.autodns.com/v1/"
+    AUTODNS_CONTEXT = "API context (4 for production, 1 for testing. Defaults to 4)"
+    AUTODNS_TTL = "The TTL of the TXT record used for the DNS challenge"
+    AUTODNS_POLLING_INTERVAL = "Time between DNS propagation check"
+    AUTODNS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    AUTODNS_HTTP_TIMEOUT = "API request timeout, defaults to 30 seconds"
+
+[Links]
+  API = "https://help.internetx.com/display/APIJSONEN"

+ 28 - 0
server/pkg/cert/config/azure.toml

@@ -0,0 +1,28 @@
+Name = "Azure"
+Description = ''''''
+URL = "https://azure.microsoft.com/services/dns/"
+Code = "azure"
+Since = "v0.4.0"
+
+Example = ''''''
+
+[Configuration]
+  [Configuration.Credentials]
+    AZURE_ENVIRONMENT = "Azure environment, one of: public, usgovernment, german, and china"
+    AZURE_CLIENT_ID = "Client ID"
+    AZURE_CLIENT_SECRET = "Client secret"
+    AZURE_SUBSCRIPTION_ID = "Subscription ID"
+    AZURE_TENANT_ID = "Tenant ID"
+    AZURE_RESOURCE_GROUP = "Resource group"
+    'instance metadata service' = "If the credentials are **not** set via the environment, then it will attempt to get a bearer token via the [instance metadata service](https://docs.microsoft.com/en-us/azure/virtual-machines/windows/instance-metadata-service)."
+  [Configuration.Additional]
+    AZURE_METADATA_ENDPOINT = "Metadata Service endpoint URL"
+    AZURE_PRIVATE_ZONE = "Set to true to use Azure Private DNS Zones and not public"
+    AZURE_ZONE_NAME = "Zone name to use inside Azure DNS service to add the TXT record in"
+    AZURE_POLLING_INTERVAL = "Time between DNS propagation check"
+    AZURE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    AZURE_TTL = "The TTL of the TXT record used for the DNS challenge"
+
+[Links]
+  API = "https://docs.microsoft.com/en-us/go/azure/"
+  GoClient = "https://github.com/Azure/azure-sdk-for-go"

+ 22 - 0
server/pkg/cert/config/bindman.toml

@@ -0,0 +1,22 @@
+Name = "Bindman"
+Description = ''''''
+URL = "https://github.com/labbsr0x/bindman-dns-webhook"
+Code = "bindman"
+Since = "v2.6.0"
+
+Example = '''
+BINDMAN_MANAGER_ADDRESS=<your bindman manager address> \
+lego --email you@example.com --dns bindman --domains my.example.org run
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    BINDMAN_MANAGER_ADDRESS = "The server URL, should have scheme, hostname, and port (if required) of the Bindman-DNS Manager server"
+  [Configuration.Additional]
+    BINDMAN_POLLING_INTERVAL = "Time between DNS propagation check"
+    BINDMAN_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    BINDMAN_HTTP_TIMEOUT = "API request timeout"
+
+[Links]
+  API = "https://gitlab.isc.org/isc-projects/bind9"
+  GoClient = "https://github.com/labbsr0x/bindman-dns-webhook"

+ 31 - 0
server/pkg/cert/config/bluecat.toml

@@ -0,0 +1,31 @@
+Name = "Bluecat"
+Description = ''''''
+URL = "https://www.bluecatnetworks.com"
+Code = "bluecat"
+Since = "v0.5.0"
+
+Example = '''
+BLUECAT_PASSWORD=mypassword \
+BLUECAT_DNS_VIEW=myview \
+BLUECAT_USER_NAME=myusername \
+BLUECAT_CONFIG_NAME=myconfig \
+BLUECAT_SERVER_URL=https://bam.example.com \
+BLUECAT_TTL=30 \
+lego --email you@example.com --dns bluecat --domains my.example.org run
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    BLUECAT_SERVER_URL = "The server URL, should have scheme, hostname, and port (if required) of the authoritative Bluecat BAM serve"
+    BLUECAT_USER_NAME = "API username"
+    BLUECAT_PASSWORD = "API password"
+    BLUECAT_CONFIG_NAME = "Configuration name"
+    BLUECAT_DNS_VIEW = "External DNS View Name"
+  [Configuration.Additional]
+    BLUECAT_POLLING_INTERVAL = "Time between DNS propagation check"
+    BLUECAT_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    BLUECAT_TTL = "The TTL of the TXT record used for the DNS challenge"
+    BLUECAT_HTTP_TIMEOUT = "API request timeout"
+
+[Links]
+  API = "https://docs.bluecatnetworks.com/r/Address-Manager-API-Guide/REST-API/9.1.0"

+ 22 - 0
server/pkg/cert/config/bunny.toml

@@ -0,0 +1,22 @@
+Name = "Bunny"
+Description = ''''''
+URL = "https://bunny.net"
+Code = "bunny"
+Since = "v4.11.0"
+
+Example = '''
+BUNNY_API_KEY=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \
+lego --email you@example.com --dns bunny --domains my.example.org run
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    BUNNY_API_KEY = "API key"
+  [Configuration.Additional]
+    BUNNY_POLLING_INTERVAL = "Time between DNS propagation check"
+    BUNNY_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    BUNNY_TTL = "The TTL of the TXT record used for the DNS challenge"
+
+[Links]
+  API = "https://docs.bunny.net/reference/dnszonepublic_index"
+  bunny-go = "https://github.com/simplesurance/bunny-go"

+ 25 - 0
server/pkg/cert/config/checkdomain.toml

@@ -0,0 +1,25 @@
+Name = "Checkdomain"
+Description = ''''''
+URL = "https://checkdomain.de/"
+Code = "checkdomain"
+Since = "v3.3.0"
+
+Example = '''
+CHECKDOMAIN_TOKEN=yoursecrettoken \
+lego --email you@example.com --dns checkdomain --domains my.example.org run
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    CHECKDOMAIN_TOKEN = "API token"
+  [Configuration.Additional]
+    CHECKDOMAIN_ENDPOINT = "API endpoint URL, defaults to https://api.checkdomain.de"
+    CHECKDOMAIN_TTL = "The TTL of the TXT record used for the DNS challenge"
+    CHECKDOMAIN_POLLING_INTERVAL = "Time between DNS propagation check"
+    CHECKDOMAIN_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    CHECKDOMAIN_HTTP_TIMEOUT = "API request timeout, defaults to 30 seconds"
+
+[Links]
+  API = "https://developer.checkdomain.de/reference/"
+  Guide = "https://developer.checkdomain.de/guide/"
+  Settings = "https://www.checkdomain.net/en/login/data/api/"

+ 21 - 0
server/pkg/cert/config/civo.toml

@@ -0,0 +1,21 @@
+Name = "Civo"
+Description = ''''''
+URL = "https://civo.com"
+Code = "civo"
+Since = "v4.9.0"
+
+Example = '''
+CIVO_TOKEN=xxxxxx \
+lego --email you@example.com --dns civo --domains my.example.org run
+'''
+
+[Configuration]
+    [Configuration.Credentials]
+        CIVO_TOKEN = "Authentication token"
+    [Configuration.Additional]
+        CIVO_POLLING_INTERVAL = "Time between DNS propagation check"
+        CIVO_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+        CIVO_TTL = "The TTL of the TXT record used for the DNS challenge"
+
+[Links]
+    API = "https://www.civo.com/api/dns"

+ 28 - 0
server/pkg/cert/config/clouddns.toml

@@ -0,0 +1,28 @@
+Name = "CloudDNS"
+Description = ''''''
+URL = "https://vshosting.eu/"
+Code = "clouddns"
+Since = "v3.6.0"
+
+Example = '''
+CLOUDDNS_CLIENT_ID=bLsdFAks23429841238feb177a572aX \
+CLOUDDNS_EMAIL=you@example.com \
+CLOUDDNS_PASSWORD=b9841238feb177a84330f \
+lego --email you@example.com --dns clouddns --domains my.example.org run
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    CLOUDDNS_CLIENT_ID = "Client ID"
+    CLOUDDNS_EMAIL = "Account email"
+    CLOUDDNS_PASSWORD = "Account password"
+  [Configuration.Additional]
+    CLOUDDNS_POLLING_INTERVAL = "Time between DNS propagation check"
+    CLOUDDNS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    CLOUDDNS_TTL = "The TTL of the TXT record used for the DNS challenge"
+    CLOUDDNS_HTTP_TIMEOUT = "API request timeout"
+
+[Links]
+  API = "https://admin.vshosting.cloud/clouddns/swagger/"
+  APIAdmin = "https://admin.vshosting.cloud/api/public/swagger/"
+  Documentation = "https://github.com/vshosting/clouddns"

+ 78 - 0
server/pkg/cert/config/cloudflare.toml

@@ -0,0 +1,78 @@
+Name = "Cloudflare"
+Description = ''''''
+URL = "https://www.cloudflare.com/dns/"
+Code = "cloudflare"
+Since = "v0.3.0"
+
+Example = '''
+CLOUDFLARE_EMAIL=you@example.com \
+CLOUDFLARE_API_KEY=b9841238feb177a84330febba8a83208921177bffe733 \
+lego --email you@example.com --dns cloudflare --domains my.example.org run
+
+# or
+
+CLOUDFLARE_DNS_API_TOKEN=1234567890abcdefghijklmnopqrstuvwxyz \
+lego --email you@example.com --dns cloudflare --domains my.example.org run
+'''
+
+Additional = '''
+## Description
+
+You may use `CF_API_EMAIL` and `CF_API_KEY` to authenticate, or `CF_DNS_API_TOKEN`, or `CF_DNS_API_TOKEN` and `CF_ZONE_API_TOKEN`.
+
+### API keys
+
+If using API keys (`CF_API_EMAIL` and `CF_API_KEY`), the Global API Key needs to be used, not the Origin CA Key.
+
+Please be aware, that this in principle allows Lego to read and change *everything* related to this account.
+
+### API tokens
+
+With API tokens (`CF_DNS_API_TOKEN`, and optionally `CF_ZONE_API_TOKEN`),
+very specific access can be granted to your resources at Cloudflare.
+See this [Cloudflare announcement](https://blog.cloudflare.com/api-tokens-general-availability/) for details.
+
+The main resources Lego cares for are the DNS entries for your Zones.
+It also need to resolve a domain name to an internal Zone ID in order to manipulate DNS entries.
+
+Hence, you should create an API token with the following permissions:
+
+* Zone / Zone / Read
+* Zone / DNS / Edit
+
+You also need to scope the access to all your domains for this to work.
+Then pass the API token as `CF_DNS_API_TOKEN` to Lego.
+
+**Alternatively,** if you prefer a more strict set of privileges,
+you can split the access tokens:
+
+* Create one with *Zone / Zone / Read* permissions and scope it to all your zones.
+  This is needed to resolve domain names to Zone IDs and can be shared among multiple Lego installations.
+  Pass this API token as `CF_ZONE_API_TOKEN` to Lego.
+* Create another API token with *Zone / DNS / Edit* permissions and set the scope to the domains you want to manage with a single Lego installation.
+  Pass this token as `CF_DNS_API_TOKEN` to Lego.
+* Repeat the previous step for each host you want to run Lego on.
+
+This "paranoid" setup is mainly interesting for users who manage many zones/domains with a single Cloudflare account.
+It follows the principle of least privilege and limits the possible damage, should one of the hosts become compromised.
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    CF_API_EMAIL = "Account email"
+    CF_API_KEY = "API key"
+    CF_DNS_API_TOKEN = "API token with DNS:Edit permission (since v3.1.0)"
+    CF_ZONE_API_TOKEN = "API token with Zone:Read permission (since v3.1.0)"
+    CLOUDFLARE_EMAIL = "Alias to CF_API_EMAIL"
+    CLOUDFLARE_API_KEY = "Alias to CF_API_KEY"
+    CLOUDFLARE_DNS_API_TOKEN = "Alias to CF_DNS_API_TOKEN"
+    CLOUDFLARE_ZONE_API_TOKEN = "Alias to CF_ZONE_API_TOKEN"
+  [Configuration.Additional]
+    CLOUDFLARE_POLLING_INTERVAL = "Time between DNS propagation check"
+    CLOUDFLARE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    CLOUDFLARE_TTL = "The TTL of the TXT record used for the DNS challenge"
+    CLOUDFLARE_HTTP_TIMEOUT = "API request timeout"
+
+[Links]
+  API = "https://api.cloudflare.com/"
+  GoClient = "https://github.com/cloudflare/cloudflare-go"

+ 25 - 0
server/pkg/cert/config/cloudns.toml

@@ -0,0 +1,25 @@
+Name = "ClouDNS"
+Description = ''''''
+URL = "https://www.cloudns.net"
+Code = "cloudns"
+Since = "v2.3.0"
+
+Example = '''
+CLOUDNS_AUTH_ID=xxxx \
+CLOUDNS_AUTH_PASSWORD=yyyy \
+lego --email you@example.com --dns cloudns --domains my.example.org run
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    CLOUDNS_AUTH_ID = "The API user ID"
+    CLOUDNS_AUTH_PASSWORD = "The password for API user ID"
+  [Configuration.Additional]
+    CLOUDNS_SUB_AUTH_ID = "The API sub user ID"
+    CLOUDNS_POLLING_INTERVAL = "Time between DNS propagation check"
+    CLOUDNS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    CLOUDNS_TTL = "The TTL of the TXT record used for the DNS challenge"
+    CLOUDNS_HTTP_TIMEOUT = "API request timeout"
+
+[Links]
+  API = "https://www.cloudns.net/wiki/article/42/"

+ 24 - 0
server/pkg/cert/config/cloudxns.toml

@@ -0,0 +1,24 @@
+Name = "CloudXNS"
+Description = """"""
+URL = "https://www.cloudxns.net/"
+Code = "cloudxns"
+Since = "v0.5.0"
+
+Example = '''
+CLOUDXNS_API_KEY=xxxx \
+CLOUDXNS_SECRET_KEY=yyyy \
+lego --email you@example.com --dns cloudxns --domains my.example.org run
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    CLOUDXNS_API_KEY = "The API key"
+    CLOUDXNS_SECRET_KEY = "The API secret key"
+  [Configuration.Additional]
+    CLOUDXNS_POLLING_INTERVAL = "Time between DNS propagation check"
+    CLOUDXNS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    CLOUDXNS_TTL = "The TTL of the TXT record used for the DNS challenge"
+    CLOUDXNS_HTTP_TIMEOUT = "API request timeout"
+
+[Links]
+  API = "https://www.cloudxns.net/Public/Doc/CloudXNS_api2.0_doc_zh-cn.zip"

+ 6 - 0
server/pkg/cert/config/config.go

@@ -0,0 +1,6 @@
+package config
+
+import "embed"
+
+//go:embed *
+var DistFS embed.FS

+ 27 - 0
server/pkg/cert/config/conoha.toml

@@ -0,0 +1,27 @@
+Name = "ConoHa"
+Description = ''''''
+URL = "https://www.conoha.jp/"
+Code = "conoha"
+Since = "v1.2.0"
+
+Example = '''
+CONOHA_TENANT_ID=487727e3921d44e3bfe7ebb337bf085e \
+CONOHA_API_USERNAME=xxxx \
+CONOHA_API_PASSWORD=yyyy \
+lego --email you@example.com --dns conoha --domains my.example.org run
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    CONOHA_TENANT_ID = "Tenant ID"
+    CONOHA_API_USERNAME = "The API username"
+    CONOHA_API_PASSWORD = "The API password"
+  [Configuration.Additional]
+    CONOHA_POLLING_INTERVAL = "Time between DNS propagation check"
+    CONOHA_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    CONOHA_TTL = "The TTL of the TXT record used for the DNS challenge"
+    CONOHA_HTTP_TIMEOUT = "API request timeout"
+    CONOHA_REGION = "The region"
+
+[Links]
+  API = "https://www.conoha.jp/docs/"

+ 24 - 0
server/pkg/cert/config/constellix.toml

@@ -0,0 +1,24 @@
+Name = "Constellix"
+Description = ''''''
+URL = "https://constellix.com"
+Code = "constellix"
+Since = "v3.4.0"
+
+Example = '''
+CONSTELLIX_API_KEY=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \
+CONSTELLIX_SECRET_KEY=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \
+lego --email you@example.com --dns constellix --domains my.example.org run
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    CONSTELLIX_API_KEY = "User API key"
+    CONSTELLIX_SECRET_KEY = "User secret key"
+  [Configuration.Additional]
+    CONSTELLIX_POLLING_INTERVAL = "Time between DNS propagation check"
+    CONSTELLIX_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    CONSTELLIX_TTL = "The TTL of the TXT record used for the DNS challenge"
+    CONSTELLIX_HTTP_TIMEOUT = "API request timeout"
+
+[Links]
+  API = "https://api-docs.constellix.com"

+ 22 - 0
server/pkg/cert/config/desec.toml

@@ -0,0 +1,22 @@
+Name = "deSEC.io"
+Description = ''''''
+URL = "https://desec.io"
+Code = "desec"
+Since = "v3.7.0"
+
+Example = '''
+DESEC_TOKEN=x-xxxxxxxxxxxxxxxxxxxxxxxxxx \
+lego --email you@example.com --dns desec --domains my.example.org run
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    DESEC_TOKEN = "Domain token"
+  [Configuration.Additional]
+    DESEC_POLLING_INTERVAL = "Time between DNS propagation check"
+    DESEC_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    DESEC_TTL = "The TTL of the TXT record used for the DNS challenge"
+    DESEC_HTTP_TIMEOUT = "API request timeout"
+
+[Links]
+  API = "https://desec.readthedocs.io/en/latest/"

+ 68 - 0
server/pkg/cert/config/designate.toml

@@ -0,0 +1,68 @@
+Name = "Designate DNSaaS for Openstack"
+Description = ''''''
+URL = "https://docs.openstack.org/designate/latest/"
+Code = "designate"
+Since = "v2.2.0"
+
+Example = '''
+# With a `clouds.yaml`
+OS_CLOUD=my_openstack \
+lego --email you@example.com --dns designate --domains my.example.org run
+
+# or
+
+OS_AUTH_URL=https://openstack.example.org \
+OS_REGION_NAME=RegionOne \
+OS_PROJECT_ID=23d4522a987d4ab529f722a007c27846
+OS_USERNAME=myuser \
+OS_PASSWORD=passw0rd \
+lego --email you@example.com --dns designate --domains my.example.org run
+
+# or
+
+OS_AUTH_URL=https://openstack.example.org \
+OS_REGION_NAME=RegionOne \
+OS_AUTH_TYPE=v3applicationcredential \
+OS_APPLICATION_CREDENTIAL_ID=imn74uq0or7dyzz20dwo1ytls4me8dry \
+OS_APPLICATION_CREDENTIAL_SECRET=68FuSPSdQqkFQYH5X1OoriEIJOwyLtQ8QSqXZOc9XxFK1A9tzZT6He2PfPw0OMja \
+lego --email you@example.com --dns designate --domains my.example.org run
+'''
+
+Additional = '''
+## Description
+
+There are three main ways of authenticating with Designate:
+
+1. The first one is by using the `OS_CLOUD` environment variable and a `clouds.yaml` file.
+2. The second one is using your username and password, via the `OS_USERNAME`, `OS_PASSWORD` and `OS_PROJECT_NAME` environment variables.
+3. The third one is by using an application credential, via the `OS_APPLICATION_CREDENTIAL_*` and `OS_USER_ID` environment variables.
+
+For the username/password and application methods, the `OS_AUTH_URL` and `OS_REGION_NAME` environment variables are required.
+
+For more information, you can read about the different methods of authentication with OpenStack in the Keystone's documentation and the gophercloud documentation:
+
+- [Keystone username/password](https://docs.openstack.org/keystone/latest/user/supported_clients.html)
+- [Keystone application credentials](https://docs.openstack.org/keystone/latest/user/application_credentials.html)
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    OS_AUTH_URL = "Identity endpoint URL"
+    OS_USERNAME = "Username"
+    OS_PASSWORD = "Password"
+    OS_USER_ID = "User ID"
+    OS_APPLICATION_CREDENTIAL_ID = "Application credential ID"
+    OS_APPLICATION_CREDENTIAL_NAME = "Application credential name"
+    OS_APPLICATION_CREDENTIAL_SECRET = "Application credential secret"
+    OS_PROJECT_NAME = "Project name"
+    OS_REGION_NAME = "Region name"
+  [Configuration.Additional]
+    OS_PROJECT_ID = "Project ID"
+    OS_TENANT_NAME = "Tenant name (deprecated see OS_PROJECT_NAME and OS_PROJECT_ID)"
+    DESIGNATE_POLLING_INTERVAL = "Time between DNS propagation check"
+    DESIGNATE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    DESIGNATE_TTL = "The TTL of the TXT record used for the DNS challenge"
+
+[Links]
+  API = "https://docs.openstack.org/designate/latest/"
+  GoClient = "https://godoc.org/github.com/gophercloud/gophercloud/openstack/dns/v2"

+ 23 - 0
server/pkg/cert/config/digitalocean.toml

@@ -0,0 +1,23 @@
+Name = "Digital Ocean"
+Description = ''''''
+URL = "https://www.digitalocean.com/docs/networking/dns/"
+Code = "digitalocean"
+Since = "v0.3.0"
+
+Example = '''
+DO_AUTH_TOKEN=xxxxxx \
+lego --email you@example.com --dns digitalocean --domains my.example.org run
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    DO_AUTH_TOKEN = "Authentication token"
+  [Configuration.Additional]
+    DO_API_URL = "The URL of the API"
+    DO_POLLING_INTERVAL = "Time between DNS propagation check"
+    DO_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    DO_TTL = "The TTL of the TXT record used for the DNS challenge"
+    DO_HTTP_TIMEOUT = "API request timeout"
+
+[Links]
+  API = "https://developers.digitalocean.com/documentation/v2/#domain-records"

+ 22 - 0
server/pkg/cert/config/dnshomede.toml

@@ -0,0 +1,22 @@
+Name = "dnsHome.de"
+Description = ''''''
+URL = "https://www.dnshome.de"
+Code = "dnshomede"
+Since = "v4.10.0"
+
+Example = '''
+DNSHOMEDE_CREDENTIALS=sub.example.org:password \
+lego --email you@example.com --dns dnshomede --domains example.org --domains '*.example.org' run
+
+DNSHOMEDE_CREDENTIALS=my.example.org:password1,demo.example.org:password2 \
+lego --email you@example.com --dns dnshomede --domains my.example.org --domains demo.example.org
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    DNSHOMEDE_CREDENTIALS = "Comma-separated list of domain:password credential pairs"
+  [Configuration.Addtional]
+    DNSHOMEDE_POLLING_INTERVAL = "Time between DNS propagation checks"
+    DNSHOMEDE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation; defaults to 300s (5 minutes)"
+    DNSHOMEDE_SEQUENCE_INTERVAL = "Time between sequential requests"
+    DNSHOMEDE_HTTP_TIMEOUT = "API request timeout"

+ 41 - 0
server/pkg/cert/config/dnsimple.toml

@@ -0,0 +1,41 @@
+Name = "DNSimple"
+Description = ''''''
+URL = "https://dnsimple.com/"
+Code = "dnsimple"
+Since = "v0.3.0"
+
+Example = '''
+DNSIMPLE_OAUTH_TOKEN=1234567890abcdefghijklmnopqrstuvwxyz \
+lego --email you@example.com --dns dnsimple --domains my.example.org run
+'''
+
+Additional = '''
+## Description
+
+`DNSIMPLE_BASE_URL` is optional and must be set to production (https://api.dnsimple.com).
+if `DNSIMPLE_BASE_URL` is not defined or empty, the production URL is used by default.
+
+While you can manage DNS records in the [DNSimple Sandbox environment](https://developer.dnsimple.com/sandbox/),
+DNS records will not resolve and you will not be able to satisfy the ACME DNS challenge.
+
+To authenticate you need to provide a valid API token.
+HTTP Basic Authentication is intentionally not supported.
+
+### API tokens
+
+You can [generate a new API token](https://support.dnsimple.com/articles/api-access-token/) from your account page.
+Only Account API tokens are supported, if you try to use an User API token you will receive an error message.
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    DNSIMPLE_OAUTH_TOKEN = "OAuth token"
+  [Configuration.Additional]
+    DNSIMPLE_BASE_URL = "API endpoint URL"
+    DNSIMPLE_POLLING_INTERVAL = "Time between DNS propagation check"
+    DNSIMPLE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    DNSIMPLE_TTL = "The TTL of the TXT record used for the DNS challenge"
+
+[Links]
+  API = "https://developer.dnsimple.com/v2/"
+  GoClient = "https://github.com/dnsimple/dnsimple-go"

+ 25 - 0
server/pkg/cert/config/dnsmadeeasy.toml

@@ -0,0 +1,25 @@
+Name = "DNS Made Easy"
+Description = ''''''
+URL = "https://dnsmadeeasy.com/"
+Code = "dnsmadeeasy"
+Since = "v0.4.0"
+
+Example = '''
+DNSMADEEASY_API_KEY=xxxxxx \
+DNSMADEEASY_API_SECRET=yyyyy \
+lego --email you@example.com --dns dnsmadeeasy --domains my.example.org run
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    DNSMADEEASY_API_KEY = "The API key"
+    DNSMADEEASY_API_SECRET = "The API Secret key"
+  [Configuration.Additional]
+    DNSMADEEASY_SANDBOX = "Activate the sandbox (boolean)"
+    DNSMADEEASY_POLLING_INTERVAL = "Time between DNS propagation check"
+    DNSMADEEASY_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    DNSMADEEASY_TTL = "The TTL of the TXT record used for the DNS challenge"
+    DNSMADEEASY_HTTP_TIMEOUT = "API request timeout"
+
+[Links]
+  API = "https://api-docs.dnsmadeeasy.com/"

+ 25 - 0
server/pkg/cert/config/dnspod.toml

@@ -0,0 +1,25 @@
+Name = "DNSPod (deprecated)"
+Description = '''
+Use the Tencent Cloud provider instead.
+'''
+URL = "https://www.dnspod.com/"
+Code = "dnspod"
+Since = "v0.4.0"
+
+Example = '''
+DNSPOD_API_KEY=xxxxxx \
+lego --email you@example.com --dns dnspod --domains my.example.org run
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    DNSPOD_API_KEY = "The user token"
+  [Configuration.Additional]
+    DNSPOD_POLLING_INTERVAL = "Time between DNS propagation check"
+    DNSPOD_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    DNSPOD_TTL = "The TTL of the TXT record used for the DNS challenge"
+    DNSPOD_HTTP_TIMEOUT = "API request timeout"
+
+[Links]
+  API = "https://docs.dnspod.com/api/"
+  GoClient = "https://github.com/nrdcg/dnspod-go"

+ 23 - 0
server/pkg/cert/config/dode.toml

@@ -0,0 +1,23 @@
+Name = "Domain Offensive (do.de)"
+Description = ''''''
+URL = "https://www.do.de/"
+Code = "dode"
+Since = "v2.4.0"
+
+Example = '''
+DODE_TOKEN=xxxxxx \
+lego --email you@example.com --dns dode --domains my.example.org run
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    DODE_TOKEN = "API token"
+  [Configuration.Additional]
+    DODE_POLLING_INTERVAL = "Time between DNS propagation check"
+    DODE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    DODE_TTL = "The TTL of the TXT record used for the DNS challenge"
+    DODE_HTTP_TIMEOUT = "API request timeout"
+    DODE_SEQUENCE_INTERVAL = "Time between sequential requests"
+
+[Links]
+  API = "https://www.do.de/wiki/LetsEncrypt_-_Entwickler"

+ 31 - 0
server/pkg/cert/config/domeneshop.toml

@@ -0,0 +1,31 @@
+Name = "Domeneshop"
+Description = ''''''
+URL = "https://domene.shop"
+Code = "domeneshop"
+Since = "v4.3.0"
+
+Example = '''
+DOMENESHOP_API_TOKEN=<token> \
+DOMENESHOP_API_SECRET=<secret> \
+lego --email example@example.com --dns domeneshop --domains example.com run
+'''
+
+Additional = '''
+### API credentials
+
+Visit the following page for information on how to create API credentials with Domeneshop:
+
+  https://api.domeneshop.no/docs/#section/Authentication
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    DOMENESHOP_API_TOKEN = "API token"
+    DOMENESHOP_API_SECRET = "API secret"
+  [Configuration.Additional]
+    DOMENESHOP_POLLING_INTERVAL = "Time between DNS propagation check"
+    DOMENESHOP_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    DOMENESHOP_HTTP_TIMEOUT = "API request timeout"
+
+[Links]
+  API = "https://api.domeneshop.no/docs"

+ 22 - 0
server/pkg/cert/config/dreamhost.toml

@@ -0,0 +1,22 @@
+Name = "DreamHost"
+Description = ''''''
+URL = "https://www.dreamhost.com"
+Code = "dreamhost"
+Since = "v1.1.0"
+
+Example = '''
+DREAMHOST_API_KEY="YOURAPIKEY" \
+lego --email you@example.com --dns dreamhost --domains my.example.org run
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    DREAMHOST_API_KEY = "The API key"
+  [Configuration.Additional]
+    DREAMHOST_POLLING_INTERVAL = "Time between DNS propagation check"
+    DREAMHOST_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    DREAMHOST_TTL = "The TTL of the TXT record used for the DNS challenge"
+    DREAMHOST_HTTP_TIMEOUT = "API request timeout"
+
+[Links]
+  API = "https://help.dreamhost.com/hc/en-us/articles/217560167-API_overview"

+ 23 - 0
server/pkg/cert/config/duckdns.toml

@@ -0,0 +1,23 @@
+Name = "Duck DNS"
+Description = ''''''
+URL = "https://www.duckdns.org/"
+Code = "duckdns"
+Since = "v0.5.0"
+
+Example = '''
+DUCKDNS_TOKEN=xxxxxx \
+lego --email you@example.com --dns duckdns --domains my.example.org run
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    DUCKDNS_TOKEN = "Account token"
+  [Configuration.Additional]
+    DUCKDNS_POLLING_INTERVAL = "Time between DNS propagation check"
+    DUCKDNS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    DUCKDNS_TTL = "The TTL of the TXT record used for the DNS challenge"
+    DUCKDNS_HTTP_TIMEOUT = "API request timeout"
+    DUCKDNS_SEQUENCE_INTERVAL = "Time between sequential requests"
+
+[Links]
+  API = "https://www.duckdns.org/spec.jsp"

+ 26 - 0
server/pkg/cert/config/dyn.toml

@@ -0,0 +1,26 @@
+Name = "Dyn"
+Description = ''''''
+URL = "https://dyn.com/"
+Code = "dyn"
+Since = "v0.3.0"
+
+Example = '''
+DYN_CUSTOMER_NAME=xxxxxx \
+DYN_USER_NAME=yyyyy \
+DYN_PASSWORD=zzzz \
+lego --email you@example.com --dns dyn --domains my.example.org run
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    DYN_CUSTOMER_NAME = "Customer name"
+    DYN_USER_NAME = "User name"
+    DYN_PASSWORD = "Password"
+  [Configuration.Additional]
+    DYN_POLLING_INTERVAL = "Time between DNS propagation check"
+    DYN_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    DYN_TTL = "The TTL of the TXT record used for the DNS challenge"
+    DYN_HTTP_TIMEOUT = "API request timeout"
+
+[Links]
+  API = "https://help.dyn.com/rest/"

+ 22 - 0
server/pkg/cert/config/dynu.toml

@@ -0,0 +1,22 @@
+Name = "Dynu"
+Description = ''''''
+URL = "https://www.dynu.com/"
+Code = "dynu"
+Since = "v3.5.0"
+
+Example = '''
+DYNU_API_KEY=1234567890abcdefghijklmnopqrstuvwxyz \
+lego --email you@example.com --dns dynu --domains my.example.org run
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    DYNU_API_KEY = "API key"
+  [Configuration.Additional]
+    DYNU_POLLING_INTERVAL = "Time between DNS propagation check"
+    DYNU_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    DYNU_TTL = "The TTL of the TXT record used for the DNS challenge"
+    DYNU_HTTP_TIMEOUT = "API request timeout"
+
+[Links]
+  API = "https://www.dynu.com/en-US/Support/API"

+ 30 - 0
server/pkg/cert/config/easydns.toml

@@ -0,0 +1,30 @@
+Name = "EasyDNS"
+Description = ''''''
+URL = "https://easydns.com/"
+Code = "easydns"
+Since = "v2.6.0"
+
+Example = '''
+EASYDNS_TOKEN=<your token> \
+EASYDNS_KEY=<your key> \
+lego --email you@example.com --dns easydns --domains my.example.org run
+'''
+
+Additional = '''
+To test with the sandbox environment set ```EASYDNS_ENDPOINT=https://sandbox.rest.easydns.net```
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    EASYDNS_TOKEN = "API Token"
+    EASYDNS_KEY = "API Key"
+  [Configuration.Additional]
+    EASYDNS_ENDPOINT = "The endpoint URL of the API Server"
+    EASYDNS_POLLING_INTERVAL = "Time between DNS propagation check"
+    EASYDNS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    EASYDNS_SEQUENCE_INTERVAL = "Time between sequential requests"
+    EASYDNS_TTL = "The TTL of the TXT record used for the DNS challenge"
+    EASYDNS_HTTP_TIMEOUT = "API request timeout"
+
+[Links]
+  API = "https://docs.sandbox.rest.easydns.net"

+ 63 - 0
server/pkg/cert/config/edgedns.toml

@@ -0,0 +1,63 @@
+Name = "Akamai EdgeDNS"
+Description = '''
+Akamai edgedns supersedes FastDNS; implementing a DNS provider for solving the DNS-01 challenge using Akamai EdgeDNS
+'''
+URL = "https://www.akamai.com/us/en/products/security/edge-dns.jsp"
+Code = "edgedns"
+Since = "v3.9.0"
+
+Example = '''
+AKAMAI_CLIENT_SECRET=abcdefghijklmnopqrstuvwxyz1234567890ABCDEFG= \
+AKAMAI_CLIENT_TOKEN=akab-mnbvcxzlkjhgfdsapoiuytrewq1234567 \
+AKAMAI_HOST=akab-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.luna.akamaiapis.net \
+AKAMAI_ACCESS_TOKEN=akab-1234567890qwerty-asdfghjklzxcvtnu \
+lego --email you@example.com --dns edgedns --domains my.example.org run
+'''
+
+Additional = '''
+Akamai credentials are automatically detected in the following locations and prioritized in the following order:
+
+1. Section-specific environment variables (where `{SECTION}` is specified using `AKAMAI_EDGERC_SECTION`):
+  - `AKAMAI_{SECTION}_HOST`
+  - `AKAMAI_{SECTION}_ACCESS_TOKEN`
+  - `AKAMAI_{SECTION}_CLIENT_TOKEN`
+  - `AKAMAI_{SECTION}_CLIENT_SECRET`
+2. If `AKAMAI_EDGERC_SECTION` is not defined or is set to `default`, environment variables:
+  - `AKAMAI_HOST`
+  - `AKAMAI_ACCESS_TOKEN`
+  - `AKAMAI_CLIENT_TOKEN`
+  - `AKAMAI_CLIENT_SECRET`
+3. `.edgerc` file located at `AKAMAI_EDGERC`
+  - defaults to `~/.edgerc`, sections can be specified using `AKAMAI_EDGERC_SECTION`
+4. Default environment variables:
+  - `AKAMAI_HOST`
+  - `AKAMAI_ACCESS_TOKEN`
+  - `AKAMAI_CLIENT_TOKEN`
+  - `AKAMAI_CLIENT_SECRET`
+
+See also:
+
+- [Setting up Akamai credentials](https://developer.akamai.com/api/getting-started)
+- [.edgerc Format](https://developer.akamai.com/legacy/introduction/Conf_Client.html#edgercformat)
+- [API Client Authentication](https://developer.akamai.com/legacy/introduction/Client_Auth.html)
+- [Config from Env](https://github.com/akamai/AkamaiOPEN-edgegrid-golang/blob/master/edgegrid/config.go#L118)
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    AKAMAI_HOST = "API host, managed by the Akamai EdgeGrid client"
+    AKAMAI_CLIENT_TOKEN = "Client token, managed by the Akamai EdgeGrid client"
+    AKAMAI_CLIENT_SECRET = "Client secret, managed by the Akamai EdgeGrid client"
+    AKAMAI_ACCESS_TOKEN = "Access token, managed by the Akamai EdgeGrid client"
+    AKAMAI_EDGERC = "Path to the .edgerc file, managed by the Akamai EdgeGrid client"
+    AKAMAI_EDGERC_SECTION = "Configuration section, managed by the Akamai EdgeGrid client"
+  [Configuration.Additional]
+    AKAMAI_POLLING_INTERVAL = "Time between DNS propagation check. Default: 15 seconds"
+    AKAMAI_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation. Default: 3 minutes"
+    AKAMAI_TTL = "The TTL of the TXT record used for the DNS challenge"
+
+[Links]
+  API = "https://developer.akamai.com/api/cloud_security/edge_dns_zone_management/v2.html"
+  GoClient = "https://github.com/akamai/AkamaiOPEN-edgegrid-golang"
+
+

+ 22 - 0
server/pkg/cert/config/epik.toml

@@ -0,0 +1,22 @@
+Name = "Epik"
+Description = ''''''
+URL = "https://www.epik.com/"
+Code = "epik"
+Since = "v4.5.0"
+
+Example = '''
+EPIK_SIGNATURE=xxxxxxxxxxxxxxxxxxxxxxxxxx \
+lego --email you@example.com --dns epik --domains my.example.org run
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    EPIK_SIGNATURE = "Epik API signature (https://registrar.epik.com/account/api-settings/)"
+  [Configuration.Additional]
+    EPIK_POLLING_INTERVAL = "Time between DNS propagation check"
+    EPIK_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    EPIK_TTL = "The TTL of the TXT record used for the DNS challenge"
+    EPIK_HTTP_TIMEOUT = "API request timeout"
+
+[Links]
+  API = "https://docs.userapi.epik.com/v2/#/"

+ 112 - 0
server/pkg/cert/config/exec.toml

@@ -0,0 +1,112 @@
+Name = "External program"
+Description = "Solving the DNS-01 challenge using an external program."
+URL = "/dns/exec"
+Code = "exec"
+Since = "v0.5.0"
+
+Example = '''
+EXEC_PATH=/the/path/to/myscript.sh \
+lego --email you@example.com --dns exec --domains my.example.org run
+'''
+
+Additional = '''
+
+## Base Configuration
+
+| Environment Variable Name | Description                           |
+|---------------------------|---------------------------------------|
+| `EXEC_MODE`               | `RAW`, none                           |
+| `EXEC_PATH`               | The path of the the external program. |
+
+
+## Additional Configuration
+
+| Environment Variable Name  | Description                               |
+|----------------------------|-------------------------------------------|
+| `EXEC_POLLING_INTERVAL`    | Time between DNS propagation check.       |
+| `EXEC_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation. |
+| `EXEC_SEQUENCE_INTERVAL`   | Time between sequential requests.         |
+
+
+## Description
+
+The file name of the external program is specified in the environment variable `EXEC_PATH`.
+
+When it is run by lego, three command-line parameters are passed to it:
+The action ("present" or "cleanup"), the fully-qualified domain name and the value for the record.
+
+For example, requesting a certificate for the domain 'my.example.org' can be achieved by calling lego as follows:
+
+```bash
+EXEC_PATH=./update-dns.sh \
+	lego --email you@example.com \
+	--dns exec \
+	--domains my.example.org run
+```
+
+It will then call the program './update-dns.sh' with like this:
+
+```bash
+./update-dns.sh "present" "_acme-challenge.my.example.org." "MsijOYZxqyjGnFGwhjrhfg-Xgbl5r68WPda0J9EgqqI"
+```
+
+The program then needs to make sure the record is inserted.
+When it returns an error via a non-zero exit code, lego aborts.
+
+When the record is to be removed again,
+the program is called with the first command-line parameter set to `cleanup` instead of `present`.
+
+If you want to use the raw domain, token, and keyAuth values with your program, you can set `EXEC_MODE=RAW`:
+
+```bash
+EXEC_MODE=RAW \
+EXEC_PATH=./update-dns.sh \
+	lego --email you@example.com \
+	--dns exec \
+	--domains my.example.org run
+```
+
+It will then call the program `./update-dns.sh` like this:
+
+```bash
+./update-dns.sh "present" "my.example.org." "--" "some-token" "KxAy-J3NwUmg9ZQuM-gP_Mq1nStaYSaP9tYQs5_-YsE.ksT-qywTd8058G-SHHWA3RAN72Pr0yWtPYmmY5UBpQ8"
+```
+
+## Commands
+
+{{% notice note %}}
+The `--` is because the token MAY start with a `-`, and the called program may try and interpret a `-` as indicating a flag.
+In the case of urfave, which is commonly used,
+you can use the `--` delimiter to specify the start of positional arguments, and handle such a string safely.
+{{% /notice %}}
+
+### Present
+
+| Mode    | Command                                            |
+|---------|----------------------------------------------------|
+| default | `myprogram present -- <FQDN> <record>`             |
+| `RAW`   | `myprogram present -- <domain> <token> <key_auth>` |
+
+### Cleanup
+
+| Mode    | Command                                            |
+|---------|----------------------------------------------------|
+| default | `myprogram cleanup -- <FQDN> <record>`             |
+| `RAW`   | `myprogram cleanup -- <domain> <token> <key_auth>` |
+
+### Timeout
+
+The command have to display propagation timeout and polling interval into Stdout.
+
+The values must be formatted as JSON, and times are in seconds.
+Example: `{"timeout": 30, "interval": 5}`
+
+If an error occurs or if the command is not provided:
+the default display propagation timeout and polling interval are used.
+
+| Mode    | Command                                            |
+|---------|----------------------------------------------------|
+| default | `myprogram timeout`                                |
+| `RAW`   | `myprogram timeout`                                |
+
+'''

+ 27 - 0
server/pkg/cert/config/exoscale.toml

@@ -0,0 +1,27 @@
+Name = "Exoscale"
+Description = ''''''
+URL = "https://www.exoscale.com/"
+Code = "exoscale"
+Since = "v0.4.0"
+
+Example = '''
+EXOSCALE_API_KEY=abcdefghijklmnopqrstuvwx \
+EXOSCALE_API_SECRET=xxxxxxx \
+lego --email you@example.com --dns exoscale --domains my.example.org run
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    EXOSCALE_API_KEY = "API key"
+    EXOSCALE_API_SECRET = "API secret"
+  [Configuration.Additional]
+    EXOSCALE_ENDPOINT = "API endpoint URL"
+    EXOSCALE_API_ZONE = "API zone"
+    EXOSCALE_POLLING_INTERVAL = "Time between DNS propagation check"
+    EXOSCALE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    EXOSCALE_TTL = "The TTL of the TXT record used for the DNS challenge"
+    EXOSCALE_HTTP_TIMEOUT = "API request timeout"
+
+[Links]
+  API = "https://openapi-v2.exoscale.com/#endpoint-dns"
+  GoClient = "https://github.com/exoscale/egoscale"

+ 23 - 0
server/pkg/cert/config/freemyip.toml

@@ -0,0 +1,23 @@
+Name = "freemyip.com"
+Description = ''''''
+URL = "https://freemyip.com/"
+Code = "freemyip"
+Since = "v4.5.0"
+
+Example = '''
+FREEMYIP_TOKEN=xxxxxx \
+lego --email you@example.com --dns freemyip --domains my.example.org run
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    FREEMYIP_TOKEN = "Account token"
+  [Configuration.Additional]
+    FREEMYIP_POLLING_INTERVAL = "Time between DNS propagation check"
+    FREEMYIP_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    FREEMYIP_TTL = "The TTL of the TXT record used for the DNS challenge"
+    FREEMYIP_HTTP_TIMEOUT = "API request timeout"
+    FREEMYIP_SEQUENCE_INTERVAL = "Time between sequential requests"
+
+[Links]
+  API = "https://freemyip.com/help"

+ 22 - 0
server/pkg/cert/config/gandi.toml

@@ -0,0 +1,22 @@
+Name = "Gandi"
+Description = """"""
+URL = "https://www.gandi.net"
+Code = "gandi"
+Since = "v0.3.0"
+
+Example = '''
+GANDI_API_KEY=abcdefghijklmnopqrstuvwx \
+lego --email you@example.com --dns gandi --domains my.example.org run
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    GANDI_API_KEY = "API key"
+  [Configuration.Additional]
+    GANDI_POLLING_INTERVAL = "Time between DNS propagation check"
+    GANDI_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    GANDI_TTL = "The TTL of the TXT record used for the DNS challenge"
+    GANDI_HTTP_TIMEOUT = "API request timeout"
+
+[Links]
+  API = "https://doc.rpc.gandi.net/index.html"

+ 22 - 0
server/pkg/cert/config/gandiv5.toml

@@ -0,0 +1,22 @@
+Name = "Gandi Live DNS (v5)"
+Description = ''''''
+URL = "https://www.gandi.net"
+Code = "gandiv5"
+Since = "v0.5.0"
+
+Example = '''
+GANDIV5_API_KEY=abcdefghijklmnopqrstuvwx \
+lego --email you@example.com --dns gandiv5 --domains my.example.org run
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    GANDIV5_API_KEY = "API key"
+  [Configuration.Additional]
+    GANDIV5_POLLING_INTERVAL = "Time between DNS propagation check"
+    GANDIV5_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    GANDIV5_TTL = "The TTL of the TXT record used for the DNS challenge"
+    GANDIV5_HTTP_TIMEOUT = "API request timeout"
+
+[Links]
+  API = "https://api.gandi.net/docs/livedns/"

+ 30 - 0
server/pkg/cert/config/gcloud.toml

@@ -0,0 +1,30 @@
+Name = "Google Cloud"
+Description = ''''''
+URL = "https://cloud.google.com"
+Code = "gcloud"
+Since = "v0.3.0"
+
+Example = '''
+GCE_PROJECT="gc-project-id" GCE_SERVICE_ACCOUNT_FILE="/path/to/svc/account/file.json" lego \
+    --email="abc@email.com" \
+    --domains="example.com" \
+    --dns="gcloud" \
+    --path="${HOME}/.lego" \
+    run
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    GCE_PROJECT = "Project name (by default, the project name is auto-detected by using the metadata service)"
+    'Application Default Credentials' = "[Documentation](https://cloud.google.com/docs/authentication/production#providing_credentials_to_your_application)"
+    GCE_SERVICE_ACCOUNT_FILE = "Account file path"
+    GCE_SERVICE_ACCOUNT = "Account"
+  [Configuration.Additional]
+    GCE_ALLOW_PRIVATE_ZONE = "Allows requested domain to be in private DNS zone, works only with a private ACME server (by default: false)"
+    GCE_POLLING_INTERVAL = "Time between DNS propagation check"
+    GCE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    GCE_TTL = "The TTL of the TXT record used for the DNS challenge"
+
+[Links]
+  API = "https://cloud.google.com/dns/api/v1/"
+  GoClient = "https://github.com/googleapis/google-api-go-client"

+ 22 - 0
server/pkg/cert/config/gcore.toml

@@ -0,0 +1,22 @@
+Name = "G-Core Labs"
+Description = ''''''
+URL = "https://gcorelabs.com/dns/"
+Code = "gcore"
+Since = "v4.5.0"
+
+Example = '''
+GCORE_PERMANENT_API_TOKEN=xxxxx \
+lego --email you@example.com --dns gcore --domains my.example.org run
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    GCORE_PERMANENT_API_TOKEN = "Permanent API tokene (https://gcorelabs.com/blog/permanent-api-token-explained/)"
+  [Configuration.Additional]
+    GCORE_POLLING_INTERVAL = "Time between DNS propagation check"
+    GCORE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    GCORE_TTL = "The TTL of the TXT record used for the DNS challenge"
+    GCORE_HTTP_TIMEOUT = "API request timeout"
+
+[Links]
+  API = "https://dnsapi.gcorelabs.com/docs#tag/zonesV2"

+ 24 - 0
server/pkg/cert/config/glesys.toml

@@ -0,0 +1,24 @@
+Name = "Glesys"
+Description = ''''''
+URL = "https://glesys.com/"
+Code = "glesys"
+Since = "v0.5.0"
+
+Example = '''
+GLESYS_API_USER=xxxxx \
+GLESYS_API_KEY=yyyyy \
+lego --email you@example.com --dns glesys --domains my.example.org run
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    GLESYS_API_USER = "API user"
+    GLESYS_API_KEY = "API key"
+  [Configuration.Additional]
+    GLESYS_POLLING_INTERVAL = "Time between DNS propagation check"
+    GLESYS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    GLESYS_TTL = "The TTL of the TXT record used for the DNS challenge"
+    GLESYS_HTTP_TIMEOUT = "API request timeout"
+
+[Links]
+  API = "https://github.com/GleSYS/API/wiki/API-Documentation"

+ 24 - 0
server/pkg/cert/config/godaddy.toml

@@ -0,0 +1,24 @@
+Name = "Go Daddy"
+Description = ''''''
+URL = "https://godaddy.com"
+Code = "godaddy"
+Since = "v0.5.0"
+
+Example = '''
+GODADDY_API_KEY=xxxxxxxx \
+GODADDY_API_SECRET=yyyyyyyy \
+lego --email you@example.com --dns godaddy --domains my.example.org run
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    GODADDY_API_KEY = "API key"
+    GODADDY_API_SECRET = "API secret"
+  [Configuration.Additional]
+    GODADDY_POLLING_INTERVAL = "Time between DNS propagation check"
+    GODADDY_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    GODADDY_TTL = "The TTL of the TXT record used for the DNS challenge"
+    GODADDY_HTTP_TIMEOUT = "API request timeout"
+
+[Links]
+  API = "https://developer.godaddy.com/doc/endpoint/domains"

+ 22 - 0
server/pkg/cert/config/googledomains.toml

@@ -0,0 +1,22 @@
+Name = "Google Domains"
+Description = ''''''
+URL = "https://domains.google"
+Code = "googledomains"
+Since = "v4.11.0"
+
+Example = '''
+GOOGLE_DOMAINS_ACCESS_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \
+lego --email you@example.com --dns gdomains --domains my.example.org run
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    GOOGLE_DOMAINS_ACCESS_TOKEN = "Access token"
+  [Configuration.Additional]
+    GOOGLE_DOMAINS_POLLING_INTERVAL = "Time between DNS propagation check"
+    GOOGLE_DOMAINS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    GOOGLE_DOMAINS_HTTP_TIMEOUT = "API request timeout"
+
+[Links]
+  GoClient = "https://github.com/googleapis/google-api-go-client"
+

+ 22 - 0
server/pkg/cert/config/hetzner.toml

@@ -0,0 +1,22 @@
+Name = "Hetzner"
+Description = ''''''
+URL = "https://hetzner.com"
+Code = "hetzner"
+Since = "v3.7.0"
+
+Example = '''
+HETZNER_API_KEY=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \
+lego --email you@example.com --dns hetzner --domains my.example.org run
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    HETZNER_API_KEY = "API key"
+  [Configuration.Additional]
+    HETZNER_POLLING_INTERVAL = "Time between DNS propagation check"
+    HETZNER_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    HETZNER_TTL = "The TTL of the TXT record used for the DNS challenge"
+    HETZNER_HTTP_TIMEOUT = "API request timeout"
+
+[Links]
+  API = "https://dns.hetzner.com/api-docs"

+ 25 - 0
server/pkg/cert/config/hostingde.toml

@@ -0,0 +1,25 @@
+Name = "Hosting.de"
+Description = ''''''
+URL = "https://www.hosting.de/"
+Code = "hostingde"
+Since = "v1.1.0"
+
+Example = '''
+HOSTINGDE_API_KEY=xxxxxxxx \
+lego --email you@example.com --dns hostingde --domains my.example.org run
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    HOSTINGDE_API_KEY = "API key"
+  [Configuration.Additional]
+    HOSTINGDE_ZONE_NAME = "Zone name in ACE format"
+    HOSTINGDE_POLLING_INTERVAL = "Time between DNS propagation check"
+    HOSTINGDE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    HOSTINGDE_TTL = "The TTL of the TXT record used for the DNS challenge"
+    HOSTINGDE_HTTP_TIMEOUT = "API request timeout"
+
+[Links]
+  API = "https://www.hosting.de/api/#dns"
+
+

+ 23 - 0
server/pkg/cert/config/hosttech.toml

@@ -0,0 +1,23 @@
+Name = "Hosttech"
+Description = ''''''
+URL = "https://www.hosttech.eu/"
+Code = "hosttech"
+Since = "v4.5.0"
+
+Example = '''
+HOSTTECH_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxx \
+lego --email you@example.com --dns hosttech --domains my.example.org run
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    HOSTTECH_API_KEY = "API login"
+    HOSTTECH_PASSWORD = "API password"
+  [Configuration.Additional]
+    HOSTTECH_POLLING_INTERVAL = "Time between DNS propagation check"
+    HOSTTECH_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    HOSTTECH_TTL = "The TTL of the TXT record used for the DNS challenge"
+    HOSTTECH_HTTP_TIMEOUT = "API request timeout"
+
+[Links]
+  API = "https://api.ns1.hosttech.eu/api/documentation"

+ 61 - 0
server/pkg/cert/config/httpreq.toml

@@ -0,0 +1,61 @@
+Name = "HTTP request"
+Description = ''''''
+URL = "/lego/dns/httpreq/"
+Code = "httpreq"
+Since = "v2.0.0"
+
+Example = '''
+HTTPREQ_ENDPOINT=http://my.server.com:9090 \
+lego --email you@example.com --dns httpreq --domains my.example.org run
+'''
+
+Additional = '''
+## Description
+
+The server must provide:
+
+- `POST` `/present`
+- `POST` `/cleanup`
+
+The URL of the server must be define by `HTTPREQ_ENDPOINT`.
+
+### Mode
+
+There are 2 modes (`HTTPREQ_MODE`):
+
+- default mode:
+```json
+{
+  "fqdn": "_acme-challenge.domain.",
+  "value": "LHDhK3oGRvkiefQnx7OOczTY5Tic_xZ6HcMOc_gmtoM"
+}
+```
+
+- `RAW`
+```json
+{
+  "domain": "domain",
+  "token": "token",
+  "keyAuth": "key"
+}
+```
+
+### Authentication
+
+Basic authentication (optional) can be set with some environment variables:
+
+- `HTTPREQ_USERNAME` and `HTTPREQ_PASSWORD`
+- both values must be set, otherwise basic authentication is not defined.
+
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    HTTPREQ_MODE = "`RAW`, none"
+    HTTPREQ_ENDPOINT = "The URL of the server"
+  [Configuration.Additional]
+    HTTPREQ_USERNAME = "Basic authentication username"
+    HTTPREQ_PASSWORD = "Basic authentication password"
+    HTTPREQ_POLLING_INTERVAL = "Time between DNS propagation check"
+    HTTPREQ_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    HTTPREQ_HTTP_TIMEOUT = "API request timeout"

+ 48 - 0
server/pkg/cert/config/hurricane.toml

@@ -0,0 +1,48 @@
+Name = "Hurricane Electric DNS"
+Description = ''''''
+URL = "https://dns.he.net/"
+Code = "hurricane"
+Since = "v4.3.0"
+
+Example = '''
+HURRICANE_TOKENS=example.org:token \
+lego --email you@example.com --dns hurricane --domains example.org --domains '*.example.org' run
+
+HURRICANE_TOKENS=my.example.org:token1,demo.example.org:token2 \
+lego --email you@example.com --dns hurricane --domains my.example.org --domains demo.example.org
+'''
+
+Additional = """
+Before using lego to request a certificate for a given domain or wildcard (such as `my.example.org` or `*.my.example.org`),
+create a TXT record named `_acme-challenge.my.example.org`, and enable dynamic updates on it.
+Generate a token for each URL with Hurricane Electric's UI, and copy it down.
+Stick to alphanumeric tokens for greatest reliability.
+
+To authenticate with the Hurricane Electric API,
+add each record name/token pair you want to update to the `HURRICANE_TOKENS` environment variable, as shown in the examples.
+Record names (without the `_acme-challenge.` component) and their tokens are separated with colons,
+while the credential pairs are concatenated into a comma-separated list, like so:
+
+```
+HURRICANE_TOKENS=my.example.org:token1,demo.example.org:token2
+```
+
+If you are issuing both a wildcard certificate and a standard certificate for a given subdomain,
+you should not have repeat entries for that name, as both will use the same credential.
+
+```
+HURRICANE_TOKENS=example.org:token
+```
+"""
+
+[Configuration]
+  [Configuration.Credentials]
+    HURRICANE_TOKENS = "TXT record names and tokens"
+  [Configuration.Addtional]
+    HURRICANE_POLLING_INTERVAL = "Time between DNS propagation checks"
+    HURRICANE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation; defaults to 300s (5 minutes)"
+    HURRICANE_SEQUENCE_INTERVAL = "Time between sequential requests"
+    HURRICANE_HTTP_TIMEOUT = "API request timeout"
+
+[Links]
+  API = "https://dns.he.net/"

+ 49 - 0
server/pkg/cert/config/hyperone.toml

@@ -0,0 +1,49 @@
+Name = "HyperOne"
+Description = ''''''
+URL = "https://www.hyperone.com"
+Code = "hyperone"
+Since = "v3.9.0"
+
+Example = '''
+lego --email you@example.com --dns hyperone --domains my.example.org run
+'''
+
+Additional = '''
+## Description
+
+Default configuration does not require any additional environment variables,
+just a passport file in `~/.h1/passport.json` location.
+
+### Generating passport file using H1 CLI
+
+To use this application you have to generate passport file for `sa`:
+
+```
+h1 iam project sa credential generate --name my-passport --project <project ID> --sa <sa ID> --passport-output-file ~/.h1/passport.json
+```
+
+### Required permissions
+
+The application requires following permissions:
+-  `dns/zone/list`
+-  `dns/zone.recordset/list`
+-  `dns/zone.recordset/create`
+-  `dns/zone.recordset/delete`
+-  `dns/zone.record/create`
+-  `dns/zone.record/list`
+-  `dns/zone.record/delete`
+
+All required permissions are available via platform role `tool.lego`.
+'''
+
+[Configuration]
+  [Configuration.Additional]
+    HYPERONE_PASSPORT_LOCATION = "Allows to pass custom passport file location (default ~/.h1/passport.json)"
+    HYPERONE_API_URL = "Allows to pass custom API Endpoint to be used in the challenge (default https://api.hyperone.com/v2)"
+    HYPERONE_LOCATION_ID = "Specifies location (region) to be used in API calls. (default pl-waw-1)"
+    HYPERONE_TTL = "The TTL of the TXT record used for the DNS challenge"
+    HYPERONE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    HYPERONE_POLLING_INTERVAL = "Time between DNS propagation check"
+
+[Links]
+  API = "https://api.hyperone.com/v2/docs"

+ 25 - 0
server/pkg/cert/config/ibmcloud.toml

@@ -0,0 +1,25 @@
+Name = "IBM Cloud (SoftLayer)"
+Description = ''''''
+URL = "https://www.ibm.com/cloud/"
+Code = "ibmcloud"
+Since = "v4.5.0"
+
+Example = '''
+SOFTLAYER_USERNAME=xxxxx \
+SOFTLAYER_API_KEY=yyyyy \
+lego --email you@example.com --dns ibmcloud --domains my.example.org run
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    SOFTLAYER_USERNAME = "User name (IBM Cloud is <accountID>_<emailAddress>)"
+    SOFTLAYER_API_KEY = "Classic Infrastructure API key"
+  [Configuration.Additional]
+    SOFTLAYER_POLLING_INTERVAL = "Time between DNS propagation check"
+    SOFTLAYER_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    SOFTLAYER_TTL = "The TTL of the TXT record used for the DNS challenge"
+    SOFTLAYER_TIMEOUT = "API request timeout"
+
+[Links]
+  API = "https://cloud.ibm.com/docs/dns?topic=dns-getting-started-with-the-dns-api"
+  GoClient = "https://github.com/softlayer/softlayer-go"

+ 26 - 0
server/pkg/cert/config/iij.toml

@@ -0,0 +1,26 @@
+Name = "Internet Initiative Japan"
+Description = ''''''
+URL = "https://www.iij.ad.jp/en/"
+Code = "iij"
+Since = "v1.1.0"
+
+Example = '''
+IIJ_API_ACCESS_KEY=xxxxxxxx \
+IIJ_API_SECRET_KEY=yyyyyy \
+IIJ_DO_SERVICE_CODE=zzzzzz \
+lego --email you@example.com --dns iij --domains my.example.org run
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    IIJ_API_ACCESS_KEY = "API access key"
+    IIJ_API_SECRET_KEY = "API secret key"
+    IIJ_DO_SERVICE_CODE = "DO service code"
+  [Configuration.Additional]
+    IIJ_POLLING_INTERVAL = "Time between DNS propagation check"
+    IIJ_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    IIJ_TTL = "The TTL of the TXT record used for the DNS challenge"
+
+[Links]
+  API = "https://manual.iij.jp/p2/pubapi/"
+  GoClient = "https://github.com/iij/doapi"

+ 25 - 0
server/pkg/cert/config/iijdpf.toml

@@ -0,0 +1,25 @@
+Name = "IIJ DNS Platform Service"
+Description = ''''''
+URL = "https://www.iij.ad.jp/en/biz/dns-pfm/"
+Code = "iijdpf"
+Since = "v4.7.0"
+
+Example = '''
+IIJ_DPF_API_TOKEN=xxxxxxxx \
+IIJ_DPF_DPM_SERVICE_CODE=yyyyyy \
+lego --email you@example.com --dns iijdpf --domains my.example.org run
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    IIJ_DPF_API_TOKEN = "API token"
+    IIJ_DPF_DPM_SERVICE_CODE = "IIJ Managed DNS Service's service code"
+  [Configuration.Additional]
+    IIJ_DPF_API_ENDPOINT = "API endpoint URL, defaults to https://api.dns-platform.jp/dpf/v1"
+    IIJ_DPF_POLLING_INTERVAL = "Time between DNS propagation check, defaults to 5 second"
+    IIJ_DPF_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation, defaults to 660 second"
+    IIJ_DPF_TTL = "The TTL of the TXT record used for the DNS challenge, default to 300"
+
+[Links]
+  API = "https://manual.iij.jp/dpf/dpfapi/"
+  GoClient = "https://github.com/mimuret/golang-iij-dpf"

+ 36 - 0
server/pkg/cert/config/infoblox.toml

@@ -0,0 +1,36 @@
+Name = "Infoblox"
+Description = ''''''
+URL = "https://www.infoblox.com/"
+Code = "infoblox"
+Since = "v4.4.0"
+
+Example = '''
+INFOBLOX_USERNAME=api-user-529 \
+INFOBLOX_PASSWORD=b9841238feb177a84330febba8a83208921177bffe733 \
+INFOBLOX_HOST=infoblox.example.org
+lego --email you@example.com --dns infoblox --domains my.example.org run
+'''
+
+Additional = '''
+When creating an API's user ensure it has the proper permissions for the view you are working with.
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    INFOBLOX_USERNAME = "Account Username"
+    INFOBLOX_PASSWORD = "Account Password"
+    INFOBLOX_HOST = "Host URI"
+  [Configuration.Additional]
+    INFOBLOX_DNS_VIEW = "The view for the TXT records, default: External"
+    INFOBLOX_WAPI_VERSION = "The version of WAPI being used, default: 2.11"
+    INFOBLOX_PORT = "The port for the infoblox grid manager, default: 443"
+    INFOBLOX_SSL_VERIFY = "Whether or not to verify the TLS certificate, default: true"
+    INFOBLOX_POLLING_INTERVAL = "Time between DNS propagation check"
+    INFOBLOX_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    INFOBLOX_TTL = "The TTL of the TXT record used for the DNS challenge"
+    INFOBLOX_HTTP_TIMEOUT = "HTTP request timeout"
+
+
+[Links]
+  API = "https://your.infoblox.server/wapidoc/"
+  GoClient = "https://github.com/infobloxopen/infoblox-go-client"

+ 30 - 0
server/pkg/cert/config/infomaniak.toml

@@ -0,0 +1,30 @@
+Name = "Infomaniak"
+Description = ''''''
+URL = "https://www.infomaniak.com/"
+Code = "infomaniak"
+Since = "v4.1.0"
+
+Example = '''
+INFOMANIAK_ACCESS_TOKEN=1234567898765432 \
+lego --email you@example.com --dns infomaniak --domains my.example.org run
+'''
+
+Additional = '''
+## Access token
+
+Access token can be created at the url https://manager.infomaniak.com/v3/infomaniak-api.
+You will need domain scope.
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    INFOMANIAK_ACCESS_TOKEN = "Access token"
+  [Configuration.Additional]
+    INFOMANIAK_ENDPOINT = "https://api.infomaniak.com"
+    INFOMANIAK_POLLING_INTERVAL = "Time between DNS propagation check"
+    INFOMANIAK_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    INFOMANIAK_TTL = "The TTL of the TXT record used for the DNS challenge in seconds"
+    INFOMANIAK_HTTP_TIMEOUT = "API request timeout"
+
+[Links]
+  API = "https://api.infomaniak.com/doc"

+ 24 - 0
server/pkg/cert/config/internetbs.toml

@@ -0,0 +1,24 @@
+Name = "Internet.bs"
+Description = ''''''
+URL = "https://internetbs.net"
+Code = "internetbs"
+Since = "v4.5.0"
+
+Example = '''
+INTERNET_BS_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxx \
+INTERNET_BS_PASSWORD=yyyyyyyyyyyyyyyyyyyyyyyyyy \
+lego --email you@example.com --dns internetbs --domains my.example.org run
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    INTERNET_BS_API_KEY = "API key"
+    INTERNET_BS_PASSWORD = "API password"
+  [Configuration.Additional]
+    INTERNET_BS_POLLING_INTERVAL = "Time between DNS propagation check"
+    INTERNET_BS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    INTERNET_BS_TTL = "The TTL of the TXT record used for the DNS challenge"
+    INTERNET_BS_HTTP_TIMEOUT = "API request timeout"
+
+[Links]
+  API = "https://internetbs.net/internet-bs-api.pdf"

+ 32 - 0
server/pkg/cert/config/inwx.toml

@@ -0,0 +1,32 @@
+Name = "INWX"
+Description = ''''''
+URL = "https://www.inwx.de/en"
+Code = "inwx"
+Since = "v2.0.0"
+
+Example = '''
+INWX_USERNAME=xxxxxxxxxx \
+INWX_PASSWORD=yyyyyyyyyy \
+lego --email you@example.com --dns inwx --domains my.example.org run
+
+# 2FA
+INWX_USERNAME=xxxxxxxxxx \
+INWX_PASSWORD=yyyyyyyyyy \
+INWX_SHARED_SECRET=zzzzzzzzzz \
+lego --email you@example.com --dns inwx --domains my.example.org run
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    INWX_USERNAME = "Username"
+    INWX_PASSWORD = "Password"
+  [Configuration.Additional]
+    INWX_SHARED_SECRET = "shared secret related to 2FA"
+    INWX_POLLING_INTERVAL = "Time between DNS propagation check"
+    INWX_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation (default 360s)"
+    INWX_TTL = "The TTL of the TXT record used for the DNS challenge"
+    INWX_SANDBOX = "Activate the sandbox (boolean)"
+
+[Links]
+  API = "https://www.inwx.de/en/help/apidoc"
+  GoClient = "https://github.com/nrdcg/goinwx"

+ 22 - 0
server/pkg/cert/config/ionos.toml

@@ -0,0 +1,22 @@
+Name = "Ionos"
+Description = ''''''
+URL = "https://ionos.com"
+Code = "ionos"
+Since = "v4.2.0"
+
+Example = '''
+IONOS_API_KEY=xxxxxxxx \
+lego --email you@example.com --dns ionos --domains my.example.org run
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    IONOS_API_KEY = "API key `<prefix>.<secret>` https://developer.hosting.ionos.com/docs/getstarted"
+  [Configuration.Additional]
+    IONOS_POLLING_INTERVAL = "Time between DNS propagation check"
+    IONOS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    IONOS_TTL = "The TTL of the TXT record used for the DNS challenge"
+    IONOS_HTTP_TIMEOUT = "API request timeout"
+
+[Links]
+  API = "https://developer.hosting.ionos.com/docs/dns"

+ 24 - 0
server/pkg/cert/config/iwantmyname.toml

@@ -0,0 +1,24 @@
+Name = "iwantmyname"
+Description = ''''''
+URL = "https://iwantmyname.com"
+Code = "iwantmyname"
+Since = "v4.7.0"
+
+Example = '''
+IWANTMYNAME_USERNAME=xxxxxxxx \
+IWANTMYNAME_PASSWORD=xxxxxxxx \
+lego --email you@example.com --dns iwantmyname --domains my.example.org run
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    IWANTMYNAME_USERNAME = "API username"
+    IWANTMYNAME_PASSWORD = "API password"
+  [Configuration.Additional]
+    IWANTMYNAME_POLLING_INTERVAL = "Time between DNS propagation check"
+    IWANTMYNAME_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    IWANTMYNAME_TTL = "The TTL of the TXT record used for the DNS challenge"
+    IWANTMYNAME_HTTP_TIMEOUT = "API request timeout"
+
+[Links]
+  API = "https://iwantmyname.com/developer/domain-dns-api"

+ 59 - 0
server/pkg/cert/config/joker.toml

@@ -0,0 +1,59 @@
+Name = "Joker"
+Description = ''''''
+URL = "https://joker.com"
+Code = "joker"
+Since = "v2.6.0"
+
+Example = '''
+# SVC
+JOKER_API_MODE=SVC \
+JOKER_USERNAME=<your email> \
+JOKER_PASSWORD=<your password> \
+lego --email you@example.com --dns joker --domains my.example.org run
+
+# DMAPI
+JOKER_API_MODE=DMAPI \
+JOKER_USERNAME=<your email> \
+JOKER_PASSWORD=<your password> \
+lego --email you@example.com --dns joker --domains my.example.org run
+## or
+JOKER_API_MODE=DMAPI \
+JOKER_API_KEY=<your API key> \
+lego --email you@example.com --dns joker --domains my.example.org run
+'''
+
+Additional = '''
+## SVC mode
+
+In the SVC mode, username and passsword are not your email and account passwords, but those displayed in Joker.com domain dashboard when enabling Dynamic DNS.
+
+As per [Joker.com documentation](https://joker.com/faq/content/6/496/en/let_s-encrypt-support.html):
+
+> 1. please login at Joker.com, visit 'My Domains',
+>    find the domain you want to add  Let's Encrypt certificate for, and chose "DNS" in the menu
+>
+> 2. on the top right, you will find the setting for 'Dynamic DNS'.
+>    If not already active, please activate it.
+>    It will not affect any other already existing DNS records of this domain.
+>
+> 3. please take a note of the credentials which are now shown as 'Dynamic DNS Authentication', consisting of a 'username' and a 'password'.
+>
+> 4. this is all you have to do here - and only once per domain.
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    JOKER_API_MODE = "'DMAPI' or 'SVC'. DMAPI is for resellers accounts. (Default: DMAPI)"
+    JOKER_USERNAME = "Joker.com username"
+    JOKER_PASSWORD = "Joker.com password"
+    JOKER_API_KEY = "API key (only with DMAPI mode)"
+  [Configuration.Additional]
+    JOKER_POLLING_INTERVAL = "Time between DNS propagation check"
+    JOKER_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    JOKER_TTL = "The TTL of the TXT record used for the DNS challenge"
+    JOKER_HTTP_TIMEOUT = "API request timeout"
+    JOKER_SEQUENCE_INTERVAL = "Time between sequential requests (only with 'SVC' mode)"
+
+[Links]
+  API = "https://joker.com/faq/category/39/22-dmapi.html"
+  API_SVC = "https://joker.com/faq/content/6/496/en/let_s-encrypt-support.html"

+ 22 - 0
server/pkg/cert/config/liara.toml

@@ -0,0 +1,22 @@
+Name = "Liara"
+Description = ''''''
+URL = "https://liara.ir"
+Code = "liara"
+Since = "v4.10.0"
+
+Example = '''
+LIARA_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \
+lego --email myemail@example.com --dns liara --domains my.example.org run
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    LIARA_API_KEY = "The API key"
+  [Configuration.Additional]
+    LIARA_POLLING_INTERVAL = "Time between DNS propagation check"
+    LIARA_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    LIARA_TTL = "The TTL of the TXT record used for the DNS challenge"
+    LIARA_HTTP_TIMEOUT = "API request timeout"
+
+[Links]
+  API = "https://dns-service.iran.liara.ir/swagger"

+ 59 - 0
server/pkg/cert/config/lightsail.toml

@@ -0,0 +1,59 @@
+Name = "Amazon Lightsail"
+Description = ''''''
+URL = "https://aws.amazon.com/lightsail/"
+Code = "lightsail"
+Since = "v0.5.0"
+
+Example = ''''''
+
+Additional = '''
+## Description
+
+AWS Credentials are automatically detected in the following locations and prioritized in the following order:
+
+1. Environment variables: `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, [`AWS_SESSION_TOKEN`]
+2. Shared credentials file (defaults to `~/.aws/credentials`, profiles can be specified using `AWS_PROFILE`)
+3. Amazon EC2 IAM role
+
+AWS region is not required to set as the Lightsail DNS zone is in global (us-east-1) region.
+
+## Policy
+
+The following AWS IAM policy document describes the minimum permissions required for lego to complete the DNS challenge.
+
+```json
+{
+  "Version": "2012-10-17",
+  "Statement": [
+    {
+      "Effect": "Allow",
+      "Action": [
+        "lightsail:DeleteDomainEntry",
+        "lightsail:CreateDomainEntry"
+      ],
+      "Resource": "<Lightsail DNS zone ARN>"
+    }
+  ]
+}
+```
+
+Replace the `Resource` value with your Lightsail DNS zone ARN.
+You can retrieve the ARN using aws cli by running `aws lightsail get-domains --region us-east-1` (Lightsail web console does not show the ARN, unfortunately).
+It should be in the format of `arn:aws:lightsail:global:<ACCOUNT ID>:Domain/<DOMAIN ID>`.
+You also need to replace the region in the ARN to `us-east-1` (instead of `global`).
+
+Alternatively, you can also set the `Resource` to `*` (wildcard), which allow to access all domain, but this is not recommended.
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    AWS_ACCESS_KEY_ID = "Managed by the AWS client. Access key ID (`AWS_ACCESS_KEY_ID_FILE` is not supported, use `AWS_SHARED_CREDENTIALS_FILE` instead)"
+    AWS_SECRET_ACCESS_KEY = "Managed by the AWS client. Secret access key (`AWS_SECRET_ACCESS_KEY_FILE` is not supported, use `AWS_SHARED_CREDENTIALS_FILE` instead)"
+    DNS_ZONE = "Domain name of the DNS zone"
+  [Configuration.Additional]
+    AWS_SHARED_CREDENTIALS_FILE = "Managed by the AWS client. Shared credentials file."
+    LIGHTSAIL_POLLING_INTERVAL = "Time between DNS propagation check"
+    LIGHTSAIL_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+
+[Links]
+  GoClient = "https://github.com/aws/aws-sdk-go/"

+ 23 - 0
server/pkg/cert/config/linode.toml

@@ -0,0 +1,23 @@
+Name = "Linode (v4)"
+Description = ''''''
+URL = "https://www.linode.com/"
+Code = "linode"
+Since = "v1.1.0"
+
+Example = '''
+LINODE_TOKEN=xxxxx \
+lego --email you@example.com --dns linode --domains my.example.org run
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    LINODE_TOKEN = "API token"
+  [Configuration.Additional]
+    LINODE_POLLING_INTERVAL = "Time between DNS propagation check"
+    LINODE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    LINODE_TTL = "The TTL of the TXT record used for the DNS challenge"
+    LINODE_HTTP_TIMEOUT = "API request timeout"
+
+[Links]
+  API = "https://developers.linode.com/api/v4"
+  GoClient = "https://github.com/linode/linodego"

+ 28 - 0
server/pkg/cert/config/liquidweb.toml

@@ -0,0 +1,28 @@
+Name = "Liquid Web"
+Description = ''''''
+URL = "https://liquidweb.com"
+Code = "liquidweb"
+Since = "v3.1.0"
+
+Example = '''
+LIQUID_WEB_USERNAME=someuser \
+LIQUID_WEB_PASSWORD=somepass \
+LIQUID_WEB_ZONE=tacoman.com.net \
+lego --email you@example.com --dns liquidweb --domains my.example.org run
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    LIQUID_WEB_USERNAME = "Storm API Username"
+    LIQUID_WEB_PASSWORD = "Storm API Password"
+    LIQUID_WEB_ZONE = "DNS Zone"
+  [Configuration.Additional]
+    LIQUID_WEB_URL = "Storm API endpoint"
+    LIQUID_WEB_TTL = "The TTL of the TXT record used for the DNS challenge"
+    LIQUID_WEB_POLLING_INTERVAL = "Time between DNS propagation check"
+    LIQUID_WEB_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    LIQUID_WEB_HTTP_TIMEOUT = "Maximum waiting time for the DNS records to be created (not verified)"
+
+[Links]
+  API = "https://cart.liquidweb.com/storm/api/docs/v1/"
+  GoClient = "https://github.com/liquidweb/liquidweb-go"

+ 38 - 0
server/pkg/cert/config/loopia.toml

@@ -0,0 +1,38 @@
+Name = "Loopia"
+Description = ''''''
+URL = "https://loopia.com"
+Code = "loopia"
+Since = "v4.2.0"
+
+Example = '''
+LOOPIA_API_USER=xxxxxxxx \
+LOOPIA_API_PASSWORD=yyyyyyyy \
+lego --email my@email.com --dns loopia --domains my.domain.com run
+'''
+
+Additional = '''
+### API user
+
+You can [generate a new API user](https://customerzone.loopia.com/api/) from your account page.
+
+It needs to have the following permissions:
+
+* addZoneRecord
+* getZoneRecords
+* removeZoneRecord
+* removeSubdomain
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    LOOPIA_API_USER = "API username"
+    LOOPIA_API_PASSWORD = "API password"
+  [Configuration.Additional]
+    LOOPIA_API_URL = "API endpoint. Ex: https://api.loopia.se/RPCSERV or https://api.loopia.rs/RPCSERV"
+    LOOPIA_POLLING_INTERVAL = "Time between DNS propagation check"
+    LOOPIA_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    LOOPIA_TTL = "The TTL of the TXT record used for the DNS challenge"
+    LOOPIA_HTTP_TIMEOUT = "API request timeout"
+
+[Links]
+  API = "https://www.loopia.com/api"

+ 24 - 0
server/pkg/cert/config/luadns.toml

@@ -0,0 +1,24 @@
+Name = "LuaDNS"
+Description = ''''''
+URL = "https://luadns.com"
+Code = "luadns"
+Since = "v3.7.0"
+
+Example = '''
+LUADNS_API_USERNAME=youremail \
+LUADNS_API_TOKEN=xxxxxxxx \
+lego --email you@example.com --dns luadns --domains my.example.org run
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    LUADNS_API_USERNAME = "Username (your email)"
+    LUADNS_API_TOKEN = "API token"
+  [Configuration.Additional]
+    LUADNS_POLLING_INTERVAL = "Time between DNS propagation check"
+    LUADNS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    LUADNS_TTL = "The TTL of the TXT record used for the DNS challenge"
+    LUADNS_HTTP_TIMEOUT = "API request timeout"
+
+[Links]
+  API = "https://luadns.com/api.html"

+ 24 - 0
server/pkg/cert/config/mydnsjp.toml

@@ -0,0 +1,24 @@
+Name = "MyDNS.jp"
+Description = ''''''
+URL = "https://www.mydns.jp"
+Code = "mydnsjp"
+Since = "v1.2.0"
+
+Example = '''
+MYDNSJP_MASTER_ID=xxxxx \
+MYDNSJP_PASSWORD=xxxxx \
+lego --email you@example.com --dns mydnsjp --domains my.example.org run
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    MYDNSJP_MASTER_ID = "Master ID"
+    MYDNSJP_PASSWORD = "Password"
+  [Configuration.Additional]
+    MYDNSJP_POLLING_INTERVAL = "Time between DNS propagation check"
+    MYDNSJP_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    MYDNSJP_TTL = "The TTL of the TXT record used for the DNS challenge"
+    MYDNSJP_HTTP_TIMEOUT = "API request timeout"
+
+[Links]
+  API = "https://www.mydns.jp/?MENU=030"

+ 33 - 0
server/pkg/cert/config/mythicbeasts.toml

@@ -0,0 +1,33 @@
+Name = "MythicBeasts"
+Description = ''''''
+URL = "https://www.mythic-beasts.com/"
+Code = "mythicbeasts"
+Since = "v0.3.7"
+
+Example = '''
+MYTHICBEASTS_USERNAME=myuser \
+MYTHICBEASTS_PASSWORD=mypass \
+lego --email you@example.com --dns mythicbeasts --domains my.example.org run
+'''
+
+Additional = '''
+If you are using specific API keys, then the username is the API ID for your API key, and the password is the API secret.
+
+Your API key name is not needed to operate lego.
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    MYTHICBEASTS_USERNAME = "User name"
+    MYTHICBEASTS_PASSWORD = "Password"
+  [Configuration.Additional]
+    MYTHICBEASTS_API_ENDPOINT = "The endpoint for the API (must implement v2)"
+    MYTHICBEASTS_AUTH_API_ENDPOINT = "The endpoint for Mythic Beasts' Authentication"
+    MYTHICBEASTS_POLLING_INTERVAL = "Time between DNS propagation check"
+    MYTHICBEASTS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    MYTHICBEASTS_TTL = "The TTL of the TXT record used for the DNS challenge"
+    MYTHICBEASTS_HTTP_TIMEOUT = "API request timeout"
+
+[Links]
+  API = "https://www.mythic-beasts.com/support/api/dnsv2"
+  APIAuth = "https://auth.mythic-beasts.com/login"

+ 32 - 0
server/pkg/cert/config/namecheap.toml

@@ -0,0 +1,32 @@
+Name = "Namecheap"
+URL = "https://www.namecheap.com"
+Code = "namecheap"
+Since = "v0.3.0"
+Description = '''
+
+Configuration for [Namecheap](https://www.namecheap.com).
+
+**To enable API access on the Namecheap production environment, some opaque requirements must be met.**
+More information in the section [Enabling API Access](https://www.namecheap.com/support/api/intro/) of the Namecheap documentation.
+(2020-08: Account balance of $50+, 20+ domains in your account, or purchases totaling $50+ within the last 2 years.)
+'''
+
+Example = '''
+NAMECHEAP_API_USER=user \
+NAMECHEAP_API_KEY=key \
+lego --email you@example.com --dns namecheap --domains my.example.org run
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    NAMECHEAP_API_USER = "API user"
+    NAMECHEAP_API_KEY = "API key"
+  [Configuration.Additional]
+    NAMECHEAP_POLLING_INTERVAL = "Time between DNS propagation check"
+    NAMECHEAP_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    NAMECHEAP_TTL = "The TTL of the TXT record used for the DNS challenge"
+    NAMECHEAP_HTTP_TIMEOUT = "API request timeout"
+    NAMECHEAP_SANDBOX = "Activate the sandbox (boolean)"
+
+[Links]
+  API = "https://www.namecheap.com/support/api/methods.aspx"

+ 26 - 0
server/pkg/cert/config/namedotcom.toml

@@ -0,0 +1,26 @@
+Name = "Name.com"
+Description = ''''''
+URL = "https://www.name.com"
+Code = "namedotcom"
+Since = "v0.5.0"
+
+Example = '''
+NAMECOM_USERNAME=foo.bar \
+NAMECOM_API_TOKEN=a379a6f6eeafb9a55e378c118034e2751e682fab \
+lego --email you@example.com --dns namedotcom --domains my.example.org run
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    NAMECOM_USERNAME = "Username"
+    NAMECOM_API_TOKEN = "API token"
+  [Configuration.Additional]
+    NAMECOM_POLLING_INTERVAL = "Time between DNS propagation check"
+    NAMECOM_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    NAMECOM_TTL = "The TTL of the TXT record used for the DNS challenge"
+    NAMECOM_HTTP_TIMEOUT = "API request timeout"
+
+[Links]
+  API = "https://www.name.com/api-docs/DNS"
+  GoClient = "https://github.com/namedotcom/go"
+

+ 22 - 0
server/pkg/cert/config/namesilo.toml

@@ -0,0 +1,22 @@
+Name = "Namesilo"
+Description = ''''''
+URL = "https://www.namesilo.com/"
+Code = "namesilo"
+Since = "v2.7.0"
+
+Example = '''
+NAMESILO_API_KEY=b9841238feb177a84330febba8a83208921177bffe733 \
+lego --email you@example.com --dns namesilo --domains my.example.org run
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    NAMESILO_API_KEY = "Client ID"
+  [Configuration.Additional]
+    NAMESILO_POLLING_INTERVAL = "Time between DNS propagation check"
+    NAMESILO_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation, it is better to set larger than 15m"
+    NAMESILO_TTL = "The TTL of the TXT record used for the DNS challenge, should be in [3600, 2592000]"
+
+[Links]
+  API = "https://www.namesilo.com/api_reference.php"
+  GoClient = "https://github.com/nrdcg/namesilo"

+ 25 - 0
server/pkg/cert/config/nearlyfreespeech.toml

@@ -0,0 +1,25 @@
+Name = "NearlyFreeSpeech.NET"
+Description = ''''''
+URL = "https://nearlyfreespeech.net/"
+Code = "nearlyfreespeech"
+Since = "v4.8.0"
+
+Example = '''
+NEARLYFREESPEECH_API_KEY=xxxxxx \
+NEARLYFREESPEECH_LOGIN=xxxx \
+lego --email you@example.com --dns nearlyfreespeech --domains my.example.org run
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    NEARLYFREESPEECH_API_KEY = "API Key for API requests"
+    NEARLYFREESPEECH_LOGIN = "Username for API requests"
+  [Configuration.Additional]
+    NEARLYFREESPEECH_POLLING_INTERVAL = "Time between DNS propagation check"
+    NEARLYFREESPEECH_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    NEARLYFREESPEECH_TTL = "The TTL of the TXT record used for the DNS challenge"
+    NEARLYFREESPEECH_HTTP_TIMEOUT = "API request timeout"
+    NEARLYFREESPEECH_SEQUENCE_INTERVAL = "Time between sequential requests"
+
+[Links]
+  API = "https://members.nearlyfreespeech.net/wiki/API/Reference"

部分文件因文件數量過多而無法顯示