I'm trying to fetch a URL from a Jekins server. Until somewhat recently I was able to use the pattern described on this page (HOWTO Fetch Internet Resources Using urllib2) to create a password-manager that correctly responded to BasicAuth challenges with the user-name & password. All was fine until the Jenkins team changed their security model, and that code no longer worked.
# DOES NOT WORK!
import urllib2
password_mgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
top_level_url = "http://localhost:8080"
password_mgr.add_password(None, top_level_url, 'sal', 'foobar')
handler = urllib2.HTTPBasicAuthHandler(password_mgr)
opener = urllib2.build_opener(handler)
a_url = 'http://localhost:8080/job/foo/4/api/python'
print opener.open(a_url).read()
Stacktrace:
Traceback (most recent call last):
File "/home/sal/workspace/jenkinsapi/src/examples/password.py", line 11, in <module>
print opener.open(a_url).read()
File "/usr/lib/python2.7/urllib2.py", line 410, in open
response = meth(req, response)
File "/usr/lib/python2.7/urllib2.py", line 523, in http_response
'http', request, response, code, msg, hdrs)
File "/usr/lib/python2.7/urllib2.py", line 448, in error
return self._call_chain(*args)
File "/usr/lib/python2.7/urllib2.py", line 382, in _call_chain
result = func(*args)
File "/usr/lib/python2.7/urllib2.py", line 531, in http_error_default
raise HTTPError(req.get_full_url(), code, msg, hdrs, fp)
urllib2.HTTPError: HTTP Error 403: Forbidden
[Finished in 0.0s with exit code 1]
The problem appears to be that Jenkins returns not with the expected 401 code, but a 403 which urllib2 interprets as an end of conversation. It never actually sends the password. After some surfing around github found another developer's solution which works...
# WORKS... SORTA
def auth_headers(username, password):
return 'Basic ' + base64.encodestring('%s:%s' % (username, password))[:-1]
auth = auth_headers('sal', 'foobar')
top_level_url = "http://localhost:8080"
a_url = 'http://localhost:8080/job/foo/4/api/python'
req = urllib2.Request(a_url)
req.add_header('Authorization', auth)
print urllib2.urlopen(req).read()
But that seems rather unsatisfying. It's not bothering to check whether the domain is relevant to the username and password... it's just sending my login details regardless!
Can anybody suggest a way to make the original script work? I'd like to use a urllib2 password manager in such a way that I can login to Jenkins.
See this gist as well: https://gist.github.com/dnozay/194d816aa6517dc67ca1
Jenkins does not return
401 - retry
HTTP error code when you need to access a page that needs authentication; instead it returns403 - forbidden
. In the wiki, https://wiki.jenkins-ci.org/display/JENKINS/Authenticating+scripted+clients, it shows that using the command-line toolwget
you need to usewget --auth-no-challenge
which is exactly because of that behavior.Retrying with basic auth when you get a
403 - forbidden
:let's say you defined:
You can subclass a
urllib2.HTTPBasicAuthHandler
to handle403
HTTP responses.Then it is a matter of using that handler, e.g. you can install it so it works for all
urllib2.urlopen
calls:and here is a simple test to see if it works okay.
Using pre-emptive authentication.
There is a good example in this answer: https://stackoverflow.com/a/8513913/1733117. Rather than retrying when you get a
403 forbidden
you would send theAuthorization
header when the url matches.Rather than defining your own handler and installing it globally or using it for individual requests, it's a lot easier to just add the header to the request: