python自动获取家里宽带的公网IP,并发送邮件

python自动获取家里宽带的公网IP,并发送邮件

前言

有时候需要在公网访问家里的NAS或者家里的服务器或者连接家里的远程桌面,但是没有公网IP无法访问家里的设备。
目前已知的解决方案:

  1. 使用第三方公司提供的内网穿透服务,如:花生壳等等,缺点:要钱
  2. 购买一台云服务器,获得一个公网IP;通过在用frp、ngrok、nps等等开源工具在本地和云服务上建立隧道,缺点:要钱、不稳定,网速受限于云服务器
  3. 使用ssh反向隧道,缺点:需要公网服务器
  4. 跟宽带运营商联系申请公网IP,缺点:申请过程漫长,受限于联通和电信宽带
  5. 本地定时自动获取公网IP,发送通知,如:邮件

由于本人没钱所以还是选择省钱的方式吧,本地定时自动获取公网IP发送通知邮件。所以。。开始搞吧

一、准备工作

准备条件:

  1. 已经安装宽带
  2. 1台路由器(最好是可以刷第三方固件,或者已经刷成了三方固件)
  3. 1台可以敲代码的电脑

二、获取公网IP并发送邮件(重头戏)

最简单的获取公网IP的方式:打开浏览器,打开百度,输入ip, ok 你就可以看到自己的公网IP了,如图:

baidu_ip.png

很显然这种方式并不合适。

那么第二种,打开路由器管理界面查看公网IP 也不推荐

第三种通过代码获取,这才是比较推荐的方式
那么我所了解的通过代码获取IP的方式归纳起来应该有两种:
一种是直接访问提供查看公网IP功能的服务器,获取访问的IP
例如:

import requests

url='http://jsonip.com'
# 获取IP地址
resp = requests.get(url)
info = resp.json()
public_ip = info.get('ip')

OK 5行代码就搞定了获取公网IP的问题。但是这方式有两个缺点:1.需要寄托于别人的服务器正常运行的情况,如果出现服务器维护的时候那就获取不到公网IP,2.如果你在路由器中配置了代理,那么你获取到的公网IP会是你代理服务器的IP,然而通过代理IP是无法访问家里的设备的,所以不推荐这种方式。

另一种就是直接在路由器中获取公网IP
基本思路:使用代码访问路由器管理界面,获取公网IP。可选的技术:1.使用爬虫的方式,2.使用selenium

首先使用爬虫的方式:requests库,只是简单的获取IP,就没有必要使用scrapy

我使用的是路由器刷了三方固件PandoraBox,只是做演示,提供思路,不是通用的,如需尝试需要依据自己的路由情况

分析页面:
首先需要登录,需要找到登录按钮提交的链接:

luyouqi_login.png

查找方法:
第一种,在浏览器中按F12,查看form节点,其action属性值就是访问链接,或者也可以在浏览器中按下F12切换到Network选项卡中,点击一次提交,查看提交数据的URI; 所以提交登录的链接为:http://192.168.1.1/cgi-bin/luci

image.png

第二种,使用fiddler等等抓包工具,抓取提交登录的链接:

image.png

接下来就是使用代码访问这个链接自动登录

import requests

login_info = {
    'username': 'root',
    'password': 'XXXXX'
}
url = 'http://192.168.1.1/cgi-bin/luci'
headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.105 Safari/537.36"
}

def get_ip():
    info_dict = {}
    session = requests.session()
    resp = session.post(url, headers=headers, data=login_info)
    resp_info= resp.text

然后分析resp.text返回的内容,发现其中并没有我们要的IP数据,通过查看网页源代可以知道,公网IP等信息有独立的url。
如图:

image.png

根据源代码,注意看:XHR.poll(5, '/cgi-bin/luci/;stok=4d9ab104d86153a97b35619e7f89dad9', { status: 1 }, 这就是路由器后台管理返回数据的真实URL, 由此我们可以知道公网IP的真实链接为:http://192.168.1.1/cgi-bin/luci/;stok=b92111c0fa47d24429f29de8b974d6b8?status=1

由于链接中的stok是动态的所以需要先获取该值,然后构造一个新的链接。

在浏览器中访问该链接,会首先让你登录,登录之后会返回如图信息:


image.png

接下来实现自动登录获取IP信息:

import requests
import re

login_info = {
    'username': 'root',
    'password': 'XXXXX'
}

url = 'http://192.168.1.1/cgi-bin/luci'
headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.105 Safari/537.36"
}

def get_ip():
    info_dict = {}
    session = requests.session()
    resp = session.post(url, headers=headers, data=login_info)
    # resp_info = resp.json()

    patter = "XHR.poll\(5, '/cgi-bin/luci/;stok=(.*?)', { status: 1 },"
    stock = re.compile(patter).findall(resp.text)
    info_url = url + '/;stok=' + ''.join(stock) + '?status=1'
    resp_info = session.get(info_url, headers=headers).json()

    if not resp_info:
        info_dict = {'msg': '获取信息出错'}
    wan_info = resp_info.get('wan')
    leases_info = resp_info.get('leases')
    # 由于返回的信息太多了,只需要获取自己想要的数据
    if wan_info and leases_info:
        leases_str = ''.join([str(leases) for leases in leases_info])
        info_dict = {
            'wan信息': {
                '类型': wan_info.get('proto'),
                'IP地址': wan_info.get('ipaddr'),
                '子网掩码': wan_info.get('netmask'),
                '网关': wan_info.get('gwaddr'),
                'DNS': wan_info.get('dns'),
                '已连接': '{}天'.format(wan_info.get('uptime') / 60 / 60 / 24),
            },
            '连接的设备数量': len(leases_info),
            'DHCP分配设备信息': leases_str.replace('expires', '剩余租期').replace('macaddr', 'MAC地址').replace('ipaddr', 'IPV4地址').replace('hostname', '主机名')
        }
    return info_dict

