自己搞了很多应用放在家里,比如 NAS,PLEX,TRANSMISSION,NEXTCLOUD。。为了方便管理都是单独申请的免费域名+ssl 证书
因为没有公网 IP,ACME.SH 就不能自动续签证书了
上手撸个自用的脚本
#! /usr/bin/env python3
# -*- coding:utf-8 -*-
import sqlite3
import os,sys
import imaplib
import time
from datetime import datetime
import ssl,socket
DB = 'domain.db'
SEND_EMAIL = 'youremail@domain'
PASSWORD = 'password'
RECEIVE_EMAIL = '1001@qq.com'
SMTP_SERVER = 'smtp.qq.com'
ALERT_DAYS = 3
def create_domain_table():
conn = sqlite3.connect(DB)
c = conn.cursor()
c.execute('''CREATE TABLE DOMAIN
(ID INTEGER PRIMARY KEY AUTOINCREMENT,
check_time TEXT,
domain TEXT,
s_time TEXT,
e_time TEXT,
remain INT );''')
conn.commit()
c.close()
conn.close()
def insert_domain_table(sslinfo):
conn = sqlite3.connect(DB)
conn.text_factory = str
c = conn.cursor()
check_time = sslinfo['check_time']
domain = sslinfo['domain']
s_time = sslinfo['s_time']
e_time = sslinfo['e_time']
remain = sslinfo['remain']
c.execute("INSERT INTO DOMAIN (check_time,domain,s_time,e_time,remain) VALUES(?,?,?,?,?);",(check_time,domain,s_time,e_time,remain))
conn.commit()
c.close()
conn.close()
def get_ssl_info(domain):
server_name = domain
sslinfo = {}
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
context.verify_mode = ssl.CERT_REQUIRED
context.check_hostname = True
context.load_default_certs()
s = socket.socket()
s = context.wrap_socket(s,server_hostname=server_name)
s.connect((server_name,443))
s.do_handshake()
cert = s.getpeercert()
e_time = ssl.cert_time_to_seconds(cert['notAfter'])
remain = e_time - time.time()
remain = round(remain/86400)
e_time = datetime.utcfromtimestamp(e_time)
s_time = ssl.cert_time_to_seconds(cert['notBefore'])
s_time = datetime.utcfromtimestamp(s_time)
check_time = datetime.utcnow()
sslinfo['check_time'] = str(check_time)
sslinfo['domain'] = server_name
sslinfo['s_time'] = str(s_time)
sslinfo['e_time'] = str(e_time)
sslinfo['remain'] = remain
return sslinfo
def add_domain(domain):
if not os.path.isfile(DB):
create_domain_table()
sslinfo = get_ssl_info(domain)
insert_domain_table(sslinfo)
def del_domain(domain):
conn = sqlite3.connect(DB)
c = conn.cursor()
c.execute('delete from DOMAIN where domain=?;',(domain,))
conn.commit()
c.close()
conn.close()
def get_domain_info(domain):
conn = sqlite3.connect(DB)
c = conn.cursor()
domainInfo = c.execute('select * from DOMAIN where domain=?;',(domain,))
domainInfo = domainInfo.fetchone()
c.close()
conn.close()
return domainInfo
def generation_html_file(htmlfile):
html = [
' <!DOCTYPE html>',
' <html>',
' <head>',
' <meta charset="utf-8">',
' <meta http-equiv="X-UA-Compatible" content="IE=edge">',
' <title>SSL Status</title>',
' <meta name="description" content="SSL Status">',
' <meta name="viewport" content="width=device-width, initial-scale=1">',
' <style>',
' body { -webkit-font-smoothing: antialiased; min-height: 100vh; display: flex; flex-direction: column; } *, *:after, *:before { box-sizing: border-box; } * { margin: 0; padding: 0; } a { text-decoration-style: none; text-decoration: none; color: inherit; cursor: pointer; } p { font-size: 16px; line-height: 1.5; font-weight: 400; color: #5A5B68; } p.small { font-size: 15px; } p.tiny { font-size: 14px; } .tiny { font-size: 14px; } .section { margin-left: auto; margin-right: auto; display: flex; flex-direction: column; max-width: 1342px; /*56 + 1230 + 56*/ padding-left: 8px; padding-right: 8px; left: 0; right: 0; width: 100%; } .container { display: flex; flex-direction: column; align-items: center; } .flex_column { display: flex; flex-direction: column; } .justify_content_center { justify-content: center; } h1 { font-size: 48px; letter-spacing: -1px; line-height: 1.2; font-weight: 700; color: #323648; } .header { display: flex; flex-direction: row; justify-content: space-between; align-items: center; } .width_100 { width: 100%; } .align_center { align-items: center; } .text_center { text-align: center; } .raven_gray { color: #5A5B68; } .black_licorice { color: #323648; } .bg_lighter_gray { background-color: #F5F5F5; } .bg_white { background-color: white; } .semi_bold { font-weight: 700; } .underline { text-decoration: underline; } #services_legend .header { background-color: #F5F5F5; padding-left: 20px; padding-right: 20px; height: 44px; border-left: 1px solid #E8E8E8; border-right: 1px solid #E8E8E8; border-top: 1px solid #E8E8E8; justify-content: center; } @media (min-width: 768px) { #services_legend .header { flex-wrap: wrap; justify-content: space-between; align-content: center; height: 74px; } } @media (min-width: 1072px) { #services_legend .header { height: 56px; } } #services { margin-bottom: 56px; border-style: solid; border-color: #E8E8E8; border-width: 1px 0 0 1px; display: flex; flex-direction: row; flex-wrap: wrap; } #services .service { padding: 20px; border-style: solid; border-color: #E8E8E8; border-width: 0 1px 1px 0; width: 100%; } @media (min-width: 1072px) { #services .service { width: 50%; } } @media (max-width: 767px) { h1 { font-size: 28px; } } @media (min-width: 768px) { .section { padding-left: 56px; padding-right: 56px; } } /* CALENDAR START */ /* CALENDAR END */ /* CALENDAR TOOLTIPS START */ /* CALENDAR TOOLTIPS END */ /* DAY INCIDENTS START */ /* DAY INCIDENTS END */ /* INCIDENT START */ /* INCIDENT END */ /* FOOTER START */ .footer { display: flex; flex-direction: column; flex-wrap: wrap; justify-content: center; align-items: center; height: 200px; } .footer p { text-align: center; } .footer > .info { order: 1; display: flex; flex-direction: column; justify-content: center; align-items: center; } .footer .sub_info { display: flex; flex-direction: column; align-items: center; } .footer > .links { display: flex; flex-direction: row; justify-content: space-between; align-items: center; order: 2; width: 250px; margin-top: 25px; } @media (min-width: 768px) { .footer { height: 112px; } .footer > .links { margin-top: 20px; } .footer .sub_info { flex-direction: row; } } @media (min-width: 1072px) { .footer { height: 90px; align-items: stretch; } .footer > .links { flex-basis: 60%; order: 1; margin-top: 0; } .footer > .info { align-items: flex-end; flex-basis: 50%; order: 2; } /* FOOTER END */ }',
' </style>',
' </head>',
' <body class="bg_lighter_gray">',
' <div class="bg_white width_100">',
' <div class="container">',
' <h1 class="black_licorice text_center width_100">SSL status</h1>',
' </div>',
' <div id="services_legend" class="section justify_content_center">',
' <div class="header">',
' <p class="title semi_bold black_licorice">Sites SSL Status</p>',
' </div>',
' </div>',
' <div class="section">',
' <div id="services">',
]
conn = sqlite3.connect(DB)
c = conn.cursor()
c.execute('select * from DOMAIN')
domain = c.fetchall()
c.close()
conn.close()
for i in domain:
html += [
'<div class="service header align_center">',
'<div class="flex_column">',
'<p class="black_licorice semi_bold">' + i[2] + '</p>',
'<p class="small raven_gray">last check: ' + i[1] + '</p>',
'<p class="small raven_gray">issue date: ' + i[3] + '</p>',
'<p class="small raven_gray">expire date: ' + i[4] + '</p>',
'<p class="small raven_gray">remain: ' + str(i[5]) + ' Days' + '</p>',
'</div>',
'</div>',
]
html += [
'</div>',
'</div>',
'</div>',
'<div class="section">',
'<div class="footer">',
'<div class="info">',
'<div class="sub_info">',
'<p class="tiny"><a href="https://github.com" class="underline">github</a></p>',
'</div>',
'</div>',
'<div class="links">',
'<p class="semi_bold tiny">SSL Status</p>',
'</div>',
'</div>',
'</div>',
'</body>',
'</html>',
]
if not htmlfile:
htmlfile = open('sslstatus.html','w')
htmlfile = open(htmlfile,'w')
htmlfile.write('\n'.join(html))
htmlfile.close()
def send_alert_email(msg):
from email.mime.text import MIMEText
import smtplib
msg = MIMEText(msg,'plain','utf-8')
msg['From'] = SEND_EMAIL
msg['To'] = RECEIVE_EMAIL
msg['Subject'] = 'SSL Alert'
server = smtplib.SMTP(SMTP_SERVER,587)
server.starttls()
server.login(SEND_EMAIL,PASSWORD)
server.sendmail(SEND_EMAIL,[RECEIVE_EMAIL],msg.as_string())
server.quit()
def get_expired_domain():
conn = sqlite3.connect(DB)
c = conn.cursor()
c.execute('select * from DOMAIN')
domain = c.fetchall()
c.close()
conn.close()
msg = ' '
for i in domain:
if i[5] < ALERT_DAYS:
msg = msg + ' ' +i[2] + ' remain ' + str(i[5]) + ' Days'
if not msg:
send_alert_email(msg)
def usage():
print('''Usage:
add|del domain "add or remove the domain for monitoring"
output /path/to/html "output the ssl status to assigned html file"
email "send email"''')
def main():
argc = len(sys.argv)
exitcode = 0
if argc < 2:
usage()
exitcode = 1
else:
if sys.argv[1] == 'add':
add_domain(sys.argv[2])
elif sys.argv[1] == 'del':
del_domain(sys.argv[2])
elif sys.argv[1] == 'email':
get_expired_domain()
elif sys.argv[1] == 'output':
htmlfile = sys.argv[2]
generation_html_file(htmlfile)
else:
usage()
exitcode = 1
sys.exit(exitcode)
if __name__ == '__main__':
main()
可生成 HTML 文件查看,也可用定时任务发邮件到绑定了微信的 QQ 邮箱和企业邮箱。
后续把 acme.sh dns 模式自动续签加上
这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。
V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。
V2EX is a community of developers, designers and creative people.