Review Board

beta

Add HTTP Authentication support and clean up the cookie handling code in post-review

Submitted
Updated 7 months, 3 weeks ago

Marc Hedlund Reviewers
reviewboard
None Review Board SVN
This is a more extensive change to post-review.

First, I moved some of the global definitions out of the top of the script file.  I encapsulated all of the HTTP handling code in the ReviewBoardServer class, since that's the only class that needs HTTP support, and it was previously relying on globals from distant parts of the file to take care of cookie handling.  I also moved the 'homepath' discovery code down to the main() method since only that method should need to know the user's home dir.

Second, I added a ReviewBoardHTTPPasswordMgr class to provide HTTP Authentication support.  This was a little more complicated than I'd hoped since http auth is a little busted in Python 2.4.  I think this addition is reasonably secure, and I've tested it extensively with several http-auth-protected servers, using Python 2.4, with good results.

Third, I cleaned up the cookie handling code quite a bit.  It used to be the case (I think, at least) that if you had *any* cookies in your ~/.post-review-cookies.txt file, you would never be prompted to log in, effectively limiting you to one RB server at a time.  Since I'm using two (my company's and the Review Board RB itself), that wasn't working for me.  (I went all anal on this part of things and added a bunch of cookie checks and debug messages, so that exactly what was going on with the cookie search would be apparent.)

Finally, I changed the login prompts so that the Review Board login and any HTTP Auth logins would be similar but (hopefully) clearly distinguished.  (Note: HTTP Auth credentials are not saved between sessions, and arguably should be.  Nor are they settable from command-line options.)

I'm hoping that the result of this is that the script works in more circumstances and is a little cleaner -- less reliance on globals and a little better encapsulation.

