Downloading attachments from web app












0












$begingroup$


Preface: This is my first work script, normally I'm just mucking around in console or writing snippets. This script has a lot of firsts for me (configs, logging instead of print, try/except). Typically when I just write snippets I don't care if things are breaking. Given this is set up as a scheduled task for work, I tried to make it a bit less... um... brittle.



Problem



The web app i'm logging into stores log files on a system admin profile as attachments. I needed to write a script to automatically log in and download these attachments as it only has a small retention period before they disappear.



General Plan of Action




  1. GET webapp/ page to retrieve cookies

  2. GET webapp/login to retrieve form input ids used for posting credentials


GET login response



form    -23nuyjabt3o6jyz20bze4lt19wjxqihqpzcja0kiy0osistaqb,-2jnri62v92sbs0qooyazpdfrwv2561kl49fulslch1thh43c0s,2qvew89xgsjkg0u0uhhmuxjxqp8kb71t6k827iaofkmepu7c47



  1. POST username to webapp/uuid to retrieve UUID


uuid params



userName    testuser


uuid response



uuid    c8637a56-1495-4388-888b-0c35aff86974



  1. Hash password using UUID and random string (SHA3-KACCAK)

  2. POST credentials (hashed pw, username) mapped to the random form IDS from above.


POST /login params using the form IDS from previous GET /login



-23nuyjabt3o6jyz20bze4lt19wjxqihqpzcja0kiy0osistaqb 
-2jnri62v92sbs0qooyazpdfrwv2561kl49fulslch1thh43c0s a3a3574d05e0de1fa30d898e8ac425e44be2f19b7f8119a1ae27eeb4e3d0e445679554ba12fd44f120784465bc18cc512eaababec00a4ec9c03f11b2d64208ae
2qvew89xgsjkg0u0uhhmuxjxqp8kb71t6k827iaofkmepu7c47 testuser



  1. Get the current users profile page id by finding "View My Profile" link and getting value of kUserID parameter.


  2. GET worker profile page and find all attachments


  3. Download attachments to disk if they don't already exist.


log_downloader.py



Logs into web app, navigates to own worker profile and downloads attachment files that match the whitelist.



import configparser
import logging
import requests
import sha3
from logging.handlers import RotatingFileHandler
from pathlib import Path
from urllib.parse import urlparse, parse_qsl
from bs4 import BeautifulSoup
from getpass import getpass

logger = logging.getLogger(__name__)
handler = logging.StreamHandler()
fh = RotatingFileHandler('log.txt', maxBytes=1024, backupCount=5)
formatter = logging.Formatter(
'%(asctime)s %(name)-12s %(levelname)-8s %(message)s')
handler.setFormatter(formatter)
fh.setFormatter(formatter)
logger.addHandler(handler)
logger.addHandler(fh)
logger.setLevel(logging.DEBUG)


config = configparser.ConfigParser()
config.read('config.ini')
URL = config['Penelope']['url']

sess = requests.session()


def get_uuid(username: str):
"""
Returns a UUID for the provided username.
:param username: Penelope User Name
:return: str
"""
try:
r = sess.get(f'{URL}acm_loginControl')
r = sess.post(f'{URL}acm_loginControl/uuid', {'userName': username})
_uuid = r.json()['uuid']
logger.debug(f'UUID Retrieved: {_uuid}')
return _uuid
except Exception as e:
print(e)


def digest_password(uuid: str, password: str):
"""
Digests a password using **SHA3 (Keccak)** to match Crypto.JS lib used in Penelope login
:param uuid: Generated UUID for username.
:param password: pw to be digested
:return: str
"""
h = sha3.keccak_512()

h.update(uuid.encode())
h.update('algo != null && algo !== 0'.encode()) # Random string in Penelope JS script
h.update(password.encode())

hexdigest = h.hexdigest()
logger.debug(f'Password Hashed: {hexdigest}')
return hexdigest


def login(username: str, digest: str):
if 'authentype' not in sess.cookies or 'JSESSIONID' not in sess.cookies:
try:
# Gets token from login page
r = sess.get(f'{URL}acm_loginControl')
# Gets form IDs generated by server based off timestamp
r = sess.get(f'{URL}acm_loginControl/login')

# Set Credentials to form IDS
form_ids = r.json()['form'].split(',')
creds = {form_ids[0]: '', form_ids[2]: username, form_ids[1]: digest}

# Post Credentials
logger.debug('Trying to Log In')
r = sess.post(f'{URL}acm_loginControl/login', creds)
logger.debug(r.json())
if r.json()['state'] == 'ok':
logger.debug('Logged in Successfully')
r = sess.get(f'{URL}acm_loginControl/create')
r.raise_for_status()
else:
raise ValueError(r.json()['errorCode'])
return r
except ValueError as e:
logger.error(e)

except Exception as e:
logger.error(e)

else:
logger.debug('Not Logging in')
return f'Session already exists: {sess.cookies}'


def navigate_to_workerprofile(user_id: int):
return sess.get(f'{URL}acm_userProfileControl?actionType=view&kUserID={user_id}')


def get_user_id(login_response: requests.models.Response):
"""Get Worker Profile kUserID (wruser table)"""
soup = BeautifulSoup(login_response.content, 'html.parser')
home_page_link = soup.find(id='frm_content_id')['src']
r = sess.get(f'{URL}{home_page_link}')
soup = BeautifulSoup(r.content, 'html.parser')
href = next(link.get('href') for link in soup.find_all('a') if link.text == 'View My Profile')
qs = urlparse(href)
params = dict(parse_qsl(qs.query))
logger.debug(f'kUserID Retrieved: {params["kUserID"]}')
return params['kUserID']

def find_log_attachments():
logger.debug('Searching for stdout log files')
soup = BeautifulSoup(r.content, 'html.parser')
attachment_table = soup.find(id='attachListTable_worker')
logs = [link.text for link in attachment_table.find_all('a') if link.text.startswith('stdout')]
logger.debug(logs)
return logs


def download_attachment(file_name):
download_url = f'{URL}acm_attachmentControl?actionType=download'
f'&attachCat=worker&attachCatID={kuserid}&attachName={file_name}'
logger.debug(f'Downloading {file_name}')
r = sess.get(download_url)
if 'JSESSIONID' not in sess.cookies:
raise ConnectionError
return r


def save_log_files():
log_attachments.sort()
for log_name in log_attachments:
log_file_path = Path(f'E:/Penelope Logs/RAQ.Athena-au.com/{log_name}')

if not log_file_path.exists():
try:
log_download_response = download_attachment(log_name)
with open(log_file_path, 'wb') as log_file:
log_file.write(log_download_response.content)
logger.debug(f'Log File {log_file_path} saved to disk')
except ConnectionError as e:
logger.error('Connection no longer valid. No session token.')
else:
logger.debug(f'{log_file_path} already exists')

