diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..12687d2 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,31 @@ +name: Test ipwatch + +on: [push] +jobs: + install_and_test: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + python-version: ["3.9", "3.10", "3.11", "3.12"] # , "pypy3.9", "pypy3.10"] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Upgrade pip + run: | + python -m pip install --upgrade pip + + - name: Install package + run: python -m pip install . + + - name: Test scripts + run: | + ipget + ipwatch test_config.txt + ipwatch test_config.txt \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2e2a41c --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +config.txt +serverCache.json +oldip.txt +saved_ip.txt +__pycache__ +.venv diff --git a/README.md b/README.md index 3389856..0777549 100644 --- a/README.md +++ b/README.md @@ -11,33 +11,42 @@ https://github.com/begleysm/ipwatch ## Description This program gets your external & internal IP addresses, checks them against your "saved" IP addresses and, if a difference is found, emails you the new IP's. This is useful for servers at residential locations whose IP address may change periodically due to actions by the ISP. -## Usage Examples -[config] = path to an IPWatch configuration file +## Installation -1. `python3 ipwatch.py [config]` -2. `./ipwatch.py [config]` -3. `python3 ipwatch.py config.txt` -4. `./ipwatch.py config.txt` -5. `python3 /path/to/dir/ipwatch.py /path/to/dir/config.txt` -6. `./path/to/dir/ipwatch.py /path/to/dir/config/txt` +### Install python3, git and optionally nano + +On Debian-based system (e.g. Ubuntu) you can do: -## Installation -### Debian based Linux systems -Install python3, git, & nano by running ```bash sudo apt install python3 git nano ``` +## Create a Python virtual environment (venv) + +```bash +sudo python3 -m venv /opt/ipwatch +``` + +## Install the ipwatch package in this venv + Clone the ipwatch repo by running + ```bash -sudo git clone https://github.com/begleysm/ipwatch /opt/ipwatch +git clone https://github.com/begleysm/ipwatch ``` -Copy `example_config.txt` to `config.txt` by running +Install the package in the venv + ```bash -sudo cp /opt/ipwatch/example_config.txt /opt/ipwatch/config.txt +. /opt/ipwatch/bin/activate +cd ipwatch +pip install ``` +Copy `example_config.txt` to `config.txt` by running +```bash +sudo cp example_config.txt /opt/ipwatch/config.txt +``` Since `config.txt` will contain an email password, make it viewable & editable by `root` only by running ```bash sudo chmod 600 /opt/ipwatch/config.txt @@ -50,10 +59,21 @@ sudo nano /opt/ipwatch/config.txt You can test the setup by running ```bash -sudo python3 /opt/ipwatch/ipwatch.py /opt/ipwatch/config.txt +sudo /opt/ipwatch/bin/ipwatch /opt/ipwatch/config.txt ``` + Check out the **Cronjob** section below to make this utility run on its own so that you may be quickly alerted to any IP changes on your system. +## Usage + +[config] = path to an IPWatch configuration file + +```bash +. /opt/ipwatch/bin/activate +ipwatch [config] + +``` + ## Config File ipwatch uses a config file to define how to send an email. An example and description is below. A similar config file is in the repo as example_config.txt. You should copy it by running something like `sudo cp example_config.txt config.txt` and then modify `config.txt`. It is recommended that you adjust the permissions of your config file so that no one but you and/or root can read it since it will contain the sender email password. @@ -70,6 +90,7 @@ smtp_addr=smtp.gmail.com:587 #this is the SMTP address for the send save_ip_path=/opt/ipwatch/oldip.txt #this is the location where the saved ip address will be stored try_count=10 #this defines how many times the system will try to find the current IP before exiting ip_blacklist=192.168.0.255,192.168.0.1,192.168.1.255,192.168.1.1 #this is a list of IP address to ignore if received +dry_run=0 # do not send email when dry_run=1 ``` ## Cronjob diff --git a/example_config.txt b/example_config.txt index 67beb58..f35ad9b 100644 --- a/example_config.txt +++ b/example_config.txt @@ -12,4 +12,5 @@ smtp_addr=smtp.gmail.com:587 save_ip_path=/user/bob/ipwatch/oldip.txt try_count=10 ip_blacklist=192.168.0.255,192.168.0.1,192.168.1.255,192.168.1.1 +dry_run=0 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..2379846 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,34 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "ipwatch" +dynamic = ["version"] +description = """This program gets your external & internal IP addresses, checks them against +your "saved" IP addresses and, if a difference is found, emails you the new +IP's. This is useful for servers at residential locations whose IP address may +change periodically due to actions by the ISP.""" +readme = "README.md" +requires-python = ">=3.8" +license = "MIT" +keywords = [] +authors = [ + { name = "Sean Begley", email = "begleysm@gmail.com" }, + { name = "Tom Vander Aa", email = "tom.vanderaa@gmail.com" }, +] + +[project.urls] +Documentation = "https://github.com/begleysm/ipwatch#readme" +Issues = "https://github.com/begleysm/ipwatch/issues" +Source = "https://github.com/begleysm/ipwatch" + +[project.scripts] +ipwatch = "ipwatch:main" +ipget = "ipwatch.ipgetter:main" + +[template.plugins.default] +src-layout = true + +[tool.hatch.version] +path = "src/ipwatch/__about__.py" diff --git a/src/ipwatch/__about__.py b/src/ipwatch/__about__.py new file mode 100644 index 0000000..c31eed2 --- /dev/null +++ b/src/ipwatch/__about__.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2024-present Tom Vander Aa +# +# SPDX-License-Identifier: MIT +__version__ = "0.0.1" diff --git a/src/ipwatch/__init__.py b/src/ipwatch/__init__.py new file mode 100644 index 0000000..23f3c88 --- /dev/null +++ b/src/ipwatch/__init__.py @@ -0,0 +1,6 @@ +# SPDX-FileCopyrightText: 2024-present Tom Vander Aa +# +# SPDX-License-Identifier: MIT + +from .ipwatch import main +from . import ipgetter \ No newline at end of file diff --git a/ipgetter.py b/src/ipwatch/ipgetter.py similarity index 99% rename from ipgetter.py rename to src/ipwatch/ipgetter.py index 097be7f..707d35d 100644 --- a/ipgetter.py +++ b/src/ipwatch/ipgetter.py @@ -34,7 +34,7 @@ import socket import ssl import json -import os +import os from datetime import datetime, timedelta from sys import version_info @@ -76,7 +76,7 @@ def __init__(self): theList = json.load (infile) except: pass - + if (theList is None or "expiry" not in theList or "expiryDisplay" not in theList @@ -106,7 +106,7 @@ def __init__(self): self.server_list = theList["servers"] theList = None - + def get_externalip(self): ''' This function gets your IP from a random server @@ -119,8 +119,8 @@ def get_externalip(self): if myip != '': break return myip,server - - + + def get_local_ip(self): # From https://stackoverflow.com/questions/166506/finding-local-ip-addresses-using-pythons-stdlib s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) @@ -133,13 +133,13 @@ def get_local_ip(self): finally: s.close() return IP - - + + def get_ips(self): local_ip = self.get_local_ip() external_ip, server = self.get_externalip() return external_ip, local_ip, server - + def fetch(self, server): ''' @@ -197,6 +197,9 @@ def test(self): print('\n') print(resultdict) -if __name__ == '__main__': +def main(): print(myip()) +if __name__ == '__main__': + main() + diff --git a/ipwatch.py b/src/ipwatch/ipwatch.py similarity index 78% rename from ipwatch.py rename to src/ipwatch/ipwatch.py index 108e3a7..cf0a39e 100644 --- a/ipwatch.py +++ b/src/ipwatch/ipwatch.py @@ -23,7 +23,7 @@ from pathlib import Path import re import smtplib -import ipgetter +from . import ipgetter ################ @@ -131,6 +131,8 @@ def readconfig(filepath, configObj): configObj.try_count = value elif (param == "ip_blacklist"): configObj.ip_blacklist = value.split(',') + elif (param == "dry_run"): + configObj.dry_run = bool(int(value)) else: print ("ERROR: unexpected line found in config file: %s" % line) configfile.close() @@ -163,13 +165,13 @@ def getips(try_count, blacklist): good_ip = 0 counter = 0 pattern = re.compile("^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$") - + #try up to config.try_count servers for an IP while(good_ip == 0) and(counter < try_count): - + #get an IP external_ip, local_ip, server = ipgetter.myip() - + #check to see that it has a ###.###.###.### format if pattern.match(external_ip) and external_ip not in blacklist: good_ip = 1 @@ -179,10 +181,10 @@ def getips(try_count, blacklist): print ("GetIP: Try %d: Bad IP (in Blacklist): %s" % (counter+1, external_ip)) else: print ("GetIP: Try %d: Bad IP (malformed): %s" % (counter+1, external_ip)) - + #increment the counter counter = counter + 1 - + #print ("My IP = %s\r\n" % external_ip) #print ("Server used = %s\r\n" % server) return external_ip, local_ip, server @@ -232,18 +234,19 @@ def updateoldips(filepath, new_external_ip, new_local_ip): savefile.close() #send mail with new IP address -def sendmail(old_exernal_ip, old_local_ip, new_external_ip, new_local_ip, server, sender, - sender_email, receivers, receiver_emails, username, password, subject, machine, smtp_addr): +def sendmail(old_external_ip, old_local_ip, new_external_ip, new_local_ip, server, sender, + sender_email, receivers, receiver_emails, username, password, subject, machine, smtp_addr, + dry_run = False): "Function to send an email with the new IP address" - + messages = [None]*len(receiver_emails) error_flag = 0 - + print("") for i in range(len(receiver_emails)): #print(str(i) + ": receiver = " + receivers[i] + "\t\t receiver email = " + receiver_emails[i]) - + messages[i] = ("""From: """ + sender + """ <"""+ sender_email + """> To: """ + receivers[i] + """ <""" + receiver_emails[i] + """> Subject: """ + subject + """ @@ -255,7 +258,6 @@ def sendmail(old_exernal_ip, old_local_ip, new_external_ip, new_local_ip, server + old_local_ip + """\r\nNew external IP = """ + new_external_ip + """\r\nNew local IP = """ + new_local_ip + """\r\nThe Server queried was """ + server) - #print (messages) #print (smtp_addr) #print (username) @@ -263,6 +265,11 @@ def sendmail(old_exernal_ip, old_local_ip, new_external_ip, new_local_ip, server #print (sender) #print (receiver_emails) #print (message) + + if dry_run: + print("DRY RUN: not sending email to ", receiver_emails) + return 0 + try: smtpObj = smtplib.SMTP(smtp_addr) smtpObj.starttls() @@ -275,7 +282,7 @@ def sendmail(old_exernal_ip, old_local_ip, new_external_ip, new_local_ip, server print ("ERROR: unable to send email " + str(i+1) + " of " + str(len(receiver_emails)) + " to " + receiver_emails[i]) print ("EXCEPTION: " + str(ex) + "\r\n") error_flag = 1 - + if (error_flag == 1): return 1 else: @@ -286,62 +293,63 @@ def sendmail(old_exernal_ip, old_local_ip, new_external_ip, new_local_ip, server ##### MAIN ##### ################ -#parse arguments -if (len(sys.argv) != 2): - printhelp() - #print ("len = %d\r\n" % len(sys.argv)) -else: - config_path = str(sys.argv[1]) - #print ("email = %s" % email) - #print ("machine = %s" % machine) - #print ("savefile = %s" % savefile_path) - - #parse config file - config = ConfigInfo() - rc_ret = readconfig(config_path, config) - if (rc_ret == "nofile"): - sys.exit(1) - elif (rc_ret == "badline"): - sys.exit(2) - - #print (config.sender) - #print (config.sender_email) - #print (config.sender_username) - #print (config.sender_password) - #print (config.receiver) - #print (config.receiver_email) - #print (config.subject_line) - #print (config.machine) - #print (config.smtp_addr) - #print (config.save_ip_path) - - #get the old ip address - old_external_ip, old_local_ip = getoldips(config.save_ip_path) - #print ("Old IP = %s" % oldip) - - #get current, external, IP address - curr_external_ip, curr_local_ip, server = getips(int(config.try_count), config.ip_blacklist) - #print ("Curr IP = %s" % external_ip) - #print ("Server used = %s" % server) - - #check to see if the IP address has changed - if ((curr_external_ip != old_external_ip) or (curr_local_ip != old_local_ip)): - #send email - print ("Current IP differs from old IP.") - sm_ret = sendmail(old_external_ip, old_local_ip, curr_external_ip, curr_local_ip, server, config.sender, config.sender_email, config.receiver, config.receiver_email, config.sender_username, config.sender_password, config.subject_line, config.machine, config.smtp_addr) - - # only update the file if the email was successfully sent - if (sm_ret == 0): - #update file - updateoldips(config.save_ip_path, curr_external_ip, curr_local_ip) - print ("Saved IP address updated.") +def main(): + #parse arguments + if (len(sys.argv) != 2): + printhelp() + #print ("len = %d\r\n" % len(sys.argv)) + else: + config_path = str(sys.argv[1]) + #print ("email = %s" % email) + #print ("machine = %s" % machine) + #print ("savefile = %s" % savefile_path) + + #parse config file + config = ConfigInfo() + rc_ret = readconfig(config_path, config) + if (rc_ret == "nofile"): + sys.exit(1) + elif (rc_ret == "badline"): + sys.exit(2) + + #print (config.sender) + #print (config.sender_email) + #print (config.sender_username) + #print (config.sender_password) + #print (config.receiver) + #print (config.receiver_email) + #print (config.subject_line) + #print (config.machine) + #print (config.smtp_addr) + #print (config.save_ip_path) + + #get the old ip address + old_external_ip, old_local_ip = getoldips(config.save_ip_path) + #print ("Old IP = %s" % oldip) + + #get current, external, IP address + curr_external_ip, curr_local_ip, server = getips(int(config.try_count), config.ip_blacklist) + #print ("Curr IP = %s" % external_ip) + #print ("Server used = %s" % server) + + #check to see if the IP address has changed + if ((curr_external_ip != old_external_ip) or (curr_local_ip != old_local_ip)): + #send email + print ("Current IP differs from old IP.") + sm_ret = sendmail(old_external_ip, old_local_ip, curr_external_ip, curr_local_ip, server, config.sender, config.sender_email, config.receiver, config.receiver_email, config.sender_username, config.sender_password, config.subject_line, config.machine, config.smtp_addr, config.dry_run) + + # only update the file if the email was successfully sent + if (sm_ret == 0): + #update file + updateoldips(config.save_ip_path, curr_external_ip, curr_local_ip) + print ("Saved IP address updated.") + else: + print ("Saved IP address NOT updated.") + else: - print ("Saved IP address NOT updated.") + print ("Current IP = Old IP. No need to send email.") - else: - print ("Current IP = Old IP. No need to send email.") - - sys.exit(0) + sys.exit(0) diff --git a/test_config.txt b/test_config.txt new file mode 100644 index 0000000..1f6927c --- /dev/null +++ b/test_config.txt @@ -0,0 +1,16 @@ +#IP Watch Config File + +sender=Bob Sender +sender_email=bobsender@gmail.com +sender_username=bobsender +sender_password=password1 +receiver=Tom Receiver +receiver_email=tomreceive@gmail.com +subject_line=My IP Has Changed! +machine=Test_Machine +smtp_addr=smtp.gmail.com:587 +save_ip_path=./oldip.txt +try_count=10 +ip_blacklist=192.168.0.255,192.168.0.1,192.168.1.255,192.168.1.1 +dry_run=1 +