Bladeren bron

refactor: auto certificate options

1. Add OCSP Must Staple options #292
2. Add LEGO_DISABLE_CNAME_SUPPORT options #407
Jacky 1 jaar geleden
bovenliggende
commit
4660a46a7e

+ 10 - 8
api/certificate/issue.go

@@ -117,14 +117,16 @@ func IssueCert(c *gin.Context) {
 	}
 
 	err = certModel.Updates(&model.Cert{
-		Domains:               payload.ServerName,
-		SSLCertificatePath:    payload.GetCertificatePath(),
-		SSLCertificateKeyPath: payload.GetCertificateKeyPath(),
-		AutoCert:              model.AutoCertEnabled,
-		KeyType:               payload.KeyType,
-		ChallengeMethod:       payload.ChallengeMethod,
-		DnsCredentialID:       payload.DNSCredentialID,
-		Resource:              payload.Resource,
+		Domains:                 payload.ServerName,
+		SSLCertificatePath:      payload.GetCertificatePath(),
+		SSLCertificateKeyPath:   payload.GetCertificateKeyPath(),
+		AutoCert:                model.AutoCertEnabled,
+		KeyType:                 payload.KeyType,
+		ChallengeMethod:         payload.ChallengeMethod,
+		DnsCredentialID:         payload.DNSCredentialID,
+		Resource:                payload.Resource,
+		MustStaple:              payload.MustStaple,
+		LegoDisableCNAMESupport: payload.LegoDisableCNAMESupport,
 	})
 
 	if err != nil {

+ 15 - 9
app/src/api/auto_cert.ts

@@ -5,21 +5,27 @@ export interface DNSProvider {
   code?: string
   provider?: string
   configuration: {
-    credentials: {
-      [key: string]: string
-    }
-    additional: {
-      [key: string]: string
-    }
+    credentials: Record<string, string>
+    additional: Record<string, string>
   }
   links?: {
     api: string
     go_client: string
   }
 }
-export interface DnsChallenge extends DNSProvider {
-  dns_credential_id: number | null
-  challenge_method: string
+
+export interface AutoCertOptions {
+  name?: string
+  domains: string[]
+  code?: string
+  dns_credential_id?: number | null
+  challenge_method?: string
+  configuration?: DNSProvider['configuration']
+  key_type: string
+  acme_user_id?: number
+  provider?: string
+  must_staple?: boolean
+  lego_disable_cname_support?: boolean
 }
 
 const auto_cert = {

+ 11 - 9
app/src/views/certificate/ACMEUserSelector.vue

@@ -3,18 +3,19 @@ import type { SelectProps } from 'ant-design-vue'
 import type { Ref } from 'vue'
 import type { AcmeUser } from '@/api/acme_user'
 import acme_user from '@/api/acme_user'
-import type { Cert } from '@/api/cert'
+import type { AutoCertOptions } from '@/api/auto_cert'
 
 const users = ref([]) as Ref<AcmeUser[]>
 
-// This data is provided by the Top StdCurd component,
-// is the object that you are trying to modify it
-// we externalize the dns_credential_id to the parent component,
-// this is used to tell the backend which dns_credential to use
-const data = inject('data') as Ref<Cert>
+const data = defineModel<AutoCertOptions>('options', {
+  default: () => {
+    return {}
+  },
+  required: true,
+})
 
 const id = computed(() => {
-  return data.value.acme_user_id
+  return data.value?.acme_user_id
 })
 
 const user_idx = ref()
@@ -35,7 +36,7 @@ watch(id, init)
 
 watch(current, () => {
   if (mounted.value)
-    data.value.acme_user_id = current.value.id
+    data.value!.acme_user_id = current.value.id
 })
 
 onMounted(async () => {
@@ -84,8 +85,9 @@ const filterOption = (input: string, option: { label: string }) => {
     <AFormItem :label="$gettext('ACME User')">
       <ASelect
         v-model:value="user_idx"
+        :placeholder="$gettext('System Initial User')"
         show-search
-        :options="options"
+        :options
         :filter-option="filterOption"
       />
     </AFormItem>

+ 9 - 7
app/src/views/certificate/CertificateEditor.vue

@@ -52,12 +52,6 @@ function save() {
   })
 }
 
-provide('data', data)
-
-provide('no_server_name', computed(() => {
-  return false
-}))
-
 const log = computed(() => {
   const logs = data.value.log?.split('\n')
 
@@ -134,9 +128,17 @@ const isManaged = computed(() => {
         </AForm>
 
         <template v-if="isManaged">
-          <RenewCert @renewed="init" />
+          <RenewCert
+            :options="{
+              name: data.name,
+              domains: data.domains,
+              key_type: data.key_type,
+            }"
+            @renewed="init"
+          />
 
           <AutoCertStepOne
+            v-model:options="data"
             style="max-width: 600px"
             hide-note
           />

+ 0 - 7
app/src/views/certificate/CertificateList/Certificate.vue

@@ -5,13 +5,6 @@ import StdTable from '@/components/StdDesign/StdDataDisplay/StdTable.vue'
 import cert from '@/api/cert'
 import WildcardCertificate from '@/views/certificate/WildcardCertificate.vue'
 
-// DO NOT REMOVE THESE LINES
-const no_server_name = computed(() => {
-  return false
-})
-
-provide('no_server_name', no_server_name)
-
 const refWildcard = ref()
 const refTable = ref()
 </script>

+ 9 - 5
app/src/views/certificate/RenewCert.vue

@@ -1,8 +1,11 @@
 <script setup lang="ts">
-import type { Ref } from 'vue'
 import { message } from 'ant-design-vue'
 import ObtainCertLive from '@/views/domain/cert/components/ObtainCertLive.vue'
-import type { Cert } from '@/api/cert'
+import type { AutoCertOptions } from '@/api/auto_cert'
+
+const props = defineProps<{
+  options: AutoCertOptions
+}>()
 
 const emit = defineEmits<{
   renewed: [void]
@@ -13,12 +16,12 @@ const modalClosable = ref(true)
 
 const refObtainCertLive = ref()
 
-const data = inject('data') as Ref<Cert>
-
 const issueCert = () => {
   modalVisible.value = true
 
-  refObtainCertLive.value.issue_cert(data.value.name, data.value.domains, data.value.key_type).then(() => {
+  const { name, domains, key_type } = props.options
+
+  refObtainCertLive.value.issue_cert(name, domains, key_type).then(() => {
     message.success($gettext('Renew successfully'))
     emit('renewed')
   })
@@ -52,6 +55,7 @@ provide('issuing_cert', issuing_cert)
         ref="refObtainCertLive"
         v-model:modal-closable="modalClosable"
         v-model:modal-visible="modalVisible"
+        :options
       />
     </AModal>
   </div>

+ 13 - 20
app/src/views/certificate/WildcardCertificate.vue

@@ -1,10 +1,9 @@
 <script setup lang="ts">
 import type { Ref } from 'vue'
 import { message } from 'ant-design-vue'
-import type { Cert } from '@/api/cert'
 import ObtainCertLive from '@/views/domain/cert/components/ObtainCertLive.vue'
-import DNSChallenge from '@/views/domain/cert/components/DNSChallenge.vue'
-import { PrivateKeyTypeList } from '@/constants'
+import type { AutoCertOptions } from '@/api/auto_cert'
+import AutoCertStepOne from '@/views/domain/cert/components/AutoCertStepOne.vue'
 
 const emit = defineEmits<{
   issued: [void]
@@ -12,10 +11,9 @@ const emit = defineEmits<{
 
 const step = ref(0)
 const visible = ref(false)
-const data = ref({}) as Ref<Cert>
+const data = ref({}) as Ref<AutoCertOptions>
 const issuing_cert = ref(false)
 
-provide('data', data)
 provide('issuing_cert', issuing_cert)
 function open() {
   visible.value = true
@@ -23,7 +21,7 @@ function open() {
   data.value = {
     challenge_method: 'dns01',
     key_type: '2048',
-  } as Cert
+  } as AutoCertOptions
 }
 
 defineExpose({
@@ -66,8 +64,6 @@ const issueCert = () => {
       force-render
     >
       <template v-if="step === 0">
-        <DNSChallenge />
-
         <AForm layout="vertical">
           <AFormItem :label="$gettext('Domain')">
             <AInput
@@ -75,19 +71,15 @@ const issueCert = () => {
               addon-before="*."
             />
           </AFormItem>
-
-          <AFormItem :label="$gettext('Key Type')">
-            <ASelect v-model:value="data.key_type">
-              <ASelectOption
-                v-for="t in PrivateKeyTypeList"
-                :key="t.key"
-                :value="t.key"
-              >
-                {{ t.name }}
-              </ASelectOption>
-            </ASelect>
-          </AFormItem>
         </AForm>
+
+        <AutoCertStepOne
+          v-model:options="data"
+          style="max-width: 600px"
+          hide-note
+          force-dns-challenge
+        />
+
         <div
           v-if="step === 0"
           class="flex justify-end"
@@ -106,6 +98,7 @@ const issueCert = () => {
         ref="refObtainCertLive"
         v-model:modal-closable="modalClosable"
         v-model:modal-visible="modalVisible"
+        :options="data"
       />
     </AModal>
   </div>

+ 7 - 9
app/src/views/domain/cert/IssueCert.vue

@@ -2,11 +2,9 @@
 import ObtainCert from '@/views/domain/cert/components/ObtainCert.vue'
 import type { NgxDirective } from '@/api/ngx'
 
-export interface Props {
+defineProps<{
   configName: string
-}
-
-const props = defineProps<Props>()
+}>()
 
 const issuing_cert = ref(false)
 const obtain_cert = ref()
@@ -16,18 +14,16 @@ const enabled = defineModel<boolean>('enabled', {
   default: () => false,
 })
 
-const no_server_name = computed(() => {
+const noServerName = computed(() => {
   if (!directivesMap.value.server_name)
     return true
 
   return directivesMap.value.server_name.length === 0
 })
 
-provide('no_server_name', no_server_name)
-provide('props', props)
 provide('issuing_cert', issuing_cert)
 
-watch(no_server_name, () => {
+watch(noServerName, () => {
   enabled.value = false
 })
 
@@ -45,6 +41,8 @@ async function onchange() {
   <ObtainCert
     ref="obtain_cert"
     :key="update"
+    :no-server-name="noServerName"
+    :config-name="configName"
     @update:auto_cert="r => enabled = r"
   />
   <div class="issue-cert">
@@ -52,7 +50,7 @@ async function onchange() {
       <ASwitch
         :loading="issuing_cert"
         :checked="enabled"
-        :disabled="no_server_name"
+        :disabled="noServerName"
         @change="onchange"
       />
     </AFormItem>

+ 50 - 28
app/src/views/domain/cert/components/AutoCertStepOne.vue

@@ -1,43 +1,38 @@
 <script setup lang="ts">
-import type { Ref } from 'vue'
-import type { DnsChallenge } from '@/api/auto_cert'
+import { $gettext } from '../../../../gettext'
+import type { AutoCertOptions } from '@/api/auto_cert'
 import DNSChallenge from '@/views/domain/cert/components/DNSChallenge.vue'
-import type { Cert } from '@/api/cert'
 import ACMEUserSelector from '@/views/certificate/ACMEUserSelector.vue'
 import { PrivateKeyTypeList } from '@/constants'
 
-defineProps<{
+const props = defineProps<{
   hideNote?: boolean
+  forceDnsChallenge?: boolean
 }>()
 
-const no_server_name = inject('no_server_name')
-
-// Provide by ObtainCert.vue
-const data = inject('data') as Ref<DnsChallenge & Cert>
+const data = defineModel<AutoCertOptions>('options', {
+  default: () => {
+    return {}
+  },
+  required: true,
+})
 
 onMounted(() => {
   if (!data.value.key_type)
     data.value.key_type = '2048'
+
+  if (props.forceDnsChallenge)
+    data.value.challenge_method = 'dns01'
+})
+
+watch(() => props.forceDnsChallenge, v => {
+  if (v)
+    data.value.challenge_method = 'dns01'
 })
 </script>
 
 <template>
   <div>
-    <template v-if="no_server_name">
-      <AAlert
-        :message="$gettext('Warning')"
-        type="warning"
-        show-icon
-      >
-        <template #description>
-          <span v-if="no_server_name">
-            {{ $gettext('server_name parameter is required') }}
-          </span>
-        </template>
-      </AAlert>
-      <br>
-    </template>
-
     <AAlert
       v-if="!hideNote"
       type="info"
@@ -52,8 +47,8 @@ onMounted(() => {
             + 'multiple domains.') }}
         </p>
         <p>
-          {{ $gettext('The certificate for the domain will be checked 5 minutes, '
-            + 'and will be renewed if it has been more than 1 week since it was last issued.') }}
+          {{ $gettext('The certificate for the domain will be checked 30 minutes, '
+            + 'and will be renewed if it has been more than 1 week or the period you set in settings since it was last issued.') }}
         </p>
         <p v-if="data.challenge_method === 'http01'">
           {{ $gettext('Make sure you have configured a reverse proxy for .well-known '
@@ -67,7 +62,10 @@ onMounted(() => {
       </template>
     </AAlert>
     <AForm layout="vertical">
-      <AFormItem :label="$gettext('Challenge Method')">
+      <AFormItem
+        v-if="!forceDnsChallenge"
+        :label="$gettext('Challenge Method')"
+      >
         <ASelect v-model:value="data.challenge_method">
           <ASelectOption value="http01">
             {{ $gettext('HTTP01') }}
@@ -89,8 +87,32 @@ onMounted(() => {
         </ASelect>
       </AFormItem>
     </AForm>
-    <ACMEUserSelector />
-    <DNSChallenge v-if="data.challenge_method === 'dns01'" />
+    <ACMEUserSelector v-model:options="data" />
+    <DNSChallenge
+      v-if="data.challenge_method === 'dns01'"
+      v-model:options="data"
+    />
+    <AForm layout="vertical">
+      <AFormItem :label="$gettext('OCSP Must Staple')">
+        <template #help>
+          <p>
+            {{ $gettext('Do not enable this option unless you are sure that you need it.') }}
+            {{ $gettext('OCSP Must Staple may cause errors for some users on first access using Firefox.') }}
+            <a href="https://github.com/0xJacky/nginx-ui/issues/322">#322</a>
+          </p>
+        </template>
+        <ASwitch v-model:checked="data.must_staple" />
+      </AFormItem>
+      <AFormItem :label="$gettext('Lego disable CNAME Support')">
+        <template #help>
+          <p>
+            {{ $gettext('If your domain has CNAME records and you cannot obtain certificates, '
+              + 'you need to enable this option.') }}
+          </p>
+        </template>
+        <ASwitch v-model:checked="data.lego_disable_cname_support" />
+      </AFormItem>
+    </AForm>
   </div>
 </template>
 

+ 9 - 9
app/src/views/domain/cert/components/DNSChallenge.vue

@@ -1,19 +1,19 @@
 <script setup lang="ts">
 import type { SelectProps } from 'ant-design-vue'
 import type { Ref } from 'vue'
-import type { SelectValue } from 'ant-design-vue/es/select'
-import type { DNSProvider } from '@/api/auto_cert'
+import type { AutoCertOptions, DNSProvider } from '@/api/auto_cert'
 import auto_cert from '@/api/auto_cert'
 import dns_credential from '@/api/dns_credential'
 
 const providers = ref([]) as Ref<DNSProvider[]>
 const credentials = ref<SelectProps['options']>([])
 
-// This data is provided by the Top StdCurd component,
-// is the object that you are trying to modify it
-// we externalize the dns_credential_id to the parent component,
-// this is used to tell the backend which dns_credential to use
-const data = inject('data') as Ref<DNSProvider & { dns_credential_id: SelectValue }>
+const data = defineModel<AutoCertOptions>('options', {
+  default: () => {
+    return {}
+  },
+  required: true,
+})
 
 const code = computed(() => {
   return data.value.code
@@ -95,7 +95,7 @@ const filterOption = (input: string, option: { label: string }) => {
       <ASelect
         v-model:value="provider_idx"
         show-search
-        :options="options"
+        :options
         :filter-option="filterOption"
       />
     </AFormItem>
@@ -105,7 +105,7 @@ const filterOption = (input: string, option: { label: string }) => {
       :rules="[{ required: true }]"
     >
       <ASelect
-        v-model:value="data.dns_credential_id"
+        v-model:value="data.dns_credential_id as any"
         :options="credentials"
       />
     </AFormItem>

+ 15 - 11
app/src/views/domain/cert/components/ObtainCert.vue

@@ -4,12 +4,16 @@ import type { ComputedRef, Ref } from 'vue'
 import domain from '@/api/domain'
 import AutoCertStepOne from '@/views/domain/cert/components/AutoCertStepOne.vue'
 import type { NgxConfig, NgxDirective } from '@/api/ngx'
-import type { Props } from '@/views/domain/cert/IssueCert.vue'
-import type { DnsChallenge } from '@/api/auto_cert'
+import type { AutoCertOptions } from '@/api/auto_cert'
 import ObtainCertLive from '@/views/domain/cert/components/ObtainCertLive.vue'
 import type { CertificateResult } from '@/api/cert'
 import type { PrivateKeyType } from '@/constants'
 
+const props = defineProps<{
+  configName: string
+  noServerName?: boolean
+}>()
+
 const emit = defineEmits(['update:auto_cert'])
 
 const modalVisible = ref(false)
@@ -26,15 +30,11 @@ const data = ref({
     credentials: {},
     additional: {},
   },
-}) as Ref<DnsChallenge>
+}) as Ref<AutoCertOptions>
 
 const modalClosable = ref(true)
 
-provide('data', data)
-
 const save_config = inject('save_config') as () => Promise<void>
-const no_server_name = inject('no_server_name') as Ref<boolean>
-const props = inject('props') as Props
 const issuing_cert = inject('issuing_cert') as Ref<boolean>
 const ngx_config = inject('ngx_config') as NgxConfig
 const current_server_directives = inject('current_server_directives') as ComputedRef<NgxDirective[]>
@@ -61,8 +61,8 @@ function change_auto_cert(status: boolean, key_type?: PrivateKeyType) {
   if (status) {
     domain.add_auto_cert(props.configName, {
       domains: name.value.trim().split(' '),
-      challenge_method: data.value.challenge_method,
-      dns_credential_id: data.value.dns_credential_id,
+      challenge_method: data.value.challenge_method!,
+      dns_credential_id: data.value.dns_credential_id!,
       key_type: key_type!,
     }).then(() => {
       message.success($gettext('Auto-renewal enabled for %{name}', { name: name.value }))
@@ -98,7 +98,7 @@ function job() {
   modalClosable.value = false
   issuing_cert.value = true
 
-  if (no_server_name.value) {
+  if (props.noServerName) {
     message.error($gettext('server_name not found in directives'))
     issuing_cert.value = false
 
@@ -183,13 +183,17 @@ function next() {
       force-render
     >
       <template v-if="step === 1">
-        <AutoCertStepOne />
+        <AutoCertStepOne
+          v-model:options="data"
+          :no-server-name="noServerName"
+        />
       </template>
       <template v-else-if="step === 2">
         <ObtainCertLive
           ref="refObtainCertLive"
           v-model:modal-closable="modalClosable"
           v-model:modal-visible="modalVisible"
+          :options="data"
         />
       </template>
       <div

+ 6 - 29
app/src/views/domain/cert/components/ObtainCertLive.vue

@@ -1,40 +1,17 @@
 <script setup lang="ts">
 import type { Ref } from 'vue'
 import websocket from '@/lib/websocket'
-import type { DnsChallenge } from '@/api/auto_cert'
-import Error from '@/views/other/Error.vue'
 import type { CertificateResult } from '@/api/cert'
+import type { AutoCertOptions } from '@/api/auto_cert'
 
 const props = defineProps<{
-  modalClosable: boolean
-  modalVisible: boolean
+  options: AutoCertOptions
 }>()
 
-const emit = defineEmits<{
-  'update:modalClosable': [value: boolean]
-  'update:modalVisible': [value: boolean]
-}>()
-
-const modalClosable = computed({
-  get() {
-    return props.modalClosable
-  },
-  set(value) {
-    emit('update:modalClosable', value)
-  },
-})
-
-const modalVisible = computed({
-  get() {
-    return props.modalVisible
-  },
-  set(value) {
-    emit('update:modalVisible', value)
-  },
-})
+const modalVisible = defineModel<boolean>('modalVisible')
+const modalClosable = defineModel<boolean>('modalClosable')
 
 const issuing_cert = inject('issuing_cert') as Ref<boolean>
-const data = inject('data') as Ref<DnsChallenge>
 
 const progressStrokeColor = {
   from: '#108ee9',
@@ -71,8 +48,8 @@ const issue_cert = async (config_name: string, server_name: string[], key_type:
     ws.onopen = () => {
       ws.send(JSON.stringify({
         server_name,
+        ...props.options,
         key_type,
-        ...data.value,
       }))
     }
 
@@ -114,7 +91,7 @@ const issue_cert = async (config_name: string, server_name: string[], key_type:
           }
           else {
             progressStatus.value = 'exception'
-            reject(new Error($gettext('Fail to obtain certificate')))
+            reject($gettext('Fail to obtain certificate'))
           }
           break
       }

+ 8 - 6
internal/cert/auto_cert.go

@@ -71,12 +71,14 @@ func autoCert(certModel *model.Cert) {
 
 	// support SAN certification
 	payload := &ConfigPayload{
-		CertID:          certModel.ID,
-		ServerName:      certModel.Domains,
-		ChallengeMethod: certModel.ChallengeMethod,
-		DNSCredentialID: certModel.DnsCredentialID,
-		KeyType:         certModel.GetKeyType(),
-		NotBefore:       certInfo.NotBefore,
+		CertID:                  certModel.ID,
+		ServerName:              certModel.Domains,
+		ChallengeMethod:         certModel.ChallengeMethod,
+		DNSCredentialID:         certModel.DnsCredentialID,
+		KeyType:                 certModel.GetKeyType(),
+		NotBefore:               certInfo.NotBefore,
+		MustStaple:              certModel.MustStaple,
+		LegoDisableCNAMESupport: certModel.LegoDisableCNAMESupport,
 	}
 
 	if certModel.Resource != nil {

+ 12 - 1
internal/cert/cert.go

@@ -130,7 +130,6 @@ func IssueCert(payload *ConfigPayload, logChan chan string, errChan chan error)
 			errChan <- errors.Wrap(err, "environment configuration is empty")
 			return
 		}
-
 	}
 
 	if err != nil {
@@ -138,6 +137,18 @@ func IssueCert(payload *ConfigPayload, logChan chan string, errChan chan error)
 		return
 	}
 
+	// fix #407
+	if payload.LegoDisableCNAMESupport {
+		err = os.Setenv("LEGO_DISABLE_CNAME_SUPPORT", "true")
+		if err != nil {
+			errChan <- errors.Wrap(err, "set env flag to disable lego CNAME support error")
+			return
+		}
+		defer func() {
+			_ = os.Unsetenv("LEGO_DISABLE_CNAME_SUPPORT")
+		}()
+	}
+
 	if time.Now().Sub(payload.NotBefore).Hours()/24 <= 21 &&
 		payload.Resource != nil && payload.Resource.Certificate != nil {
 		renew(payload, client, l, errChan)

+ 3 - 2
internal/cert/obtain.go

@@ -10,8 +10,9 @@ import (
 
 func obtain(payload *ConfigPayload, client *lego.Client, l *log.Logger, errChan chan error) {
 	request := certificate.ObtainRequest{
-		Domains: payload.ServerName,
-		Bundle:  true,
+		Domains:    payload.ServerName,
+		Bundle:     true,
+		MustStaple: payload.MustStaple,
 	}
 
 	l.Println("[INFO] [Nginx UI] Obtaining certificate")

+ 13 - 11
internal/cert/payload.go

@@ -16,17 +16,19 @@ import (
 )
 
 type ConfigPayload struct {
-	CertID                int                        `json:"cert_id"`
-	ServerName            []string                   `json:"server_name"`
-	ChallengeMethod       string                     `json:"challenge_method"`
-	DNSCredentialID       int                        `json:"dns_credential_id"`
-	ACMEUserID            int                        `json:"acme_user_id"`
-	KeyType               certcrypto.KeyType         `json:"key_type"`
-	Resource              *model.CertificateResource `json:"resource,omitempty"`
-	NotBefore             time.Time                  `json:"-"`
-	CertificateDir        string                     `json:"-"`
-	SSLCertificatePath    string                     `json:"-"`
-	SSLCertificateKeyPath string                     `json:"-"`
+	CertID                  int                        `json:"cert_id"`
+	ServerName              []string                   `json:"server_name"`
+	ChallengeMethod         string                     `json:"challenge_method"`
+	DNSCredentialID         int                        `json:"dns_credential_id"`
+	ACMEUserID              int                        `json:"acme_user_id"`
+	KeyType                 certcrypto.KeyType         `json:"key_type"`
+	Resource                *model.CertificateResource `json:"resource,omitempty"`
+	MustStaple              bool                       `json:"must_staple"`
+	LegoDisableCNAMESupport bool                       `json:"lego_disable_cname_support"`
+	NotBefore               time.Time                  `json:"-"`
+	CertificateDir          string                     `json:"-"`
+	SSLCertificatePath      string                     `json:"-"`
+	SSLCertificateKeyPath   string                     `json:"-"`
 }
 
 func (c *ConfigPayload) GetACMEUser() (user *model.AcmeUser, err error) {

+ 27 - 26
internal/cert/renew.go

@@ -1,38 +1,39 @@
 package cert
 
 import (
-    "github.com/0xJacky/Nginx-UI/model"
-    "github.com/go-acme/lego/v4/certificate"
-    "github.com/go-acme/lego/v4/lego"
-    "github.com/pkg/errors"
-    "log"
+	"github.com/0xJacky/Nginx-UI/model"
+	"github.com/go-acme/lego/v4/certificate"
+	"github.com/go-acme/lego/v4/lego"
+	"github.com/pkg/errors"
+	"log"
 )
 
 func renew(payload *ConfigPayload, client *lego.Client, l *log.Logger, errChan chan error) {
-    if payload.Resource == nil {
-        errChan <- errors.New("resource is nil")
-        return
-    }
+	if payload.Resource == nil {
+		errChan <- errors.New("resource is nil")
+		return
+	}
 
-    options := &certificate.RenewOptions{
-        Bundle: true,
-    }
+	options := &certificate.RenewOptions{
+		Bundle:     true,
+		MustStaple: payload.MustStaple,
+	}
 
-    cert, err := client.Certificate.RenewWithOptions(payload.Resource.GetResource(), options)
-    if err != nil {
-        errChan <- errors.Wrap(err, "renew cert error")
-        return
-    }
+	cert, err := client.Certificate.RenewWithOptions(payload.Resource.GetResource(), options)
+	if err != nil {
+		errChan <- errors.Wrap(err, "renew cert error")
+		return
+	}
 
-    payload.Resource = &model.CertificateResource{
-        Resource:          cert,
-        PrivateKey:        cert.PrivateKey,
-        Certificate:       cert.Certificate,
-        IssuerCertificate: cert.IssuerCertificate,
-        CSR:               cert.CSR,
-    }
+	payload.Resource = &model.CertificateResource{
+		Resource:          cert,
+		PrivateKey:        cert.PrivateKey,
+		Certificate:       cert.Certificate,
+		IssuerCertificate: cert.IssuerCertificate,
+		CSR:               cert.CSR,
+	}
 
-    payload.WriteFile(l, errChan)
+	payload.WriteFile(l, errChan)
 
-    l.Println("[INFO] [Nginx UI] Certificate renewed successfully")
+	l.Println("[INFO] [Nginx UI] Certificate renewed successfully")
 }

+ 17 - 15
model/cert.go

@@ -30,21 +30,23 @@ type CertificateResource struct {
 
 type Cert struct {
 	Model
-	Name                  string               `json:"name"`
-	Domains               pq.StringArray       `json:"domains" gorm:"type:text[]"`
-	Filename              string               `json:"filename"`
-	SSLCertificatePath    string               `json:"ssl_certificate_path"`
-	SSLCertificateKeyPath string               `json:"ssl_certificate_key_path"`
-	AutoCert              int                  `json:"auto_cert"`
-	ChallengeMethod       string               `json:"challenge_method"`
-	DnsCredentialID       int                  `json:"dns_credential_id"`
-	DnsCredential         *DnsCredential       `json:"dns_credential,omitempty"`
-	ACMEUserID            int                  `json:"acme_user_id"`
-	ACMEUser              *AcmeUser            `json:"acme_user,omitempty"`
-	KeyType               certcrypto.KeyType   `json:"key_type"`
-	Log                   string               `json:"log"`
-	Resource              *CertificateResource `json:"-" gorm:"serializer:json"`
-	SyncNodeIds           []int                `json:"sync_node_ids" gorm:"serializer:json"`
+	Name                    string               `json:"name"`
+	Domains                 pq.StringArray       `json:"domains" gorm:"type:text[]"`
+	Filename                string               `json:"filename"`
+	SSLCertificatePath      string               `json:"ssl_certificate_path"`
+	SSLCertificateKeyPath   string               `json:"ssl_certificate_key_path"`
+	AutoCert                int                  `json:"auto_cert"`
+	ChallengeMethod         string               `json:"challenge_method"`
+	DnsCredentialID         int                  `json:"dns_credential_id"`
+	DnsCredential           *DnsCredential       `json:"dns_credential,omitempty"`
+	ACMEUserID              int                  `json:"acme_user_id"`
+	ACMEUser                *AcmeUser            `json:"acme_user,omitempty"`
+	KeyType                 certcrypto.KeyType   `json:"key_type"`
+	Log                     string               `json:"log"`
+	Resource                *CertificateResource `json:"-" gorm:"serializer:json"`
+	SyncNodeIds             []int                `json:"sync_node_ids" gorm:"serializer:json"`
+	MustStaple              bool                 `json:"must_staple"`
+	LegoDisableCNAMESupport bool                 `json:"lego_disable_cname_support"`
 }
 
 func FirstCert(confName string) (c Cert, err error) {