Refactored nginx configuration editor

This commit is contained in:
0xJacky 2022-07-28 13:59:39 +08:00
parent f526cd0ade
commit b19ecdda9c
31 changed files with 1476 additions and 956 deletions

View file

@ -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) {

View file

@ -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
frontend/src/api/ngx.js Normal file
View file

@ -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

View file

@ -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) {

View file

@ -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: {

View file

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

View file

@ -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>
<std-data-entry :data-list="columnsSSL" :data-source="config" :error="error" />
<a-space style="margin-right: 10px">
<a-button
v-if="current_step===1"
@click="current_step++"
>
<translate>Skip</translate>
</a-button>
</a-space>
<template slot="description">
<span v-translate>
server_name parameter is required
</span>
</template>
</a-alert>
<br/>
</template>
<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"
/>
</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,53 +71,43 @@
</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.$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'))
this.$api.domain.enable(this.config.name).then(() => {
@ -105,61 +118,92 @@ export default {
})
}).catch(r => {
this.$message.error($interpolate($gettext('Save error %{msg}'), {msg: r.message ?? ""}), 10)
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.current_step++
}).catch(r => {
this.$message.error($interpolate($gettext('Save error %{msg}'), {msg: r.message ?? ""}), 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
},
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 {
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}))
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 {
// 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
}
}
}

View file

@ -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()
on_mode_change(advance_mode) {
if (advance_mode) {
this.build_config()
} 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
})
} 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;
}
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>

View file

@ -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>

View file

@ -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;

View file

@ -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>

View file

@ -1,4 +1,4 @@
import $gettext from "@/lib/translate/gettext";
import $gettext from '@/lib/translate/gettext'
const columns = [{
title: $gettext('Configuration Name'),
@ -12,6 +12,13 @@ const columns = [{
edit: {
type: 'input'
}
}, {
title: $gettext('HTTP Listen Port'),
dataIndex: 'http_listen_port',
edit: {
type: 'number',
min: 80
}
}, {
title: $gettext('Root Directory (root)'),
dataIndex: 'root',
@ -24,23 +31,17 @@ const columns = [{
edit: {
type: 'input'
}
}, {
title: $gettext('HTTP Listen Port'),
dataIndex: 'http_listen_port',
edit: {
type: 'number',
min: 80
}
}, {
}]
const columnsSSL = [
{
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: {
@ -50,25 +51,26 @@ const columnsSSL = [{
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}

View file

@ -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}

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

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

View file

@ -1,8 +1,6 @@
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"
@ -48,31 +46,22 @@ func IssueCert(c *gin.Context) {
}
}(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)
log.Println(err)
return
}
if mt == websocket.TextMessage && string(message) == "go" {
err = tool.IssueCert(domain)
if err != nil {
log.Println(err)
m, err = json.Marshal(gin.H{
err = ws.WriteJSON(gin.H{
"status": "error",
"message": err.Error(),
})
@ -82,13 +71,6 @@ func IssueCert(c *gin.Context) {
return
}
err = ws.WriteMessage(mt, m)
if err != nil {
log.Println(err)
return
}
return
}
@ -101,7 +83,8 @@ func IssueCert(c *gin.Context) {
}
log.Println("[found]", "fullchain.cer")
m, err = json.Marshal(gin.H{
err = ws.WriteJSON(gin.H{
"status": "success",
"message": "[found] fullchain.cer",
})
@ -111,13 +94,6 @@ func IssueCert(c *gin.Context) {
return
}
err = ws.WriteMessage(mt, m)
if err != nil {
log.Println(err)
return
}
sslCertificateKeyPath := nginx.GetNginxConfPath("ssl/" + domain + "/" + domain + ".key")
_, err = os.Stat(sslCertificateKeyPath)
@ -127,38 +103,26 @@ func IssueCert(c *gin.Context) {
}
log.Println("[found]", "cert key")
m, err = json.Marshal(gin.H{
err = ws.WriteJSON(gin.H{
"status": "success",
"message": "[found] cert key",
"message": "[found] Certificate Key",
})
if err != nil {
log.Println(err)
return
}
err = ws.WriteMessage(mt, m)
if err != nil {
log.Println(err)
}
log.Println("申请成功")
m, err = json.Marshal(gin.H{
err = ws.WriteJSON(gin.H{
"status": "success",
"message": "申请成功",
"message": "Issued certificate successfully",
"ssl_certificate": sslCertificatePath,
"ssl_certificate_key": sslCertificateKeyPath,
})
if err != nil {
log.Println(err)
}
err = ws.WriteMessage(mt, m)
if err != nil {
log.Println(err)
}
return
}
}
}

View file

@ -85,6 +85,7 @@ func GetDomain(c *gin.Context) {
"name": name,
"config": config.BuildConfig(),
"tokenized": config,
"auto_cert": err == nil,
})
}
@ -104,7 +105,7 @@ func EditDomain(c *gin.Context) {
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{
@ -137,12 +138,14 @@ func EnableDomain(c *gin.Context) {
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()

42
server/api/ngx.go Normal file
View file

@ -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)
}

View file

@ -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,
"template": ngxConfig.BuildConfig(),
"tokenized": ngxConfig,
})
}

View file

@ -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

View file

@ -49,18 +49,26 @@ func InitRouter() *gin.Engine {
g := g.Group("/", authRequired())
{
g.GET("/analytic", api.Analytic)
g.GET("/analytic/init", api.GetAnalyticInit)
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("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)
@ -70,17 +78,17 @@ func InitRouter() *gin.Engine {
g.POST("config", api.AddConfig)
g.POST("config/:name", api.EditConfig)
g.GET("backups", api.GetFileBackupList)
g.GET("backup/:id", api.GetFileBackup)
//g.GET("backups", api.GetFileBackupList)
//g.GET("backup/:id", api.GetFileBackup)
g.GET("template/:name", api.GetTemplate)
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

View file

@ -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 }};
}
}

View file

@ -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 }};
}
}

View file

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

View file

@ -123,6 +123,7 @@ func IssueCert(domain string) error {
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.
@ -157,7 +158,7 @@ func IssueCert(domain string) error {
return errors.Wrap(err, "issue cert fail to obtain")
}
saveDir := nginx.GetNginxConfPath("ssl/" + domain)
if _, err := os.Stat(saveDir); os.IsNotExist(err) {
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")
@ -168,12 +169,15 @@ func IssueCert(domain string) error {
// 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")

View file

@ -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())
}
}

View file

@ -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)
}

View file

@ -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"`