最后再加上发送邮件的代码

直接上代码吧:

# coding: utf-8

import requests
import re
import smtplib
from email.mime.text import MIMEText
from email.header import Header

login_info = {
    'username': 'XXXX',
    'password': 'XXXXX'
}
url = 'http://192.168.1.1/cgi-bin/luci'
headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.105 Safari/537.36"
}
mail_info = {
    'recv_address': 'XXXX@qq.com',
    'sender_name': 'XXXX@qq.com',
    'sender_pwd': 'XXXXXXX',
    'smtp_server': 'smtp.qq.com',
    'subject': '路由器IP信息已更新',
    'content': '您的公网IP信息: {},其他相关信息如下:{}'

}


def get_ip():
    info_dict = {}
    session = requests.session()
    resp = session.post(url, headers=headers, data=login_info)
    # resp_info = resp.json()

    patter = "XHR.poll\(5, '/cgi-bin/luci/;stok=(.*?)', { status: 1 },"
    stock = re.compile(patter).findall(resp.text)
    info_url = url + '/;stok=' + ''.join(stock) + '?status=1&_=0.3290479886974037'
    resp_info = session.get(info_url, headers=headers).json()

    if not resp_info:
        info_dict = {'msg': '获取信息出错'}
    wan_info = resp_info.get('wan')
    leases_info = resp_info.get('leases')

    if wan_info and leases_info:
        leases_str = ''.join([str(leases) for leases in leases_info])
        info_dict = {
            'wan信息': {
                '类型': wan_info.get('proto'),
                'IP地址': wan_info.get('ipaddr'),
                '子网掩码': wan_info.get('netmask'),
                '网关': wan_info.get('gwaddr'),
                'DNS': wan_info.get('dns'),
                '已连接': '{}天'.format(wan_info.get('uptime') / 60 / 60 / 24),
            },
            '连接的设备数量': len(leases_info),
            'DHCP分配设备信息': leases_str.replace('expires', '剩余租期').replace('macaddr', 'MAC地址').replace('ipaddr', 'IPV4地址').replace('hostname', '主机名')
        }
    return info_dict


def send_message(content):
    # 设置发送邮件的内容
    msg = MIMEText(content, 'plain', 'utf-8')
    msg['From'] = Header(mail_info.get('sender_name'))
    msg['Subject'] = Header(mail_info.get('subject'), 'utf-8')
    msg['To'] = Header(mail_info.get('recv_address'))
    # 发送邮件
    smtp = smtplib.SMTP()
    smtp.connect(mail_info['smtp_server'])
    smtp.login(mail_info['sender_name'], mail_info['sender_pwd'])
    smtp.sendmail(mail_info['sender_name'], mail_info['recv_address'], msg.as_string())


info_dict = get_ip()
content = ''
if info_dict.get('msg'):
    content = info_dict.get('msg')
else:
    content = mail_info.get('content').format(info_dict.get('wan信息').get('IP地址'), str(info_dict))
send_message(content)

** 另外一种直接使用selenium的方式,直接放代码:


import platform
import time
import os
from selenium import webdriver

url = 'http://192.168.1.1/cgi-bin/luci'
username = 'xxxx'
password = 'xxxx'


def get_ip():
    option = webdriver.ChromeOptions()
    option.add_argument("--headless")  # 通过ChromeOptions设置隐藏浏览器
    option.add_argument('--no-sandbox')  # 在Linux上禁用浏览器沙盒
    driver_path = loading_file_path(
        'chromedriver.exe') if platform.system() == 'Windows' else loading_file_path(
        'chromedriver')
    driver = webdriver.Chrome(executable_path=driver_path, options=option)
    driver.get(url)

    user = driver.find_element_by_xpath('//form/div[1]/fieldset/fieldset/div[1]/div/input')
    user.clear()
    user.send_keys(username)
    pwd = driver.find_element_by_id('focus_password')
    pwd.clear()
    pwd.send_keys(password)
    login_btn = driver.find_element_by_xpath('//*[@id="maincontent"]/form/div[2]/input[1]')
    login_btn.click()

    time.sleep(2)
    ip_info = driver.find_element_by_id('wan4_s').text
    return ip_info


def loading_file_path(filename):
    # 获取当前文件路径
    current_path = os.path.abspath(__file__)
    # 获取当前文件的父目录
    father_path = os.path.abspath(os.path.dirname(current_path) + os.path.sep + ".")
    # chromedriver文件路径,获取当前目录的父目录与chromedriver拼接
    webdriver_path = os.path.join(father_path, filename)
    return webdriver_path


wan_ip = get_ip()

是不是觉得这种方式很简单,确实使用selenium可以很简单的获取到公网IP 信息,但是这种方式部署的时候不适合NAS或者路由器

三、部署脚本

1.可以在nas中添加一个定时任务,不做演示
2.在路由器中添加定时任务,不做演示(需要使用刷了三方固件的路由器)

推荐阅读更多精彩内容