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,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.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.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)
})
}).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
}
}
}

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

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,51 +31,46 @@ const columns = [{
edit: {
type: 'input'
}
}, {
title: $gettext('HTTP Listen Port'),
dataIndex: 'http_listen_port',
edit: {
type: 'number',
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
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'
}
}
}, {
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,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
},
}
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
}
// 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)
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
// read
mt, message, err := ws.ReadMessage()
if err != nil {
log.Println(err)
return
}
if settings.ServerSettings.Demo {
m, _ = json.Marshal(gin.H{
"status": "error",
"message": "this feature is not available in demo",
})
_ = ws.WriteMessage(mt, m)
return
}
if mt == websocket.TextMessage && string(message) == "go" {
err = tool.IssueCert(domain)
err = tool.IssueCert(domain)
if err != nil {
if err != nil {
log.Println(err)
log.Println(err)
m, err = json.Marshal(gin.H{
"status": "error",
"message": err.Error(),
})
err = ws.WriteJSON(gin.H{
"status": "error",
"message": err.Error(),
})
if err != nil {
log.Println(err)
return
}
if err != nil {
log.Println(err)
return
}
err = ws.WriteMessage(mt, m)
return
}
if err != nil {
log.Println(err)
return
}
sslCertificatePath := nginx.GetNginxConfPath("ssl/" + domain + "/fullchain.cer")
_, err = os.Stat(sslCertificatePath)
return
}
if err != nil {
log.Println(err)
return
}
sslCertificatePath := nginx.GetNginxConfPath("ssl/" + domain + "/fullchain.cer")
_, err = os.Stat(sslCertificatePath)
log.Println("[found]", "fullchain.cer")
if err != nil {
log.Println(err)
return
}
err = ws.WriteJSON(gin.H{
"status": "success",
"message": "[found] fullchain.cer",
})
log.Println("[found]", "fullchain.cer")
m, err = json.Marshal(gin.H{
"status": "success",
"message": "[found] fullchain.cer",
})
if err != nil {
log.Println(err)
return
}
if err != nil {
log.Println(err)
return
}
sslCertificateKeyPath := nginx.GetNginxConfPath("ssl/" + domain + "/" + domain + ".key")
_, err = os.Stat(sslCertificateKeyPath)
err = ws.WriteMessage(mt, m)
if err != nil {
log.Println(err)
return
}
if err != nil {
log.Println(err)
return
}
log.Println("[found]", "cert key")
err = ws.WriteJSON(gin.H{
"status": "success",
"message": "[found] Certificate Key",
})
sslCertificateKeyPath := nginx.GetNginxConfPath("ssl/" + domain + "/" + domain + ".key")
_, err = os.Stat(sslCertificateKeyPath)
if err != nil {
log.Println(err)
return
}
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,
})
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)
}
}
}
if err != nil {
log.Println(err)
return
}
}
}

View file

@ -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")
orderBy := c.Query("order_by")
sort := c.DefaultQuery("sort", "desc")
mySort := map[string]string{
"enabled": "bool",
"name": "string",
"modify": "time",
}
mySort := map[string]string{
"enabled": "bool",
"name": "string",
"modify": "time",
}
configFiles, err := ioutil.ReadDir(nginx.GetNginxConfPath("sites-available"))
configFiles, err := ioutil.ReadDir(nginx.GetNginxConfPath("sites-available"))
if err != nil {
ErrHandler(c, err)
return
}
if err != nil {
ErrHandler(c, err)
return
}
enabledConfig, err := ioutil.ReadDir(filepath.Join(nginx.GetNginxConfPath("sites-enabled")))
enabledConfig, err := ioutil.ReadDir(filepath.Join(nginx.GetNginxConfPath("sites-enabled")))
if err != nil {
ErrHandler(c, err)
return
}
if err != nil {
ErrHandler(c, err)
return
}
enabledConfigMap := make(map[string]bool)
for i := range enabledConfig {
enabledConfigMap[enabledConfig[i].Name()] = true
}
enabledConfigMap := make(map[string]bool)
for i := range enabledConfig {
enabledConfigMap[enabledConfig[i].Name()] = true
}
var configs []gin.H
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()],
})
}
}
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)
configs = tool.Sort(orderBy, sort, mySort[orderBy], configs)
c.JSON(http.StatusOK, gin.H{
"configs": 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)
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
}
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
}
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()
output := nginx.ReloadNginx()
if output != "" && strings.Contains(output, "error") {
c.JSON(http.StatusInternalServerError, gin.H{
"message": output,
})
return
}
}
if output != "" && strings.Contains(output, "error") {
c.JSON(http.StatusInternalServerError, gin.H{
"message": output,
})
return
}
}
GetDomain(c)
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"))
configFilePath := filepath.Join(nginx.GetNginxConfPath("sites-available"), c.Param("name"))
enabledConfigFilePath := filepath.Join(nginx.GetNginxConfPath("sites-enabled"), c.Param("name"))
_, err := os.Stat(configFilePath)
_, err := os.Stat(configFilePath)
if err != nil {
ErrHandler(c, err)
return
}
if err != nil {
ErrHandler(c, err)
return
}
err = os.Symlink(configFilePath, enabledConfigFilePath)
if _, err = os.Stat(enabledConfigFilePath); os.IsNotExist(err) {
err = os.Symlink(configFilePath, enabledConfigFilePath)
if err != nil {
ErrHandler(c, err)
return
}
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
}
// 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()
output := nginx.ReloadNginx()
if output != "" && strings.Contains(output, "error") {
c.JSON(http.StatusInternalServerError, gin.H{
"message": output,
})
return
}
if output != "" && strings.Contains(output, "error") {
c.JSON(http.StatusInternalServerError, gin.H{
"message": output,
})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "ok",
})
c.JSON(http.StatusOK, gin.H{
"message": "ok",
})
}
func DisableDomain(c *gin.Context) {
enabledConfigFilePath := filepath.Join(nginx.GetNginxConfPath("sites-enabled"), c.Param("name"))
enabledConfigFilePath := filepath.Join(nginx.GetNginxConfPath("sites-enabled"), c.Param("name"))
_, err := os.Stat(enabledConfigFilePath)
_, err := os.Stat(enabledConfigFilePath)
if err != nil {
ErrHandler(c, err)
return
}
if err != nil {
ErrHandler(c, err)
return
}
err = os.Remove(enabledConfigFilePath)
err = os.Remove(enabledConfigFilePath)
if err != nil {
ErrHandler(c, err)
return
}
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
}
// delete auto cert record
cert := model.Cert{Domain: c.Param("name")}
err = cert.Remove()
if err != nil {
ErrHandler(c, err)
return
}
output := nginx.ReloadNginx()
output := nginx.ReloadNginx()
if output != "" {
c.JSON(http.StatusInternalServerError, gin.H{
"message": output,
})
return
}
if output != "" {
c.JSON(http.StatusInternalServerError, gin.H{
"message": output,
})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "ok",
})
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)
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(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
}
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()
cert := model.Cert{Domain: name}
_ = cert.Remove()
err = os.Remove(availablePath)
err = os.Remove(availablePath)
if err != nil {
ErrHandler(c, err)
return
}
if err != nil {
ErrHandler(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
"message": "ok",
})
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()
cert := model.Cert{
Domain: c.Param("domain"),
}
err := cert.Remove()
if err != nil {
ErrHandler(c, err)
return
}
c.JSON(http.StatusOK, nil)
if err != nil {
ErrHandler(c, err)
return
}
c.JSON(http.StatusOK, nil)
}

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,
"message": "ok",
"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

