Merge lp:~allenap/launchpad/trac-sniffing-broken-bug-504310 into lp:launchpad

Proposed by Gavin Panella
Status: Merged
Approved by: Gavin Panella
Approved revision: not available
Merged at revision: not available
Proposed branch: lp:~allenap/launchpad/trac-sniffing-broken-bug-504310
Merge into: lp:launchpad
Diff against target: 149 lines (+60/-14)
2 files modified
lib/lp/bugs/doc/externalbugtracker-trac.txt (+35/-4)
lib/lp/bugs/externalbugtracker/trac.py (+25/-10)
To merge this branch: bzr merge lp:~allenap/launchpad/trac-sniffing-broken-bug-504310
Reviewer Review Type Date Requested Status
Eleanor Berger (community) Approve
Review via email: mp+17139@code.launchpad.net

Commit message

Work around broken Trac installations that return HTTP 200 when the resource is not found. This has caused Launchpad to believe that the trac-launchpad plugin is installed when it is not.

To post a comment you must log in.
Revision history for this message
Gavin Panella (allenap) wrote :

Pre-implementation call with Graham Binns.

Recently we've noticed that several Trac installations return HTTP 200 for page not found (i.e. should be a 404). This breaks the sniffing we do to detect the presence of the Launchpad plugin <https://launchpad.net/trac-launchpad>.

The fix proposed here is to check all HTTP 200 responses for the presence of a trac_auth cookie. This should not be present for a should-be-a-404 response. In the very very unlikely event that the authentication token we pass when sniffing (a deliberately bogus token: "check") is valid, the trac_auth cookie will be present.

I've tested this with http://trac.macports.org/ - one of the main culprits of broken 404 behaviour - and it works fine.

Lint free, other than the usual pylint nonsense.

Test: bin/test -vvt 'external.*bug'

