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
@@ -0,0 +1,15 @@ | |||
<?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> |
@@ -0,0 +1,4 @@ | |||
<?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> |
@@ -0,0 +1,8 @@ | |||
<?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> |
@@ -0,0 +1,121 @@ | |||
<?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> |
@@ -0,0 +1,184 @@ | |||
#!/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) |