撸了个 ssl 证书监控和邮件通知的小脚本

2018-10-30 21:09:45 +08:00
 ucun

自己搞了很多应用放在家里,比如 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 模式自动续签加上

2286 次点击
所在节点    Python
3 条回复
airyland
2018-10-30 21:30:53 +08:00
我也做了个直接推送到微信的,改天放出来。
msg7086
2018-10-30 21:42:34 +08:00
公网 SSL 监控的话,有 https://letsmonitor.org/
SukkaW
2018-10-31 08:41:30 +08:00

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/502735

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX