
自己搞了很多应用放在家里,比如 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 = '[email protected]' SMTP_SERVER = 'smtp.qq.com' ALERT_DAYS = 3 def create_domain_table(): cOnn= sqlit3.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 模式自动续签加上
1 airyland Oct 30, 2018 我也做了个直接推送到微信的,改天放出来。 |
2 msg7086 Oct 30, 2018 公网 SSL 监控的话,有 https://letsmonitor.org/ 。 |
3 SukkaW Oct 31, 2018 via Android |