@ -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 := gin.New()
r.Use(gin.Logger())
r.Use(recovery())
r.Use(recovery())
r.Use(cacheJs())
r.Use(cacheJs())
r.Use(static.Serve("/", mustFS("")))
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
}
})
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 := r.Group("/api")
{
g.GET("settings", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"demo": settings.ServerSettings.Demo,
})
})
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.GET("install", api.InstallLockCheck)
g.POST("install", api.InstallNginxUI)
g.POST("/login", api.Login)
g.DELETE("/logout", api.Logout)
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 := 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("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("domains", api.GetDomains)
g.GET("domain/:name", api.GetDomain)
g.GET("configs", api.GetConfigs)
g.GET("config/:name", api.GetConfig)
g.POST("config", api.AddConfig)
g.POST("config/:name", api.EditConfig)
// Modify site configuration directly
g.POST("domain/:name", api.EditDomain)
g.GET("backups", api.GetFileBackupList)
g.GET("backup/:id", api.GetFileBackup)
// 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.GET("template/:name", api.GetTemplate)
g.POST("domain/:name/enable", api.EnableDomain)
g.POST("domain/:name/disable", api.DisableDomain)
g.DELETE("domain/:name", api.DeleteDomain)
g.GET("cert/issue/:domain", api.IssueCert)
g.GET("cert/:domain/info", api.CertInfo)
g.GET("configs", api.GetConfigs)
g.GET("config/:name", api.GetConfig)
g.POST("config", api.AddConfig)
g.POST("config/:name", api.EditConfig)
// 添加域名到自动续期列表
g.POST("cert/:domain", api.AddDomainToAutoCert)
// 从自动续期列表中删除域名
g.DELETE("cert/:domain", api.RemoveDomainFromAutoCert)
//g.GET("backups", api.GetFileBackupList)
//g.GET("backup/:id", api.GetFileBackup)
// pty
g.GET("pty", api.Pty)
}
}
g.GET("template", api.GetTemplate)
return r
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
}

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

@ -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
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,
}
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)
response, err = client.Get("https://" + domain)
if err != nil {
err = errors.Wrap(err, "get cert info error")
return
}
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)
defer func(Body io.ReadCloser) {
err = Body.Close()
if err != nil {
log.Println(err)
return
}
}(response.Body)
key = response.TLS.PeerCertificates[0]
key = response.TLS.PeerCertificates[0]
return
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")
}
// 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,
}
myUser := MyUser{
Email: settings.ServerSettings.Email,
key: privateKey,
}
config := lego.NewConfig(&myUser)
config := lego.NewConfig(&myUser)
if settings.ServerSettings.Demo {
config.CADirURL = "https://acme-staging-v02.api.letsencrypt.org/directory"
}
config.Certificate.KeyType = certcrypto.RSA2048
if settings.ServerSettings.Demo {
config.CADirURL = "https://acme-staging-v02.api.letsencrypt.org/directory"
}
// 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")
}
config.Certificate.KeyType = certcrypto.RSA2048
err = client.Challenge.SetHTTP01Provider(
http01.NewProviderServer("",
settings.ServerSettings.HTTPChallengePort,
),
)
if err != nil {
return errors.Wrap(err, "issue cert challenge fail")
}
// 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")
}
// 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
err = client.Challenge.SetHTTP01Provider(
http01.NewProviderServer("",
settings.ServerSettings.HTTPChallengePort,
),
)
if err != nil {
return errors.Wrap(err, "issue cert challenge fail")
}
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")
}
}
// 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
// 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")
}
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")
}
}
nginx.ReloadNginx()
// 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)
return nil
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
}

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