V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐学习书目
Learn Python the Hard Way
Python Sites
PyPI - Python Package Index
http://diveintopython.org/toc/index.html
Pocoo
值得关注的项目
PyPy
Celery
Jinja2
Read the Docs
gevent
pyenv
virtualenv
Stackless Python
Beautiful Soup
结巴中文分词
Green Unicorn
Sentry
Shovel
Pyflakes
pytest
Python 编程
pep8 Checker
Styles
PEP 8
Google Python Style Guide
Code Style from The Hitchhiker's Guide
ucun
V2EX  ›  Python

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

  •  
  •   ucun · 2018-10-30 21:09:45 +08:00 · 2236 次点击
    这是一个创建于 1976 天前的主题,其中的信息可能已经有所发展或是发生改变。

    自己搞了很多应用放在家里,比如 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 = 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 邮箱和企业邮箱。

    ssl.png

    后续把 acme.sh dns 模式自动续签加上

    3 条回复    2018-10-31 08:41:30 +08:00
    airyland
        1
    airyland  
       2018-10-30 21:30:53 +08:00
    我也做了个直接推送到微信的,改天放出来。
    msg7086
        2
    msg7086  
       2018-10-30 21:42:34 +08:00
    公网 SSL 监控的话,有 https://letsmonitor.org/
    SukkaW
        3
    SukkaW  
       2018-10-31 08:41:30 +08:00 via Android   ❤️ 1
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   5401 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 32ms · UTC 07:46 · PVG 15:46 · LAX 00:46 · JFK 03:46
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.