if __name__ == '__main__':
if not config.has_option('Penelope', 'user'):
user = input('Enter Username to login with')
config['Penelope']['user'] = user
if not config.has_option('Penelope', 'password'):
password = getpass('Enter Password')
user = config['Penelope']['user']
uuid = get_uuid(user)
digest = digest_password(uuid, password)
config['Penelope']['password'] = digest
with open('config.ini', 'w') as config_file:
config.write(config_file)

user = config['Penelope']['user']
digest = config['Penelope']['password']

r = login(user, digest)
kuserid = get_user_id(r)
r = navigate_to_workerprofile(kuserid)
log_attachments = find_log_attachments()
save_log_files()


Output log file - PW already hashed / 1 new log file to DL



2019-03-01 08:00:00,969 urllib3.connectionpool DEBUG    Starting new HTTPS connection (1): mywebapp:443
2019-03-01 08:00:01,099 urllib3.connectionpool DEBUG https://mywebapp:443 "GET /acm_loginControl HTTP/1.1" 200 None
2019-03-01 08:00:01,140 urllib3.connectionpool DEBUG https://mywebapp:443 "GET /acm_loginControl/login HTTP/1.1" 200 None
2019-03-01 08:00:01,141 root DEBUG Trying to Log In
2019-03-01 08:00:01,183 urllib3.connectionpool DEBUG https://mywebapp:443 "POST /acm_loginControl/login HTTP/1.1" 200 None
2019-03-01 08:00:01,222 root DEBUG {'next': 'create', 'state': 'ok'}
2019-03-01 08:00:01,223 root DEBUG Logged in Successfully
2019-03-01 08:00:01,331 urllib3.connectionpool DEBUG https://mywebapp:443 "GET /acm_loginControl/create HTTP/1.1" 302 83
2019-03-01 08:00:01,378 urllib3.connectionpool DEBUG https://mywebapp:443 "GET /acm_topFrameControl HTTP/1.1" 200 None
2019-03-01 08:00:01,491 urllib3.connectionpool DEBUG https://mywebapp:443 "GET //acm_homepageControl?actionType=top_frame&curval=1551391201355 HTTP/1.1" 200 None
2019-03-01 08:00:01,823 root DEBUG kUserID Retrieved: 1077
2019-03-01 08:00:01,877 urllib3.connectionpool DEBUG https://mywebapp:443 "GET /acm_userProfileControl?actionType=view&kUserID=1077 HTTP/1.1" 200 None
2019-03-01 08:00:01,941 root DEBUG Searching for stdout log files
2019-03-01 08:00:01,960 root DEBUG ['stdout_2019-02-26.0.log.gz', 'stdout_2019-02-25.2.log.gz', 'stdout_2019-02-25.1.log.gz', 'stdout_2019-02-25.0.log.gz', 'stdout_2019-02-24.0.log.gz', 'stdout_2019-02-23.0.log.gz', 'stdout_2019-02-22.1.log.gz', 'stdout_2019-02-22.0.log.gz', 'stdout_2019-02-28.1.log.gz', 'stdout_2019-02-28.0.log.gz', 'stdout_2019-02-27.1.log.gz', 'stdout_2019-02-27.0.log.gz', 'stdout_2019-02-26.2.log.gz', 'stdout_2019-02-26.1.log.gz']
2019-03-01 08:00:01,960 root DEBUG E:Penelope Logsmywebappstdout_2019-02-22.0.log.gz already exists
2019-03-01 08:00:01,961 root DEBUG E:Penelope Logsmywebappstdout_2019-02-22.1.log.gz already exists
2019-03-01 08:00:01,961 root DEBUG E:Penelope Logsmywebappstdout_2019-02-23.0.log.gz already exists
2019-03-01 08:00:01,961 root DEBUG E:Penelope Logsmywebappstdout_2019-02-24.0.log.gz already exists
2019-03-01 08:00:01,962 root DEBUG E:Penelope Logsmywebappstdout_2019-02-25.0.log.gz already exists
2019-03-01 08:00:01,962 root DEBUG E:Penelope Logsmywebappstdout_2019-02-25.1.log.gz already exists
2019-03-01 08:00:01,962 root DEBUG E:Penelope Logsmywebappstdout_2019-02-25.2.log.gz already exists
2019-03-01 08:00:01,962 root DEBUG E:Penelope Logsmywebappstdout_2019-02-26.0.log.gz already exists
2019-03-01 08:00:01,963 root DEBUG E:Penelope Logsmywebappstdout_2019-02-26.1.log.gz already exists
2019-03-01 08:00:01,963 root DEBUG E:Penelope Logsmywebappstdout_2019-02-26.2.log.gz already exists
2019-03-01 08:00:01,963 root DEBUG E:Penelope Logsmywebappstdout_2019-02-27.0.log.gz already exists
2019-03-01 08:00:01,964 root DEBUG E:Penelope Logsmywebappstdout_2019-02-27.1.log.gz already exists
2019-03-01 08:00:01,964 root DEBUG E:Penelope Logsmywebappstdout_2019-02-28.0.log.gz already exists
2019-03-01 08:00:01,964 root DEBUG Downloading stdout_2019-02-28.1.log.gz
2019-03-01 08:00:01,989 urllib3.connectionpool DEBUG https://mywebapp:443 "GET /acm_attachmentControl?actionType=download&attachCat=worker&attachCatID=1077&attachName=stdout_2019-02-28.1.log.gz HTTP/1.1" 200 8964328
2019-03-01 08:00:02,509 root DEBUG Log File E:Penelope Logsmywebappstdout_2019-02-28.1.log.gz saved to disk









share|improve this question









New contributor




Solus is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.