(One other note: I didn't change the script version, but I think that would be worth doing before committing this, since the http aspects of it are fairly different.)
I've tested this version of post-review against both http://reviews.review-board.org and my company's Review Board server.  The tests covered a non-http-authenticated server (this one), an https/http-auth-protected server, and an https/http-auth-protected server on a non-standard port (which triggered a bug in python 2.4, worked around -- see comments).  All of my tests were with Python 2.4.

Changes between revision 1 and 2

1 2
1 2

  1. /trunk/reviewboard/contrib/tools/post-review: 18 changes [ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 ]
  /trunk/reviewboard/contrib/tools/post-review
Diff Revision 1 Diff Revision 2
  51
    def __init__(self, reviewboard_url):
51
    def __init__(self, reviewboard_url):
52
        self.passwd  = {}
52
        self.passwd  = {}
53
        self.rb_url  = reviewboard_url
53
        self.rb_url  = reviewboard_url
54
        self.rb_user = None
54
        self.rb_user = None
55
        self.rb_pass = None
55
        self.rb_pass = None
  56
    
56
  57
    def find_user_password(self, realm, uri):
57
    def find_user_password(self, realm, uri):
58
        if uri.startswith(self.rb_url):
58
        if uri.startswith(self.rb_url):
59
            if self.rb_user is None or self.rb_pass is None:
59
            if self.rb_user is None or self.rb_pass is None:
60
                print "==> HTTP Authentication Required"
60
                print "==> HTTP Authentication Required"
  61
                print "Enter username and password for \"%s\" at %s" % \
61
                print 'Enter username and password for "%s" at %s' % \
  62
                    (realm, urlparse(uri)[1])
62
                    (realm, urlparse(uri)[1])
63
                self.rb_user = raw_input('Username: ')
63
                self.rb_user = raw_input('Username: ')
64
                self.rb_pass = getpass.getpass('Password: ')
64
                self.rb_pass = getpass.getpass('Password: ')
  65
            
65
  66
            return self.rb_user, self.rb_pass
66
            return self.rb_user, self.rb_pass
67
        else:
67
        else:
 
68
  68
            # If this is an auth request for some other domain (since HTTP
69
            # If this is an auth request for some other domain (since HTTP
69
            # handlers are global), fall back to standard password management.
70
            # handlers are global), fall back to standard password management.
70
            return urllib2.HTTPPasswordMgr.find_user_password(self, realm, uri)
71
            return urllib2.HTTPPasswordMgr.find_user_password(self, realm, uri)
  71
    
  72
72
 
73
  73
class ReviewBoardServer:
74
class ReviewBoardServer:
74
    """
75
    """
75
    An instance of a Review Board server.
76
    An instance of a Review Board server.
76
    """
77
    """
77
    def __init__(self, url, info, cookie_file):
78
    def __init__(self, url, info, cookie_file):
78
        self.url = url
79
        self.url = url
79
        self.info = info
80
        self.info = info
80
        self.cookie_file = cookie_file
81
        self.cookie_file = cookie_file
81
        self.cookie_jar  = cookielib.MozillaCookieJar(self.cookie_file)
82
        self.cookie_jar  = cookielib.MozillaCookieJar(self.cookie_file)
  82
        
83
  83
        # Set up the HTTP libraries to support all of the features we need.
84
        # Set up the HTTP libraries to support all of the features we need.
84
        cookie_handler = urllib2.HTTPCookieProcessor(self.cookie_jar)
85
        cookie_handler = urllib2.HTTPCookieProcessor(self.cookie_jar)
85
        password_mgr   = ReviewBoardHTTPPasswordMgr(self.url)
86
        password_mgr   = ReviewBoardHTTPPasswordMgr(self.url)
86
        auth_handler   = urllib2.HTTPBasicAuthHandler(password_mgr)
87
        auth_handler   = urllib2.HTTPBasicAuthHandler(password_mgr)
  87
        
88
  88
        opener = urllib2.build_opener(cookie_handler, auth_handler)
89
        opener = urllib2.build_opener(cookie_handler, auth_handler)
89
        opener.addheaders = [('User-agent', 'post-review/' + VERSION)]
90
        opener.addheaders = [('User-agent', 'post-review/' + VERSION)]
90
        urllib2.install_opener(opener)
91
        urllib2.install_opener(opener)
  91
    
92
  92
    def login(self):
93
    def login(self):
93
        """
94
        """
94
        Logs in to a Review Board server, prompting the user for login
95
        Logs in to a Review Board server, prompting the user for login
95
        information if needed.
96
        information if needed.
96
        """
97
        """
  112
113
113
            die("Unable to log in: %s (%s)" % (rsp["err"]["msg"],
114
            die("Unable to log in: %s (%s)" % (rsp["err"]["msg"],
114
                                               rsp["err"]["code"]))
115
                                               rsp["err"]["code"]))
115
116
116
        debug("Logged in.")
117
        debug("Logged in.")
  117
    
118
  118
    def has_valid_cookie(self):
119
    def has_valid_cookie(self):
119
        """
120
        """
120
        Load up the user's cookie file, and see if they have a valid
121
        Load up the user's cookie file, and see if they have a valid
121
        'sessionid' cookie for the current Review Board server.  Returns
122
        'sessionid' cookie for the current Review Board server.  Returns
122
        true if so and false otherwise.
123
        true if so and false otherwise.
123
        """
124
        """
124
        try:
125
        try:
125
            parsed_url = urlparse(self.url)
126
            parsed_url = urlparse(self.url)
  126
            
  127
            host = parsed_url[1]
127
            host = parsed_url[1]
  128
            # Cookie files don't store port numbers, unfortunately...
129
            port_idx = host.find(':')
130
            if port_idx != -1:
131
                host = host[0:port_idx]
132
            
  133
            path = parsed_url[2] or '/'
128
            path = parsed_url[2] or '/'
 
129
130
            # Cookie files don't store port numbers, unfortunately, so
131
            # get rid of the port number if it's present.
132
            host = host.split(":")[0]
133
  134
            debug("Looking for '%s %s' cookie in %s" % \
134
            debug("Looking for '%s %s' cookie in %s" % \
135
                (host, path, self.cookie_file))
135
                  (host, path, self.cookie_file))
136
            self.cookie_jar.load(self.cookie_file, ignore_expires=True)
136
            self.cookie_jar.load(self.cookie_file, ignore_expires=True)
 
137
  137
            try:
138
            try:
138
                cookie = self.cookie_jar._cookies[host][path]['sessionid']
139
                cookie = self.cookie_jar._cookies[host][path]['sessionid']
 
140
  139
                if not cookie.is_expired():
141
                if not cookie.is_expired():
140
                    debug("Loaded valid cookie -- no login required")
142
                    debug("Loaded valid cookie -- no login required")
141
                    return True
143
                    return True
142
                else:
144
                else:
143
                    debug("Cookie file loaded, but cookie has expired")
145
                    debug("Cookie file loaded, but cookie has expired")
144
            except KeyError:
146
            except KeyError:
145
                debug("Cookie file loaded, but no cookie for this server")
147
                debug("Cookie file loaded, but no cookie for this server")
146
        except IOError, error:
148
        except IOError, error:
147
            debug("Couldn't load cookie file: %s" % error)
149
            debug("Couldn't load cookie file: %s" % error)
 
150
  148
        return False
151
        return False
  149
    
152
  150
    def new_review_request(self, changenum, submit_as=None):
153
    def new_review_request(self, changenum, submit_as=None):
151
        """
154
        """
152
        Creates a review request on a Review Board server, updating an
155
        Creates a review request on a Review Board server, updating an
153
        existing one if the changeset number already exists.
156
        existing one if the changeset number already exists.
154
157
  1146
    if 'USERPROFILE' in os.environ:
1149
    if 'USERPROFILE' in os.environ:
1147
        homepath = os.path.join(os.environ["USERPROFILE"], "Local Settings",
1150
        homepath = os.path.join(os.environ["USERPROFILE"], "Local Settings",
1148
                                "Application Data")
1151
                                "Application Data")
1149
    else:
1152
    else:
1150
        homepath = os.environ["HOME"]
1153
        homepath = os.environ["HOME"]
  1151
    
1154
  1152
    # Load the config and cookie files
1155
    # Load the config and cookie files
1153
    globals()['user_config'] = \
1156
    globals()['user_config'] = \
1154
        load_config_file(os.path.join(homepath, ".reviewboardrc"))
1157
        load_config_file(os.path.join(homepath, ".reviewboardrc"))
1155
    cookie_file = os.path.join(homepath, ".post-review-cookies.txt")
1158
    cookie_file = os.path.join(homepath, ".post-review-cookies.txt")
1156
1159
  1. /trunk/reviewboard/contrib/tools/post-review: 18 changes [ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 ]