Welcome, guest | Sign In | My Account | Store | Cart

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.

Python, 177 lines
  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...