Revision history for this message
Eleanor Berger (intellectronica) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/bugs/doc/externalbugtracker-trac.txt'
--- lib/lp/bugs/doc/externalbugtracker-trac.txt 2009-06-15 11:10:49 +0000
+++ lib/lp/bugs/doc/externalbugtracker-trac.txt 2010-01-11 14:05:35 +0000
@@ -48,15 +48,46 @@
48 >>> chosen_trac.baseurl48 >>> chosen_trac.baseurl
49 'http://example.com'49 'http://example.com'
5050
51In the unlikely case where the token would be valid, a 200 would be51Some Trac instances in the wild return HTTP 200 when the resource is
52returned. This also causes a TracLPPlugin to be used.52not found (HTTP 404 would be more appropriate). A distinguishing
53difference between a 200 response from a broken Trac and a response
54from a Trac with the plugin installed (when we've accidentally passed
55a valid token, which is very unlikely), is that the broken Trac will
56not include a "trac_auth" cookie.
5357
58 >>> from httplib import HTTPMessage
59 >>> from urllib import addinfourl
54 >>> from StringIO import StringIO60 >>> from StringIO import StringIO
61
62 >>> class TracReturning200ForPageNotFound(Trac):
63 ... def urlopen(self, url):
64 ... print url
65 ... return addinfourl(
66 ... StringIO(''), HTTPMessage(StringIO('')), url)
67
55 >>> class TracHavingLPPlugin200(Trac):68 >>> class TracHavingLPPlugin200(Trac):
56 ... def urlopen(self, url):69 ... def urlopen(self, url):
57 ... print url70 ... print url
58 ... return StringIO('')71 ... return addinfourl(
59 >>> trac = TracHavingLPPlugin401('http://example.com/')72 ... StringIO(''), HTTPMessage(
73 ... StringIO('Set-Cookie: trac_auth=1234')), url)
74
75The plain, non-plugin, external bug tracker is selected for broken
76Trac installations:
77
78 >>> trac = TracReturning200ForPageNotFound('http://example.com/')
79 >>> chosen_trac = trac.getExternalBugTrackerToUse()
80 http://example.com/launchpad-auth/check
81
82 >>> isinstance(chosen_trac, TracLPPlugin)
83 False
84 >>> chosen_trac.baseurl
85 'http://example.com'
86
87In the event that our deliberately bogus token is considered valid,
88the external bug tracker that groks the plugin is selected:
89
90 >>> trac = TracHavingLPPlugin200('http://example.com/')
60 >>> chosen_trac = trac.getExternalBugTrackerToUse()91 >>> chosen_trac = trac.getExternalBugTrackerToUse()
61 http://example.com/launchpad-auth/check92 http://example.com/launchpad-auth/check
6293
6394
=== modified file 'lib/lp/bugs/externalbugtracker/trac.py'
--- lib/lp/bugs/externalbugtracker/trac.py 2010-01-06 14:35:52 +0000
+++ lib/lp/bugs/externalbugtracker/trac.py 2010-01-11 14:05:35 +0000
@@ -12,13 +12,13 @@
12import urllib212import urllib2
13import xmlrpclib13import xmlrpclib
1414
15from Cookie import SimpleCookie
15from cookielib import CookieJar16from cookielib import CookieJar
16from datetime import datetime17from datetime import datetime
17from email.Utils import parseaddr18from email.Utils import parseaddr
18from zope.component import getUtility19from zope.component import getUtility
19from zope.interface import implements20from zope.interface import implements
2021
21from canonical.cachedproperty import cachedproperty
22from canonical.config import config22from canonical.config import config
23from lp.bugs.externalbugtracker.base import (23from lp.bugs.externalbugtracker.base import (
24 BugNotFound, BugTrackerAuthenticationError, ExternalBugTracker,24 BugNotFound, BugTrackerAuthenticationError, ExternalBugTracker,
@@ -48,6 +48,7 @@
48# Fault code constants for the LP Plugin48# Fault code constants for the LP Plugin
49FAULT_TICKET_NOT_FOUND = 100149FAULT_TICKET_NOT_FOUND = 1001
5050
51
51class Trac(ExternalBugTracker):52class Trac(ExternalBugTracker):
52 """An ExternalBugTracker instance for handling Trac bugtrackers."""53 """An ExternalBugTracker instance for handling Trac bugtrackers."""
5354
@@ -61,11 +62,26 @@
61 # Any token will do.62 # Any token will do.
62 auth_url = urlappend(base_auth_url, 'check')63 auth_url = urlappend(base_auth_url, 'check')
63 try:64 try:
64 self.urlopen(auth_url)65 response = self.urlopen(auth_url)
65 except urllib2.HTTPError, error:66 except urllib2.HTTPError, error:
66 if error.code != 401:67 # If the error is HTTP 401 Unauthorized then we're
67 return self68 # probably talking to the LP plugin.
68 return TracLPPlugin(self.baseurl)69 if error.code == 401:
70 return TracLPPlugin(self.baseurl)
71 else:
72 return self
73 else:
74 # If the response contains a trac_auth cookie then we're
75 # talking to the LP plugin. However, it's unlikely that
76 # the remote system will authorize the bogus auth token we
77 # sent, so this check is really intended to detect broken
78 # Trac instances that return HTTP 200 for a missing page.
79 for set_cookie in response.headers.getheaders('Set-Cookie'):
80 cookie = SimpleCookie(set_cookie)
81 if 'trac_auth' in cookie:
82 return TracLPPlugin(self.baseurl)
83 else:
84 return self
6985
70 def supportsSingleExports(self, bug_ids):86 def supportsSingleExports(self, bug_ids):
71 """Return True if the Trac instance provides CSV exports for single87 """Return True if the Trac instance provides CSV exports for single
@@ -83,11 +99,11 @@
83 # We try to retrive the ticket in HTML form, since that will99 # We try to retrive the ticket in HTML form, since that will
84 # tell us whether or not it is actually a valid ticket100 # tell us whether or not it is actually a valid ticket
85 ticket_id = int(bug_ids.pop())101 ticket_id = int(bug_ids.pop())
86 html_data = self.urlopen(html_ticket_url % ticket_id)102 self.urlopen(html_ticket_url % ticket_id)
87 except (ValueError, urllib2.HTTPError):103 except (ValueError, urllib2.HTTPError):
88 # If we get an HTTP error we can consider the ticket to be104 # If we get an HTTP error we can consider the ticket to be
89 # invalid. If we get a ValueError then the ticket_id couldn't105 # invalid. If we get a ValueError then the ticket_id couldn't
90 # be intified and it's of no use to us anyway.106 # be identified and it's of no use to us anyway.
91 pass107 pass
92 else:108 else:
93 # If we didn't get an error we can try to get the ticket in109 # If we didn't get an error we can try to get the ticket in
@@ -345,7 +361,7 @@
345 auth_url = urlappend(base_auth_url, token_text)361 auth_url = urlappend(base_auth_url, token_text)
346362
347 try:363 try:
348 response = self.urlopen(auth_url)364 self.urlopen(auth_url)
349 except urllib2.HTTPError, error:365 except urllib2.HTTPError, error:
350 raise BugTrackerAuthenticationError(366 raise BugTrackerAuthenticationError(
351 self.baseurl, '%s "%s"' % (error.code, error.msg))367 self.baseurl, '%s "%s"' % (error.code, error.msg))
@@ -481,7 +497,7 @@
481 launchpad_bug_id = 0497 launchpad_bug_id = 0
482498
483 try:499 try:
484 timestamp = self._server.launchpad.set_launchpad_bug(500 self._server.launchpad.set_launchpad_bug(
485 remote_bug, launchpad_bug_id)501 remote_bug, launchpad_bug_id)
486 except xmlrpclib.Fault, fault:502 except xmlrpclib.Fault, fault:
487 # Deal with "Ticket does not exist" faults. We re-raise503 # Deal with "Ticket does not exist" faults. We re-raise
@@ -490,4 +506,3 @@
490 raise BugNotFound(remote_bug)506 raise BugNotFound(remote_bug)
491 else:507 else:
492 raise508 raise
493