فهرست منبع

Refactored nginx configuration editor

0xJacky 3 سال پیش
والد
کامیت
b19ecdda9c
31فایلهای تغییر یافته به همراه1566 افزوده شده و 1046 حذف شده
  1. 2 2
      frontend/src/api/domain.js
  2. 3 1
      frontend/src/api/index.js
  3. 13 0
      frontend/src/api/ngx.js
  4. 5 4
      frontend/src/components/StdDataDisplay/StdTable.vue
  5. 5 11
      frontend/src/components/VueItextarea/VueItextarea.vue
  6. 0 1
      frontend/src/layouts/BaseLayout.vue
  7. 144 100
      frontend/src/views/domain/DomainAdd.vue
  8. 70 149
      frontend/src/views/domain/DomainEdit.vue
  9. 44 0
      frontend/src/views/domain/cert/Cert.vue
  10. 10 2
      frontend/src/views/domain/cert/CertInfo.vue
  11. 163 0
      frontend/src/views/domain/cert/IssueCert.vue
  12. 45 43
      frontend/src/views/domain/columns.js
  13. 4 29
      frontend/src/views/domain/methods.js
  14. 78 0
      frontend/src/views/domain/ngx_conf/LocationEditor.vue
  15. 104 0
      frontend/src/views/domain/ngx_conf/NgxConfigEditor.vue
  16. 74 0
      frontend/src/views/domain/ngx_conf/directive/DirectiveAdd.vue
  17. 92 0
      frontend/src/views/domain/ngx_conf/directive/DirectiveEditor.vue
  18. 1 0
      frontend/src/views/domain/ngx_conf/ngx_constant.js
  19. 116 152
      server/api/cert.go
  20. 235 232
      server/api/domain.go
  21. 42 0
      server/api/ngx.go
  22. 42 18
      server/api/template.go
  23. 2 2
      server/router/middleware.go
  24. 93 85
      server/router/routers.go
  25. 0 13
      server/template/http-conf
  26. 0 25
      server/template/https-conf
  27. 0 6
      server/template/template.go
  28. 164 160
      server/tool/cert.go
  29. 1 1
      server/tool/nginx/build_config.go
  30. 14 8
      server/tool/nginx/parse.go
  31. 0 2
      server/tool/nginx/type.go

+ 2 - 2
frontend/src/api/domain.js

@@ -27,8 +27,8 @@ const domain = {
         return http.post(base_url + '/' + name + '/disable')
     },
 
