First draft taking code from Walter Huf and adding pieces of the JIRACookieAuth (early drafts, needs revision) to handle relogins. Original repo was https://bitbucket.org/geekytwink/dudesnude_mail.git for reference to Walter's code.master
<?xml version="1.0" encoding="UTF-8"?> | |||||
<module type="PYTHON_MODULE" version="4"> | |||||
<component name="NewModuleRootManager"> | |||||
<content url="file://$MODULE_DIR$" /> | |||||
<orderEntry type="inheritedJdk" /> | |||||
<orderEntry type="sourceFolder" forTests="false" /> | |||||
</component> | |||||
<component name="PackageRequirementsSettings"> | |||||
<option name="requirementsPath" value="" /> | |||||
</component> | |||||
<component name="TestRunnerService"> | |||||
<option name="projectConfiguration" value="Twisted Trial" /> | |||||
<option name="PROJECT_TEST_RUNNER" value="Twisted Trial" /> | |||||
</component> | |||||
</module> |
<?xml version="1.0" encoding="UTF-8"?> | |||||
<project version="4"> | |||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 2.7" project-jdk-type="Python SDK" /> | |||||
</project> |
<?xml version="1.0" encoding="UTF-8"?> | |||||
<project version="4"> | |||||
<component name="ProjectModuleManager"> | |||||
<modules> | |||||
<module fileurl="file://$PROJECT_DIR$/.idea/dudesnude_client.iml" filepath="$PROJECT_DIR$/.idea/dudesnude_client.iml" /> | |||||
</modules> | |||||
</component> | |||||
</project> |
<?xml version="1.0" encoding="UTF-8"?> | |||||
<project version="4"> | |||||
<component name="ChangeListManager"> | |||||
<list default="true" id="b72e990d-b17a-470e-b0c4-327d1e6d5129" name="Default" comment="" /> | |||||
<option name="EXCLUDED_CONVERTED_TO_IGNORED" value="true" /> | |||||
<option name="TRACKING_ENABLED" value="true" /> | |||||
<option name="SHOW_DIALOG" value="false" /> | |||||
<option name="HIGHLIGHT_CONFLICTS" value="true" /> | |||||
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" /> | |||||
<option name="LAST_RESOLUTION" value="IGNORE" /> | |||||
</component> | |||||
<component name="FileEditorManager"> | |||||
<leaf> | |||||
<file leaf-file-name="client.py" pinned="false" current-in-tab="true"> | |||||
<entry file="file://$PROJECT_DIR$/client.py"> | |||||
<provider selected="true" editor-type-id="text-editor" /> | |||||
</entry> | |||||
</file> | |||||
</leaf> | |||||
</component> | |||||
<component name="ProjectFrameBounds" extendedState="1"> | |||||
<option name="x" value="140" /> | |||||
<option name="y" value="20" /> | |||||
<option name="width" value="1400" /> | |||||
<option name="height" value="990" /> | |||||
</component> | |||||
<component name="ProjectView"> | |||||
<navigator proportions="" version="1"> | |||||
<foldersAlwaysOnTop value="true" /> | |||||
</navigator> | |||||
<panes> | |||||
<pane id="Scope" /> | |||||
<pane id="ProjectPane"> | |||||
<subPane> | |||||
<expand> | |||||
<path> | |||||
<item name="dudesnude_client" type="b2602c69:ProjectViewProjectNode" /> | |||||
<item name="dudesnude_client" type="462c0819:PsiDirectoryNode" /> | |||||
</path> | |||||
</expand> | |||||
<select /> | |||||
</subPane> | |||||
</pane> | |||||
</panes> | |||||
</component> | |||||
<component name="PropertiesComponent"> | |||||
<property name="last_opened_file_path" value="$PROJECT_DIR$" /> | |||||
<property name="settings.editor.selected.configurable" value="preferences.startup.tasks" /> | |||||
</component> | |||||
<component name="RunDashboard"> | |||||
<option name="ruleStates"> | |||||
<list> | |||||
<RuleState> | |||||
<option name="name" value="ConfigurationTypeDashboardGroupingRule" /> | |||||
</RuleState> | |||||
<RuleState> | |||||
<option name="name" value="StatusDashboardGroupingRule" /> | |||||
</RuleState> | |||||
</list> | |||||
</option> | |||||
</component> | |||||
<component name="SvnConfiguration"> | |||||
<configuration /> | |||||
</component> | |||||
<component name="TaskManager"> | |||||
<task active="true" id="Default" summary="Default task"> | |||||
<changelist id="b72e990d-b17a-470e-b0c4-327d1e6d5129" name="Default" comment="" /> | |||||
<created>1530625076390</created> | |||||
<option name="number" value="Default" /> | |||||
<option name="presentableId" value="Default" /> | |||||
<updated>1530625076390</updated> | |||||
</task> | |||||
<servers /> | |||||
</component> | |||||
<component name="ToolWindowManager"> | |||||
<frame x="140" y="20" width="1400" height="990" extended-state="0" /> | |||||
<editor active="true" /> | |||||
<layout> | |||||
<window_info active="true" content_ui="combo" id="Project" order="0" visible="true" weight="0.25" /> | |||||
<window_info anchor="bottom" id="TODO" order="6" /> | |||||
<window_info anchor="bottom" id="Event Log" side_tool="true" /> | |||||
<window_info anchor="bottom" id="Run" order="2" /> | |||||
<window_info anchor="bottom" id="Version Control" show_stripe_button="false" /> | |||||
<window_info anchor="bottom" id="Python Console" /> | |||||
<window_info id="Structure" order="1" side_tool="true" weight="0.25" /> | |||||
<window_info anchor="bottom" id="Terminal" /> | |||||
<window_info anchor="bottom" id="Debug" order="3" weight="0.4" /> | |||||
<window_info id="Favorites" side_tool="true" /> | |||||
<window_info anchor="bottom" id="Find" order="1" /> | |||||
<window_info anchor="right" id="Commander" internal_type="SLIDING" order="0" type="SLIDING" weight="0.4" /> | |||||
<window_info anchor="bottom" id="Inspection" order="5" weight="0.4" /> | |||||
<window_info anchor="right" content_ui="combo" id="Hierarchy" order="2" weight="0.25" /> | |||||
<window_info anchor="right" id="Ant Build" order="1" weight="0.25" /> | |||||
<window_info anchor="bottom" id="Cvs" order="4" weight="0.25" /> | |||||
<window_info anchor="bottom" id="Message" order="0" /> | |||||
</layout> | |||||
</component> | |||||
<component name="VcsContentAnnotationSettings"> | |||||
<option name="myLimit" value="2678400000" /> | |||||
</component> | |||||
<component name="editorHistoryManager"> | |||||
<entry file="file://$PROJECT_DIR$/client.py"> | |||||
<provider selected="true" editor-type-id="text-editor" /> | |||||
</entry> | |||||
</component> | |||||
<component name="masterDetails"> | |||||
<states> | |||||
<state key="ScopeChooserConfigurable.UI"> | |||||
<settings> | |||||
<splitter-proportions> | |||||
<option name="proportions"> | |||||
<list> | |||||
<option value="0.2" /> | |||||
</list> | |||||
</option> | |||||
</splitter-proportions> | |||||
</settings> | |||||
</state> | |||||
</states> | |||||
</component> | |||||
</project> |
#!/usr/bin/env python3 | |||||
# -*- coding: utf-8 -*- | |||||
# Standard library imports | |||||
import argparse | |||||
import errno | |||||
import logging | |||||
import os | |||||
import re | |||||
import threading | |||||
# Related third party imports (If you used pip/apt/yum to install) | |||||
import arrow | |||||
from bs4 import BeautifulSoup as bs | |||||
from expand_path import expand_path | |||||
import requests | |||||
from requests.auth import AuthBase | |||||
from shm_dict import SHMDict | |||||
__author__ = "Nate Bohman" | |||||
__license__ = "GPL-3" | |||||
__version__ = "1.1.0" | |||||
__maintainer__ = "Nate Bohman" | |||||
__email__ = "natrinicle@gmail.com" | |||||
logger = logging.getLogger(__name__) | |||||
try: | |||||
unichr | |||||
except NameError: | |||||
unichr = chr | |||||
try: | |||||
unicode | |||||
except NameError: | |||||
unicode = str | |||||
DEFAULT_CFGDIR_BASE = expand_path("~/.config/dudesnude") | |||||
LOGIN_URL = {"default": "https://dudesnude.com/login.php", | |||||
"guest": "http://dudesnude.com/warning.php"} | |||||
LOGIN_INPUT_RGX = re.compile(r'<input.*name="(?P<name>[^"]+)".*value="(?P<value>[^"]+)"', re.I) | |||||
class DudesNudeCookieAuth(AuthBase): | |||||
def __init__(self, email, password="", cookie_dir=DEFAULT_CFGDIR_BASE): | |||||
self.cookie_dict = None | |||||
self._cookie_dir = "" | |||||
self.cookie_dir = cookie_dir | |||||
self._email = "" | |||||
self.email = email | |||||
self.password = password | |||||
self.session = requests.Session() | |||||
@property | |||||
def email(self): | |||||
return self._email | |||||
@email.setter | |||||
def email(self, value): | |||||
if self._email != value: | |||||
self._email = value | |||||
self.cookie_dict = SHMDict(name=self.cookie_file, | |||||
size=8192, is_file=True) | |||||
@property | |||||
def cookie_dir(self): | |||||
return self._cookie_dir | |||||
@cookie_dir.setter | |||||
def cookie_dir(self, value): | |||||
value = expand_path(value) | |||||
self._cookie_dir = value | |||||
try: | |||||
os.makedirs(value, mode=0o750) | |||||
except OSError as e: | |||||
if e.errno != errno.EEXIST: | |||||
raise | |||||
@property | |||||
def cookie_file(self): | |||||
return "{}/{}_cookies".format(self.cookie_dir, | |||||
self.email) | |||||
@property | |||||
def login_form_fields(self): | |||||
r = self.session.get(LOGIN_URL.get("default")) | |||||
self.cookie_dict['cookies'] = self.session.cookies | |||||
login_form_fields = {} | |||||
if LOGIN_INPUT_RGX.search(r.text): | |||||
for login_input in LOGIN_INPUT_RGX.finditer(r.text): | |||||
login_input = login_input.groupdict() | |||||
login_form_fields[login_input.get("name")] = login_input.get("value") | |||||
return login_form_fields | |||||
@property | |||||
def login_data(self): | |||||
if self.email == "guest": | |||||
return {'go': '1', 'iagree': '1', 'token': '', 'win_x': '1080', 'win_y': '720'} | |||||
else: | |||||
login_form_fields = self.login_form_fields | |||||
assert login_form_fields.get('s_token') is not None | |||||
assert self.cookie_dict['cookies'].get('PHPSESSID') is not None | |||||
login_data = { | |||||
'PHPSESSID': self.cookie_dict['cookies'].get('PHPSESSID'), | |||||
's_token': login_form_fields.get('s_token'), | |||||
'email': self.email, | |||||
'pass': self.password, | |||||
'logon': '1', | |||||
'seeking': '0', # 0 -, 1 chat only, 2 hookups | |||||
'tz': 0, # Force GMT | |||||
'win_x': '1080', # Force desktop | |||||
'win_y': '720', # Force desktop | |||||
} | |||||
return login_data | |||||
def login(self): | |||||
with self.cookie_dict.exclusive_lock(): | |||||
if self.cookie_dict.get('last_refresh') is None: | |||||
log_message = "Creating cookie {}".format(self.cookie_file) | |||||
else: | |||||
log_message = "Updating cookie that was last refreshed {}".format( | |||||
self.cookie_dict.get('last_refresh').humanize()) | |||||
logger.info(log_message) | |||||
r = self.session.post(LOGIN_URL.get(self.email, LOGIN_URL.get("default")), | |||||
data=self.login_data) | |||||
# Check for HTTP error codes | |||||
r.raise_for_status() | |||||
# Check for bad URL | |||||
if self.email == "guest" and r.url != 'http://dudesnude.com/search_menu.php': | |||||
raise Exception('Failure to log in') | |||||
if self.email != "guest" and r.url == 'http://dudesnude.com/login.php': | |||||
raise Exception('Failure to log in') | |||||
self.cookie_dict['cookies'] = self.session.cookies | |||||
self.cookie_dict['last_refresh'] = arrow.utcnow() | |||||
def handle_relogin(self, r, **kwargs): | |||||
"""Handle logging back in if response URL indicates not logged in.""" | |||||
if not r.url == 'http://dudesnude.com/enable_cookies.php': | |||||
return r | |||||
self.login() | |||||
def __call__(self, req): | |||||
"""Handle every call through requests to webpage.""" | |||||
# Login if cookies dict doesn't exist | |||||
if self.cookie_dict.get('last_refresh') is None: | |||||
self.login() | |||||
# Clean up, since we can't generate new cookies otherwise | |||||
if 'Cookie' in req.headers: | |||||
del req.headers['Cookie'] | |||||
# Add saved cookies to request | |||||
req.prepare_cookies(self.cookie_dict.get('cookies')) | |||||
# Register a method that is called after every request | |||||
# to ensure that we don't have to login. | |||||
req.register_hook('response', self.handle_relogin) | |||||
return req | |||||
class DudesNudeClient(object): | |||||
def __init__(self, email, password, config_dir=DEFAULT_CFGDIR_BASE): | |||||
self._config_dir = "" | |||||
self.config_dir = config_dir | |||||
#self.guest_session = requests.Session() | |||||
#self.guest_session.auth = DudesNudeCookieAuth(email="guest") | |||||
self.session = requests.Session() | |||||
self.session.auth = DudesNudeCookieAuth(email=email, password=password) |