$endgroup$

















    0












    $begingroup$


    Preface: This is my first work script, normally I'm just mucking around in console or writing snippets. This script has a lot of firsts for me (configs, logging instead of print, try/except). Typically when I just write snippets I don't care if things are breaking. Given this is set up as a scheduled task for work, I tried to make it a bit less... um... brittle.



    Problem



    The web app i'm logging into stores log files on a system admin profile as attachments. I needed to write a script to automatically log in and download these attachments as it only has a small retention period before they disappear.



    General Plan of Action




    1. GET webapp/ page to retrieve cookies

    2. GET webapp/login to retrieve form input ids used for posting credentials


    GET login response



    form    -23nuyjabt3o6jyz20bze4lt19wjxqihqpzcja0kiy0osistaqb,-2jnri62v92sbs0qooyazpdfrwv2561kl49fulslch1thh43c0s,2qvew89xgsjkg0u0uhhmuxjxqp8kb71t6k827iaofkmepu7c47



    1. POST username to webapp/uuid to retrieve UUID


    uuid params



    userName    testuser


    uuid response



    uuid    c8637a56-1495-4388-888b-0c35aff86974



    1. Hash password using UUID and random string (SHA3-KACCAK)

    2. POST credentials (hashed pw, username) mapped to the random form IDS from above.


    POST /login params using the form IDS from previous GET /login



    -23nuyjabt3o6jyz20bze4lt19wjxqihqpzcja0kiy0osistaqb 
    -2jnri62v92sbs0qooyazpdfrwv2561kl49fulslch1thh43c0s a3a3574d05e0de1fa30d898e8ac425e44be2f19b7f8119a1ae27eeb4e3d0e445679554ba12fd44f120784465bc18cc512eaababec00a4ec9c03f11b2d64208ae
    2qvew89xgsjkg0u0uhhmuxjxqp8kb71t6k827iaofkmepu7c47 testuser



    1. Get the current users profile page id by finding "View My Profile" link and getting value of kUserID parameter.


    2. GET worker profile page and find all attachments


    3. Download attachments to disk if they don't already exist.


    log_downloader.py



    Logs into web app, navigates to own worker profile and downloads attachment files that match the whitelist.



    import configparser
    import logging
    import requests
    import sha3
    from logging.handlers import RotatingFileHandler
    from pathlib import Path
    from urllib.parse import urlparse, parse_qsl
    from bs4 import BeautifulSoup
    from getpass import getpass

    logger = logging.getLogger(__name__)
    handler = logging.StreamHandler()
    fh = RotatingFileHandler('log.txt', maxBytes=1024, backupCount=5)
    formatter = logging.Formatter(
    '%(asctime)s %(name)-12s %(levelname)-8s %(message)s')
    handler.setFormatter(formatter)
    fh.setFormatter(formatter)
    logger.addHandler(handler)
    logger.addHandler(fh)
    logger.setLevel(logging.DEBUG)


    config = configparser.ConfigParser()
    config.read('config.ini')
    URL = config['Penelope']['url']

    sess = requests.session()


    def get_uuid(username: str):
    """
    Returns a UUID for the provided username.
    :param username: Penelope User Name
    :return: str
    """
    try:
    r = sess.get(f'{URL}acm_loginControl')
    r = sess.post(f'{URL}acm_loginControl/uuid', {'userName': username})
    _uuid = r.json()['uuid']
    logger.debug(f'UUID Retrieved: {_uuid}')
    return _uuid
    except Exception as e:
    print(e)


    def digest_password(uuid: str, password: str):
    """
    Digests a password using **SHA3 (Keccak)** to match Crypto.JS lib used in Penelope login
    :param uuid: Generated UUID for username.
    :param password: pw to be digested
    :return: str
    """
    h = sha3.keccak_512()

    h.update(uuid.encode())
    h.update('algo != null && algo !== 0'.encode()) # Random string in Penelope JS script
    h.update(password.encode())

    hexdigest = h.hexdigest()
    logger.debug(f'Password Hashed: {hexdigest}')
    return hexdigest


    def login(username: str, digest: str):
    if 'authentype' not in sess.cookies or 'JSESSIONID' not in sess.cookies:
    try:
    # Gets token from login page
    r = sess.get(f'{URL}acm_loginControl')
    # Gets form IDs generated by server based off timestamp
    r = sess.get(f'{URL}acm_loginControl/login')

    # Set Credentials to form IDS
    form_ids = r.json()['form'].split(',')
    creds = {form_ids[0]: '', form_ids[2]: username, form_ids[1]: digest}

    # Post Credentials
    logger.debug('Trying to Log In')
    r = sess.post(f'{URL}acm_loginControl/login', creds)
    logger.debug(r.json())
    if r.json()['state'] == 'ok':
    logger.debug('Logged in Successfully')
    r = sess.get(f'{URL}acm_loginControl/create')
    r.raise_for_status()
    else:
    raise ValueError(r.json()['errorCode'])
    return r
    except ValueError as e:
    logger.error(e)

    except Exception as e:
    logger.error(e)

    else:
    logger.debug('Not Logging in')
    return f'Session already exists: {sess.cookies}'


    def navigate_to_workerprofile(user_id: int):
    return sess.get(f'{URL}acm_userProfileControl?actionType=view&kUserID={user_id}')


    def get_user_id(login_response: requests.models.Response):
    """Get Worker Profile kUserID (wruser table)"""
    soup = BeautifulSoup(login_response.content, 'html.parser')
    home_page_link = soup.find(id='frm_content_id')['src']
    r = sess.get(f'{URL}{home_page_link}')
    soup = BeautifulSoup(r.content, 'html.parser')
    href = next(link.get('href') for link in soup.find_all('a') if link.text == 'View My Profile')
    qs = urlparse(href)
    params = dict(parse_qsl(qs.query))
    logger.debug(f'kUserID Retrieved: {params["kUserID"]}')
    return params['kUserID']

    def find_log_attachments():
    logger.debug('Searching for stdout log files')
    soup = BeautifulSoup(r.content, 'html.parser')
    attachment_table = soup.find(id='attachListTable_worker')
    logs = [link.text for link in attachment_table.find_all('a') if link.text.startswith('stdout')]
    logger.debug(logs)
    return logs


    def download_attachment(file_name):
    download_url = f'{URL}acm_attachmentControl?actionType=download'
    f'&attachCat=worker&attachCatID={kuserid}&attachName={file_name}'
    logger.debug(f'Downloading {file_name}')
    r = sess.get(download_url)
    if 'JSESSIONID' not in sess.cookies:
    raise ConnectionError
    return r


    def save_log_files():
    log_attachments.sort()
    for log_name in log_attachments:
    log_file_path = Path(f'E:/Penelope Logs/RAQ.Athena-au.com/{log_name}')

    if not log_file_path.exists():
    try:
    log_download_response = download_attachment(log_name)
    with open(log_file_path, 'wb') as log_file:
    log_file.write(log_download_response.content)
    logger.debug(f'Log File {log_file_path} saved to disk')
    except ConnectionError as e:
    logger.error('Connection no longer valid. No session token.')
    else:
    logger.debug(f'{log_file_path} already exists')

    if __name__ == '__main__':
    if not config.has_option('Penelope', 'user'):
    user = input('Enter Username to login with')
    config['Penelope']['user'] = user
    if not config.has_option('Penelope', 'password'):
    password = getpass('Enter Password')
    user = config['Penelope']['user']
    uuid = get_uuid(user)
    digest = digest_password(uuid, password)
    config['Penelope']['password'] = digest
    with open('config.ini', 'w') as config_file:
    config.write(config_file)

    user = config['Penelope']['user']
    digest = config['Penelope']['password']

    r = login(user, digest)
    kuserid = get_user_id(r)
    r = navigate_to_workerprofile(kuserid)
    log_attachments = find_log_attachments()
    save_log_files()


    Output log file - PW already hashed / 1 new log file to DL



    2019-03-01 08:00:00,969 urllib3.connectionpool DEBUG    Starting new HTTPS connection (1): mywebapp:443
    2019-03-01 08:00:01,099 urllib3.connectionpool DEBUG https://mywebapp:443 "GET /acm_loginControl HTTP/1.1" 200 None
    2019-03-01 08:00:01,140 urllib3.connectionpool DEBUG https://mywebapp:443 "GET /acm_loginControl/login HTTP/1.1" 200 None
    2019-03-01 08:00:01,141 root DEBUG Trying to Log In
    2019-03-01 08:00:01,183 urllib3.connectionpool DEBUG https://mywebapp:443 "POST /acm_loginControl/login HTTP/1.1" 200 None
    2019-03-01 08:00:01,222 root DEBUG {'next': 'create', 'state': 'ok'}
    2019-03-01 08:00:01,223 root DEBUG Logged in Successfully
    2019-03-01 08:00:01,331 urllib3.connectionpool DEBUG https://mywebapp:443 "GET /acm_loginControl/create HTTP/1.1" 302 83
    2019-03-01 08:00:01,378 urllib3.connectionpool DEBUG https://mywebapp:443 "GET /acm_topFrameControl HTTP/1.1" 200 None
    2019-03-01 08:00:01,491 urllib3.connectionpool DEBUG https://mywebapp:443 "GET //acm_homepageControl?actionType=top_frame&curval=1551391201355 HTTP/1.1" 200 None
    2019-03-01 08:00:01,823 root DEBUG kUserID Retrieved: 1077
    2019-03-01 08:00:01,877 urllib3.connectionpool DEBUG https://mywebapp:443 "GET /acm_userProfileControl?actionType=view&kUserID=1077 HTTP/1.1" 200 None
    2019-03-01 08:00:01,941 root DEBUG Searching for stdout log files
    2019-03-01 08:00:01,960 root DEBUG ['stdout_2019-02-26.0.log.gz', 'stdout_2019-02-25.2.log.gz', 'stdout_2019-02-25.1.log.gz', 'stdout_2019-02-25.0.log.gz', 'stdout_2019-02-24.0.log.gz', 'stdout_2019-02-23.0.log.gz', 'stdout_2019-02-22.1.log.gz', 'stdout_2019-02-22.0.log.gz', 'stdout_2019-02-28.1.log.gz', 'stdout_2019-02-28.0.log.gz', 'stdout_2019-02-27.1.log.gz', 'stdout_2019-02-27.0.log.gz', 'stdout_2019-02-26.2.log.gz', 'stdout_2019-02-26.1.log.gz']
    2019-03-01 08:00:01,960 root DEBUG E:Penelope Logsmywebappstdout_2019-02-22.0.log.gz already exists
    2019-03-01 08:00:01,961 root DEBUG E:Penelope Logsmywebappstdout_2019-02-22.1.log.gz already exists
    2019-03-01 08:00:01,961 root DEBUG E:Penelope Logsmywebappstdout_2019-02-23.0.log.gz already exists
    2019-03-01 08:00:01,961 root DEBUG E:Penelope Logsmywebappstdout_2019-02-24.0.log.gz already exists
    2019-03-01 08:00:01,962 root DEBUG E:Penelope Logsmywebappstdout_2019-02-25.0.log.gz already exists
    2019-03-01 08:00:01,962 root DEBUG E:Penelope Logsmywebappstdout_2019-02-25.1.log.gz already exists
    2019-03-01 08:00:01,962 root DEBUG E:Penelope Logsmywebappstdout_2019-02-25.2.log.gz already exists
    2019-03-01 08:00:01,962 root DEBUG E:Penelope Logsmywebappstdout_2019-02-26.0.log.gz already exists
    2019-03-01 08:00:01,963 root DEBUG E:Penelope Logsmywebappstdout_2019-02-26.1.log.gz already exists
    2019-03-01 08:00:01,963 root DEBUG E:Penelope Logsmywebappstdout_2019-02-26.2.log.gz already exists
    2019-03-01 08:00:01,963 root DEBUG E:Penelope Logsmywebappstdout_2019-02-27.0.log.gz already exists
    2019-03-01 08:00:01,964 root DEBUG E:Penelope Logsmywebappstdout_2019-02-27.1.log.gz already exists
    2019-03-01 08:00:01,964 root DEBUG E:Penelope Logsmywebappstdout_2019-02-28.0.log.gz already exists
    2019-03-01 08:00:01,964 root DEBUG Downloading stdout_2019-02-28.1.log.gz
    2019-03-01 08:00:01,989 urllib3.connectionpool DEBUG https://mywebapp:443 "GET /acm_attachmentControl?actionType=download&attachCat=worker&attachCatID=1077&attachName=stdout_2019-02-28.1.log.gz HTTP/1.1" 200 8964328
    2019-03-01 08:00:02,509 root DEBUG Log File E:Penelope Logsmywebappstdout_2019-02-28.1.log.gz saved to disk









    share|improve this question









    New contributor




    Solus is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
    Check out our Code of Conduct.







    $endgroup$















      0












      0








      0





      $begingroup$


      Preface: This is my first work script, normally I'm just mucking around in console or writing snippets. This script has a lot of firsts for me (configs, logging instead of print, try/except). Typically when I just write snippets I don't care if things are breaking. Given this is set up as a scheduled task for work, I tried to make it a bit less... um... brittle.



      Problem



      The web app i'm logging into stores log files on a system admin profile as attachments. I needed to write a script to automatically log in and download these attachments as it only has a small retention period before they disappear.



      General Plan of Action




      1. GET webapp/ page to retrieve cookies

      2. GET webapp/login to retrieve form input ids used for posting credentials


      GET login response



      form    -23nuyjabt3o6jyz20bze4lt19wjxqihqpzcja0kiy0osistaqb,-2jnri62v92sbs0qooyazpdfrwv2561kl49fulslch1thh43c0s,2qvew89xgsjkg0u0uhhmuxjxqp8kb71t6k827iaofkmepu7c47



      1. POST username to webapp/uuid to retrieve UUID


      uuid params



      userName    testuser


      uuid response



      uuid    c8637a56-1495-4388-888b-0c35aff86974



      1. Hash password using UUID and random string (SHA3-KACCAK)

      2. POST credentials (hashed pw, username) mapped to the random form IDS from above.


      POST /login params using the form IDS from previous GET /login



      -23nuyjabt3o6jyz20bze4lt19wjxqihqpzcja0kiy0osistaqb 
      -2jnri62v92sbs0qooyazpdfrwv2561kl49fulslch1thh43c0s a3a3574d05e0de1fa30d898e8ac425e44be2f19b7f8119a1ae27eeb4e3d0e445679554ba12fd44f120784465bc18cc512eaababec00a4ec9c03f11b2d64208ae
      2qvew89xgsjkg0u0uhhmuxjxqp8kb71t6k827iaofkmepu7c47 testuser



      1. Get the current users profile page id by finding "View My Profile" link and getting value of kUserID parameter.


      2. GET worker profile page and find all attachments


      3. Download attachments to disk if they don't already exist.


      log_downloader.py



      Logs into web app, navigates to own worker profile and downloads attachment files that match the whitelist.



      import configparser
      import logging
      import requests
      import sha3
      from logging.handlers import RotatingFileHandler
      from pathlib import Path
      from urllib.parse import urlparse, parse_qsl
      from bs4 import BeautifulSoup
      from getpass import getpass

      logger = logging.getLogger(__name__)
      handler = logging.StreamHandler()
      fh = RotatingFileHandler('log.txt', maxBytes=1024, backupCount=5)
      formatter = logging.Formatter(
      '%(asctime)s %(name)-12s %(levelname)-8s %(message)s')
      handler.setFormatter(formatter)
      fh.setFormatter(formatter)
      logger.addHandler(handler)
      logger.addHandler(fh)
      logger.setLevel(logging.DEBUG)


      config = configparser.ConfigParser()
      config.read('config.ini')
      URL = config['Penelope']['url']

      sess = requests.session()


      def get_uuid(username: str):
      """
      Returns a UUID for the provided username.
      :param username: Penelope User Name
      :return: str
      """
      try:
      r = sess.get(f'{URL}acm_loginControl')
      r = sess.post(f'{URL}acm_loginControl/uuid', {'userName': username})
      _uuid = r.json()['uuid']
      logger.debug(f'UUID Retrieved: {_uuid}')
      return _uuid
      except Exception as e:
      print(e)


      def digest_password(uuid: str, password: str):
      """
      Digests a password using **SHA3 (Keccak)** to match Crypto.JS lib used in Penelope login
      :param uuid: Generated UUID for username.
      :param password: pw to be digested
      :return: str
      """
      h = sha3.keccak_512()

      h.update(uuid.encode())
      h.update('algo != null && algo !== 0'.encode()) # Random string in Penelope JS script
      h.update(password.encode())

      hexdigest = h.hexdigest()
      logger.debug(f'Password Hashed: {hexdigest}')
      return hexdigest


      def login(username: str, digest: str):
      if 'authentype' not in sess.cookies or 'JSESSIONID' not in sess.cookies:
      try:
      # Gets token from login page
      r = sess.get(f'{URL}acm_loginControl')
      # Gets form IDs generated by server based off timestamp
      r = sess.get(f'{URL}acm_loginControl/login')

      # Set Credentials to form IDS
      form_ids = r.json()['form'].split(',')
      creds = {form_ids[0]: '', form_ids[2]: username, form_ids[1]: digest}

      # Post Credentials
      logger.debug('Trying to Log In')
      r = sess.post(f'{URL}acm_loginControl/login', creds)
      logger.debug(r.json())
      if r.json()['state'] == 'ok':
      logger.debug('Logged in Successfully')
      r = sess.get(f'{URL}acm_loginControl/create')
      r.raise_for_status()
      else:
      raise ValueError(r.json()['errorCode'])
      return r
      except ValueError as e:
      logger.error(e)

      except Exception as e:
      logger.error(e)

      else:
      logger.debug('Not Logging in')
      return f'Session already exists: {sess.cookies}'


      def navigate_to_workerprofile(user_id: int):
      return sess.get(f'{URL}acm_userProfileControl?actionType=view&kUserID={user_id}')


      def get_user_id(login_response: requests.models.Response):
      """Get Worker Profile kUserID (wruser table)"""
      soup = BeautifulSoup(login_response.content, 'html.parser')
      home_page_link = soup.find(id='frm_content_id')['src']
      r = sess.get(f'{URL}{home_page_link}')
      soup = BeautifulSoup(r.content, 'html.parser')
      href = next(link.get('href') for link in soup.find_all('a') if link.text == 'View My Profile')
      qs = urlparse(href)
      params = dict(parse_qsl(qs.query))
      logger.debug(f'kUserID Retrieved: {params["kUserID"]}')
      return params['kUserID']

      def find_log_attachments():
      logger.debug('Searching for stdout log files')
      soup = BeautifulSoup(r.content, 'html.parser')
      attachment_table = soup.find(id='attachListTable_worker')
      logs = [link.text for link in attachment_table.find_all('a') if link.text.startswith('stdout')]
      logger.debug(logs)
      return logs


      def download_attachment(file_name):
      download_url = f'{URL}acm_attachmentControl?actionType=download'
      f'&attachCat=worker&attachCatID={kuserid}&attachName={file_name}'
      logger.debug(f'Downloading {file_name}')
      r = sess.get(download_url)
      if 'JSESSIONID' not in sess.cookies:
      raise ConnectionError
      return r


      def save_log_files():
      log_attachments.sort()
      for log_name in log_attachments:
      log_file_path = Path(f'E:/Penelope Logs/RAQ.Athena-au.com/{log_name}')

      if not log_file_path.exists():
      try:
      log_download_response = download_attachment(log_name)
      with open(log_file_path, 'wb') as log_file:
      log_file.write(log_download_response.content)
      logger.debug(f'Log File {log_file_path} saved to disk')
      except ConnectionError as e:
      logger.error('Connection no longer valid. No session token.')
      else:
      logger.debug(f'{log_file_path} already exists')

      if __name__ == '__main__':
      if not config.has_option('Penelope', 'user'):
      user = input('Enter Username to login with')
      config['Penelope']['user'] = user
      if not config.has_option('Penelope', 'password'):
      password = getpass('Enter Password')
      user = config['Penelope']['user']
      uuid = get_uuid(user)
      digest = digest_password(uuid, password)
      config['Penelope']['password'] = digest
      with open('config.ini', 'w') as config_file:
      config.write(config_file)

      user = config['Penelope']['user']
      digest = config['Penelope']['password']

      r = login(user, digest)
      kuserid = get_user_id(r)
      r = navigate_to_workerprofile(kuserid)
      log_attachments = find_log_attachments()
      save_log_files()


      Output log file - PW already hashed / 1 new log file to DL



      2019-03-01 08:00:00,969 urllib3.connectionpool DEBUG    Starting new HTTPS connection (1): mywebapp:443
      2019-03-01 08:00:01,099 urllib3.connectionpool DEBUG https://mywebapp:443 "GET /acm_loginControl HTTP/1.1" 200 None
      2019-03-01 08:00:01,140 urllib3.connectionpool DEBUG https://mywebapp:443 "GET /acm_loginControl/login HTTP/1.1" 200 None
      2019-03-01 08:00:01,141 root DEBUG Trying to Log In
      2019-03-01 08:00:01,183 urllib3.connectionpool DEBUG https://mywebapp:443 "POST /acm_loginControl/login HTTP/1.1" 200 None
      2019-03-01 08:00:01,222 root DEBUG {'next': 'create', 'state': 'ok'}
      2019-03-01 08:00:01,223 root DEBUG Logged in Successfully
      2019-03-01 08:00:01,331 urllib3.connectionpool DEBUG https://mywebapp:443 "GET /acm_loginControl/create HTTP/1.1" 302 83
      2019-03-01 08:00:01,378 urllib3.connectionpool DEBUG https://mywebapp:443 "GET /acm_topFrameControl HTTP/1.1" 200 None
      2019-03-01 08:00:01,491 urllib3.connectionpool DEBUG https://mywebapp:443 "GET //acm_homepageControl?actionType=top_frame&curval=1551391201355 HTTP/1.1" 200 None
      2019-03-01 08:00:01,823 root DEBUG kUserID Retrieved: 1077
      2019-03-01 08:00:01,877 urllib3.connectionpool DEBUG https://mywebapp:443 "GET /acm_userProfileControl?actionType=view&kUserID=1077 HTTP/1.1" 200 None
      2019-03-01 08:00:01,941 root DEBUG Searching for stdout log files
      2019-03-01 08:00:01,960 root DEBUG ['stdout_2019-02-26.0.log.gz', 'stdout_2019-02-25.2.log.gz', 'stdout_2019-02-25.1.log.gz', 'stdout_2019-02-25.0.log.gz', 'stdout_2019-02-24.0.log.gz', 'stdout_2019-02-23.0.log.gz', 'stdout_2019-02-22.1.log.gz', 'stdout_2019-02-22.0.log.gz', 'stdout_2019-02-28.1.log.gz', 'stdout_2019-02-28.0.log.gz', 'stdout_2019-02-27.1.log.gz', 'stdout_2019-02-27.0.log.gz', 'stdout_2019-02-26.2.log.gz', 'stdout_2019-02-26.1.log.gz']
      2019-03-01 08:00:01,960 root DEBUG E:Penelope Logsmywebappstdout_2019-02-22.0.log.gz already exists
      2019-03-01 08:00:01,961 root DEBUG E:Penelope Logsmywebappstdout_2019-02-22.1.log.gz already exists
      2019-03-01 08:00:01,961 root DEBUG E:Penelope Logsmywebappstdout_2019-02-23.0.log.gz already exists
      2019-03-01 08:00:01,961 root DEBUG E:Penelope Logsmywebappstdout_2019-02-24.0.log.gz already exists
      2019-03-01 08:00:01,962 root DEBUG E:Penelope Logsmywebappstdout_2019-02-25.0.log.gz already exists
      2019-03-01 08:00:01,962 root DEBUG E:Penelope Logsmywebappstdout_2019-02-25.1.log.gz already exists
      2019-03-01 08:00:01,962 root DEBUG E:Penelope Logsmywebappstdout_2019-02-25.2.log.gz already exists
      2019-03-01 08:00:01,962 root DEBUG E:Penelope Logsmywebappstdout_2019-02-26.0.log.gz already exists
      2019-03-01 08:00:01,963 root DEBUG E:Penelope Logsmywebappstdout_2019-02-26.1.log.gz already exists
      2019-03-01 08:00:01,963 root DEBUG E:Penelope Logsmywebappstdout_2019-02-26.2.log.gz already exists
      2019-03-01 08:00:01,963 root DEBUG E:Penelope Logsmywebappstdout_2019-02-27.0.log.gz already exists
      2019-03-01 08:00:01,964 root DEBUG E:Penelope Logsmywebappstdout_2019-02-27.1.log.gz already exists
      2019-03-01 08:00:01,964 root DEBUG E:Penelope Logsmywebappstdout_2019-02-28.0.log.gz already exists
      2019-03-01 08:00:01,964 root DEBUG Downloading stdout_2019-02-28.1.log.gz
      2019-03-01 08:00:01,989 urllib3.connectionpool DEBUG https://mywebapp:443 "GET /acm_attachmentControl?actionType=download&attachCat=worker&attachCatID=1077&attachName=stdout_2019-02-28.1.log.gz HTTP/1.1" 200 8964328
      2019-03-01 08:00:02,509 root DEBUG Log File E:Penelope Logsmywebappstdout_2019-02-28.1.log.gz saved to disk









      share|improve this question









      New contributor




      Solus is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
      Check out our Code of Conduct.







      $endgroup$




      Preface: This is my first work script, normally I'm just mucking around in console or writing snippets. This script has a lot of firsts for me (configs, logging instead of print, try/except). Typically when I just write snippets I don't care if things are breaking. Given this is set up as a scheduled task for work, I tried to make it a bit less... um... brittle.



      Problem



      The web app i'm logging into stores log files on a system admin profile as attachments. I needed to write a script to automatically log in and download these attachments as it only has a small retention period before they disappear.



      General Plan of Action




      1. GET webapp/ page to retrieve cookies

      2. GET webapp/login to retrieve form input ids used for posting credentials


      GET login response



      form    -23nuyjabt3o6jyz20bze4lt19wjxqihqpzcja0kiy0osistaqb,-2jnri62v92sbs0qooyazpdfrwv2561kl49fulslch1thh43c0s,2qvew89xgsjkg0u0uhhmuxjxqp8kb71t6k827iaofkmepu7c47



      1. POST username to webapp/uuid to retrieve UUID


      uuid params



      userName    testuser


      uuid response



      uuid    c8637a56-1495-4388-888b-0c35aff86974



      1. Hash password using UUID and random string (SHA3-KACCAK)

      2. POST credentials (hashed pw, username) mapped to the random form IDS from above.


      POST /login params using the form IDS from previous GET /login



      -23nuyjabt3o6jyz20bze4lt19wjxqihqpzcja0kiy0osistaqb 
      -2jnri62v92sbs0qooyazpdfrwv2561kl49fulslch1thh43c0s a3a3574d05e0de1fa30d898e8ac425e44be2f19b7f8119a1ae27eeb4e3d0e445679554ba12fd44f120784465bc18cc512eaababec00a4ec9c03f11b2d64208ae
      2qvew89xgsjkg0u0uhhmuxjxqp8kb71t6k827iaofkmepu7c47 testuser



      1. Get the current users profile page id by finding "View My Profile" link and getting value of kUserID parameter.


      2. GET worker profile page and find all attachments


      3. Download attachments to disk if they don't already exist.


      log_downloader.py



      Logs into web app, navigates to own worker profile and downloads attachment files that match the whitelist.



      import configparser
      import logging
      import requests
      import sha3
      from logging.handlers import RotatingFileHandler
      from pathlib import Path
      from urllib.parse import urlparse, parse_qsl
      from bs4 import BeautifulSoup
      from getpass import getpass

      logger = logging.getLogger(__name__)
      handler = logging.StreamHandler()
      fh = RotatingFileHandler('log.txt', maxBytes=1024, backupCount=5)
      formatter = logging.Formatter(
      '%(asctime)s %(name)-12s %(levelname)-8s %(message)s')
      handler.setFormatter(formatter)
      fh.setFormatter(formatter)
      logger.addHandler(handler)
      logger.addHandler(fh)
      logger.setLevel(logging.DEBUG)


      config = configparser.ConfigParser()
      config.read('config.ini')
      URL = config['Penelope']['url']

      sess = requests.session()


      def get_uuid(username: str):
      """
      Returns a UUID for the provided username.
      :param username: Penelope User Name
      :return: str
      """
      try:
      r = sess.get(f'{URL}acm_loginControl')
      r = sess.post(f'{URL}acm_loginControl/uuid', {'userName': username})
      _uuid = r.json()['uuid']
      logger.debug(f'UUID Retrieved: {_uuid}')
      return _uuid
      except Exception as e:
      print(e)


      def digest_password(uuid: str, password: str):
      """
      Digests a password using **SHA3 (Keccak)** to match Crypto.JS lib used in Penelope login
      :param uuid: Generated UUID for username.
      :param password: pw to be digested
      :return: str
      """
      h = sha3.keccak_512()

      h.update(uuid.encode())
      h.update('algo != null && algo !== 0'.encode()) # Random string in Penelope JS script
      h.update(password.encode())

      hexdigest = h.hexdigest()
      logger.debug(f'Password Hashed: {hexdigest}')
      return hexdigest


      def login(username: str, digest: str):
      if 'authentype' not in sess.cookies or 'JSESSIONID' not in sess.cookies:
      try:
      # Gets token from login page
      r = sess.get(f'{URL}acm_loginControl')
      # Gets form IDs generated by server based off timestamp
      r = sess.get(f'{URL}acm_loginControl/login')

      # Set Credentials to form IDS
      form_ids = r.json()['form'].split(',')
      creds = {form_ids[0]: '', form_ids[2]: username, form_ids[1]: digest}

      # Post Credentials
      logger.debug('Trying to Log In')
      r = sess.post(f'{URL}acm_loginControl/login', creds)
      logger.debug(r.json())
      if r.json()['state'] == 'ok':
      logger.debug('Logged in Successfully')
      r = sess.get(f'{URL}acm_loginControl/create')
      r.raise_for_status()
      else:
      raise ValueError(r.json()['errorCode'])
      return r
      except ValueError as e:
      logger.error(e)

      except Exception as e:
      logger.error(e)

      else:
      logger.debug('Not Logging in')
      return f'Session already exists: {sess.cookies}'


      def navigate_to_workerprofile(user_id: int):
      return sess.get(f'{URL}acm_userProfileControl?actionType=view&kUserID={user_id}')


      def get_user_id(login_response: requests.models.Response):
      """Get Worker Profile kUserID (wruser table)"""
      soup = BeautifulSoup(login_response.content, 'html.parser')
      home_page_link = soup.find(id='frm_content_id')['src']
      r = sess.get(f'{URL}{home_page_link}')
      soup = BeautifulSoup(r.content, 'html.parser')
      href = next(link.get('href') for link in soup.find_all('a') if link.text == 'View My Profile')
      qs = urlparse(href)
      params = dict(parse_qsl(qs.query))
      logger.debug(f'kUserID Retrieved: {params["kUserID"]}')
      return params['kUserID']

      def find_log_attachments():
      logger.debug('Searching for stdout log files')
      soup = BeautifulSoup(r.content, 'html.parser')
      attachment_table = soup.find(id='attachListTable_worker')
      logs = [link.text for link in attachment_table.find_all('a') if link.text.startswith('stdout')]
      logger.debug(logs)
      return logs


      def download_attachment(file_name):
      download_url = f'{URL}acm_attachmentControl?actionType=download'
      f'&attachCat=worker&attachCatID={kuserid}&attachName={file_name}'
      logger.debug(f'Downloading {file_name}')
      r = sess.get(download_url)
      if 'JSESSIONID' not in sess.cookies:
      raise ConnectionError
      return r


      def save_log_files():
      log_attachments.sort()
      for log_name in log_attachments:
      log_file_path = Path(f'E:/Penelope Logs/RAQ.Athena-au.com/{log_name}')

      if not log_file_path.exists():
      try:
      log_download_response = download_attachment(log_name)
      with open(log_file_path, 'wb') as log_file:
      log_file.write(log_download_response.content)
      logger.debug(f'Log File {log_file_path} saved to disk')
      except ConnectionError as e:
      logger.error('Connection no longer valid. No session token.')
      else:
      logger.debug(f'{log_file_path} already exists')

      if __name__ == '__main__':
      if not config.has_option('Penelope', 'user'):
      user = input('Enter Username to login with')
      config['Penelope']['user'] = user
      if not config.has_option('Penelope', 'password'):
      password = getpass('Enter Password')
      user = config['Penelope']['user']
      uuid = get_uuid(user)
      digest = digest_password(uuid, password)
      config['Penelope']['password'] = digest
      with open('config.ini', 'w') as config_file:
      config.write(config_file)

      user = config['Penelope']['user']
      digest = config['Penelope']['password']

      r = login(user, digest)
      kuserid = get_user_id(r)
      r = navigate_to_workerprofile(kuserid)
      log_attachments = find_log_attachments()
      save_log_files()


      Output log file - PW already hashed / 1 new log file to DL



      2019-03-01 08:00:00,969 urllib3.connectionpool DEBUG    Starting new HTTPS connection (1): mywebapp:443
      2019-03-01 08:00:01,099 urllib3.connectionpool DEBUG https://mywebapp:443 "GET /acm_loginControl HTTP/1.1" 200 None
      2019-03-01 08:00:01,140 urllib3.connectionpool DEBUG https://mywebapp:443 "GET /acm_loginControl/login HTTP/1.1" 200 None
      2019-03-01 08:00:01,141 root DEBUG Trying to Log In
      2019-03-01 08:00:01,183 urllib3.connectionpool DEBUG https://mywebapp:443 "POST /acm_loginControl/login HTTP/1.1" 200 None
      2019-03-01 08:00:01,222 root DEBUG {'next': 'create', 'state': 'ok'}
      2019-03-01 08:00:01,223 root DEBUG Logged in Successfully
      2019-03-01 08:00:01,331 urllib3.connectionpool DEBUG https://mywebapp:443 "GET /acm_loginControl/create HTTP/1.1" 302 83
      2019-03-01 08:00:01,378 urllib3.connectionpool DEBUG https://mywebapp:443 "GET /acm_topFrameControl HTTP/1.1" 200 None
      2019-03-01 08:00:01,491 urllib3.connectionpool DEBUG https://mywebapp:443 "GET //acm_homepageControl?actionType=top_frame&curval=1551391201355 HTTP/1.1" 200 None
      2019-03-01 08:00:01,823 root DEBUG kUserID Retrieved: 1077
      2019-03-01 08:00:01,877 urllib3.connectionpool DEBUG https://mywebapp:443 "GET /acm_userProfileControl?actionType=view&kUserID=1077 HTTP/1.1" 200 None
      2019-03-01 08:00:01,941 root DEBUG Searching for stdout log files
      2019-03-01 08:00:01,960 root DEBUG ['stdout_2019-02-26.0.log.gz', 'stdout_2019-02-25.2.log.gz', 'stdout_2019-02-25.1.log.gz', 'stdout_2019-02-25.0.log.gz', 'stdout_2019-02-24.0.log.gz', 'stdout_2019-02-23.0.log.gz', 'stdout_2019-02-22.1.log.gz', 'stdout_2019-02-22.0.log.gz', 'stdout_2019-02-28.1.log.gz', 'stdout_2019-02-28.0.log.gz', 'stdout_2019-02-27.1.log.gz', 'stdout_2019-02-27.0.log.gz', 'stdout_2019-02-26.2.log.gz', 'stdout_2019-02-26.1.log.gz']
      2019-03-01 08:00:01,960 root DEBUG E:Penelope Logsmywebappstdout_2019-02-22.0.log.gz already exists
      2019-03-01 08:00:01,961 root DEBUG E:Penelope Logsmywebappstdout_2019-02-22.1.log.gz already exists
      2019-03-01 08:00:01,961 root DEBUG E:Penelope Logsmywebappstdout_2019-02-23.0.log.gz already exists
      2019-03-01 08:00:01,961 root DEBUG E:Penelope Logsmywebappstdout_2019-02-24.0.log.gz already exists
      2019-03-01 08:00:01,962 root DEBUG E:Penelope Logsmywebappstdout_2019-02-25.0.log.gz already exists
      2019-03-01 08:00:01,962 root DEBUG E:Penelope Logsmywebappstdout_2019-02-25.1.log.gz already exists
      2019-03-01 08:00:01,962 root DEBUG E:Penelope Logsmywebappstdout_2019-02-25.2.log.gz already exists
      2019-03-01 08:00:01,962 root DEBUG E:Penelope Logsmywebappstdout_2019-02-26.0.log.gz already exists
      2019-03-01 08:00:01,963 root DEBUG E:Penelope Logsmywebappstdout_2019-02-26.1.log.gz already exists
      2019-03-01 08:00:01,963 root DEBUG E:Penelope Logsmywebappstdout_2019-02-26.2.log.gz already exists
      2019-03-01 08:00:01,963 root DEBUG E:Penelope Logsmywebappstdout_2019-02-27.0.log.gz already exists
      2019-03-01 08:00:01,964 root DEBUG E:Penelope Logsmywebappstdout_2019-02-27.1.log.gz already exists
      2019-03-01 08:00:01,964 root DEBUG E:Penelope Logsmywebappstdout_2019-02-28.0.log.gz already exists
      2019-03-01 08:00:01,964 root DEBUG Downloading stdout_2019-02-28.1.log.gz
      2019-03-01 08:00:01,989 urllib3.connectionpool DEBUG https://mywebapp:443 "GET /acm_attachmentControl?actionType=download&attachCat=worker&attachCatID=1077&attachName=stdout_2019-02-28.1.log.gz HTTP/1.1" 200 8964328
      2019-03-01 08:00:02,509 root DEBUG Log File E:Penelope Logsmywebappstdout_2019-02-28.1.log.gz saved to disk






      python






      share|improve this question









      New contributor




      Solus is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
      Check out our Code of Conduct.











      share|improve this question









      New contributor




      Solus is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
      Check out our Code of Conduct.









      share|improve this question




      share|improve this question








      edited 3 mins ago







      Solus













      New contributor




      Solus is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
      Check out our Code of Conduct.









      asked 10 mins ago









      SolusSolus

      11




      11




      New contributor




      Solus is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
      Check out our Code of Conduct.





      New contributor





      Solus is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
      Check out our Code of Conduct.






      Solus is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
      Check out our Code of Conduct.






















          0






          active

          oldest

          votes











          Your Answer





          StackExchange.ifUsing("editor", function () {
          return StackExchange.using("mathjaxEditing", function () {
          StackExchange.MarkdownEditor.creationCallbacks.add(function (editor, postfix) {
          StackExchange.mathjaxEditing.prepareWmdForMathJax(editor, postfix, [["\$", "\$"]]);
          });
          });
          }, "mathjax-editing");

          StackExchange.ifUsing("editor", function () {
          StackExchange.using("externalEditor", function () {
          StackExchange.using("snippets", function () {
          StackExchange.snippets.init();
          });
          });
          }, "code-snippets");

          StackExchange.ready(function() {
          var channelOptions = {
          tags: "".split(" "),
          id: "196"
          };
          initTagRenderer("".split(" "), "".split(" "), channelOptions);

          StackExchange.using("externalEditor", function() {
          // Have to fire editor after snippets, if snippets enabled
          if (StackExchange.settings.snippets.snippetsEnabled) {
          StackExchange.using("snippets", function() {
          createEditor();
          });
          }
          else {
          createEditor();
          }
          });

          function createEditor() {
          StackExchange.prepareEditor({
          heartbeatType: 'answer',
          autoActivateHeartbeat: false,
          convertImagesToLinks: false,
          noModals: true,
          showLowRepImageUploadWarning: true,
          reputationToPostImages: null,
          bindNavPrevention: true,
          postfix: "",
          imageUploader: {
          brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
          contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
          allowUrls: true
          },
          onDemand: true,
          discardSelector: ".discard-answer"
          ,immediatelyShowMarkdownHelp:true
          });


          }
          });






          Solus is a new contributor. Be nice, and check out our Code of Conduct.










          draft saved

          draft discarded


















          StackExchange.ready(
          function () {
          StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f214513%2fdownloading-attachments-from-web-app%23new-answer', 'question_page');
          }
          );

          Post as a guest















          Required, but never shown

























          0






          active

          oldest

          votes








          0






          active

          oldest

          votes









          active

          oldest

          votes






          active

          oldest

          votes








          Solus is a new contributor. Be nice, and check out our Code of Conduct.










          draft saved

          draft discarded


















          Solus is a new contributor. Be nice, and check out our Code of Conduct.













          Solus is a new contributor. Be nice, and check out our Code of Conduct.












          Solus is a new contributor. Be nice, and check out our Code of Conduct.
















          Thanks for contributing an answer to Code Review Stack Exchange!


          • Please be sure to answer the question. Provide details and share your research!

          But avoid



          • Asking for help, clarification, or responding to other answers.

          • Making statements based on opinion; back them up with references or personal experience.


          Use MathJax to format equations. MathJax reference.


          To learn more, see our tips on writing great answers.




          draft saved


          draft discarded














          StackExchange.ready(
          function () {
          StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f214513%2fdownloading-attachments-from-web-app%23new-answer', 'question_page');
          }
          );

          Post as a guest















          Required, but never shown





















































          Required, but never shown














          Required, but never shown












          Required, but never shown







          Required, but never shown

































          Required, but never shown














          Required, but never shown












          Required, but never shown







          Required, but never shown







          Popular posts from this blog

          Сан-Квентин

          Алькесар

          Josef Freinademetz