From 94eb4d2ab4c070933f9221750dc273a5e4e8e865 Mon Sep 17 00:00:00 2001 From: Tom Vander Aa Date: Sat, 21 Jan 2023 16:04:45 +0100 Subject: [PATCH 1/9] Ignore generated files (.venv, __pycache__, ...) --- .gitignore | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .gitignore 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 From 53323e6bfc45fecadd3a2b5185da917ef7d4c229 Mon Sep 17 00:00:00 2001 From: Tom Vander Aa Date: Mon, 17 Feb 2025 09:45:17 +0100 Subject: [PATCH 2/9] pyproject: mv src files to src/ipwatch --- ipgetter.py => src/ipwatch/ipgetter.py | 0 ipwatch.py => src/ipwatch/ipwatch.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename ipgetter.py => src/ipwatch/ipgetter.py (100%) rename ipwatch.py => src/ipwatch/ipwatch.py (100%) diff --git a/ipgetter.py b/src/ipwatch/ipgetter.py similarity index 100% rename from ipgetter.py rename to src/ipwatch/ipgetter.py diff --git a/ipwatch.py b/src/ipwatch/ipwatch.py similarity index 100% rename from ipwatch.py rename to src/ipwatch/ipwatch.py From fcdc02630264fbadc903bec3de44827ca9cd37da Mon Sep 17 00:00:00 2001 From: Tom Vander Aa Date: Mon, 17 Feb 2025 09:52:24 +0100 Subject: [PATCH 3/9] pyproject: add toml --- pyproject.toml | 34 ++++++++++++++++++++++++++++++++++ src/ipwatch/__about__.py | 4 ++++ 2 files changed, 38 insertions(+) create mode 100644 pyproject.toml create mode 100644 src/ipwatch/__about__.py 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" From a9cd45ff3806e7c38ba1a3607a9e1fb4f12cb068 Mon Sep 17 00:00:00 2001 From: Tom Vander Aa Date: Mon, 17 Feb 2025 11:22:35 +0100 Subject: [PATCH 4/9] ipwatch and ipgetter: add main() --- src/ipwatch/ipgetter.py | 21 ++++--- src/ipwatch/ipwatch.py | 131 ++++++++++++++++++++-------------------- 2 files changed, 78 insertions(+), 74 deletions(-) diff --git a/src/ipwatch/ipgetter.py b/src/ipwatch/ipgetter.py index 097be7f..707d35d 100644 --- a/src/ipwatch/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/src/ipwatch/ipwatch.py b/src/ipwatch/ipwatch.py index 108e3a7..065974f 100644 --- a/src/ipwatch/ipwatch.py +++ b/src/ipwatch/ipwatch.py @@ -23,7 +23,7 @@ from pathlib import Path import re import smtplib -import ipgetter +from . import ipgetter ################ @@ -163,13 +163,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 +179,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 +232,18 @@ 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, +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): "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 + """ @@ -275,7 +275,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 +286,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) + + # 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) From bc836dc68666aabf8646637c92b2357eb7c14403 Mon Sep 17 00:00:00 2001 From: Tom Vander Aa Date: Mon, 17 Feb 2025 11:22:46 +0100 Subject: [PATCH 5/9] ipwatch: __init__.py --- src/ipwatch/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 src/ipwatch/__init__.py 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 From c5c99c928591f045d884403e27cd69d66d8b2ece Mon Sep 17 00:00:00 2001 From: Tom Vander Aa Date: Mon, 17 Feb 2025 11:46:03 +0100 Subject: [PATCH 6/9] ipwatch: fix typo (old_exernal_ip) --- src/ipwatch/ipwatch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ipwatch/ipwatch.py b/src/ipwatch/ipwatch.py index 065974f..1003370 100644 --- a/src/ipwatch/ipwatch.py +++ b/src/ipwatch/ipwatch.py @@ -232,7 +232,7 @@ 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, +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): "Function to send an email with the new IP address" From 549a47ebfc51816d027fdd17d0a4e4e50edced21 Mon Sep 17 00:00:00 2001 From: Tom Vander Aa Date: Mon, 17 Feb 2025 13:21:34 +0100 Subject: [PATCH 7/9] config: add dry_run for testing --- example_config.txt | 1 + src/ipwatch/ipwatch.py | 13 ++++++++++--- test_config.txt | 16 ++++++++++++++++ 3 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 test_config.txt 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/src/ipwatch/ipwatch.py b/src/ipwatch/ipwatch.py index 1003370..cf0a39e 100644 --- a/src/ipwatch/ipwatch.py +++ b/src/ipwatch/ipwatch.py @@ -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() @@ -233,7 +235,8 @@ def updateoldips(filepath, new_external_ip, new_local_ip): #send mail with new IP address 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): + 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) @@ -255,7 +258,6 @@ def sendmail(old_external_ip, old_local_ip, new_external_ip, new_local_ip, serve + 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_external_ip, old_local_ip, new_external_ip, new_local_ip, serve #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() @@ -329,7 +336,7 @@ def main(): 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) + 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): 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 + From b7c24f5079c4c8c9706166b921b451a067f64d9b Mon Sep 17 00:00:00 2001 From: Tom Vander Aa Date: Mon, 17 Feb 2025 13:25:27 +0100 Subject: [PATCH 8/9] ci: test pip install and cmdline --- .github/workflows/tests.yml | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .github/workflows/tests.yml 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 From 1301906c8579ea08942256e39f933ed4784f2975 Mon Sep 17 00:00:00 2001 From: Tom Vander Aa Date: Mon, 17 Feb 2025 13:47:01 +0100 Subject: [PATCH 9/9] pyproject: update README to use pip --- README.md | 51 ++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 36 insertions(+), 15 deletions(-) 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