Handler (python 3.x urllib.request style) for web pages where cosign authentication is required.
See http://weblogin.org/ for details of the cosign authentication system.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 | import urllib.request
import urllib.parse
import getpass
class CosignPasswordMgr(object):
"""A password manager for CosignHandler objects.
"""
def newcred(self):
"""Default callback.
Ask user for username and password."""
return {'login': input('username: '),
'password': getpass.getpass()}
def __init__(self, cred=None, max_tries=5, callback=newcred):
"""Create a new CosignPasswordMgr.
Args:
cred: Initial credentials. Will be returned by the first
call to get_cred(). Should be a dictionary of the form:
{'login': username, 'password': password}
max_tries: Maximum number of times get_cred() may be called
before IndexError is raised.
callback: A function to be called to get new
credentials. The current object instance (self) will be
passed as the first argument.
"""
self.set_cred(cred)
self.try_count = 1
self.max_tries = max_tries
self.callback = callback
def set_cred(self, cred):
"""Set stored credentials to cred.
cred should be of the form:
{'login': username, 'password': password}
"""
self.cred = cred
self.dirty = False
def get_cred(self):
"""Get new credentials.
Return a credentials dictionary (see set_cred()). Raise an
IndexError exception if self.max_tries have already been made.
"""
if not self.dirty and self.cred is not None:
self.try_count = self.try_count + 1
self.dirty = True
return self.cred
if self.try_count > self.max_tries:
raise IndexError("Exceeded max_tries ({})".format(self.max_tries))
self.cred = self.callback(self)
self.try_count = self.try_count + 1
self.dirty = True
return self.cred
class CosignHandler(urllib.request.BaseHandler):
"""urllib.request style handler for Cosign protected URLs.
See http://weblogin.org
SYNOPSIS:
# Cosign relies on cookies.
cj = http.cookiejar.MozillaCookieJar('cookies.txt')
# We need an opener that handles cookies and any cosign redirects and
# logins.
opener = urllib.request.build_opener(
urllib.request.HTTPCookieProcessor(cj),
# Here's the CosignHandler.
CosignHandler('https://cosign.login/page',
cj,
CosignPasswordMgr()
# If you've got one big program you'll probably
# want to keep the cookies in memory, but for
# lots of little programs we get single sign on
# behaviour by saving and loading to/from a
# file.
save_cookies=True
)
)
# Construct a request for the page we actually want
req = urllib.request.Request(
url='https://some.cosign.protected/url',
)
# make the request
res = opener.open(req)
# If all went well, res encapsulates the desired result, use res.read()
# to get at the data and so on.
"""
def __init__(self, login_url, cj, pw_mgr, save_cookies=True):
"""Construct new CosignHandler.
Args:
login_url: URL of cosign login page. Used to figure out if we
have been redirected to the login page after a failed
authentication, and as the URL to POST to to log in.
cj: An http.cookiejar.CookieJar or equivalent. You'll need
something that implements the FileCookieJar interface if
you want to load/save cookies.
pw_mgr: A CosignPasswordMgr object or equivalent. This
object will provide (and if necessary prompt for) the
username and password.
save_cookies: Whether or not to save cookies to a file after
each request. Required for single sign on between
different scripts.
"""
super().__init__()
self.login_url = login_url
self.cj = cj
self.pw_mgr = pw_mgr
self.save_cookies = save_cookies
# try to load cookies from file (specified when constructing cj)
try:
self.cj.load(ignore_discard=True)
except IOError:
pass
def https_response(self, req, res):
"""Handle https_response.
If the response is from the cosign login page (starts with
self.login_url) then log in to cosign and retry. Otherwise
continue as normal.
"""
if res.code == 200 and res.geturl().startswith(self.login_url + '?'):
# Been redirected to login page.
# We'll need the cosign cookies later
self.cj.extract_cookies(res, req)
# Grab a username and password.
data = urllib.parse.urlencode(self.pw_mgr.get_cred())
# Construct a login POST request to the login page.
req2 = urllib.request.Request(
self.login_url,
data.encode('iso-8859-1'),
)
# We need a different opener that doesn't have a CosignHandler.
opener = urllib.request.build_opener(
urllib.request.HTTPCookieProcessor(self.cj)
)
# Try the login
res2 = opener.open(req2)
# Cookies, cookies, cookies
self.cj.extract_cookies(res2, req2)
# We should be logged in, go back and get what was asked for
res = opener.open(req)
# If we end up back at the login page then login failed
if res.geturl().startswith(self.login_url + '?'):
raise Exception('Login failed.')
if self.save_cookies:
self.cj.extract_cookies(res,req)
self.cj.save(ignore_discard=True)
return res
|
I looked for quite a while for code to automate fetching from cosign protected URLs. I didn't find anything so I wrote my own - which is always dangerous. I did have some shell scripts driving curl and also a perl module I had written before. This is the code after I learned enough about urllib.request's handler system to squeeze it into that form.
This will successfully download a cosign protected URL and will fetch it again with no need for password entry on subsequent tries. A lot of things could be improved with the code though...