-    get_template(name) {
-        return http.get('template/' + name)
+    get_template() {
+        return http.get('template')
     },
 
     cert_info(domain) {

+ 3 - 1
frontend/src/api/index.js

@@ -5,6 +5,7 @@ import user from './user'
 import install from './install'
 import analytic from './analytic'
 import settings from './settings'
+import ngx from './ngx'
 
 export default {
     domain,
@@ -13,5 +14,6 @@ export default {
     user,
     install,
     analytic,
-    settings
+    settings,
+    ngx
 }

+ 13 - 0
frontend/src/api/ngx.js

@@ -0,0 +1,13 @@
+import http from '@/lib/http'
+
+const ngx = {
+    build_config(ngxConfig) {
+        return http.post('/ngx/build_config', ngxConfig)
+    },
+
+    tokenize_config(content) {
+        return http.post('/ngx/tokenize_config', {content})
+    }
+}
+
+export default ngx

+ 5 - 4
frontend/src/components/StdDataDisplay/StdTable.vue

@@ -72,7 +72,7 @@
                         :okText="ok_text"
                         :title="restore_title_text"
                         @confirm="restore(record[rowKey])">
-                        <a href="javascript:;">{{restore_action_text}}</a>
+                        <a href="javascript:;">{{ restore_action_text }}</a>
                     </a-popconfirm>
                     <a-popconfirm
                         v-else
@@ -80,7 +80,7 @@
                         :okText="ok_text"
                         :title="destroy_title_text"
                         @confirm="destroy(record[rowKey])">
-                        <a href="javascript:;">{{destroy_action_text}}</a>
+                        <a href="javascript:;">{{ destroy_action_text }}</a>
                     </a-popconfirm>
                 </template>
             </div>
@@ -93,6 +93,7 @@
 import StdPagination from './StdPagination'
 import moment from 'moment'
 import StdDataEntry from '@/components/StdDataEntry/StdDataEntry'
+import $gettext, {$interpolate} from '@/lib/translate/gettext'
 
 export default {
     name: 'StdTable',
@@ -230,10 +231,10 @@ export default {
         destroy(id) {
             this.api.destroy(id).then(() => {
                 this.get_list()
-                this.$message.success('删除 ID: ' + id + ' 成功')
+                this.$message.success($interpolate($gettext('Delete ID: %{id}'), {id: id}))
             }).catch(e => {
                 console.log(e)
-                this.$message.error(e?.message ?? '系统错误')
+                this.$message.error(e?.message ?? $gettext('Server error'))
             })
         },
         get_list(page_num = null) {

+ 5 - 11
frontend/src/components/VueItextarea/VueItextarea.vue

@@ -1,5 +1,5 @@
 <template>
-    <editor v-model="current_value" @init="editorInit" lang="nginx" theme="monokai" width="100%" height="1000"></editor>
+    <editor v-model="current_value" @init="editorInit" lang="nginx" theme="monokai" width="100%" :height="defaultTextHeight"></editor>
 </template>
 <style lang="less">
 .cm-s-monokai {
@@ -20,6 +20,10 @@ export default {
     },
     props: {
         value: {},
+        defaultTextHeight: {
+            type: Number,
+            default: 1000
+        }
     },
     model: {
         prop: 'value',
@@ -36,16 +40,6 @@ export default {
     data() {
         return {
             current_value: this.value ?? '',
-            cmOptions: {
-                tabSize: 4,
-                mode: 'text/x-nginx-conf',
-                theme: 'monokai',
-                lineNumbers: true,
-                line: true,
-                highlightDifferences: true,
-                defaultTextHeight: 1000,
-                // more CodeMirror options...
-            }
         }
     },
     methods: {

+ 0 - 1
frontend/src/layouts/BaseLayout.vue

@@ -57,7 +57,6 @@ export default {
     data() {
         return {
             collapsed: this.collapse(),
-            zh_CN,
             clientWidth: document.body.clientWidth,
         }
     },

+ 144 - 100
frontend/src/views/domain/DomainAdd.vue

@@ -2,39 +2,62 @@
     <a-card :title="$gettext('Add Site')">
         <div class="domain-add-container">
             <a-steps :current="current_step" size="small">
-                <a-step :title="$gettext('Base information')" />
-                <a-step :title="$gettext('Configure SSL')" />
-                <a-step :title="$gettext('Finished')" />
+                <a-step :title="$gettext('Base information')"/>
+                <a-step :title="$gettext('Configure SSL')"/>
+                <a-step :title="$gettext('Finished')"/>
             </a-steps>
 
-            <std-data-entry :data-list="columns" :data-source="config" :error="error" v-show="current_step===0"/>
+            <template v-if="current_step===0">
+                <a-form-item :label="$gettext('Configuration Name')">
+                    <a-input v-model="config.name"/>
+                </a-form-item>
 
-            <template v-if="current_step===1">
-                <a-button
-                    @click="issue_cert"
-                    type="primary" ghost
-                    style="margin: 10px 0"
-                    :disabled="is_demo"
-                    :loading="issuing_cert"
+                <directive-editor :ngx_directives="ngx_config.servers[0].directives"/>
+
+                <location-editor :locations="ngx_config.servers[0].locations"/>
+
+                <a-alert
+                    v-if="!has_server_name"
+                    :message="$gettext('Warning')"
+                    type="warning"
+                    show-icon
                 >
-                    <translate>Getting Certificate from Let's Encrypt</translate>
-                </a-button>
-                <p v-if="is_demo" v-translate>This feature is not available in demo.</p>
+                    <template slot="description">
+                    <span v-translate>
+                        server_name parameter is required
+                    </span>
+                    </template>
+                </a-alert>
+                <br/>
+            </template>
 
-                <std-data-entry :data-list="columnsSSL" :data-source="config" :error="error" />
+            <template v-else-if="current_step===1">
+
+                <a-form-item :label="$gettext('Enable TLS')">
+                    <a-switch @change="change_tls"/>
+                </a-form-item>
+
+                <ngx-config-editor
+                    ref="ngx_config"
+                    :ngx_config="ngx_config"
+                    v-model="auto_cert"
+                    :enabled="enabled"
+                />
 
-                <a-space style="margin-right: 10px">
-                    <a-button
-                        v-if="current_step===1"
-                        @click="current_step++"
-                    >
-                        <translate>Skip</translate>
-                    </a-button>
-                </a-space>
             </template>
 
+            <a-space v-if="current_step<2">
+                <a-button
+                    type="primary"
+                    @click="save"
+                    :disabled="!config.name||!has_server_name"
+                >
+                    <translate>Next</translate>
+                </a-button>
+            </a-space>
+
             <a-result
-                v-if="current_step===2"
+                v-else-if="current_step===2"
                 status="success"
                 :title="$gettext('Domain Config Created Successfully')"
             >
@@ -48,118 +71,139 @@
                 </template>
             </a-result>
 
-            <a-space v-if="current_step<2">
-                <a-button
-                    type="primary"
-                    @click="save"
-                    :disabled="!config.name"
-                >
-                    <translate>Next</translate>
-                </a-button>
-            </a-space>
         </div>
     </a-card>
 </template>
 
 <script>
-import StdDataEntry from '@/components/StdDataEntry/StdDataEntry'
-import {columns, columnsSSL} from '@/views/domain/columns'
-import {unparse, issue_cert} from '@/views/domain/methods'
-import $gettext, {$interpolate} from "@/lib/translate/gettext"
+import DirectiveEditor from '@/views/domain/ngx_conf/directive/DirectiveEditor'
+import LocationEditor from '@/views/domain/ngx_conf/LocationEditor'
+import $gettext, {$interpolate} from '@/lib/translate/gettext'
+import NgxConfigEditor from '@/views/domain/ngx_conf/NgxConfigEditor'
 
 export default {
     name: 'DomainAdd',
-    components: {StdDataEntry},
+    components: {NgxConfigEditor, LocationEditor, DirectiveEditor},
     data() {
         return {
-            config: {
-                http_listen_port: 80,
-                https_listen_port: 443
+            config: {},
+            ngx_config: {
+                servers: [{}]
             },
-            columns: columns.slice(0, -1), // 隐藏SSL支持开关
             error: {},
             current_step: 0,
-            columnsSSL,
-            issuing_cert: false
+            enabled: true,
+            auto_cert: false
         }
     },
-    watch: {
-        'config.auto_cert'() {
-            this.change_auto_cert()
-        }
+    created() {
+        this.init()
     },
     methods: {
+        init() {
+            this.$api.domain.get_template().then(r => {
+                this.ngx_config = r.tokenized
+            })
+        },
         save() {
-            if (this.current_step===0) {
-                this.$api.domain.get_template('http-conf').then(r => {
-                    let text = unparse(r.template, this.config)
-
-                    this.$api.domain.save(this.config.name, {content: text, enabled: true}).then(() => {
-                        this.$message.success($gettext('Saved successfully'))
-
-                        this.$api.domain.enable(this.config.name).then(() => {
-                            this.$message.success($gettext('Enabled successfully'))
-                            this.current_step++
-                        }).catch(r => {
-                            this.$message.error(r.message ?? $gettext('Enable failed'), 10)
-                        })
+            this.$api.ngx.build_config(this.ngx_config).then(r => {
+                this.$api.domain.save(this.config.name, {content: r.content, enabled: true}).then(() => {
+                    this.$message.success($gettext('Saved successfully'))
 
-                    }).catch(r => {
-                        this.$message.error($interpolate($gettext('Save error %{msg}'), {msg: r.message ?? ""}), 10)
-                    })
-                })
-            } else if (this.current_step === 1) {
-                this.$api.domain.get_template('https-conf').then(r => {
-                    let text = unparse(r.template, this.config)
-
-                    this.$api.domain.save(this.config.name, {content: text, enabled: true}).then(() => {
-                        this.$message.success($gettext('Saved successfully'))
+                    this.$api.domain.enable(this.config.name).then(() => {
+                        this.$message.success($gettext('Enabled successfully'))
                         this.current_step++
                     }).catch(r => {
-                        this.$message.error($interpolate($gettext('Save error %{msg}'), {msg: r.message ?? ""}), 10)
+                        this.$message.error(r.message ?? $gettext('Enable failed'), 10)
                     })
-                })
-            }
 
-        },
-        issue_cert() {
-            this.issuing_cert = true
-            issue_cert(this.config.server_name, this.callback)
-        },
-        callback(ssl_certificate, ssl_certificate_key) {
-            this.$set(this.config, 'ssl_certificate', ssl_certificate)
-            this.$set(this.config, 'ssl_certificate_key', ssl_certificate_key)
-            this.issuing_cert = false
+                }).catch(r => {
+                    this.$message.error($interpolate($gettext('Save error %{msg}'), {msg: r.message ?? ''}), 10)
+                })
+            })
         },
         goto_modify() {
-            this.$router.push('/domain/'+this.config.name)
+            this.$router.push('/domain/' + this.config.name)
         },
         create_another() {
             this.current_step = 0
-            this.config = {
-                http_listen_port: 80,
-                https_listen_port: 443
+            this.config = {}
+            this.ngx_config = {
+                servers: [{}]
             }
         },
-        change_auto_cert() {
-            if (this.config.auto_cert) {
-                this.$api.domain.add_auto_cert(this.config.name).then(() => {
-                    this.$message.success($interpolate($gettext('Auto-renewal enabled for %{name}'), {name: this.config.name}))
-                }).catch(e => {
-                    this.$message.error(e.message ?? $interpolate($gettext('Enable auto-renewal failed for %{name}'), {name: this.config.name}))
+        change_tls(r) {
+            if (r) {
+                // deep copy servers[0] to servers[1]
+                const server = JSON.parse(JSON.stringify(this.ngx_config.servers[0]))
+
+                this.ngx_config.servers.push(server)
+
+                this.$refs.ngx_config.current_server_index = 1
+
+                const servers = this.ngx_config.servers
+
+
+                let i = 0
+                while (i < servers[1].directives.length) {
+                    const v = servers[1].directives[i]
+                    if (v.directive === 'listen') {
+                        servers[1].directives.splice(i, 1)
+                    } else {
+                        i++
+                    }
+                }
+
+                servers[1].directives.splice(0, 0, {
+                    directive: 'listen',
+                    params: '443 ssl http2'
+                }, {
+                    directive: 'listen',
+                    params: '[::]:443 ssl http2'
                 })
+
+                const directivesMap = this.$refs.ngx_config.directivesMap
+
+                const server_name = directivesMap['server_name'][0]
+
+                if (!directivesMap['ssl_certificate']) {
+                    servers[1].directives.splice(server_name.idx + 1, 0, {
+                        directive: 'ssl_certificate',
+                        params: ''
+                    })
+                }
+
+                setTimeout(() => {
+                    if (!directivesMap['ssl_certificate_key']) {
+                        servers[1].directives.splice(server_name.idx + 2, 0, {
+                            directive: 'ssl_certificate_key',
+                            params: ''
+                        })
+                    }
+                }, 100)
+
             } else {
-                this.$api.domain.remove_auto_cert(this.config.name).then(() => {
-                    this.$message.success($interpolate($gettext('Auto-renewal disabled for %{name}'), {name: this.config.name}))
-                }).catch(e => {
-                    this.$message.error(e.message ?? $interpolate($gettext('Disable auto-renewal failed for %{name}'), {name: this.config.name}))
-                })
+                // remove servers[1]
+                this.$refs.ngx_config.current_server_index = 0
+                if (this.ngx_config.servers.length === 2) {
+                    this.ngx_config.servers.splice(1, 1)
+                }
             }
         }
     },
     computed: {
-        is_demo() {
-            return this.$store.getters.env.demo === true
+        has_server_name() {
+            const servers = this.ngx_config.servers
+            for (const server_key in servers) {
+                for (const k in servers[server_key].directives) {
+                    const v = servers[server_key].directives[k]
+                    if (v.directive === 'server_name' && v.params.trim() !== '') {
+                        return true
+                    }
+                }
+            }
+
+            return false
         }
     }
 }

+ 70 - 149
frontend/src/views/domain/DomainEdit.vue

@@ -11,12 +11,12 @@
                 </a-tag>
             </template>
             <template v-slot:extra>
-                <a-switch size="small" v-model="advance_mode"/>
+                <a-switch size="small" v-model="advance_mode" @change="on_mode_change"/>
                 <template v-if="advance_mode">
-                    {{ $gettext('Advance') }}
+                    {{ $gettext('Advance Mode') }}
                 </template>
                 <template v-else>
-                    {{ $gettext('Basic') }}
+                    {{ $gettext('Basic Mode') }}
                 </template>
             </template>
 
@@ -29,22 +29,13 @@
                     <a-form-item :label="$gettext('Enabled')">
                         <a-switch v-model="enabled" @change="checked=>{checked?enable():disable()}"/>
                     </a-form-item>
-                    <p v-translate>The following values will only take effect if you have the corresponding fields in your configuration file. The configuration filename cannot be changed after it has been created.</p>
-                    <std-data-entry :data-list="columns" v-model="config"/>
-                    <template v-if="config.support_ssl">
-                        <cert-info :domain="name" ref="cert-info" v-if="name"/>
-                        <a-button
-                            @click="issue_cert"
-                            type="primary" ghost
-                            style="margin: 10px 0"
-                            :disabled="is_demo"
-                            :loading="issuing_cert"
-                        >
-                            <translate>Getting Certificate from Let's Encrypt</translate>
-                        </a-button>
-                        <p v-if="is_demo" v-translate>This feature is not available in demo.</p>
-                        <p v-else v-translate>Make sure you have configured a reverse proxy for .well-known directory to HTTPChallengePort (default: 9180) before getting the certificate.</p>
-                    </template>
+
+                    <ngx-config-editor
+                        ref="ngx_config"
+                        :ngx_config="ngx_config"
+                        v-model="auto_cert"
+                        :enabled="enabled"
+                    />
                 </div>
             </transition>
 
@@ -55,7 +46,7 @@
                 <a-button @click="$router.go(-1)">
                     <translate>Back</translate>
                 </a-button>
-                <a-button type="primary" @click="save">
+                <a-button type="primary" @click="save" :loading="saving">
                     <translate>Save</translate>
                 </a-button>
             </a-space>
@@ -65,60 +56,39 @@
 
 
 <script>
-import StdDataEntry from '@/components/StdDataEntry/StdDataEntry'
 import FooterToolBar from '@/components/FooterToolbar/FooterToolBar'
 import VueItextarea from '@/components/VueItextarea/VueItextarea'
-import {columns, columnsSSL} from '@/views/domain/columns'
-import {unparse, issue_cert} from '@/views/domain/methods'
-import CertInfo from '@/views/domain/CertInfo'
 import {$gettext, $interpolate} from '@/lib/translate/gettext'
+import NgxConfigEditor from '@/views/domain/ngx_conf/NgxConfigEditor'
 
 
 export default {
     name: 'DomainEdit',
-    components: {CertInfo, FooterToolBar, StdDataEntry, VueItextarea},
+    components: {NgxConfigEditor, FooterToolBar, VueItextarea},
     data() {
         return {
             name: this.$route.params.name.toString(),
-            config: {
-                http_listen_port: 80,
-                https_listen_port: null,
-                server_name: '',
-                index: '',
-                root: '',
-                ssl_certificate: '',
-                ssl_certificate_key: '',
-                support_ssl: false,
-                auto_cert: false
+            update: 0,
+            ngx_config: {
+                filename: '',
+                upstreams: [],
+                servers: []
             },
+            auto_cert: false,
+            current_server_index: 0,
             enabled: false,
             configText: '',
             ws: null,
             ok: false,
             issuing_cert: false,
             advance_mode: false,
+            saving: false
         }
     },
     watch: {
         '$route'() {
             this.init()
         },
-        config: {
-            handler() {
-                this.unparse()
-            },
-            deep: true
-        },
-        'config.support_ssl'() {
-            if (this.ok) {
-                this.change_support_ssl()
-            }
-        },
-        'config.auto_cert'() {
-            if (this.ok) {
-                this.change_auto_cert()
-            }
-        }
     },
     created() {
         this.init()
@@ -133,106 +103,53 @@ export default {
             if (this.name) {
                 this.$api.domain.get(this.name).then(r => {
                     this.configText = r.config
-                    this.config.auto_cert = r.auto_cert
                     this.enabled = r.enabled
-                    this.parse(r).then(() => {
-                        this.ok = true
-                    })
+                    this.ngx_config = r.tokenized
+                    this.auto_cert = r.auto_cert
                 }).catch(r => {
-                    console.log(r)
-                    this.$message.error($gettext('Server error'))
+                    this.$message.error(r.message ?? $gettext('Server error'))
                 })
             }
         },
-        async parse(r) {
-            const text = r.config
-            const reg = {
-                http_listen_port: /listen[\s](.*);/i,
-                https_listen_port: /listen[\s](.*) ssl/i,
-                server_name: /server_name[\s](.*);/i,
-                index: /index[\s](.*);/i,
-                root: /root[\s](.*);/i,
-                ssl_certificate: /ssl_certificate[\s](.*);/i,
-                ssl_certificate_key: /ssl_certificate_key[\s](.*);/i
-            }
-            this.config['name'] = r.name
-            for (let r in reg) {
-                const match = text.match(reg[r])
-                // console.log(r, match)
-                if (match !== null) {
-                    if (match[1] !== undefined) {
-                        this.config[r] = match[1].trim()
-                    } else {
-                        this.config[r] = match[0].trim()
-                    }
-                }
-            }
-            if (this.config.https_listen_port) {
-                this.config.support_ssl = true
-            }
-        },
-        async unparse() {
-            this.configText = unparse(this.configText, this.config)
-        },
-        async get_template() {
-            if (this.config.support_ssl) {
-                await this.$api.domain.get_template('https-conf').then(r => {
-                    this.configText = r.template
-                })
+        on_mode_change(advance_mode) {
+            if (advance_mode) {
+                this.build_config()
             } else {
-                await this.$api.domain.get_template('http-conf').then(r => {
-                    this.configText = r.template
+                return this.$api.ngx.tokenize_config(this.configText).then(r => {
+                    this.ngx_config = r
+                }).catch(r => {
+                    this.$message.error(r.message ?? $gettext('Server error'))
                 })
             }
-            await this.unparse()
         },
-        change_support_ssl() {
-            const that = this
-            this.$confirm({
-                title: $gettext('Do you want to change the template to support the TLS?'),
-                content: $gettext('This operation will lose the custom configuration.'),
-                onOk() {
-                    that.get_template()
-                },
-                onCancel() {
-                },
+        build_config() {
+            return this.$api.ngx.build_config(this.ngx_config).then(r => {
+                this.configText = r.content
+            }).catch(r => {
+                this.$message.error(r.message ?? $gettext('Server error'))
             })
         },
-        save() {
+        async save() {
+            this.saving = true
+
+            if (!this.advance_mode) {
+                await this.build_config()
+            }
+
             this.$api.domain.save(this.name, {content: this.configText}).then(r => {
-                this.parse(r)
+                this.configText = r.config
+                this.enabled = r.enabled
+                this.ngx_config = r.tokenized
                 this.$message.success($gettext('Saved successfully'))
-                if (this.name) {
-                    if (this.$refs['cert-info']) this.$refs['cert-info'].get()
-                }
+
+                this.$refs.ngx_config.update_cert_info()
+
             }).catch(r => {
                 this.$message.error($interpolate($gettext('Save error %{msg}'), {msg: r.message ?? ''}), 10)
+            }).finally(() => {
+                this.saving = false
             })
-        },
-        issue_cert() {
-            this.issuing_cert = true
-            issue_cert(this.config.server_name, this.callback)
-        },
-        callback(ssl_certificate, ssl_certificate_key) {
-            this.$set(this.config, 'ssl_certificate', ssl_certificate)
-            this.$set(this.config, 'ssl_certificate_key', ssl_certificate_key)
-            if (this.$refs['cert-info']) this.$refs['cert-info'].get()
-            this.issuing_cert = false
-        },
-        change_auto_cert() {
-            if (this.config.auto_cert) {
-                this.$api.domain.add_auto_cert(this.name).then(() => {
-                    this.$message.success($interpolate($gettext('Auto-renewal enabled for %{name}'), {name: this.name}))
-                }).catch(e => {
-                    this.$message.error(e.message ?? $interpolate($gettext('Enable auto-renewal failed for %{name}'), {name: this.name}))
-                })
-            } else {
-                this.$api.domain.remove_auto_cert(this.name).then(() => {
-                    this.$message.success($interpolate($gettext('Auto-renewal disabled for %{name}'), {name: this.name}))
-                }).catch(e => {
-                    this.$message.error(e.message ?? $interpolate($gettext('Disable auto-renewal failed for %{name}'), {name: this.name}))
-                })
-            }
+
         },
         enable() {
             this.$api.domain.enable(this.name).then(() => {
@@ -252,15 +169,6 @@ export default {
         }
     },
     computed: {
-        columns: {
-            get() {
-                if (this.config.support_ssl) {
-                    return [...columns, ...columnsSSL]
-                } else {
-                    return [...columns]
-                }
-            }
-        },
         is_demo() {
             return this.$store.getters.env.demo === true
         }
@@ -274,16 +182,15 @@ export default {
 
 <style lang="less" scoped>
 .ant-card {
-    // margin: 10px;
-    @media (max-width: 512px) {
-        margin: 10px 0;
-    }
+    margin: 10px 0;
+    box-shadow: unset;
 }
 
 .domain-edit-container {
     max-width: 800px;
     margin: 0 auto;
-    /deep/.ant-form-item-label > label::after {
+
+    /deep/ .ant-form-item-label > label::after {
         content: none;
     }
 }
@@ -291,12 +198,26 @@ export default {
 .slide-fade-enter-active {
     transition: all .5s ease-in-out;
 }
+
 .slide-fade-leave-active {
     transition: all .5s cubic-bezier(1.0, 0.5, 0.8, 1.0);
 }
+
 .slide-fade-enter, .slide-fade-leave-to
     /* .slide-fade-leave-active for below version 2.1.8 */ {
     transform: translateX(10px);
     opacity: 0;
 }
+
+.location-block {
+
+}
+
+.directive-params-wrapper {
+    margin: 10px 0;
+}
+
+.tab-content {
+    padding: 10px;
+}
 </style>

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

@@ -0,0 +1,44 @@
+<template>
+    <div>
+        <cert-info ref="info" :domain="name" v-if="name"/>
+        <issue-cert
+            :current_server_directives="current_server_directives"
+            :directives-map="directivesMap"
+            v-model="auto_cert"
+            @callback="callback"
+        />
+    </div>
+</template>
+
+<script>
+import CertInfo from '@/views/domain/cert/CertInfo'
+import IssueCert from '@/views/domain/cert/IssueCert'
+
+export default {
+    name: 'Cert',
+    components: {IssueCert, CertInfo},
+    props: {
+        directivesMap: Object,
+        current_server_directives: Array,
+        auto_cert: Boolean
+    },
+    model: {
+        prop: 'auto_cert',
+        event: 'change_auto_cert'
+    },
+    methods: {
+        callback() {
+            this.$refs.info.get()
+        }
+    },
+    computed: {
+        name() {
+            return this.directivesMap['server_name'][0].params.trim()
+        }
+    }
+}
+</script>
+
+<style scoped>
+
+</style>

+ 10 - 2
frontend/src/views/domain/CertInfo.vue → frontend/src/views/domain/cert/CertInfo.vue

@@ -1,6 +1,6 @@
 <template>
-    <div v-if="ok">
-        <h3 v-translate>Certificate Status</h3>
+    <div class="cert-info" v-if="ok">
+        <h4 v-translate>Certificate Status</h4>
         <p v-translate="{issuer: cert.issuer_name}">Intermediate Certification Authorities: %{issuer}</p>
         <p v-translate="{name: cert.subject_name}">Subject Name: %{name}</p>
         <p v-translate="{date: moment(cert.not_after).format('YYYY-MM-DD HH:mm:ss').toString()}">
@@ -57,6 +57,14 @@ export default {
 </script>
 
 <style lang="less" scoped>
+h4 {
+    padding-bottom: 10px;
+}
+
+.cert-info {
+    padding-bottom: 10px;
+}
+
 .status {
     span {
         margin-left: 10px;

+ 163 - 0
frontend/src/views/domain/cert/IssueCert.vue

@@ -0,0 +1,163 @@
+<template>
+    <div>
+        <a-form-item :label="$gettext('Encrypt website with Let\'s Encrypt')">
+            <a-switch
+                :loading="issuing_cert"
+                v-model="M_enabled"
+                @change="onchange"
+                :disabled="no_server_name||server_name_more_than_one"
+            />
+            <a-alert
+                v-if="no_server_name||server_name_more_than_one"
+                :message="$gettext('Warning')"
+                type="warning"
+                show-icon
+            >
+                <template slot="description">
+                    <span v-if="no_server_name" v-translate>
+                        server_name parameter is required
+                    </span>
+                    <span v-if="server_name_more_than_one" v-translate>
+                        server_name parameters more than one
+                    </span>
+                </template>
+            </a-alert>
+        </a-form-item>
+        <p v-translate>Note: The server_name in the current configuration must be the domain name you need to get the
+            certificate.</p>
+        <p v-if="enabled" v-translate>The certificate for the domain will be checked every hour,
+            and will be renewed if it has been more than 1 month since it was last issued.</p>
+        <p v-translate>Make sure you have configured a reverse proxy for .well-known
+            directory to HTTPChallengePort (default: 9180) before getting the certificate.</p>
+    </div>
+</template>
+
+<script>
+import {issue_cert} from '@/views/domain/methods'
+import $gettext, {$interpolate} from '@/lib/translate/gettext'
+
+export default {
+    name: 'IssueCert',
+    props: {
+        directivesMap: Object,
+        current_server_directives: Array,
+        enabled: Boolean
+    },
+    model: {
+        prop: 'enabled',
+        event: 'changeEnabled'
+    },
+    data() {
+        return {
+            issuing_cert: false,
+            M_enabled: this.enabled,
+        }
+    },
+    methods: {
+        onchange(r) {
+            this.$emit('changeEnabled', r)
+            this.change_auto_cert(r)
+            if (r) {
+                this.job()
+            }
+        },
+        job() {
+            this.issuing_cert = true
+
+            if (this.no_server_name) {
+                this.$message.error($gettext('server_name not found in directives'))
+                this.issuing_cert = false
+                return
+            }
+
+            if (this.server_name_more_than_one) {
+                this.$message.error($gettext('server_name parameters more than one'))
+                this.issuing_cert = false
+                return
+            }
+
+            const server_name = this.directivesMap['server_name'][0]
+
+            if (!this.directivesMap['ssl_certificate']) {
+                this.current_server_directives.splice(server_name.idx + 1, 0, {
+                    directive: 'ssl_certificate',
+                    params: ''
+                })
+            }
+
+            this.$nextTick(() => {
+                if (!this.directivesMap['ssl_certificate_key']) {
+                    const ssl_certificate = this.directivesMap['ssl_certificate'][0]
+                    this.current_server_directives.splice(ssl_certificate.idx + 1, 0, {
+                        directive: 'ssl_certificate_key',
+                        params: ''
+                    })
+                }
+            })
+
+            setTimeout(() => {
+                issue_cert(this.name, this.callback)
+            }, 100)
+        },
+        callback(ssl_certificate, ssl_certificate_key) {
+            this.$set(this.directivesMap['ssl_certificate'][0], 'params', ssl_certificate)
+            this.$set(this.directivesMap['ssl_certificate_key'][0], 'params', ssl_certificate_key)
+            this.issuing_cert = false
+            this.$emit('callback')
+        },
+        change_auto_cert(r) {
+            if (r) {
+                this.$api.domain.add_auto_cert(this.name).then(() => {
+                    this.$message.success($interpolate($gettext('Auto-renewal enabled for %{name}'), {name: this.name}))
+                }).catch(e => {
+                    this.$message.error(e.message ?? $interpolate($gettext('Enable auto-renewal failed for %{name}'), {name: this.name}))
+                })
+            } else {
+                this.$api.domain.remove_auto_cert(this.name).then(() => {
+                    this.$message.success($interpolate($gettext('Auto-renewal disabled for %{name}'), {name: this.name}))
+                }).catch(e => {
+                    this.$message.error(e.message ?? $interpolate($gettext('Disable auto-renewal failed for %{name}'), {name: this.name}))
+                })
+            }
+        },
+    },
+    watch: {
+        server_name_more_than_one() {
+            this.M_enabled = false
+            this.onchange(false)
+        },
+        no_server_name() {
+            this.M_enabled = false
+            this.onchange(false)
+        }
+    },
+    computed: {
+        is_demo() {
+            return this.$store.getters.env.demo === true
+        },
+        server_name_more_than_one() {
+            return this.directivesMap['server_name'] && (this.directivesMap['server_name'].length > 1 ||
+                this.directivesMap['server_name'][0].params.trim().indexOf(' ') > 0)
+        },
+        no_server_name() {
+            return !this.directivesMap['server_name']
+        },
+        name() {
+            return this.directivesMap['server_name'][0].params.trim()
+        }
+    }
+}
+</script>
+
+<style lang="less" scoped>
+.switch-wrapper {
+    position: relative;
+
+    .text {
+        position: absolute;
+        top: 50%;
+        transform: translateY(-50%);
+        margin-left: 10px;
+    }
+}
+</style>

+ 45 - 43
frontend/src/views/domain/columns.js

@@ -1,4 +1,4 @@
-import $gettext from "@/lib/translate/gettext";
+import $gettext from '@/lib/translate/gettext'
 
 const columns = [{
     title: $gettext('Configuration Name'),
@@ -12,18 +12,6 @@ const columns = [{
     edit: {
         type: 'input'
     }
-}, {
-    title: $gettext('Root Directory (root)'),
-    dataIndex: 'root',
-    edit: {
-        type: 'input'
-    }
-}, {
-    title: $gettext('Index (index)'),
-    dataIndex: 'index',
-    edit: {
-        type: 'input'
-    }
 }, {
     title: $gettext('HTTP Listen Port'),
     dataIndex: 'http_listen_port',
@@ -32,43 +20,57 @@ const columns = [{
         min: 80
     }
 }, {
-    title: $gettext('Enable TLS'),
-    dataIndex: 'support_ssl',
-    edit: {
-        type: 'switch',
-        event: 'change_support_ssl'
-    }
-}]
-
-const columnsSSL = [{
-    title: $gettext('Certificate Auto-renewal'),
-    dataIndex: 'auto_cert',
-    edit: {
-        type: 'switch',
-        event: 'change_auto_cert'
-    },
-    description: $gettext('The certificate for the domain will be checked every hour, ' +
-        'and will be renewed if it has been more than 1 month since it was last issued.' +
-        '<br/>If you do not have a certificate before, please click "Getting Certificate from Let\'s Encrypt" first.')
-}, {
-    title: $gettext('HTTPS Listen Port'),
-    dataIndex: 'https_listen_port',
-    edit: {
-        type: 'number',
-        min: 443
-    }
-}, {
-    title: $gettext('Certificate Path (ssl_certificate)'),
-    dataIndex: 'ssl_certificate',
+    title: $gettext('Root Directory (root)'),
+    dataIndex: 'root',
     edit: {
         type: 'input'
     }
 }, {
-    title: $gettext('Private Key Path (ssl_certificate_key)'),
-    dataIndex: 'ssl_certificate_key',
+    title: $gettext('Index (index)'),
+    dataIndex: 'index',
     edit: {
         type: 'input'
     }
 }]
 
+const columnsSSL = [
+    {
+        title: $gettext('Enable TLS'),
+        dataIndex: 'support_ssl',
+        edit: {
+            type: 'switch',
+            event: 'change_support_ssl'
+        }
+    }, {
+        title: $gettext('Certificate Auto-renewal'),
+        dataIndex: 'auto_cert',
+        edit: {
+            type: 'switch',
+            event: 'change_auto_cert'
+        },
+        description: $gettext('The certificate for the domain will be checked every hour, ' +
+            'and will be renewed if it has been more than 1 month since it was last issued.' +
+            '<br/>If you do not have a certificate before, please click "Getting Certificate from Let\'s Encrypt" first.')
+    }, {
+        title: $gettext('HTTPS Listen Port'),
+        dataIndex: 'https_listen_port',
+        edit: {
+            type: 'number',
+            min: 443
+        }
+    }, {
+        title: $gettext('Certificate Path (ssl_certificate)'),
+        dataIndex: 'ssl_certificate',
+        edit: {
+            type: 'input'
+        }
+    }, {
+        title: $gettext('Private Key Path (ssl_certificate_key)'),
+        dataIndex: 'ssl_certificate_key',
+        edit: {
+            type: 'input'
+        }
+    }
+]
+
 export {columns, columnsSSL}

+ 4 - 29
frontend/src/views/domain/methods.js

@@ -1,36 +1,8 @@
 import $gettext from '@/lib/translate/gettext'
 import store from '@/lib/store'
 import Vue from 'vue'
-const unparse = (text, config) => {
-    // http_listen_port: /listen (.*);/i,
-    // https_listen_port: /listen (.*) ssl/i,
-    const reg = {
-        server_name: /server_name[\s](.*);/ig,
-        index: /index[\s](.*);/i,
-        root: /root[\s](.*);/i,
-        ssl_certificate: /ssl_certificate[\s](.*);/i,
-        ssl_certificate_key: /ssl_certificate_key[\s](.*);/i
-    }
-    text = text.replace(/listen[\s](.*);/i, 'listen\t'
-        + config['http_listen_port'] + ';')
-    text = text.replace(/listen[\s](.*) ssl/i, 'listen\t'
-        + config['https_listen_port'] + ' ssl')
-
-    text = text.replace(/listen(.*):(.*);/i, 'listen\t[::]:'
-        + config['http_listen_port'] + ';')
-    text = text.replace(/listen(.*):(.*) ssl/i, 'listen\t[::]:'
-        + config['https_listen_port'] + ' ssl')
-
-    for (let k in reg) {
-        text = text.replace(new RegExp(reg[k]), k + '\t' +
-            (config[k] !== undefined ? config[k] : ' ') + ';')
-    }
-
-    return text
-}
 
 const issue_cert = (server_name, callback) => {
-    Vue.prototype.$message.info($gettext('Note: The server_name in the current configuration must be the domain name you need to get the certificate.'), 15)
     Vue.prototype.$message.info($gettext('Getting the certificate, please wait...'), 15)
     const ws = new WebSocket(Vue.prototype.getWebSocketRoot() + '/cert/issue/' + server_name
         + '?token=' + btoa(store.state.user.token))
@@ -57,6 +29,9 @@ const issue_cert = (server_name, callback) => {
             callback(r.ssl_certificate, r.ssl_certificate_key)
         }
     }
+    // setTimeout(() => {
+    //     callback('a', 'b')
+    // }, 10000)
 }
 
-export {unparse, issue_cert}
+export {issue_cert}

+ 78 - 0
frontend/src/views/domain/ngx_conf/LocationEditor.vue

@@ -0,0 +1,78 @@
+<template>
+    <a-form-item :label="$gettext('Locations')" :key="update">
+        <a-empty v-if="!locations"/>
+        <a-card v-for="(v,k) in locations" :key="k"
+                :title="$gettext('Location')" size="small">
+            <a-form-item :label="$gettext('Comments')" v-if="v.comments">
+                <p style="white-space: pre-wrap;">{{ v.comments }}</p>
+            </a-form-item>
+            <a-form-item :label="$gettext('Path')">
+                <a-input addon-before="location" v-model="v.path"/>
+            </a-form-item>
+            <a-form-item :label="$gettext('Content')">
+                <vue-itextarea v-model="v.content" :default-text-height="200"/>
+            </a-form-item>
+        </a-card>
+
+        <a-modal :title="$gettext('Add Location')" v-model="adding" @ok="save">
+            <a-form-item :label="$gettext('Comments')">
+                <a-textarea v-model="location.comments"></a-textarea>
+            </a-form-item>
+            <a-form-item :label="$gettext('Path')">
+                <a-input addon-before="location" v-model="location.path"/>
+            </a-form-item>
+            <a-form-item :label="$gettext('Content')">
+                <vue-itextarea v-model="location.content" :default-text-height="200"/>
+            </a-form-item>
+        </a-modal>
+
+        <div>
+            <a-button block @click="add">{{ $gettext('Add Location') }}</a-button>
+        </div>
+    </a-form-item>
+</template>
+
+<script>
+import VueItextarea from '@/components/VueItextarea/VueItextarea'
+
+export default {
+    name: 'LocationEditor',
+    components: {VueItextarea},
+    props: {
+        locations: Array
+    },
+    data() {
+        return {
+            adding: false,
+            location: {},
+            update: 0
+        }
+    },
+    methods: {
+        add() {
+            this.adding = true
+            this.location = {}
+        },
+        save() {
+            this.adding = false
+            if (this.locations) {
+                this.locations.push(this.location)
+            } else {
+                this.locations = [this.location]
+            }
+            this.update++
+        },
+        remove(index) {
+            this.update++
+            this.locations.splice(index, 1)
+        }
+    }
+}
+</script>
+
+<style lang="less" scoped>
+.ant-card {
+    margin: 10px 0;
+    box-shadow: unset;
+}
+</style>

+ 104 - 0
frontend/src/views/domain/ngx_conf/NgxConfigEditor.vue

@@ -0,0 +1,104 @@
+<template>
+    <a-tabs v-model="current_server_index">
+        <a-tab-pane :tab="'Server '+(k+1)" v-for="(v,k) in ngx_config.servers" :key="k">
+
+            <div class="tab-content">
+                <template v-if="support_ssl&&enabled">
+                    <cert-info :domain="name" v-if="name"/>
+                    <issue-cert
+                        :current_server_directives="current_server_directives"
+                        :directives-map="directivesMap"
+                        v-model="auto_cert"
+                    />
+                    <cert-info :current_server_directives="current_server_directives"
+                               :directives-map="directivesMap"
+                               v-model="auto_cert"/>
+                </template>
+
+                <a-form-item :label="$gettext('Comments')" v-if="v.comments">
+                    <p style="white-space: pre-wrap;">{{ v.comments }}</p>
+                </a-form-item>
+
+                <directive-editor :ngx_directives="v.directives" :key="update"/>
+
+                <location-editor :locations="v.locations"/>
+            </div>
+
+        </a-tab-pane>
+    </a-tabs>
+</template>
+
+<script>
+import CertInfo from '@/views/domain/cert/CertInfo'
+import IssueCert from '@/views/domain/cert/IssueCert'
+import DirectiveEditor from '@/views/domain/ngx_conf/directive/DirectiveEditor'
+import LocationEditor from '@/views/domain/ngx_conf/LocationEditor'
+
+export default {
+    name: 'NgxConfigEditor',
+    components: {LocationEditor, DirectiveEditor, IssueCert, CertInfo},
+    props: {
+        ngx_config: Object,
+        auto_cert: Boolean,
+        enabled: Boolean
+    },
+    data() {
+        return {
+            current_server_index: 0,
+            update: 0,
+            name: this.$route.params?.name?.toString() ?? '',
+        }
+    },
+    model: {
+        prop: 'auto_cert',
+        event: 'change_auto_cert'
+    },
+    methods: {
+        update_cert_info() {
+            if (this.name && this.$refs['cert-info' + this.current_server_index]) {
+                this.$refs['cert-info' + this.current_server_index].get()
+            }
+        }
+    },
+    computed: {
+        directivesMap: {
+            get() {
+                const map = {}
+
+                this.current_server_directives.forEach((v, k) => {
+                    v.idx = k
+                    if (map[v.directive]) {
+                        map[v.directive].push(v)
+                    } else {
+                        map[v.directive] = [v]
+                    }
+                })
+
+                return map
+            }
+        },
+        current_server_directives: {
+            get() {
+                return this.ngx_config.servers[this.current_server_index].directives
+            }
+        },
+        support_ssl: {
+            get() {
+                if (this.directivesMap.listen) {
+                    for (const v of this.directivesMap.listen) {
+                        if (v?.params.indexOf('ssl') > 0) {
+                            return true
+                        }
+                    }
+                }
+
+                return false
+            }
+        },
+    }
+}
+</script>
+
+<style scoped>
+
+</style>

+ 74 - 0
frontend/src/views/domain/ngx_conf/directive/DirectiveAdd.vue

@@ -0,0 +1,74 @@
+<template>
+    <div>
+        <div class="add-directive-temp" v-if="adding">
+            <a-select v-model="mode" default-value="default" style="min-width: 150px">
+                <a-select-option value="default">
+                    {{ $gettext('Single Directive') }}
+                </a-select-option>
+                <a-select-option value="if">
+                    if
+                </a-select-option>
+            </a-select>
+            <vue-itextarea v-if="mode===If" :default-text-height="100" v-model="directive.params"/>
+            <a-input-group compact v-else>
+                <a-input style="width: 30%" :placeholder="$gettext('Directive')" v-model="directive.directive"/>
+                <a-input style="width: 70%" :placeholder="$gettext('Params')" v-model="directive.params">
+                    <a-icon slot="suffix" type="close" style="color: rgba(0,0,0,.45);font-size: 10px;"
+                            @click="adding=false"/>
+                </a-input>
+            </a-input-group>
+        </div>
+        <a-button block v-if="!adding" @click="add">{{ $gettext('Add Directive Below') }}</a-button>
+        <a-button type="primary" v-else block @click="save"
+                  :disabled="!directive.directive&&!directive.params">{{ $gettext('Save Directive') }}
+        </a-button>
+    </div>
+</template>
+
+<script>
+import {If} from '@/views/domain/ngx_conf/ngx_constant'
+import VueItextarea from '@/components/VueItextarea/VueItextarea'
+
+export default {
+    name: 'DirectiveAdd',
+    components: {
+        VueItextarea
+    },
+    props: {
+        ngx_directives: Array,
+        idx: Number,
+    },
+    data() {
+        return {
+            adding: false,
+            directive: {},
+            mode: 'default',
+            If
+        }
+    },
+    methods: {
+        add() {
+            this.adding = true
+            this.directive = {}
+        },
+        save() {
+            this.adding = false
+            if (this.mode === If) {
+                this.directive.directive = If
+            }
+
+            if (this.idx) {
+                this.ngx_directives.splice(this.idx + 1, 0, this.directive)
+            } else {
+                this.ngx_directives.push(this.directive)
+            }
+
+            this.$emit('save', this.idx)
+        }
+    }
+}
+</script>
+
+<style lang="less" scoped>
+
+</style>

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

@@ -0,0 +1,92 @@
+<template>
+    <a-form-item :label="$gettext('Directives')">
+        <div v-for="(directive,k) in ngx_directives" :key="k" @click="current_idx=k">
+            <vue-itextarea v-if="directive.directive === If" v-model="directive.params" :default-text-height="100"/>
+            <a-input :addon-before="directive.directive" v-model="directive.params" @click="current_idx=k" v-else>
+                <a-popconfirm slot="suffix" @confirm="remove(k)"
+                              :title="$gettext('Are you sure you want to remove this directive?')"
+                              :ok-text="$gettext('Yes')"
+                              :cancel-text="$gettext('No')">
+                    <a-icon type="close"
+                            style="color: rgba(0,0,0,.45);font-size: 10px;"
+                    />
+                </a-popconfirm>
+            </a-input>
+            <transition name="slide">
+                <div v-if="current_idx===k" class="extra">
+                    <div class="extra-content">
+                        <a-form-item :label="$gettext('Comments')">
+                            <a-textarea v-model="directive.comments"/>
+                        </a-form-item>
+                        <directive-add :ngx_directives="ngx_directives" :idx="k" @save="onSave(k)"/>
+                    </div>
+                </div>
+            </transition>
+        </div>
+        <directive-add :ngx_directives="ngx_directives"/>
+    </a-form-item>
+</template>
+
+<script>
+import VueItextarea from '@/components/VueItextarea/VueItextarea'
+import {If} from '../ngx_constant'
+import DirectiveAdd from '@/views/domain/ngx_conf/directive/DirectiveAdd'
+
+export default {
+    name: 'DirectiveEditor',
+    props: {
+        ngx_directives: Array
+    },
+    components: {
+        DirectiveAdd,
+        VueItextarea
+    },
+    data() {
+        return {
+            adding: false,
+            directive: {},
+            If,
+            current_idx: -1,
+        }
+    },
+    methods: {
+        add() {
+            this.adding = true
+            this.directive = {}
+        },
+        save() {
+            this.adding = false
+            this.ngx_directives.push(this.directive)
+        },
+        remove(index) {
+            this.ngx_directives.splice(index, 1)
+        },
+        onSave(idx) {
+            const that = this
+            setTimeout(() => {
+                that.current_idx = idx + 1
+            }, 50)
+        }
+    }
+}
+</script>
+
+<style lang="less" scoped>
+.extra {
+    background-color: #fafafa;
+    padding: 10px 20px 20px;
+    margin-bottom: 10px;
+}
+
+.slide-enter-active, .slide-leave-active {
+    transition: max-height .5s ease;
+}
+
+.slide-enter, .slide-leave-to {
+    max-height: 0;
+}
+
+.slide-enter-to, .slide-leave {
+    max-height: 600px;
+}
+</style>

+ 1 - 0
frontend/src/views/domain/ngx_conf/ngx_constant.js

@@ -0,0 +1 @@
+export const If = "if"

+ 116 - 152
server/api/cert.go

@@ -1,164 +1,128 @@
 package api
 
 import (
-	"encoding/json"
-	"github.com/0xJacky/Nginx-UI/server/settings"
-	"github.com/0xJacky/Nginx-UI/server/tool"
-	"github.com/0xJacky/Nginx-UI/server/tool/nginx"
-	"github.com/gin-gonic/gin"
-	"github.com/gorilla/websocket"
-	"log"
-	"net/http"
-	"os"
+    "github.com/0xJacky/Nginx-UI/server/tool"
+    "github.com/0xJacky/Nginx-UI/server/tool/nginx"
+    "github.com/gin-gonic/gin"
+    "github.com/gorilla/websocket"
+    "log"
+    "net/http"
+    "os"
 )
 
 func CertInfo(c *gin.Context) {
-	domain := c.Param("domain")
+    domain := c.Param("domain")
 
-	key, err := tool.GetCertInfo(domain)
+    key, err := tool.GetCertInfo(domain)
 
-	c.JSON(http.StatusOK, gin.H{
-		"error":        err,
-		"subject_name": key.Subject.CommonName,
-		"issuer_name":  key.Issuer.CommonName,
-		"not_after":    key.NotAfter,
-		"not_before":   key.NotBefore,
-	})
+    c.JSON(http.StatusOK, gin.H{
+        "error":        err,
+        "subject_name": key.Subject.CommonName,
+        "issuer_name":  key.Issuer.CommonName,
+        "not_after":    key.NotAfter,
+        "not_before":   key.NotBefore,
+    })
 }
 
 func IssueCert(c *gin.Context) {
-	domain := c.Param("domain")
-	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)
-
-	for {
-		// read
-		mt, message, err := ws.ReadMessage()
-		if err != nil {
-			break
-		}
-		if string(message) == "go" {
-			var m []byte
-
-			if settings.ServerSettings.Demo {
-				m, _ = json.Marshal(gin.H{
-					"status":  "error",
-					"message": "this feature is not available in demo",
-				})
-				_ = ws.WriteMessage(mt, m)
-				return
-			}
-
-			err = tool.IssueCert(domain)
-
-			if err != nil {
-
-				log.Println(err)
-
-				m, err = json.Marshal(gin.H{
-					"status":  "error",
-					"message": err.Error(),
-				})
-
-				if err != nil {
-					log.Println(err)
-					return
-				}
-
-				err = ws.WriteMessage(mt, m)
-
-				if err != nil {
-					log.Println(err)
-					return
-				}
-
-				return
-			}
-
-			sslCertificatePath := nginx.GetNginxConfPath("ssl/" + domain + "/fullchain.cer")
-			_, err = os.Stat(sslCertificatePath)
-
-			if err != nil {
-				log.Println(err)
-				return
-			}
-
-			log.Println("[found]", "fullchain.cer")
-			m, err = json.Marshal(gin.H{
-				"status":  "success",
-				"message": "[found] fullchain.cer",
-			})
-
-			if err != nil {
-				log.Println(err)
-				return
-			}
-
-			err = ws.WriteMessage(mt, m)
-
-			if err != nil {
-				log.Println(err)
-				return
-			}
-
-			sslCertificateKeyPath := nginx.GetNginxConfPath("ssl/" + domain + "/" + domain + ".key")
-			_, err = os.Stat(sslCertificateKeyPath)
-
-			if err != nil {
-				log.Println(err)
-				return
-			}
-
-			log.Println("[found]", "cert key")
-			m, err = json.Marshal(gin.H{
-				"status":  "success",
-				"message": "[found] cert key",
-			})
-
-			if err != nil {
-				log.Println(err)
-			}
-
-			err = ws.WriteMessage(mt, m)
-
-			if err != nil {
-				log.Println(err)
-			}
-
-			log.Println("申请成功")
-			m, err = json.Marshal(gin.H{
-				"status":              "success",
-				"message":             "申请成功",
-				"ssl_certificate":     sslCertificatePath,
-				"ssl_certificate_key": sslCertificateKeyPath,
-			})
-
-			if err != nil {
-				log.Println(err)
-			}
-
-			err = ws.WriteMessage(mt, m)
-
-			if err != nil {
-				log.Println(err)
-			}
-		}
-	}
+    domain := c.Param("domain")
+    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
+    mt, message, err := ws.ReadMessage()
+    if err != nil {
+        log.Println(err)
+        return
+    }
+
+    if mt == websocket.TextMessage && string(message) == "go" {
+
+        err = tool.IssueCert(domain)
+
+        if err != nil {
+
+            log.Println(err)
+
+            err = ws.WriteJSON(gin.H{
+                "status":  "error",
+                "message": err.Error(),
+            })
+
+            if err != nil {
+                log.Println(err)
+                return
+            }
+
+            return
+        }
+
+        sslCertificatePath := nginx.GetNginxConfPath("ssl/" + domain + "/fullchain.cer")
+        _, err = os.Stat(sslCertificatePath)
+
+        if err != nil {
+            log.Println(err)
+            return
+        }
+
+        log.Println("[found]", "fullchain.cer")
+
+        err = ws.WriteJSON(gin.H{
+            "status":  "success",
+            "message": "[found] fullchain.cer",
+        })
+
+        if err != nil {
+            log.Println(err)
+            return
+        }
+
+        sslCertificateKeyPath := nginx.GetNginxConfPath("ssl/" + domain + "/" + domain + ".key")
+        _, err = os.Stat(sslCertificateKeyPath)
+
+        if err != nil {
+            log.Println(err)
+            return
+        }
+
+        log.Println("[found]", "cert key")
+        err = ws.WriteJSON(gin.H{
+            "status":  "success",
+            "message": "[found] Certificate Key",
+        })
+
+        if err != nil {
+            log.Println(err)
+            return
+        }
+
+        err = ws.WriteJSON(gin.H{
+            "status":              "success",
+            "message":             "Issued certificate successfully",
+            "ssl_certificate":     sslCertificatePath,
+            "ssl_certificate_key": sslCertificateKeyPath,
+        })
+
+        if err != nil {
+            log.Println(err)
+            return
+        }
+    }
 }

+ 235 - 232
server/api/domain.go

@@ -1,267 +1,270 @@
 package api
 
 import (
-	"github.com/0xJacky/Nginx-UI/server/model"
-	"github.com/0xJacky/Nginx-UI/server/tool"
-	"github.com/0xJacky/Nginx-UI/server/tool/nginx"
-	"github.com/gin-gonic/gin"
-	"io/ioutil"
-	"net/http"
-	"os"
-	"path/filepath"
-	"strings"
+    "github.com/0xJacky/Nginx-UI/server/model"
+    "github.com/0xJacky/Nginx-UI/server/tool"
+    "github.com/0xJacky/Nginx-UI/server/tool/nginx"
+    "github.com/gin-gonic/gin"
+    "io/ioutil"
+    "net/http"
+    "os"
+    "path/filepath"
+    "strings"
 )
 
 func GetDomains(c *gin.Context) {
-	orderBy := c.Query("order_by")
-	sort := c.DefaultQuery("sort", "desc")
-
-	mySort := map[string]string{
-		"enabled": "bool",
-		"name":    "string",
-		"modify":  "time",
-	}
-
-	configFiles, err := ioutil.ReadDir(nginx.GetNginxConfPath("sites-available"))
-
-	if err != nil {
-		ErrHandler(c, err)
-		return
-	}
-
-	enabledConfig, err := ioutil.ReadDir(filepath.Join(nginx.GetNginxConfPath("sites-enabled")))
-
-	if err != nil {
-		ErrHandler(c, err)
-		return
-	}
-
-	enabledConfigMap := make(map[string]bool)
-	for i := range enabledConfig {
-		enabledConfigMap[enabledConfig[i].Name()] = true
-	}
-
-	var configs []gin.H
-
-	for i := range configFiles {
-		file := configFiles[i]
-		if !file.IsDir() {
-			configs = append(configs, gin.H{
-				"name":    file.Name(),
-				"size":    file.Size(),
-				"modify":  file.ModTime(),
-				"enabled": enabledConfigMap[file.Name()],
-			})
-		}
-	}
-
-	configs = tool.Sort(orderBy, sort, mySort[orderBy], configs)
-
-	c.JSON(http.StatusOK, gin.H{
-		"configs": configs,
-	})
+    orderBy := c.Query("order_by")
+    sort := c.DefaultQuery("sort", "desc")
+
+    mySort := map[string]string{
+        "enabled": "bool",
+        "name":    "string",
+        "modify":  "time",
+    }
+
+    configFiles, err := ioutil.ReadDir(nginx.GetNginxConfPath("sites-available"))
+
+    if err != nil {
+        ErrHandler(c, err)
+        return
+    }
+
+    enabledConfig, err := ioutil.ReadDir(filepath.Join(nginx.GetNginxConfPath("sites-enabled")))
+
+    if err != nil {
+        ErrHandler(c, err)
+        return
+    }
+
+    enabledConfigMap := make(map[string]bool)
+    for i := range enabledConfig {
+        enabledConfigMap[enabledConfig[i].Name()] = true
+    }
+
+    var configs []gin.H
+
+    for i := range configFiles {
+        file := configFiles[i]
+        if !file.IsDir() {
+            configs = append(configs, gin.H{
+                "name":    file.Name(),
+                "size":    file.Size(),
+                "modify":  file.ModTime(),
+                "enabled": enabledConfigMap[file.Name()],
+            })
+        }
+    }
+
+    configs = tool.Sort(orderBy, sort, mySort[orderBy], configs)
+
+    c.JSON(http.StatusOK, gin.H{
+        "configs": configs,
+    })
 }
 
 func GetDomain(c *gin.Context) {
-	name := c.Param("name")
-	path := filepath.Join(nginx.GetNginxConfPath("sites-available"), name)
+    name := c.Param("name")
+    path := filepath.Join(nginx.GetNginxConfPath("sites-available"), name)
 
-	enabled := true
-	if _, err := os.Stat(filepath.Join(nginx.GetNginxConfPath("sites-enabled"), name)); os.IsNotExist(err) {
-		enabled = false
-	}
+    enabled := true
+    if _, err := os.Stat(filepath.Join(nginx.GetNginxConfPath("sites-enabled"), name)); os.IsNotExist(err) {
+        enabled = false
+    }
 
-	config, err := nginx.ParseNgxConfig(path)
+    config, err := nginx.ParseNgxConfig(path)
 
-	if err != nil {
-		ErrHandler(c, err)
-		return
-	}
+    if err != nil {
+        ErrHandler(c, err)
+        return
+    }
 
-	_, err = model.FirstCert(name)
+    _, err = model.FirstCert(name)
 
-	c.JSON(http.StatusOK, gin.H{
-		"enabled":   enabled,
-		"name":      name,
-		"config":    config.BuildConfig(),
-		"tokenized": config,
-	})
+    c.JSON(http.StatusOK, gin.H{
+        "enabled":   enabled,
+        "name":      name,
+        "config":    config.BuildConfig(),
+        "tokenized": config,
+        "auto_cert": err == nil,
+    })
 
 }
 
 func EditDomain(c *gin.Context) {
-	var err error
-	name := c.Param("name")
-	request := make(gin.H)
-	err = c.BindJSON(&request)
-	path := filepath.Join(nginx.GetNginxConfPath("sites-available"), name)
-
-	err = ioutil.WriteFile(path, []byte(request["content"].(string)), 0644)
-	if err != nil {
-		ErrHandler(c, err)
-		return
-	}
-
-	enabledConfigFilePath := filepath.Join(nginx.GetNginxConfPath("sites-enabled"), name)
-	if _, err = os.Stat(enabledConfigFilePath); err == nil {
-		// 测试配置文件
-		err = nginx.TestNginxConf()
-		if err != nil {
-			c.JSON(http.StatusInternalServerError, gin.H{
-				"message": err.Error(),
-			})
-			return
-		}
-
-		output := nginx.ReloadNginx()
-
-		if output != "" && strings.Contains(output, "error") {
-			c.JSON(http.StatusInternalServerError, gin.H{
-				"message": output,
-			})
-			return
-		}
-	}
-
-	GetDomain(c)
+    var err error
+    name := c.Param("name")
+    request := make(gin.H)
+    err = c.BindJSON(&request)
+    path := filepath.Join(nginx.GetNginxConfPath("sites-available"), name)
+
+    err = ioutil.WriteFile(path, []byte(request["content"].(string)), 0644)
+    if err != nil {
+        ErrHandler(c, err)
+        return
+    }
+
+    enabledConfigFilePath := filepath.Join(nginx.GetNginxConfPath("sites-enabled"), name)
+    if _, err = os.Stat(enabledConfigFilePath); err == nil {
+        // Test nginx configuration
+        err = nginx.TestNginxConf()
+        if err != nil {
+            c.JSON(http.StatusInternalServerError, gin.H{
+                "message": err.Error(),
+            })
+            return
+        }
+
+        output := nginx.ReloadNginx()
+
+        if output != "" && strings.Contains(output, "error") {
+            c.JSON(http.StatusInternalServerError, gin.H{
+                "message": output,
+            })
+            return
+        }
+    }
+
+    GetDomain(c)
 }
 
 func EnableDomain(c *gin.Context) {
-	configFilePath := filepath.Join(nginx.GetNginxConfPath("sites-available"), c.Param("name"))
-	enabledConfigFilePath := filepath.Join(nginx.GetNginxConfPath("sites-enabled"), c.Param("name"))
-
-	_, err := os.Stat(configFilePath)
-
-	if err != nil {
-		ErrHandler(c, err)
-		return
-	}
-
-	err = os.Symlink(configFilePath, enabledConfigFilePath)
-
-	if err != nil {
-		ErrHandler(c, err)
-		return
-	}
-
-	// Test nginx config, if not pass then rollback.
-	err = nginx.TestNginxConf()
-	if err != nil {
-		_ = os.Remove(enabledConfigFilePath)
-		c.JSON(http.StatusInternalServerError, gin.H{
-			"message": err.Error(),
-		})
-		return
-	}
-
-	output := nginx.ReloadNginx()
-
-	if output != "" && strings.Contains(output, "error") {
-		c.JSON(http.StatusInternalServerError, gin.H{
-			"message": output,
-		})
-		return
-	}
-
-	c.JSON(http.StatusOK, gin.H{
-		"message": "ok",
-	})
+    configFilePath := filepath.Join(nginx.GetNginxConfPath("sites-available"), c.Param("name"))
+    enabledConfigFilePath := filepath.Join(nginx.GetNginxConfPath("sites-enabled"), c.Param("name"))
+
+    _, err := os.Stat(configFilePath)
+
+    if err != nil {
+        ErrHandler(c, err)
+        return
+    }
+
+    if _, err = os.Stat(enabledConfigFilePath); os.IsNotExist(err) {
+        err = os.Symlink(configFilePath, enabledConfigFilePath)
+
+        if err != nil {
+            ErrHandler(c, err)
+            return
+        }
+    }
+
+    // Test nginx config, if not pass then rollback.
+    err = nginx.TestNginxConf()
+    if err != nil {
+        _ = os.Remove(enabledConfigFilePath)
+        c.JSON(http.StatusInternalServerError, gin.H{
+            "message": err.Error(),
+        })
+        return
+    }
+
+    output := nginx.ReloadNginx()
+
+    if output != "" && strings.Contains(output, "error") {
+        c.JSON(http.StatusInternalServerError, gin.H{
+            "message": output,
+        })
+        return
+    }
+
+    c.JSON(http.StatusOK, gin.H{
+        "message": "ok",
+    })
 }
 
 func DisableDomain(c *gin.Context) {
-	enabledConfigFilePath := filepath.Join(nginx.GetNginxConfPath("sites-enabled"), c.Param("name"))
-
-	_, err := os.Stat(enabledConfigFilePath)
-
-	if err != nil {
-		ErrHandler(c, err)
-		return
-	}
-
-	err = os.Remove(enabledConfigFilePath)
-
-	if err != nil {
-		ErrHandler(c, err)
-		return
-	}
-
-	// delete auto cert record
-	cert := model.Cert{Domain: c.Param("name")}
-	err = cert.Remove()
-	if err != nil {
-		ErrHandler(c, err)
-		return
-	}
-
-	output := nginx.ReloadNginx()
-
-	if output != "" {
-		c.JSON(http.StatusInternalServerError, gin.H{
-			"message": output,
-		})
-		return
-	}
-
-	c.JSON(http.StatusOK, gin.H{
-		"message": "ok",
-	})
+    enabledConfigFilePath := filepath.Join(nginx.GetNginxConfPath("sites-enabled"), c.Param("name"))
+
+    _, err := os.Stat(enabledConfigFilePath)
+
+    if err != nil {
+        ErrHandler(c, err)
+        return
+    }
+
+    err = os.Remove(enabledConfigFilePath)
+
+    if err != nil {
+        ErrHandler(c, err)
+        return
+    }
+
+    // delete auto cert record
+    cert := model.Cert{Domain: c.Param("name")}
+    err = cert.Remove()
+    if err != nil {
+        ErrHandler(c, err)
+        return
+    }
+
+    output := nginx.ReloadNginx()
+
+    if output != "" {
+        c.JSON(http.StatusInternalServerError, gin.H{
+            "message": output,
+        })
+        return
+    }
+
+    c.JSON(http.StatusOK, gin.H{
+        "message": "ok",
+    })
 }
 
 func DeleteDomain(c *gin.Context) {
-	var err error
-	name := c.Param("name")
-	availablePath := filepath.Join(nginx.GetNginxConfPath("sites-available"), name)
-	enabledPath := filepath.Join(nginx.GetNginxConfPath("sites-enabled"), name)
-
-	if _, err = os.Stat(availablePath); os.IsNotExist(err) {
-		c.JSON(http.StatusNotFound, gin.H{
-			"message": "site not found",
-		})
-		return
-	}
-
-	if _, err = os.Stat(enabledPath); err == nil {
-		c.JSON(http.StatusNotAcceptable, gin.H{
-			"message": "site is enabled",
-		})
-		return
-	}
-
-	cert := model.Cert{Domain: name}
-	_ = cert.Remove()
-
-	err = os.Remove(availablePath)
-
-	if err != nil {
-		ErrHandler(c, err)
-		return
-	}
-
-	c.JSON(http.StatusOK, gin.H{
-		"message": "ok",
-	})
+    var err error
+    name := c.Param("name")
+    availablePath := filepath.Join(nginx.GetNginxConfPath("sites-available"), name)
+    enabledPath := filepath.Join(nginx.GetNginxConfPath("sites-enabled"), name)
+
+    if _, err = os.Stat(availablePath); os.IsNotExist(err) {
+        c.JSON(http.StatusNotFound, gin.H{
+            "message": "site not found",
+        })
+        return
+    }
+
+    if _, err = os.Stat(enabledPath); err == nil {
+        c.JSON(http.StatusNotAcceptable, gin.H{
+            "message": "site is enabled",
+        })
+        return
+    }
+
+    cert := model.Cert{Domain: name}
+    _ = cert.Remove()
+
+    err = os.Remove(availablePath)
+
+    if err != nil {
+        ErrHandler(c, err)
+        return
+    }
+
+    c.JSON(http.StatusOK, gin.H{
+        "message": "ok",
+    })
 
 }
 
 func AddDomainToAutoCert(c *gin.Context) {
-	domain := c.Param("domain")
-	cert, err := model.FirstOrCreateCert(domain)
-	if err != nil {
-		ErrHandler(c, err)
-		return
-	}
-	c.JSON(http.StatusOK, cert)
+    domain := c.Param("domain")
+    cert, err := model.FirstOrCreateCert(domain)
+    if err != nil {
+        ErrHandler(c, err)
+        return
+    }
+    c.JSON(http.StatusOK, cert)
 }
 
 func RemoveDomainFromAutoCert(c *gin.Context) {
-	cert := model.Cert{
-		Domain: c.Param("domain"),
-	}
-	err := cert.Remove()
-
-	if err != nil {
-		ErrHandler(c, err)
-		return
-	}
-	c.JSON(http.StatusOK, nil)
+    cert := model.Cert{
+        Domain: c.Param("domain"),
+    }
+    err := cert.Remove()
+
+    if err != nil {
+        ErrHandler(c, err)
+        return
+    }
+    c.JSON(http.StatusOK, nil)
 }

+ 42 - 0
server/api/ngx.go

@@ -0,0 +1,42 @@
+package api
+
+import (
+	"bufio"
+	"github.com/0xJacky/Nginx-UI/server/tool/nginx"
+	"github.com/gin-gonic/gin"
+	"net/http"
+	"strings"
+)
+
+func BuildNginxConfig(c *gin.Context) {
+	var ngxConf nginx.NgxConfig
+	if !BindAndValid(c, &ngxConf) {
+		return
+	}
+
+	c.JSON(http.StatusOK, gin.H{
+		"content": ngxConf.BuildConfig(),
+	})
+}
+
+func TokenizeNginxConfig(c *gin.Context) {
+	var json struct {
+		Content string `json:"content" binding:"required"`
+	}
+
+	if !BindAndValid(c, &json) {
+		return
+	}
+
+	scanner := bufio.NewScanner(strings.NewReader(json.Content))
+
+	ngxConfig, err := nginx.ParseNgxConfigByScanner("", scanner)
+
+	if err != nil {
+		ErrHandler(c, err)
+		return
+	}
+
+	c.JSON(http.StatusOK, ngxConfig)
+
+}

+ 42 - 18
server/api/template.go

@@ -2,34 +2,58 @@ package api
 
 import (
 	"github.com/0xJacky/Nginx-UI/server/settings"
-	"github.com/0xJacky/Nginx-UI/server/template"
+	"github.com/0xJacky/Nginx-UI/server/tool/nginx"
 	"github.com/gin-gonic/gin"
 	"net/http"
-	"os"
 	"strings"
 )
 
 func GetTemplate(c *gin.Context) {
-	name := c.Param("name")
-	content, err := template.DistFS.ReadFile(name)
-
-	_content := string(content)
-	_content = strings.ReplaceAll(_content, "{{ HTTP01PORT }}",
+	content := `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:{{ HTTP01PORT }};
+`
+	content = strings.ReplaceAll(content, "{{ HTTP01PORT }}",
 		settings.ServerSettings.HTTPChallengePort)
 
-	if err != nil {
-		if os.IsNotExist(err) {
-			c.JSON(http.StatusNotFound, gin.H{
-				"message": err.Error(),
-			})
-			return
-		}
-		ErrHandler(c, err)
-		return
+	var ngxConfig *nginx.NgxConfig
+
+	ngxConfig = &nginx.NgxConfig{
+		Servers: []*nginx.NgxServer{
+			{
+				Directives: []*nginx.NgxDirective{
+					{
+						Directive: "listen",
+						Params:    "80",
+					},
+					{
+						Directive: "listen",
+						Params:    "[::]:80",
+					},
+					{
+						Directive: "server_name",
+					},
+					{
+						Directive: "root",
+					},
+					{
+						Directive: "index",
+					},
+				},
+				Locations: []*nginx.NgxLocation{
+					{
+						Path:    "/.well-known/acme-challenge",
+						Content: content,
+					},
+				},
+			},
+		},
 	}
 
 	c.JSON(http.StatusOK, gin.H{
-		"message":  "ok",
-		"template": _content,
+		"message":   "ok",
+		"template":  ngxConfig.BuildConfig(),
+		"tokenized": ngxConfig,
 	})
 }

+ 2 - 2
server/router/middleware.go

@@ -34,7 +34,7 @@ func authRequired() gin.HandlerFunc {
 			token = string(tmp)
 			if token == "" {
 				c.JSON(http.StatusForbidden, gin.H{
-					"message": "auth fail",
+					"message": "Authorization failed",
 				})
 				c.Abort()
 				return
@@ -45,7 +45,7 @@ func authRequired() gin.HandlerFunc {
 
 		if n < 1 {
 			c.JSON(http.StatusForbidden, gin.H{
-				"message": "auth fail",
+				"message": "Authorization failed",
 			})
 			c.Abort()
 			return

+ 93 - 85
server/router/routers.go

@@ -1,92 +1,100 @@
 package router
 
 import (
-    "bufio"
-    "github.com/0xJacky/Nginx-UI/server/api"
-    "github.com/0xJacky/Nginx-UI/server/settings"
-    "github.com/gin-contrib/static"
-    "github.com/gin-gonic/gin"
-    "net/http"
-    "strings"
+	"bufio"
+	"github.com/0xJacky/Nginx-UI/server/api"
+	"github.com/0xJacky/Nginx-UI/server/settings"
+	"github.com/gin-contrib/static"
+	"github.com/gin-gonic/gin"
+	"net/http"
+	"strings"
 )
 
 func InitRouter() *gin.Engine {
-    r := gin.New()
-    r.Use(gin.Logger())
-
-    r.Use(recovery())
-
-    r.Use(cacheJs())
-
-    r.Use(static.Serve("/", mustFS("")))
-
-    r.NoRoute(func(c *gin.Context) {
-        accept := c.Request.Header.Get("Accept")
-        if strings.Contains(accept, "text/html") {
-            file, _ := mustFS("").Open("index.html")
-            defer file.Close()
-            stat, _ := file.Stat()
-            c.DataFromReader(http.StatusOK, stat.Size(), "text/html",
-                bufio.NewReader(file), nil)
-            return
-        }
-    })
-
-    g := r.Group("/api")
-    {
-
-        g.GET("settings", func(c *gin.Context) {
-            c.JSON(http.StatusOK, gin.H{
-                "demo": settings.ServerSettings.Demo,
-            })
-        })
-
-        g.GET("install", api.InstallLockCheck)
-        g.POST("install", api.InstallNginxUI)
-
-        g.POST("/login", api.Login)
-        g.DELETE("/logout", api.Logout)
-
-        g := g.Group("/", authRequired())
-        {
-            g.GET("/analytic", api.Analytic)
-            g.GET("/analytic/init", api.GetAnalyticInit)
-
-            g.GET("/users", api.GetUsers)
-            g.GET("/user/:id", api.GetUser)
-            g.POST("/user", api.AddUser)
-            g.POST("/user/:id", api.EditUser)
-            g.DELETE("/user/:id", api.DeleteUser)
-
-            g.GET("domains", api.GetDomains)
-            g.GET("domain/:name", api.GetDomain)
-            g.POST("domain/:name", api.EditDomain)
-            g.POST("domain/:name/enable", api.EnableDomain)
-            g.POST("domain/:name/disable", api.DisableDomain)
-            g.DELETE("domain/:name", api.DeleteDomain)
-
-            g.GET("configs", api.GetConfigs)
-            g.GET("config/:name", api.GetConfig)
-            g.POST("config", api.AddConfig)
-            g.POST("config/:name", api.EditConfig)
-
-            g.GET("backups", api.GetFileBackupList)
-            g.GET("backup/:id", api.GetFileBackup)
-
-            g.GET("template/:name", api.GetTemplate)
-
-            g.GET("cert/issue/:domain", api.IssueCert)
-            g.GET("cert/:domain/info", api.CertInfo)
-
-            // 添加域名到自动续期列表
-            g.POST("cert/:domain", api.AddDomainToAutoCert)
-            // 从自动续期列表中删除域名
-            g.DELETE("cert/:domain", api.RemoveDomainFromAutoCert)
-
-            // pty
-            g.GET("pty", api.Pty)
-        }
-    }
-
-    return r
+	r := gin.New()
+	r.Use(gin.Logger())
+
+	r.Use(recovery())
+
+	r.Use(cacheJs())
+
+	r.Use(static.Serve("/", mustFS("")))
+
+	r.NoRoute(func(c *gin.Context) {
+		accept := c.Request.Header.Get("Accept")
+		if strings.Contains(accept, "text/html") {
+			file, _ := mustFS("").Open("index.html")
+			defer file.Close()
+			stat, _ := file.Stat()
+			c.DataFromReader(http.StatusOK, stat.Size(), "text/html",
+				bufio.NewReader(file), nil)
+			return
+		}
+	})
+
+	g := r.Group("/api")
+	{
+
+		g.GET("settings", func(c *gin.Context) {
+			c.JSON(http.StatusOK, gin.H{
+				"demo": settings.ServerSettings.Demo,
+			})
+		})
+
+		g.GET("install", api.InstallLockCheck)
+		g.POST("install", api.InstallNginxUI)
+
+		g.POST("/login", api.Login)
+		g.DELETE("/logout", api.Logout)
+
+		g := g.Group("/", authRequired())
+		{
+			g.GET("analytic", api.Analytic)
+			g.GET("analytic/init", api.GetAnalyticInit)
+
+			g.GET("users", api.GetUsers)
+			g.GET("user/:id", api.GetUser)
+			g.POST("user", api.AddUser)
+			g.POST("user/:id", api.EditUser)
+			g.DELETE("user/:id", api.DeleteUser)
+
+			g.GET("domains", api.GetDomains)
+			g.GET("domain/:name", api.GetDomain)
+
+			// Modify site configuration directly
+			g.POST("domain/:name", api.EditDomain)
+
+			// Transform NgxConf to nginx configuration
+			g.POST("ngx/build_config", api.BuildNginxConfig)
+			// Tokenized nginx configuration to NgxConf
+			g.POST("ngx/tokenize_config", api.TokenizeNginxConfig)
+
+			g.POST("domain/:name/enable", api.EnableDomain)
+			g.POST("domain/:name/disable", api.DisableDomain)
+			g.DELETE("domain/:name", api.DeleteDomain)
+
+			g.GET("configs", api.GetConfigs)
+			g.GET("config/:name", api.GetConfig)
+			g.POST("config", api.AddConfig)
+			g.POST("config/:name", api.EditConfig)
+
+			//g.GET("backups", api.GetFileBackupList)
+			//g.GET("backup/:id", api.GetFileBackup)
+
+			g.GET("template", api.GetTemplate)
+
+			g.GET("cert/issue/:domain", api.IssueCert)
+			g.GET("cert/:domain/info", api.CertInfo)
+
+			// Add domain to auto-renew cert list
+			g.POST("cert/:domain", api.AddDomainToAutoCert)
+			// Delete domain from auto-renew cert list
+			g.DELETE("cert/:domain", api.RemoveDomainFromAutoCert)
+
+			// pty
+			g.GET("pty", api.Pty)
+		}
+	}
+
+	return r
 }

+ 0 - 13
server/template/http-conf

@@ -1,13 +0,0 @@
-server {
-    listen {{ http_listen_port }};
-    listen [::]:{{ http_listen_port }};
-
-    server_name {{ server_name }};
-
-    location /.well-known {
-        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:{{ HTTP01PORT }};
-    }
-}

+ 0 - 25
server/template/https-conf

@@ -1,25 +0,0 @@
-server {
-    listen {{ http_listen_port }};
-    listen [::]:{{ http_listen_port }};
-
-    server_name {{ server_name }};
-
-    rewrite ^(.*)$  https://$host$1 permanent;
-}
-
-server {
-    listen {{ https_listen_port }} ssl http2;
-    listen [::]:{{ https_listen_port }} ssl http2;
-
-    server_name {{ server_name }};
-
-    ssl_certificate {{ ssl_certificate }};
-    ssl_certificate_key {{ ssl_certificate_key }};
-
-    location /.well-known {
-        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:{{ HTTP01PORT }};
-    }
-}

+ 0 - 6
server/template/template.go

@@ -1,6 +0,0 @@
-package template
-
-import "embed"
-
-//go:embed http-conf https-conf
-var DistFS embed.FS

+ 164 - 160
server/tool/cert.go

@@ -1,185 +1,189 @@
 package tool
 
 import (
-	"crypto"
-	"crypto/ecdsa"
-	"crypto/elliptic"
-	"crypto/rand"
-	"crypto/tls"
-	"crypto/x509"
-	"github.com/0xJacky/Nginx-UI/server/model"
-	"github.com/0xJacky/Nginx-UI/server/settings"
-	"github.com/0xJacky/Nginx-UI/server/tool/nginx"
-	"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/registration"
-	"github.com/pkg/errors"
-	"io"
-	"io/ioutil"
-	"log"
-	"net"
-	"net/http"
-	"os"
-	"path/filepath"
-	"time"
+    "crypto"
+    "crypto/ecdsa"
+    "crypto/elliptic"
+    "crypto/rand"
+    "crypto/tls"
+    "crypto/x509"
+    "github.com/0xJacky/Nginx-UI/server/model"
+    "github.com/0xJacky/Nginx-UI/server/settings"
+    "github.com/0xJacky/Nginx-UI/server/tool/nginx"
+    "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/registration"
+    "github.com/pkg/errors"
+    "io"
+    "io/ioutil"
+    "log"
+    "net"
+    "net/http"
+    "os"
+    "path/filepath"
+    "time"
 )
 
 // MyUser You'll need a user or account type that implements acme.User
 type MyUser struct {
-	Email        string
-	Registration *registration.Resource
-	key          crypto.PrivateKey
+    Email        string
+    Registration *registration.Resource
+    key          crypto.PrivateKey
 }
 
 func (u *MyUser) GetEmail() string {
-	return u.Email
+    return u.Email
 }
 func (u MyUser) GetRegistration() *registration.Resource {
-	return u.Registration
+    return u.Registration
 }
 func (u *MyUser) GetPrivateKey() crypto.PrivateKey {
-	return u.key
+    return u.key
 }
 
 func AutoCert() {
-	defer func() {
-		if err := recover(); err != nil {
-			log.Println("[AutoCert] Recover", err)
-		}
-	}()
-	log.Println("[AutoCert] Start")
-	autoCertList := model.GetAutoCertList()
-	for i := range autoCertList {
-		domain := autoCertList[i].Domain
-		key, err := GetCertInfo(domain)
-		if err != nil {
-			log.Println("GetCertInfo Err", err)
-			// 获取证书信息失败,本次跳过
-			continue
-		}
-		// 未到一个月
-		if time.Now().Before(key.NotBefore.AddDate(0, 1, 0)) {
-			continue
-		}
-		// 过一个月了,重新申请证书
-		err = IssueCert(domain)
-		if err != nil {
-			log.Println(err)
-		}
-	}
+    defer func() {
+        if err := recover(); err != nil {
+            log.Println("[AutoCert] Recover", err)
+        }
+    }()
+    log.Println("[AutoCert] Start")
+    autoCertList := model.GetAutoCertList()
+    for i := range autoCertList {
+        domain := autoCertList[i].Domain
+        key, err := GetCertInfo(domain)
+        if err != nil {
+            log.Println("GetCertInfo Err", err)
+            // 获取证书信息失败,本次跳过
+            continue
+        }
+        // 未到一个月
+        if time.Now().Before(key.NotBefore.AddDate(0, 1, 0)) {
+            continue
+        }
+        // 过一个月了,重新申请证书
+        err = IssueCert(domain)
+        if err != nil {
+            log.Println(err)
+        }
+    }
 }
 
 func GetCertInfo(domain string) (key *x509.Certificate, err error) {
 
-	var response *http.Response
-
-	client := &http.Client{
-		Transport: &http.Transport{
-			DialContext: (&net.Dialer{
-				Timeout: 5 * time.Second,
-			}).DialContext,
-			DisableKeepAlives: true,
-			TLSClientConfig:   &tls.Config{InsecureSkipVerify: true},
-		},
-		Timeout: 5 * time.Second,
-	}
-
-	response, err = client.Get("https://" + domain)
-
-	if err != nil {
-		err = errors.Wrap(err, "get cert info error")
-		return
-	}
-
-	defer func(Body io.ReadCloser) {
-		err = Body.Close()
-		if err != nil {
-			log.Println(err)
-			return
-		}
-	}(response.Body)
-
-	key = response.TLS.PeerCertificates[0]
-
-	return
+    var response *http.Response
+
+    client := &http.Client{
+        Transport: &http.Transport{
+            DialContext: (&net.Dialer{
+                Timeout: 5 * time.Second,
+            }).DialContext,
+            DisableKeepAlives: true,
+            TLSClientConfig:   &tls.Config{InsecureSkipVerify: true},
+        },
+        Timeout: 5 * time.Second,
+    }
+
+    response, err = client.Get("https://" + domain)
+
+    if err != nil {
+        err = errors.Wrap(err, "get cert info error")
+        return
+    }
+
+    defer func(Body io.ReadCloser) {
+        err = Body.Close()
+        if err != nil {
+            log.Println(err)
+            return
+        }
+    }(response.Body)
+
+    key = response.TLS.PeerCertificates[0]
+
+    return
 }
 
 func IssueCert(domain string) error {
-	// Create a user. New accounts need an email and private key to start.
-	privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
-	if err != nil {
-		return errors.Wrap(err, "issue cert generate key error")
-	}
-
-	myUser := MyUser{
-		Email: settings.ServerSettings.Email,
-		key:   privateKey,
-	}
-
-	config := lego.NewConfig(&myUser)
-
-	if settings.ServerSettings.Demo {
-		config.CADirURL = "https://acme-staging-v02.api.letsencrypt.org/directory"
-	}
-	config.Certificate.KeyType = certcrypto.RSA2048
-
-	// A client facilitates communication with the CA server.
-	client, err := lego.NewClient(config)
-	if err != nil {
-		return errors.Wrap(err, "issue cert new client error")
-	}
-
-	err = client.Challenge.SetHTTP01Provider(
-		http01.NewProviderServer("",
-			settings.ServerSettings.HTTPChallengePort,
-		),
-	)
-	if err != nil {
-		return errors.Wrap(err, "issue cert challenge fail")
-	}
-
-	// New users will need to register
-	reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
-	if err != nil {
-		log.Println(err)
-		return errors.Wrap(err, "issue cert register fail")
-	}
-	myUser.Registration = reg
-
-	request := certificate.ObtainRequest{
-		Domains: []string{domain},
-		Bundle:  true,
-	}
-	certificates, err := client.Certificate.Obtain(request)
-	if err != nil {
-		return errors.Wrap(err, "issue cert fail to obtain")
-	}
-	saveDir := nginx.GetNginxConfPath("ssl/" + domain)
-	if _, err := os.Stat(saveDir); os.IsNotExist(err) {
-		err = os.Mkdir(saveDir, 0755)
-		if err != nil {
-			return errors.Wrap(err, "issue cert fail to create")
-		}
-	}
-
-	// Each certificate comes back with the cert bytes, the bytes of the client's
-	// private key, and a certificate URL. SAVE THESE TO DISK.
-	err = ioutil.WriteFile(filepath.Join(saveDir, "fullchain.cer"),
-		certificates.Certificate, 0644)
-	if err != nil {
-		log.Println(err)
-		return errors.Wrap(err, "issue cert write fullchain.cer fail")
-	}
-	err = ioutil.WriteFile(filepath.Join(saveDir, domain+".key"),
-		certificates.PrivateKey, 0644)
-	if err != nil {
-		log.Println(err)
-		return errors.Wrap(err, "issue cert write key fail")
-	}
-
-	nginx.ReloadNginx()
-
-	return nil
+    // Create a user. New accounts need an email and private key to start.
+    privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+    if err != nil {
+        return errors.Wrap(err, "issue cert generate key error")
+    }
+
+    myUser := MyUser{
+        Email: settings.ServerSettings.Email,
+        key:   privateKey,
+    }
+
+    config := lego.NewConfig(&myUser)
+
+    if settings.ServerSettings.Demo {
+        config.CADirURL = "https://acme-staging-v02.api.letsencrypt.org/directory"
+    }
+
+    config.Certificate.KeyType = certcrypto.RSA2048
+
+    // A client facilitates communication with the CA server.
+    client, err := lego.NewClient(config)
+    if err != nil {
+        return errors.Wrap(err, "issue cert new client error")
+    }
+
+    err = client.Challenge.SetHTTP01Provider(
+        http01.NewProviderServer("",
+            settings.ServerSettings.HTTPChallengePort,
+        ),
+    )
+    if err != nil {
+        return errors.Wrap(err, "issue cert challenge fail")
+    }
+
+    // New users will need to register
+    reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
+    if err != nil {
+        log.Println(err)
+        return errors.Wrap(err, "issue cert register fail")
+    }
+    myUser.Registration = reg
+
+    request := certificate.ObtainRequest{
+        Domains: []string{domain},
+        Bundle:  true,
+    }
+    certificates, err := client.Certificate.Obtain(request)
+    if err != nil {
+        return errors.Wrap(err, "issue cert fail to obtain")
+    }
+    saveDir := nginx.GetNginxConfPath("ssl/" + domain)
+    if _, err = os.Stat(saveDir); os.IsNotExist(err) {
+        err = os.Mkdir(saveDir, 0755)
+        if err != nil {
+            return errors.Wrap(err, "issue cert fail to create")
+        }
+    }
+
+    // Each certificate comes back with the cert bytes, the bytes of the client's
+    // private key, and a certificate URL. SAVE THESE TO DISK.
+    err = ioutil.WriteFile(filepath.Join(saveDir, "fullchain.cer"),
+        certificates.Certificate, 0644)
+
+    if err != nil {
+        log.Println(err)
+        return errors.Wrap(err, "issue cert write fullchain.cer fail")
+    }
+
+    err = ioutil.WriteFile(filepath.Join(saveDir, domain+".key"),
+        certificates.PrivateKey, 0644)
+
+    if err != nil {
+        log.Println(err)
+        return errors.Wrap(err, "issue cert write key fail")
+    }
+
+    nginx.ReloadNginx()
+
+    return nil
 }

+ 1 - 1
server/tool/nginx/build_config.go

@@ -50,7 +50,7 @@ func (c *NgxConfig) BuildConfig() (content string) {
 			}
 			if directive.Directive == If {
 				server += fmt.Sprintf("%s%s\n", comments, fmtCodeWithIndent(directive.Params, 1))
-			} else {
+			} else if directive.Params != "" {
 				server += fmt.Sprintf("%s\t%s;\n", comments, directive.Orig())
 			}
 		}

+ 14 - 8
server/tool/nginx/parse.go

@@ -105,15 +105,9 @@ func parseDirective(scanner *bufio.Scanner) (d NgxDirective) {
 	return
 }
 
-func ParseNgxConfig(filename string) (c *NgxConfig, err error) {
-	file, err := os.Open(filename)
-	if err != nil {
-		return nil, errors.Wrap(err, "error open file in ParseNgxConfig")
-	}
-	defer file.Close()
-
-	scanner := bufio.NewScanner(file)
+func ParseNgxConfigByScanner(filename string, scanner *bufio.Scanner) (c *NgxConfig, err error) {
 	c = NewNgxConfig(filename)
+
 	for scanner.Scan() {
 		d := parseDirective(scanner)
 		paramsScanner := bufio.NewScanner(strings.NewReader(d.Params))
@@ -142,3 +136,15 @@ func ParseNgxConfig(filename string) (c *NgxConfig, err error) {
 
 	return c, nil
 }
+
+func ParseNgxConfig(filename string) (c *NgxConfig, err error) {
+	file, err := os.Open(filename)
+	if err != nil {
+		return nil, errors.Wrap(err, "error open file in ParseNgxConfig")
+	}
+	defer file.Close()
+
+	scanner := bufio.NewScanner(file)
+
+	return ParseNgxConfigByScanner(filename, scanner)
+}

+ 0 - 2
server/tool/nginx/type.go

@@ -36,8 +36,6 @@ type NgxDirective struct {
 	Comments  string `json:"comments"`
 }
 
-type NgxDirectives map[string][]NgxDirective
-
 type NgxLocation struct {
 	Path     string `json:"path"`
 	Content  string `json:"content"`