getpatch: use ssl context for ssl endpoints
[freebsd-maintainer-scripts/.git] / getpatch
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 #
4 # Copyright (c) 2012 Sofian Brabez <sbz@FreeBSD.org>
5 # All rights reserved.
6 #
7 # Redistribution and use in source and binary forms, with or without
8 # modification, are permitted provided that the following conditions
9 # are met:
10 # 1. Redistributions of source code must retain the above copyright
11 #    notice, this list of conditions and the following disclaimer
12 # 2. Redistributions in binary form must reproduce the above copyright
13 #    notice, this list of conditions and the following disclaimer in the
14 #    documentation and/or other materials provided with the distribution.
15 #
16 # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
17 # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19 # ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
20 # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21 # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
22 # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
23 # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
24 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
25 # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
26 #
27 # $FreeBSD: head/Tools/scripts/getpatch 362667 2014-07-23 12:14:32Z sbz $
28 #
29 # MAINTAINER=   sbz@FreeBSD.org
30
31 import argparse
32 import codecs
33 import os
34 import re
35 import ssl
36 import sys
37 if sys.version_info.major == 3:
38     import urllib.request as urllib2
39 else:
40     import urllib2
41
42 """
43 FreeBSD getpatch handles Gnats and Bugzilla patch attachments
44 """
45
46
47 def create_ssl_context(cafile):
48     if os.path.exists(cafile):
49         return ssl.create_default_context(cafile=cafile)
50     else:
51         return ssl._create_unverified_context()
52
53
54 class GetPatch(object):
55
56     def __init__(self, pr, category='ports'):
57         self.pr = pr
58         self.category = category
59         self.patchs = []
60         self.url = ""
61         self.patch = ""
62         self.output_stdout = False
63         self.default_locale = sys.getdefaultencoding()
64         self.ssl_context = create_ssl_context('/usr/local/etc/ssl/cert.pem')
65
66     def fetch(self, *largs, **kwargs):
67         raise NotImplementedError()
68
69     def write(self, filename, data):
70         if filename.endswith(('.patch', '.txt')):
71             filename = filename[:filename.rindex('.')]+'.diff'
72         f=codecs.open(filename, encoding=self.default_locale, mode='w')
73         f.write(data.decode(self.default_locale))
74         f.close()
75         self.out("[+] %s created" % filename)
76
77     def get(self,only_last=False, output_stdout=False):
78         self.output_stdout = output_stdout
79         self.fetch(self.pr, category=self.category)
80
81         if len(self.patchs) == 0:
82             self.out("[-] No patch found")
83             sys.exit(1)
84
85         if only_last:
86             self.patchs = [self.patchs.pop()]
87
88         for patch in self.patchs:
89             url = patch['url']
90             p = patch['name']
91
92             data = urllib2.urlopen(url, context=self.ssl_context).read()
93
94             if self.output_stdout:
95                 sys.stdout.write(data.decode(self.default_locale))
96             else:
97                 self.write(p, data)
98
99     def add_patch(self, url, name):
100         self.patchs.append({'url': url, 'name': name})
101
102     def out(self, s):
103         if not self.output_stdout:
104             print(s)
105
106 class GnatsGetPatch(GetPatch):
107
108     URL_BASE = 'https://www.freebsd.org/cgi'
109     URL = '%s/query-pr.cgi?pr=' % URL_BASE
110     REGEX = r'<b>Download <a href="([^"]*)">([^<]*)</a>'
111
112     def __init__(self, pr, category):
113         GetPatch.__init__(self, pr, category)
114
115     def fetch(self, *largs, **kwargs):
116         category = kwargs['category']
117         target = ("%s/%s" % (category, self.pr), "%s" % self.pr)[category=='']
118         self.out("[+] Fetching patch for pr %s" % target)
119         pattern = re.compile(self.REGEX)
120         u = urllib2.urlopen(self.URL+'%s' % target, context=self.ssl_context)
121         data = u.read()
122         if data == None:
123             self.out("[-] No patch found")
124             sys.exit(1)
125
126         for patchs in re.findall(pattern, str(data)):
127             self.add_patch(patchs[0], patchs[1])
128
129 class BzGetPatch(GetPatch):
130
131     URL_BASE= 'https://bugs.freebsd.org/bugzilla/'
132     URL_SHOW = '%s/show_bug.cgi?id=' % URL_BASE
133     REGEX_URL = r'<a href="([^<]+)">Details</a>'
134     REGEX = r'<div class="details">([^ ]+) \(text/plain(?:; charset=[-\w]+)?\)'
135
136     def __init__(self, pr, category):
137         GetPatch.__init__(self, pr, category)
138
139     def _get_patch_name(self, url):
140         match = re.search(self.REGEX, urllib2.urlopen(url, context=self.ssl_context).read())
141         if match is None:
142             return None
143         return match.group(1)
144
145     def _get_patch_urls(self, data):
146         patch_urls = {}
147         for url in re.findall(self.REGEX_URL, data):
148             url = '%s/%s' % (self.URL_BASE, url)
149             file_name = self._get_patch_name(url)
150             if file_name is None:
151                 self.out("[-] Could not determine the patch file name in %s. "
152                          "Skipping." % url)
153                 continue
154             download_url = url[:url.find('&')]
155             patch_urls[download_url] = file_name
156         return patch_urls
157
158     def fetch(self, *largs, **kwargs):
159         category = kwargs['category']
160         self.out("[+] Fetching patch for pr %s/%s" % (category, self.pr))
161         u = urllib2.urlopen(self.URL_SHOW+'%s' % self.pr, context=self.ssl_context)
162         data = u.read()
163
164         if data == None:
165             self.out("[-] No patch found")
166             sys.exit(1)
167
168         patch_urls = self._get_patch_urls(data)
169         if not patch_urls:
170             self.out("[-] No patch found")
171             sys.exit(1)
172
173         for url, file_name in patch_urls.iteritems():
174             self.add_patch(url, file_name)
175
176 def main():
177
178     parser = argparse.ArgumentParser(
179             description='Gets patch attachments from a Bug Tracking System'
180     )
181     parser.add_argument('pr', metavar='pr', type=str, nargs=1,
182             help='Pr id number')
183     parser.add_argument('--mode', type=str, choices=['gnats','bz'],
184             default='bz', help='available modes to retrieve patch')
185     parser.add_argument('--last', action='store_true',
186             help='only retrieve the latest iteration of a patch')
187     parser.add_argument('--stdout', action='store_true',
188             help='dump patch on stdout')
189
190     if len(sys.argv) == 1:
191         parser.print_help()
192         sys.exit(1)
193
194     args = parser.parse_args()
195
196     category = ""
197     pr = str(args.pr[0])
198
199     if '/' in pr and pr is not None:
200         category, pr = pr.split('/')
201
202     Clazz = globals()['%sGetPatch' % args.mode.capitalize()]
203     gp = Clazz(pr, category)
204     gp.get(only_last=args.last, output_stdout=args.stdout)
205
206 if __name__ == '__main__':
207     main()