Задался тут вопросом, как можно обойтись без статического IP для экспериментов в домашних условиях. Наткнулся на вот эту статью.
Если вы хотите развернуть свой вебсервер с доступом извне, а платить провайдеру за статический IP не хотите, то данное решение вполне себе выход, которое можно в дальнейшем подогнать под свои нужды.
Недавно я решил установить веб-сервер на Rasberry Pi, работающий за моим домашним маршрутизатором. У меня есть опыт настройки веб-серверов на виртуальных машинах Linux со статическими IP-адресами, поэтому в настройке сервера apache2 на Pi нет ничего особенного, и в Интернете есть сотни руководств. Проблема только в том, что при изменении внешнего IP вашего роутера сайт сломается! Один из способов решить эту проблему — заплатить своему интернет-провайдеру за статический IP-адрес… но тогда в чём веселье?!
Во многих своих последних проектах я использовал Google Domains для покупки доменного имени и обычно запускал сайты без собственных гугловских серверов имен на отдельном VPS/«облачном сервере». Но для моего домашнего сервера я решил использовать невероятно простую в настройке функцию переадресации IP-адресов поддоменов, чтобы перенаправить базовый домен (@.example.com) на IP-адрес моего маршрутизатора (а затем настроить мой маршрутизатор на перенаправление с http портов на https порты на Pi). Тогда мне пришлось бы вручную проверять, работает ли сайт, и менять IP при необходимости. Очевидно, это не идеальный план, поэтому я решил написать скрипт для автоматизации этого процесса.
IP = requests.get('https://api.ipify.org').text
Вот и все, это ваш внешний IP. Легко, правда? Есть много других подобных способов получить внешний IP-адрес, но я считаю, что это самый простой.
Итак, что еще нам нужно для нашего скрипта? Нам нужно где-то хранить IP-адрес, чтобы проверять будущие запросы. Изначально я решил сохранить его в простом текстовом файле. Нам также нужен способ либо уведомить пользователя, либо обновить domain.google.com, либо и то, и другое. Первый вариант довольно легко реализовать с помощью библиотеки электронной почты для Python, поэтому я настроил её для работы с моей учетной записью Gmail следующим образом (вам нужно будет разрешить менее безопасные приложения (less secure apps) в своей учетной записи Google):
from email.message import EmailMessage
def send_notification(new_ip):
msg = EmailMessage()
msg.set_content(f'IP has changed!\nNew IP: {new_ip}')
msg['Subject'] = 'IP CHANGED!'
msg['From'] = GMAIL_USER
msg['To'] = GMAIL_USER
try:
server = smtplib.SMTP_SSL('smtp.gmail.com', 465)
server.ehlo()
server.login(GMAIL_USER, GMAIL_PASSWORD)
server.send_message(msg)
server.close()
log_msg = 'Email notification sent to %s' % GMAIL_USER)
logging.info(log_msg)
except (MessageError, ConnectionError) as e:
log_msg = 'Something went wrong: %s' % e
logging.warning(log_msg)
Одна вещь, которая немного беспокоит здесь, — это сохранение вашего пароля от учетки Gmail в переменной, поэтому я решил на этом этапе хотя бы немного зашифровать его, добавив один уровень кодирования к паролю перед его сохранением. Опять же, есть много способов, которые могут предложить более или менее сносную защиту, но я решил, что для моих целей будет в достаточной мере надежно, если пароль будет закодирован в base64 при сохранении на сервере, поэтому я написал следующие функций: одна для кодирования/сохранения пароля и одна для получения/декодирования сохраненного пароля:
def enc_pwd():
pwd = base64.b64encode(getpass("What's your email password?: ").encode("utf-8"))
with open('cred.txt', 'wb') as f:
f.write(pwd)
def read_pwd():
if os.path.isfile(f"{CWD}/cred.txt"):
with open(f'{CWD}/cred.txt', 'r') as f:
if f.read():
password = base64.b64decode(pwd).decode('utf-8')
logging.info('Password read successfully')
return password
else:
enc_pwd()
read_pwd()
else:
enc_pwd()
read_pwd()
Теперь я мог бы заменить константу GMAIL_PASSWORD из функции уведомления на функцию read_pwd() (которая возвращает декодированный пароль), что всяко безопаснее.
Последнее, что мне нужно, чтобы программа стала эффективной, — это дописать функцию, которая свяжет всё вместе и сохранит/сравнит IP-адреса:
def check_ip():
if os.path.isfile('ip.txt'):
# Снова проверим предыдущий IP
with open('ip.txt', 'r') as rf:
line = rf.readlines()
if not line:
first_run = True
elif line[0] == IP:
first_run = False
change = False
else:
first_run = False
change = True
else:
first_run = True
if first_run or change:
# Запишем новый IP в файл
with open('ip.txt', 'w') as wf:
if first_run:
wf.write(IP)
elif change:
wf.write(IP)
# Уведомим пользователя:
send_notification(IP)
time.sleep(21600) # в окончательной версии на моем боевом веб-сервере я удалил это 6-часовое ожидание и рекурсивный вызов в конце и просто добавил скрипт в crontab для запуска один раз в час. Мне также пришлось сделать все пути к файлам абсолютными, поскольку по умолчанию для crontab рабочий каталог - '/'.
check_ip()
Бум! Это была первая реализация моей идеи. Когда я получаю уведомление по электронной почте, я могу просто изменить правила переадресации на сайте domains.google.com…
Стоп, что? Мне все еще нужно это делать самому? Конечно нет, есть способ проще!
Что ж, оказывается, есть два способа автоматизировать это — один значительно менее эффективный, но бесконечно более захватывающий, а другой довольно скучный, но бесконечно более эффективный! Я начну, как и сделал, с захватывающего и сложного способа: слышали когда-нибудь о Selenium для автоматизации тестирования веб-сайтов? Я решил написать сценарий, который будет физически входить в домены Google и перемещаться по сайту, чтобы изменить правила для меня, если и когда изменится IP. Однако это приводило к следующим проблемам:
Прежде всего, Google Domains (и другие сервисы Google, такие как Gmail) довольно хорошо обнаруживают автоматизацию браузера и просто не позволяют вам войти в систему через веб-драйвер Selenium. Мне посчастливилось найти этот ответ (есть и другие решения) в Stack Overflow, который предлагал войти в сам Stack Overflow с вашей учетной записью Google, прежде чем переключаться на желаемую службу Google, что действительно работало, пока я не брал под контроль браузер и при необходимости не вводил капчу. Со скриптом, запущенным на моем ноутбуке или десктопе, это замечательно — тем более, что после первоначальной проверки Captcha StackOverflow, похоже, больше не проверял тот же компьютер, поэтому после этого веб-драйвер можно было запустить в автономном режиме, но это не подходит для веб-сервера! Хотя я мог заставить веб-драйвер нормально работать с помощью PyVirtualDisplay и Geckodriver (веб-драйвер для Firefox), ему удавалось только один раз попасть на желаемую страницу «Мои домены», а в других случаях вообще не мог войти в систему из-за Captcha.
После того, как я провозился с этим и пытался дебежить/работать над этим полдня, я решил, что это безнадежное дело, так как в любом случае всё, что потребуется, — это немного изменить одну из страниц, на которой я просматривал информацию, и вся работа скрипта будет нарушена. Поэтому я решил прибегнуть к скучному методу, который до сих пор я по глупости не изучал очень глубоко.
Так уж получилось, что у Google Domains есть API именно для той цели, которая мне нужна. Я изначально отклонил APi, когда не смог заставить его работать с правилами переадресации поддоменов, которые я использовал, не понимая, что API предназначен для изменения правил динамического DNS — эта документация стала намного более понятной после того, как я настроил запись динамического DNS вместо стандартного правила «Переадресация поддоменов» (спасибо Джереми Гейлу за его отличный урок о том, как сделать что-то подобное с помощью простого сценария bash, который помог мне победить эту проблему!)
Раздел «Synthetic records» на странице конфигурации DNS Google Domains должен выглядеть следующим образом:
Затем вы получите автоматически сгенерированные учетные данные, необходимые для аутентификации с помощью API:
Теперь, зная этот метод, я смог значительно уменьшить длину и нагрузку на скрипт! TTL динамического DNS также очень низок, по умолчанию: 1 мин, что сокращает возможное время простоя — ещё один дополнительный бонус. Всё, что мне было нужно, это включить вызов API в мою функцию check_ip(), и он сделает все за меня! И эта единственная строка кода (2 с вызовом логирования) выглядит так:
req = requests.post(f'https://<auto_generated_username>:<auto_generated_password>@domains.google.com/nic/update?hostname=@.example.com&myip={IP}')
logging.info(req.content)
Я заключил всё в блок try / except, чтобы перехватить любые исключения, и таким образом этот довольно простой, но удивительно полезный скрипт был почти готов. Теперь осталось только добавить его в crontab на моем веб-сервере и дать ему работать, и (теоретически) мне больше не придется беспокоиться о смене IP!
Так, вот, о чем я позаботился! Чтобы сделать этот скрипт универсально доступным, я реорганизовал всё, чтобы использовать более объектно-ориентированный подход, основанный на классах, с классами User и IpChanger. Класс User содержит все учетные данные для входа в smtp и вызова API domains.google, а первый созданный экземпляр вместе с атрибутом previous_ip, для проверки, сохраняется в одном pickle файле (аналогично сериализации в JavaScript); в будущих экземплярах класса User() пользовательский экземпляр создается из pickle, если не указано иное. Класс User также содержит метод уведомления пользователя по электронной почте, который теперь включает условие ошибки на случай сбоя вызова API. Затем класс IpChanger вызывает класс User() каждый раз, когда он создается, а также выполняет все различные проверки и запросы, поэтому вы просто инициализируете класс IpChanger() и всё; он работает! Я также добавил несколько различных вариантов для уведомлений по электронной почте, более конкретную обработку ошибок/логирование, некоторые аргументы/параметры командной строки для управления профилем пользователя, а также смог избавиться от неуклюжей обработки файла ip.txt, сохранив всё в одном пользовательском экземпляре.
Готовый, практически уникальный (!) результат можно увидеть ниже во всей его полноте, а также его можно форкнуть/клонировать с моего GitHub (включая README.md с инструкциями по установке). Я также выпустил его как пакет Python на pypi.org, который вы можете просмотреть здесь или установить обычным способом с помощью: pip install domains-api
Источник статьи: https://habr.com/ru/post/557126/
Если вы хотите развернуть свой вебсервер с доступом извне, а платить провайдеру за статический IP не хотите, то данное решение вполне себе выход, которое можно в дальнейшем подогнать под свои нужды.
Недавно я решил установить веб-сервер на Rasberry Pi, работающий за моим домашним маршрутизатором. У меня есть опыт настройки веб-серверов на виртуальных машинах Linux со статическими IP-адресами, поэтому в настройке сервера apache2 на Pi нет ничего особенного, и в Интернете есть сотни руководств. Проблема только в том, что при изменении внешнего IP вашего роутера сайт сломается! Один из способов решить эту проблему — заплатить своему интернет-провайдеру за статический IP-адрес… но тогда в чём веселье?!
Во многих своих последних проектах я использовал Google Domains для покупки доменного имени и обычно запускал сайты без собственных гугловских серверов имен на отдельном VPS/«облачном сервере». Но для моего домашнего сервера я решил использовать невероятно простую в настройке функцию переадресации IP-адресов поддоменов, чтобы перенаправить базовый домен (@.example.com) на IP-адрес моего маршрутизатора (а затем настроить мой маршрутизатор на перенаправление с http портов на https порты на Pi). Тогда мне пришлось бы вручную проверять, работает ли сайт, и менять IP при необходимости. Очевидно, это не идеальный план, поэтому я решил написать скрипт для автоматизации этого процесса.
Есть много способов проверить ваш внешний IP-адрес, и один из самых простых, которые я нашел, был через ipify API (api.ipify.org), который просто возвращает ваш общедоступный IP-адрес. При использовании Python библиотеки requests запрос выглядит следующим образом:На заметку, вы можете найти полный скрипт на гитхабе или импортировать пакет Python с помощью pip install domains-api
IP = requests.get('https://api.ipify.org').text
Вот и все, это ваш внешний IP. Легко, правда? Есть много других подобных способов получить внешний IP-адрес, но я считаю, что это самый простой.
Итак, что еще нам нужно для нашего скрипта? Нам нужно где-то хранить IP-адрес, чтобы проверять будущие запросы. Изначально я решил сохранить его в простом текстовом файле. Нам также нужен способ либо уведомить пользователя, либо обновить domain.google.com, либо и то, и другое. Первый вариант довольно легко реализовать с помощью библиотеки электронной почты для Python, поэтому я настроил её для работы с моей учетной записью Gmail следующим образом (вам нужно будет разрешить менее безопасные приложения (less secure apps) в своей учетной записи Google):
from email.message import EmailMessage
def send_notification(new_ip):
msg = EmailMessage()
msg.set_content(f'IP has changed!\nNew IP: {new_ip}')
msg['Subject'] = 'IP CHANGED!'
msg['From'] = GMAIL_USER
msg['To'] = GMAIL_USER
try:
server = smtplib.SMTP_SSL('smtp.gmail.com', 465)
server.ehlo()
server.login(GMAIL_USER, GMAIL_PASSWORD)
server.send_message(msg)
server.close()
log_msg = 'Email notification sent to %s' % GMAIL_USER)
logging.info(log_msg)
except (MessageError, ConnectionError) as e:
log_msg = 'Something went wrong: %s' % e
logging.warning(log_msg)
Одна вещь, которая немного беспокоит здесь, — это сохранение вашего пароля от учетки Gmail в переменной, поэтому я решил на этом этапе хотя бы немного зашифровать его, добавив один уровень кодирования к паролю перед его сохранением. Опять же, есть много способов, которые могут предложить более или менее сносную защиту, но я решил, что для моих целей будет в достаточной мере надежно, если пароль будет закодирован в base64 при сохранении на сервере, поэтому я написал следующие функций: одна для кодирования/сохранения пароля и одна для получения/декодирования сохраненного пароля:
def enc_pwd():
pwd = base64.b64encode(getpass("What's your email password?: ").encode("utf-8"))
with open('cred.txt', 'wb') as f:
f.write(pwd)
def read_pwd():
if os.path.isfile(f"{CWD}/cred.txt"):
with open(f'{CWD}/cred.txt', 'r') as f:
if f.read():
password = base64.b64decode(pwd).decode('utf-8')
logging.info('Password read successfully')
return password
else:
enc_pwd()
read_pwd()
else:
enc_pwd()
read_pwd()
Теперь я мог бы заменить константу GMAIL_PASSWORD из функции уведомления на функцию read_pwd() (которая возвращает декодированный пароль), что всяко безопаснее.
Последнее, что мне нужно, чтобы программа стала эффективной, — это дописать функцию, которая свяжет всё вместе и сохранит/сравнит IP-адреса:
def check_ip():
if os.path.isfile('ip.txt'):
# Снова проверим предыдущий IP
with open('ip.txt', 'r') as rf:
line = rf.readlines()
if not line:
first_run = True
elif line[0] == IP:
first_run = False
change = False
else:
first_run = False
change = True
else:
first_run = True
if first_run or change:
# Запишем новый IP в файл
with open('ip.txt', 'w') as wf:
if first_run:
wf.write(IP)
elif change:
wf.write(IP)
# Уведомим пользователя:
send_notification(IP)
time.sleep(21600) # в окончательной версии на моем боевом веб-сервере я удалил это 6-часовое ожидание и рекурсивный вызов в конце и просто добавил скрипт в crontab для запуска один раз в час. Мне также пришлось сделать все пути к файлам абсолютными, поскольку по умолчанию для crontab рабочий каталог - '/'.
check_ip()
Бум! Это была первая реализация моей идеи. Когда я получаю уведомление по электронной почте, я могу просто изменить правила переадресации на сайте domains.google.com…
Стоп, что? Мне все еще нужно это делать самому? Конечно нет, есть способ проще!
Что ж, оказывается, есть два способа автоматизировать это — один значительно менее эффективный, но бесконечно более захватывающий, а другой довольно скучный, но бесконечно более эффективный! Я начну, как и сделал, с захватывающего и сложного способа: слышали когда-нибудь о Selenium для автоматизации тестирования веб-сайтов? Я решил написать сценарий, который будет физически входить в домены Google и перемещаться по сайту, чтобы изменить правила для меня, если и когда изменится IP. Однако это приводило к следующим проблемам:
Прежде всего, Google Domains (и другие сервисы Google, такие как Gmail) довольно хорошо обнаруживают автоматизацию браузера и просто не позволяют вам войти в систему через веб-драйвер Selenium. Мне посчастливилось найти этот ответ (есть и другие решения) в Stack Overflow, который предлагал войти в сам Stack Overflow с вашей учетной записью Google, прежде чем переключаться на желаемую службу Google, что действительно работало, пока я не брал под контроль браузер и при необходимости не вводил капчу. Со скриптом, запущенным на моем ноутбуке или десктопе, это замечательно — тем более, что после первоначальной проверки Captcha StackOverflow, похоже, больше не проверял тот же компьютер, поэтому после этого веб-драйвер можно было запустить в автономном режиме, но это не подходит для веб-сервера! Хотя я мог заставить веб-драйвер нормально работать с помощью PyVirtualDisplay и Geckodriver (веб-драйвер для Firefox), ему удавалось только один раз попасть на желаемую страницу «Мои домены», а в других случаях вообще не мог войти в систему из-за Captcha.
После того, как я провозился с этим и пытался дебежить/работать над этим полдня, я решил, что это безнадежное дело, так как в любом случае всё, что потребуется, — это немного изменить одну из страниц, на которой я просматривал информацию, и вся работа скрипта будет нарушена. Поэтому я решил прибегнуть к скучному методу, который до сих пор я по глупости не изучал очень глубоко.
Так уж получилось, что у Google Domains есть API именно для той цели, которая мне нужна. Я изначально отклонил APi, когда не смог заставить его работать с правилами переадресации поддоменов, которые я использовал, не понимая, что API предназначен для изменения правил динамического DNS — эта документация стала намного более понятной после того, как я настроил запись динамического DNS вместо стандартного правила «Переадресация поддоменов» (спасибо Джереми Гейлу за его отличный урок о том, как сделать что-то подобное с помощью простого сценария bash, который помог мне победить эту проблему!)
Раздел «Synthetic records» на странице конфигурации DNS Google Domains должен выглядеть следующим образом:
Затем вы получите автоматически сгенерированные учетные данные, необходимые для аутентификации с помощью API:
Теперь, зная этот метод, я смог значительно уменьшить длину и нагрузку на скрипт! TTL динамического DNS также очень низок, по умолчанию: 1 мин, что сокращает возможное время простоя — ещё один дополнительный бонус. Всё, что мне было нужно, это включить вызов API в мою функцию check_ip(), и он сделает все за меня! И эта единственная строка кода (2 с вызовом логирования) выглядит так:
req = requests.post(f'https://<auto_generated_username>:<auto_generated_password>@domains.google.com/nic/update?hostname=@.example.com&myip={IP}')
logging.info(req.content)
Я заключил всё в блок try / except, чтобы перехватить любые исключения, и таким образом этот довольно простой, но удивительно полезный скрипт был почти готов. Теперь осталось только добавить его в crontab на моем веб-сервере и дать ему работать, и (теоретически) мне больше не придется беспокоиться о смене IP!
Стоп! … Время рефакторинга
Так, вот, о чем я позаботился! Чтобы сделать этот скрипт универсально доступным, я реорганизовал всё, чтобы использовать более объектно-ориентированный подход, основанный на классах, с классами User и IpChanger. Класс User содержит все учетные данные для входа в smtp и вызова API domains.google, а первый созданный экземпляр вместе с атрибутом previous_ip, для проверки, сохраняется в одном pickle файле (аналогично сериализации в JavaScript); в будущих экземплярах класса User() пользовательский экземпляр создается из pickle, если не указано иное. Класс User также содержит метод уведомления пользователя по электронной почте, который теперь включает условие ошибки на случай сбоя вызова API. Затем класс IpChanger вызывает класс User() каждый раз, когда он создается, а также выполняет все различные проверки и запросы, поэтому вы просто инициализируете класс IpChanger() и всё; он работает! Я также добавил несколько различных вариантов для уведомлений по электронной почте, более конкретную обработку ошибок/логирование, некоторые аргументы/параметры командной строки для управления профилем пользователя, а также смог избавиться от неуклюжей обработки файла ip.txt, сохранив всё в одном пользовательском экземпляре.
Готовый, практически уникальный (!) результат можно увидеть ниже во всей его полноте, а также его можно форкнуть/клонировать с моего GitHub (включая README.md с инструкциями по установке). Я также выпустил его как пакет Python на pypi.org, который вы можете просмотреть здесь или установить обычным способом с помощью: pip install domains-api
Источник статьи: https://habr.com/ru/post/557126/