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') return http.post(base_url + '/' + name + '/disable')
}, },
get_template(name) { get_template() {
return http.get('template/' + name) return http.get('template')
}, },
cert_info(domain) { cert_info(domain) {

View file

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

View file

@ -1,5 +1,5 @@
<template> <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> </template>
<style lang="less"> <style lang="less">
.cm-s-monokai { .cm-s-monokai {
@ -20,6 +20,10 @@ export default {
}, },
props: { props: {
value: {}, value: {},
defaultTextHeight: {
type: Number,
default: 1000
}
}, },
model: { model: {
prop: 'value', prop: 'value',
@ -36,16 +40,6 @@ export default {
data() { data() {
return { return {
current_value: this.value ?? '', 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: { methods: {

View file

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

View file

@ -2,39 +2,62 @@
<a-card :title="$gettext('Add Site')"> <a-card :title="$gettext('Add Site')">
<div class="domain-add-container"> <div class="domain-add-container">
<a-steps :current="current_step" size="small"> <a-steps :current="current_step" size="small">
<a-step :title="$gettext('Base information')" /> <a-step :title="$gettext('Base information')"/>
<a-step :title="$gettext('Configure SSL')" /> <a-step :title="$gettext('Configure SSL')"/>
<a-step :title="$gettext('Finished')" /> <a-step :title="$gettext('Finished')"/>
</a-steps> </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"> <directive-editor :ngx_directives="ngx_config.servers[0].directives"/>
<a-button
@click="issue_cert" <location-editor :locations="ngx_config.servers[0].locations"/>
type="primary" ghost
style="margin: 10px 0" <a-alert
:disabled="is_demo" v-if="!has_server_name"
:loading="issuing_cert" :message="$gettext('Warning')"
type="warning"
show-icon
> >
<translate>Getting Certificate from Let's Encrypt</translate> <template slot="description">
</a-button> <span v-translate>
<p v-if="is_demo" v-translate>This feature is not available in demo.</p> server_name parameter is required
</span>
<std-data-entry :data-list="columnsSSL" :data-source="config" :error="error" /> </template>
</a-alert>
<a-space style="margin-right: 10px"> <br/>
<a-button
v-if="current_step===1"
@click="current_step++"
>
<translate>Skip</translate>
</a-button>
</a-space>
</template> </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 <a-result
v-if="current_step===2" v-else-if="current_step===2"
status="success" status="success"
:title="$gettext('Domain Config Created Successfully')" :title="$gettext('Domain Config Created Successfully')"
> >
@ -48,118 +71,139 @@
</template> </template>
</a-result> </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> </div>
</a-card> </a-card>
</template> </template>
<script> <script>
import StdDataEntry from '@/components/StdDataEntry/StdDataEntry' import DirectiveEditor from '@/views/domain/ngx_conf/directive/DirectiveEditor'
import {columns, columnsSSL} from '@/views/domain/columns' import LocationEditor from '@/views/domain/ngx_conf/LocationEditor'
import {unparse, issue_cert} from '@/views/domain/methods' import $gettext, {$interpolate} from '@/lib/translate/gettext'
import $gettext, {$interpolate} from "@/lib/translate/gettext" import NgxConfigEditor from '@/views/domain/ngx_conf/NgxConfigEditor'
export default { export default {
name: 'DomainAdd', name: 'DomainAdd',
components: {StdDataEntry}, components: {NgxConfigEditor, LocationEditor, DirectiveEditor},
data() { data() {
return { return {
config: { config: {},
http_listen_port: 80, ngx_config: {
https_listen_port: 443 servers: [{}]
}, },
columns: columns.slice(0, -1), // SSL
error: {}, error: {},
current_step: 0, current_step: 0,
columnsSSL, enabled: true,
issuing_cert: false auto_cert: false
} }
}, },
watch: { created() {
'config.auto_cert'() { this.init()
this.change_auto_cert()
}
}, },
methods: { methods: {
init() {
this.$api.domain.get_template().then(r => {
this.ngx_config = r.tokenized
})
},
save() { save() {
if (this.current_step===0) { this.$api.ngx.build_config(this.ngx_config).then(r => {
this.$api.domain.get_template('http-conf').then(r => { this.$api.domain.save(this.config.name, {content: r.content, enabled: true}).then(() => {
let text = unparse(r.template, this.config) this.$message.success($gettext('Saved successfully'))
this.$api.domain.save(this.config.name, {content: text, enabled: true}).then(() => { this.$api.domain.enable(this.config.name).then(() => {
this.$message.success($gettext('Saved successfully')) this.$message.success($gettext('Enabled 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.current_step++ this.current_step++
}).catch(r => { }).catch(r => {
this.$message.error($interpolate($gettext('Save error %{msg}'), {msg: r.message ?? ""}), 10) this.$message.error(r.message ?? $gettext('Enable failed'), 10)
}) })
})
}
}, }).catch(r => {
issue_cert() { this.$message.error($interpolate($gettext('Save error %{msg}'), {msg: r.message ?? ''}), 10)
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() { goto_modify() {
this.$router.push('/domain/'+this.config.name) this.$router.push('/domain/' + this.config.name)
}, },
create_another() { create_another() {
this.current_step = 0 this.current_step = 0
this.config = { this.config = {}
http_listen_port: 80, this.ngx_config = {
https_listen_port: 443 servers: [{}]
} }
}, },
change_auto_cert() { change_tls(r) {
if (this.config.auto_cert) { if (r) {
this.$api.domain.add_auto_cert(this.config.name).then(() => { // deep copy servers[0] to servers[1]
this.$message.success($interpolate($gettext('Auto-renewal enabled for %{name}'), {name: this.config.name})) const server = JSON.parse(JSON.stringify(this.ngx_config.servers[0]))
}).catch(e => {
this.$message.error(e.message ?? $interpolate($gettext('Enable auto-renewal failed for %{name}'), {name: this.config.name})) 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 { } else {
this.$api.domain.remove_auto_cert(this.config.name).then(() => { // remove servers[1]
this.$message.success($interpolate($gettext('Auto-renewal disabled for %{name}'), {name: this.config.name})) this.$refs.ngx_config.current_server_index = 0
}).catch(e => { if (this.ngx_config.servers.length === 2) {
this.$message.error(e.message ?? $interpolate($gettext('Disable auto-renewal failed for %{name}'), {name: this.config.name})) this.ngx_config.servers.splice(1, 1)
}) }
} }
} }
}, },
computed: { computed: {
is_demo() { has_server_name() {
return this.$store.getters.env.demo === true 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> </a-tag>
</template> </template>
<template v-slot:extra> <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"> <template v-if="advance_mode">
{{ $gettext('Advance') }} {{ $gettext('Advance Mode') }}
</template> </template>
<template v-else> <template v-else>
{{ $gettext('Basic') }} {{ $gettext('Basic Mode') }}
</template> </template>
</template> </template>
@ -29,22 +29,13 @@
<a-form-item :label="$gettext('Enabled')"> <a-form-item :label="$gettext('Enabled')">
<a-switch v-model="enabled" @change="checked=>{checked?enable():disable()}"/> <a-switch v-model="enabled" @change="checked=>{checked?enable():disable()}"/>
</a-form-item> </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"/> <ngx-config-editor
<template v-if="config.support_ssl"> ref="ngx_config"
<cert-info :domain="name" ref="cert-info" v-if="name"/> :ngx_config="ngx_config"
<a-button v-model="auto_cert"
@click="issue_cert" :enabled="enabled"
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>
</div> </div>
</transition> </transition>
@ -55,7 +46,7 @@
<a-button @click="$router.go(-1)"> <a-button @click="$router.go(-1)">
<translate>Back</translate> <translate>Back</translate>
</a-button> </a-button>
<a-button type="primary" @click="save"> <a-button type="primary" @click="save" :loading="saving">
<translate>Save</translate> <translate>Save</translate>
</a-button> </a-button>
</a-space> </a-space>
@ -65,60 +56,39 @@
<script> <script>
import StdDataEntry from '@/components/StdDataEntry/StdDataEntry'
import FooterToolBar from '@/components/FooterToolbar/FooterToolBar' import FooterToolBar from '@/components/FooterToolbar/FooterToolBar'
import VueItextarea from '@/components/VueItextarea/VueItextarea' 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 {$gettext, $interpolate} from '@/lib/translate/gettext'
import NgxConfigEditor from '@/views/domain/ngx_conf/NgxConfigEditor'
export default { export default {
name: 'DomainEdit', name: 'DomainEdit',
components: {CertInfo, FooterToolBar, StdDataEntry, VueItextarea}, components: {NgxConfigEditor, FooterToolBar, VueItextarea},
data() { data() {
return { return {
name: this.$route.params.name.toString(), name: this.$route.params.name.toString(),
config: { update: 0,
http_listen_port: 80, ngx_config: {
https_listen_port: null, filename: '',
server_name: '', upstreams: [],
index: '', servers: []
root: '',
ssl_certificate: '',
ssl_certificate_key: '',
support_ssl: false,
auto_cert: false
}, },
auto_cert: false,
current_server_index: 0,
enabled: false, enabled: false,
configText: '', configText: '',
ws: null, ws: null,
ok: false, ok: false,
issuing_cert: false, issuing_cert: false,
advance_mode: false, advance_mode: false,
saving: false
} }
}, },
watch: { watch: {
'$route'() { '$route'() {
this.init() 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() { created() {
this.init() this.init()
@ -133,106 +103,53 @@ export default {
if (this.name) { if (this.name) {
this.$api.domain.get(this.name).then(r => { this.$api.domain.get(this.name).then(r => {
this.configText = r.config this.configText = r.config
this.config.auto_cert = r.auto_cert
this.enabled = r.enabled this.enabled = r.enabled
this.parse(r).then(() => { this.ngx_config = r.tokenized
this.ok = true this.auto_cert = r.auto_cert
})
}).catch(r => { }).catch(r => {
console.log(r) this.$message.error(r.message ?? $gettext('Server error'))
this.$message.error($gettext('Server error'))
}) })
} }
}, },
async parse(r) { on_mode_change(advance_mode) {
const text = r.config if (advance_mode) {
const reg = { this.build_config()
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
})
} else { } else {
await this.$api.domain.get_template('http-conf').then(r => { return this.$api.ngx.tokenize_config(this.configText).then(r => {
this.configText = r.template this.ngx_config = r
}).catch(r => {
this.$message.error(r.message ?? $gettext('Server error'))
}) })
} }
await this.unparse()
}, },
change_support_ssl() { build_config() {
const that = this return this.$api.ngx.build_config(this.ngx_config).then(r => {
this.$confirm({ this.configText = r.content
title: $gettext('Do you want to change the template to support the TLS?'), }).catch(r => {
content: $gettext('This operation will lose the custom configuration.'), this.$message.error(r.message ?? $gettext('Server error'))
onOk() {
that.get_template()
},
onCancel() {
},
}) })
}, },
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.$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')) 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 => { }).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)
}).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() { enable() {
this.$api.domain.enable(this.name).then(() => { this.$api.domain.enable(this.name).then(() => {
@ -252,15 +169,6 @@ export default {
} }
}, },
computed: { computed: {
columns: {
get() {
if (this.config.support_ssl) {
return [...columns, ...columnsSSL]
} else {
return [...columns]
}
}
},
is_demo() { is_demo() {
return this.$store.getters.env.demo === true return this.$store.getters.env.demo === true
} }
@ -274,16 +182,15 @@ export default {
<style lang="less" scoped> <style lang="less" scoped>
.ant-card { .ant-card {
// margin: 10px; margin: 10px 0;
@media (max-width: 512px) { box-shadow: unset;
margin: 10px 0;
}
} }
.domain-edit-container { .domain-edit-container {
max-width: 800px; max-width: 800px;
margin: 0 auto; margin: 0 auto;
/deep/.ant-form-item-label > label::after {
/deep/ .ant-form-item-label > label::after {
content: none; content: none;
} }
} }
@ -291,12 +198,26 @@ export default {
.slide-fade-enter-active { .slide-fade-enter-active {
transition: all .5s ease-in-out; transition: all .5s ease-in-out;
} }
.slide-fade-leave-active { .slide-fade-leave-active {
transition: all .5s cubic-bezier(1.0, 0.5, 0.8, 1.0); transition: all .5s cubic-bezier(1.0, 0.5, 0.8, 1.0);
} }
.slide-fade-enter, .slide-fade-leave-to .slide-fade-enter, .slide-fade-leave-to
/* .slide-fade-leave-active for below version 2.1.8 */ { /* .slide-fade-leave-active for below version 2.1.8 */ {
transform: translateX(10px); transform: translateX(10px);
opacity: 0; opacity: 0;
} }
.location-block {
}
.directive-params-wrapper {
margin: 10px 0;
}
.tab-content {
padding: 10px;
}
</style> </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> <template>
<div v-if="ok"> <div class="cert-info" v-if="ok">
<h3 v-translate>Certificate Status</h3> <h4 v-translate>Certificate Status</h4>
<p v-translate="{issuer: cert.issuer_name}">Intermediate Certification Authorities: %{issuer}</p> <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="{name: cert.subject_name}">Subject Name: %{name}</p>
<p v-translate="{date: moment(cert.not_after).format('YYYY-MM-DD HH:mm:ss').toString()}"> <p v-translate="{date: moment(cert.not_after).format('YYYY-MM-DD HH:mm:ss').toString()}">
@ -57,6 +57,14 @@ export default {
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
h4 {
padding-bottom: 10px;
}
.cert-info {
padding-bottom: 10px;
}
.status { .status {
span { span {
margin-left: 10px; 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 = [{ const columns = [{
title: $gettext('Configuration Name'), title: $gettext('Configuration Name'),
@ -12,6 +12,13 @@ const columns = [{
edit: { edit: {
type: 'input' type: 'input'
} }
}, {
title: $gettext('HTTP Listen Port'),
dataIndex: 'http_listen_port',
edit: {
type: 'number',
min: 80
}
}, { }, {
title: $gettext('Root Directory (root)'), title: $gettext('Root Directory (root)'),
dataIndex: 'root', dataIndex: 'root',
@ -24,51 +31,46 @@ const columns = [{
edit: { edit: {
type: 'input' 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 = [{ const columnsSSL = [
title: $gettext('Certificate Auto-renewal'), {
dataIndex: 'auto_cert', title: $gettext('Enable TLS'),
edit: { dataIndex: 'support_ssl',
type: 'switch', edit: {
event: 'change_auto_cert' type: 'switch',
}, event: 'change_support_ssl'
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('Certificate Auto-renewal'),
}, { dataIndex: 'auto_cert',
title: $gettext('HTTPS Listen Port'), edit: {
dataIndex: 'https_listen_port', type: 'switch',
edit: { event: 'change_auto_cert'
type: 'number', },
min: 443 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} export {columns, columnsSSL}

View file

@ -1,36 +1,8 @@
import $gettext from '@/lib/translate/gettext' import $gettext from '@/lib/translate/gettext'
import store from '@/lib/store' import store from '@/lib/store'
import Vue from 'vue' 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) => { 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) Vue.prototype.$message.info($gettext('Getting the certificate, please wait...'), 15)
const ws = new WebSocket(Vue.prototype.getWebSocketRoot() + '/cert/issue/' + server_name const ws = new WebSocket(Vue.prototype.getWebSocketRoot() + '/cert/issue/' + server_name
+ '?token=' + btoa(store.state.user.token)) + '?token=' + btoa(store.state.user.token))
@ -57,6 +29,9 @@ const issue_cert = (server_name, callback) => {
callback(r.ssl_certificate, r.ssl_certificate_key) 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 package api
import ( import (
"encoding/json" "github.com/0xJacky/Nginx-UI/server/tool"
"github.com/0xJacky/Nginx-UI/server/settings" "github.com/0xJacky/Nginx-UI/server/tool/nginx"
"github.com/0xJacky/Nginx-UI/server/tool" "github.com/gin-gonic/gin"
"github.com/0xJacky/Nginx-UI/server/tool/nginx" "github.com/gorilla/websocket"
"github.com/gin-gonic/gin" "log"
"github.com/gorilla/websocket" "net/http"
"log" "os"
"net/http"
"os"
) )
func CertInfo(c *gin.Context) { 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{ c.JSON(http.StatusOK, gin.H{
"error": err, "error": err,
"subject_name": key.Subject.CommonName, "subject_name": key.Subject.CommonName,
"issuer_name": key.Issuer.CommonName, "issuer_name": key.Issuer.CommonName,
"not_after": key.NotAfter, "not_after": key.NotAfter,
"not_before": key.NotBefore, "not_before": key.NotBefore,
}) })
} }
func IssueCert(c *gin.Context) { func IssueCert(c *gin.Context) {
domain := c.Param("domain") domain := c.Param("domain")
var upGrader = websocket.Upgrader{ var upGrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { CheckOrigin: func(r *http.Request) bool {
return true return true
}, },
} }
// upgrade http to websocket // upgrade http to websocket
ws, err := upGrader.Upgrade(c.Writer, c.Request, nil) ws, err := upGrader.Upgrade(c.Writer, c.Request, nil)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
return return
} }
defer func(ws *websocket.Conn) { defer func(ws *websocket.Conn) {
err := ws.Close() err := ws.Close()
if err != nil { if err != nil {
log.Println("defer websocket close err", err) log.Println("defer websocket close err", err)
} }
}(ws) }(ws)
for { // read
// read mt, message, err := ws.ReadMessage()
mt, message, err := ws.ReadMessage() if err != nil {
if err != nil { log.Println(err)
break return
} }
if string(message) == "go" {
var m []byte
if settings.ServerSettings.Demo { if mt == websocket.TextMessage && string(message) == "go" {
m, _ = json.Marshal(gin.H{
"status": "error",
"message": "this feature is not available in demo",
})
_ = ws.WriteMessage(mt, m)
return
}
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{ err = ws.WriteJSON(gin.H{
"status": "error", "status": "error",
"message": err.Error(), "message": err.Error(),
}) })
if err != nil { if err != nil {
log.Println(err) log.Println(err)
return return
} }
err = ws.WriteMessage(mt, m) return
}
if err != nil { sslCertificatePath := nginx.GetNginxConfPath("ssl/" + domain + "/fullchain.cer")
log.Println(err) _, err = os.Stat(sslCertificatePath)
return
}
return if err != nil {
} log.Println(err)
return
}
sslCertificatePath := nginx.GetNginxConfPath("ssl/" + domain + "/fullchain.cer") log.Println("[found]", "fullchain.cer")
_, err = os.Stat(sslCertificatePath)
if err != nil { err = ws.WriteJSON(gin.H{
log.Println(err) "status": "success",
return "message": "[found] fullchain.cer",
} })
log.Println("[found]", "fullchain.cer") if err != nil {
m, err = json.Marshal(gin.H{ log.Println(err)
"status": "success", return
"message": "[found] fullchain.cer", }
})
if err != nil { sslCertificateKeyPath := nginx.GetNginxConfPath("ssl/" + domain + "/" + domain + ".key")
log.Println(err) _, err = os.Stat(sslCertificateKeyPath)
return
}
err = ws.WriteMessage(mt, m) if err != nil {
log.Println(err)
return
}
if err != nil { log.Println("[found]", "cert key")
log.Println(err) err = ws.WriteJSON(gin.H{
return "status": "success",
} "message": "[found] Certificate Key",
})
sslCertificateKeyPath := nginx.GetNginxConfPath("ssl/" + domain + "/" + domain + ".key") if err != nil {
_, err = os.Stat(sslCertificateKeyPath) log.Println(err)
return
}
if err != nil { err = ws.WriteJSON(gin.H{
log.Println(err) "status": "success",
return "message": "Issued certificate successfully",
} "ssl_certificate": sslCertificatePath,
"ssl_certificate_key": sslCertificateKeyPath,
})
log.Println("[found]", "cert key") if err != nil {
m, err = json.Marshal(gin.H{ log.Println(err)
"status": "success", return
"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)
}
}
}
} }

View file

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

View file

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

View file

@ -1,92 +1,100 @@
package router package router
import ( import (
"bufio" "bufio"
"github.com/0xJacky/Nginx-UI/server/api" "github.com/0xJacky/Nginx-UI/server/api"
"github.com/0xJacky/Nginx-UI/server/settings" "github.com/0xJacky/Nginx-UI/server/settings"
"github.com/gin-contrib/static" "github.com/gin-contrib/static"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"net/http" "net/http"
"strings" "strings"
) )
func InitRouter() *gin.Engine { func InitRouter() *gin.Engine {
r := gin.New() r := gin.New()
r.Use(gin.Logger()) 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) { r.NoRoute(func(c *gin.Context) {
accept := c.Request.Header.Get("Accept") accept := c.Request.Header.Get("Accept")
if strings.Contains(accept, "text/html") { if strings.Contains(accept, "text/html") {
file, _ := mustFS("").Open("index.html") file, _ := mustFS("").Open("index.html")
defer file.Close() defer file.Close()
stat, _ := file.Stat() stat, _ := file.Stat()
c.DataFromReader(http.StatusOK, stat.Size(), "text/html", c.DataFromReader(http.StatusOK, stat.Size(), "text/html",
bufio.NewReader(file), nil) bufio.NewReader(file), nil)
return return
} }
}) })
g := r.Group("/api") g := r.Group("/api")
{ {
g.GET("settings", func(c *gin.Context) { g.GET("settings", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"demo": settings.ServerSettings.Demo, "demo": settings.ServerSettings.Demo,
}) })
}) })
g.GET("install", api.InstallLockCheck) g.GET("install", api.InstallLockCheck)
g.POST("install", api.InstallNginxUI) g.POST("install", api.InstallNginxUI)
g.POST("/login", api.Login) g.POST("/login", api.Login)
g.DELETE("/logout", api.Logout) g.DELETE("/logout", api.Logout)
g := g.Group("/", authRequired()) g := g.Group("/", authRequired())
{ {
g.GET("/analytic", api.Analytic) g.GET("analytic", api.Analytic)
g.GET("/analytic/init", api.GetAnalyticInit) g.GET("analytic/init", api.GetAnalyticInit)
g.GET("/users", api.GetUsers) g.GET("users", api.GetUsers)
g.GET("/user/:id", api.GetUser) g.GET("user/:id", api.GetUser)
g.POST("/user", api.AddUser) g.POST("user", api.AddUser)
g.POST("/user/:id", api.EditUser) g.POST("user/:id", api.EditUser)
g.DELETE("/user/:id", api.DeleteUser) g.DELETE("user/:id", api.DeleteUser)
g.GET("domains", api.GetDomains) g.GET("domains", api.GetDomains)
g.GET("domain/:name", api.GetDomain) g.GET("domain/:name", api.GetDomain)
g.POST("domain/:name", api.EditDomain)
g.POST("domain/:name/enable", api.EnableDomain)
g.POST("domain/:name/disable", api.DisableDomain)
g.DELETE("domain/:name", api.DeleteDomain)
g.GET("configs", api.GetConfigs) // Modify site configuration directly
g.GET("config/:name", api.GetConfig) g.POST("domain/:name", api.EditDomain)
g.POST("config", api.AddConfig)
g.POST("config/:name", api.EditConfig)
g.GET("backups", api.GetFileBackupList) // Transform NgxConf to nginx configuration
g.GET("backup/:id", api.GetFileBackup) 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("configs", api.GetConfigs)
g.GET("cert/:domain/info", api.CertInfo) g.GET("config/:name", api.GetConfig)
g.POST("config", api.AddConfig)
g.POST("config/:name", api.EditConfig)
// 添加域名到自动续期列表 //g.GET("backups", api.GetFileBackupList)
g.POST("cert/:domain", api.AddDomainToAutoCert) //g.GET("backup/:id", api.GetFileBackup)
// 从自动续期列表中删除域名
g.DELETE("cert/:domain", api.RemoveDomainFromAutoCert)
// pty g.GET("template", api.GetTemplate)
g.GET("pty", api.Pty)
}
}
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 package tool
import ( import (
"crypto" "crypto"
"crypto/ecdsa" "crypto/ecdsa"
"crypto/elliptic" "crypto/elliptic"
"crypto/rand" "crypto/rand"
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"github.com/0xJacky/Nginx-UI/server/model" "github.com/0xJacky/Nginx-UI/server/model"
"github.com/0xJacky/Nginx-UI/server/settings" "github.com/0xJacky/Nginx-UI/server/settings"
"github.com/0xJacky/Nginx-UI/server/tool/nginx" "github.com/0xJacky/Nginx-UI/server/tool/nginx"
"github.com/go-acme/lego/v4/certcrypto" "github.com/go-acme/lego/v4/certcrypto"
"github.com/go-acme/lego/v4/certificate" "github.com/go-acme/lego/v4/certificate"
"github.com/go-acme/lego/v4/challenge/http01" "github.com/go-acme/lego/v4/challenge/http01"
"github.com/go-acme/lego/v4/lego" "github.com/go-acme/lego/v4/lego"
"github.com/go-acme/lego/v4/registration" "github.com/go-acme/lego/v4/registration"
"github.com/pkg/errors" "github.com/pkg/errors"
"io" "io"
"io/ioutil" "io/ioutil"
"log" "log"
"net" "net"
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
"time" "time"
) )
// MyUser You'll need a user or account type that implements acme.User // MyUser You'll need a user or account type that implements acme.User
type MyUser struct { type MyUser struct {
Email string Email string
Registration *registration.Resource Registration *registration.Resource
key crypto.PrivateKey key crypto.PrivateKey
} }
func (u *MyUser) GetEmail() string { func (u *MyUser) GetEmail() string {
return u.Email return u.Email
} }
func (u MyUser) GetRegistration() *registration.Resource { func (u MyUser) GetRegistration() *registration.Resource {
return u.Registration return u.Registration
} }
func (u *MyUser) GetPrivateKey() crypto.PrivateKey { func (u *MyUser) GetPrivateKey() crypto.PrivateKey {
return u.key return u.key
} }
func AutoCert() { func AutoCert() {
defer func() { defer func() {
if err := recover(); err != nil { if err := recover(); err != nil {
log.Println("[AutoCert] Recover", err) log.Println("[AutoCert] Recover", err)
} }
}() }()
log.Println("[AutoCert] Start") log.Println("[AutoCert] Start")
autoCertList := model.GetAutoCertList() autoCertList := model.GetAutoCertList()
for i := range autoCertList { for i := range autoCertList {
domain := autoCertList[i].Domain domain := autoCertList[i].Domain
key, err := GetCertInfo(domain) key, err := GetCertInfo(domain)
if err != nil { if err != nil {
log.Println("GetCertInfo Err", err) log.Println("GetCertInfo Err", err)
// 获取证书信息失败,本次跳过 // 获取证书信息失败,本次跳过
continue continue
} }
// 未到一个月 // 未到一个月
if time.Now().Before(key.NotBefore.AddDate(0, 1, 0)) { if time.Now().Before(key.NotBefore.AddDate(0, 1, 0)) {
continue continue
} }
// 过一个月了,重新申请证书 // 过一个月了,重新申请证书
err = IssueCert(domain) err = IssueCert(domain)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
} }
} }
} }
func GetCertInfo(domain string) (key *x509.Certificate, err error) { func GetCertInfo(domain string) (key *x509.Certificate, err error) {
var response *http.Response var response *http.Response
client := &http.Client{ client := &http.Client{
Transport: &http.Transport{ Transport: &http.Transport{
DialContext: (&net.Dialer{ DialContext: (&net.Dialer{
Timeout: 5 * time.Second, Timeout: 5 * time.Second,
}).DialContext, }).DialContext,
DisableKeepAlives: true, DisableKeepAlives: true,
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}, },
Timeout: 5 * time.Second, Timeout: 5 * time.Second,
} }
response, err = client.Get("https://" + domain) response, err = client.Get("https://" + domain)
if err != nil { if err != nil {
err = errors.Wrap(err, "get cert info error") err = errors.Wrap(err, "get cert info error")
return return
} }
defer func(Body io.ReadCloser) { defer func(Body io.ReadCloser) {
err = Body.Close() err = Body.Close()
if err != nil { if err != nil {
log.Println(err) log.Println(err)
return return
} }
}(response.Body) }(response.Body)
key = response.TLS.PeerCertificates[0] key = response.TLS.PeerCertificates[0]
return return
} }
func IssueCert(domain string) error { func IssueCert(domain string) error {
// Create a user. New accounts need an email and private key to start. // Create a user. New accounts need an email and private key to start.
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil { if err != nil {
return errors.Wrap(err, "issue cert generate key error") return errors.Wrap(err, "issue cert generate key error")
} }
myUser := MyUser{ myUser := MyUser{
Email: settings.ServerSettings.Email, Email: settings.ServerSettings.Email,
key: privateKey, key: privateKey,
} }
config := lego.NewConfig(&myUser) config := lego.NewConfig(&myUser)
if settings.ServerSettings.Demo { if settings.ServerSettings.Demo {
config.CADirURL = "https://acme-staging-v02.api.letsencrypt.org/directory" config.CADirURL = "https://acme-staging-v02.api.letsencrypt.org/directory"
} }
config.Certificate.KeyType = certcrypto.RSA2048
// A client facilitates communication with the CA server. config.Certificate.KeyType = certcrypto.RSA2048
client, err := lego.NewClient(config)
if err != nil {
return errors.Wrap(err, "issue cert new client error")
}
err = client.Challenge.SetHTTP01Provider( // A client facilitates communication with the CA server.
http01.NewProviderServer("", client, err := lego.NewClient(config)
settings.ServerSettings.HTTPChallengePort, if err != nil {
), return errors.Wrap(err, "issue cert new client error")
) }
if err != nil {
return errors.Wrap(err, "issue cert challenge fail")
}
// New users will need to register err = client.Challenge.SetHTTP01Provider(
reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) http01.NewProviderServer("",
if err != nil { settings.ServerSettings.HTTPChallengePort,
log.Println(err) ),
return errors.Wrap(err, "issue cert register fail") )
} if err != nil {
myUser.Registration = reg return errors.Wrap(err, "issue cert challenge fail")
}
request := certificate.ObtainRequest{ // New users will need to register
Domains: []string{domain}, reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
Bundle: true, if err != nil {
} log.Println(err)
certificates, err := client.Certificate.Obtain(request) return errors.Wrap(err, "issue cert register fail")
if err != nil { }
return errors.Wrap(err, "issue cert fail to obtain") myUser.Registration = reg
}
saveDir := nginx.GetNginxConfPath("ssl/" + domain)
if _, err := os.Stat(saveDir); os.IsNotExist(err) {
err = os.Mkdir(saveDir, 0755)
if err != nil {
return errors.Wrap(err, "issue cert fail to create")
}
}
// Each certificate comes back with the cert bytes, the bytes of the client's request := certificate.ObtainRequest{
// private key, and a certificate URL. SAVE THESE TO DISK. Domains: []string{domain},
err = ioutil.WriteFile(filepath.Join(saveDir, "fullchain.cer"), Bundle: true,
certificates.Certificate, 0644) }
if err != nil { certificates, err := client.Certificate.Obtain(request)
log.Println(err) if err != nil {
return errors.Wrap(err, "issue cert write fullchain.cer fail") return errors.Wrap(err, "issue cert fail to obtain")
} }
err = ioutil.WriteFile(filepath.Join(saveDir, domain+".key"), saveDir := nginx.GetNginxConfPath("ssl/" + domain)
certificates.PrivateKey, 0644) if _, err = os.Stat(saveDir); os.IsNotExist(err) {
if err != nil { err = os.Mkdir(saveDir, 0755)
log.Println(err) if err != nil {
return errors.Wrap(err, "issue cert write key fail") 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 { if directive.Directive == If {
server += fmt.Sprintf("%s%s\n", comments, fmtCodeWithIndent(directive.Params, 1)) 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()) server += fmt.Sprintf("%s\t%s;\n", comments, directive.Orig())
} }
} }

View file

@ -105,15 +105,9 @@ func parseDirective(scanner *bufio.Scanner) (d NgxDirective) {
return return
} }
func ParseNgxConfig(filename string) (c *NgxConfig, err error) { func ParseNgxConfigByScanner(filename string, scanner *bufio.Scanner) (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)
c = NewNgxConfig(filename) c = NewNgxConfig(filename)
for scanner.Scan() { for scanner.Scan() {
d := parseDirective(scanner) d := parseDirective(scanner)
paramsScanner := bufio.NewScanner(strings.NewReader(d.Params)) paramsScanner := bufio.NewScanner(strings.NewReader(d.Params))
@ -142,3 +136,15 @@ func ParseNgxConfig(filename string) (c *NgxConfig, err error) {
return c, nil 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"` Comments string `json:"comments"`
} }
type NgxDirectives map[string][]NgxDirective
type NgxLocation struct { type NgxLocation struct {
Path string `json:"path"` Path string `json:"path"`
Content string `json:"content"` Content string `json:"content"`