forked from samrocketman/home
-
Notifications
You must be signed in to change notification settings - Fork 0
/
jenkins-call-url
executable file
·432 lines (389 loc) · 18.3 KB
/
jenkins-call-url
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
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
#!/usr/bin/env python
#Created by Sam Gleske
#Copyright 2017 Sam Gleske
#Tue Mar 21 17:24:36 PDT 2017
#Ubuntu 16.04.1 LTS
#Linux 4.4.0-59-generic x86_64
#Python 2.7.12
#LICENSE MIT
#Source: https://github.com/samrocketman/home/blob/master/bin/jenkins-call-url
#The above comment block must remain intact for usage.
#DESCRIPTION:
# Reads Jenkins API and can make calls to arbitrary Jenkins URL endpoints.
#EXAMPLES:
# Execute script console script.
# jenkins-call-url -m POST --data-string "script=println 'hello world'" http://localhost:8080/scriptText
# Execute a whole Groovy script on script console.
# jenkins-call-url -m POST --data-string "script=" --data-file ./script.groovy http://localhost:8080/scriptText
# Set default arguments for always calling groovy scripts to script console.
# export JENKINS_CALL_ARGS="-m POST -v --data-string script= http://localhost:8080/scriptText --data-file"
# jenkins-call-url ./script.groovy
try:
from httplib import HTTPSConnection
except ModuleNotFoundError:
from http.client import HTTPSConnection
import argparse
import base64
import json
import os
import re
import ssl
import sys
import time
try:
import socket
import socks
socks_supported = True
except ImportError:
socks_supported = False
version = "0.9"
parser = argparse.ArgumentParser(description="Reads Jenkins API and can make calls to arbitrary Jenkins URL endpoints. Useful for calling Jenkins URLs for killing jobs or executing script console scripts.", epilog="""
environment variables:
JENKINS_USER Username to authenticate with Jenkins.
JENKINS_PASSWORD Password to authenticate with Jenkins.
JENKINS_CA_FILE Path to CA chain file to validate TLS connections. See also -c.
JENKINS_HEADERS_FILE Path to file to persist HTTP headers. See also --save-headers or --load-headers.
JENKINS_CALL_ARGS Additional space-separated arguments which would normally be called on command line. If spaces are required for an argument then the separator can be the pipe symbol '|'.
JENKINS_SOCKS_PROXY Define a SOCKS5 proxy to proxy traffic. Format is host:port where host is hostname and port is TCP port of the proxy.
exit status:
0 SUCCESS script run.
1 Any script run status other than SUCCESS.
examples:
Execute script console script.
jenkins-call-url -m POST --data-string "script=println 'hello world'" http://localhost:8080/scriptText
Execute a whole Groovy script on script console.
jenkins-call-url -m POST --data-string "script=" --data-file ./script.groovy http://localhost:8080/scriptText
Set default arguments for always calling groovy scripts to script console.
export JENKINS_CALL_ARGS="-m POST -v --data-string script= http://localhost:8080/scriptText --data-file"
jenkins-call-url ./script.groovy
""", formatter_class=argparse.RawTextHelpFormatter)
parser.add_argument('--version', action='version', version="%(prog)s "+version)
parser.add_argument('--curl', action='store_true', dest='print_curl', help='Print a fully formatted curl command which includes all options for debugging and exit.')
parser.add_argument('--curl-init', action='store_true', dest='print_curl_init', help='Print a formatted initial curl command for debugging and exit.')
parser.add_argument('--curl-crumb', action='store_true', dest='print_curl_crumb', help='Print a formatted curl command for getting CSRF crumb for debugging and exit.')
parser.add_argument('--data-string', action="append", default=[], metavar='arg', dest='http_data_strings', help='Data to pass via message body of an HTTP request and prepends -d. Can specify one or more -d data-file.')
parser.add_argument('--force-crumb', action='store_true', dest='force_crumb', help='Force resolving the CSRF crumb even if --load-headers option is used.')
parser.add_argument('--load-headers', default=os.getenv('JENKINS_HEADERS_FILE', ''), metavar='json-file', dest='load_headers_file', help='Loads HTTP headers from a JSON file to use. It can also be set via JENKINS_HEADERS_FILE environment variable.')
parser.add_argument('--save-headers', default=os.getenv('JENKINS_HEADERS_FILE', ''), metavar='json-file', dest='save_headers_file', help='Saves HTTP headers to a JSON file for reuse later. It can also be set via JENKINS_HEADERS_FILE environment variable.')
parser.add_argument('-a', '--automatic-jenkins-server', action='store_true', dest='auto_jenkins_web', help='Automatically determine the Jenkins root based off of JENKINS_URL url. This option ignores -s and --load-headers.')
parser.add_argument('-c', '--ca-file', default=os.getenv('JENKINS_CA_FILE'), metavar='CERT_PEM', dest='pinned_cert', help='Path to pinned CA chain in PEM format. Can be self signed to guarantee secure connection. It can also be set via JENKINS_CA_FILE environment variable.')
parser.add_argument('-d', '--data-file', action="append", default=[], metavar='data-file', dest='http_data_files', help='Data to pass via message body of an HTTP request URL encoded. Can specify one or more -d data-file.')
parser.add_argument('-m', '--http-method', default='GET', metavar='method', dest='http_method', help='HTTP method to use when calling Jenkins. Valid values are GET, HEAD, or POST.')
parser.add_argument('--proxy', default=os.getenv('JENKINS_SOCKS_PROXY'), metavar='proxy', dest='socks_proxy', help='Define a SOCKS5 proxy to proxy traffic. It can also be set via JENKINS_SOCKS_PROXY environment variable.')
parser.add_argument('-s', '--jenkins-server', default='http://localhost:8080', metavar='JENKINS_WEB', dest='jenkins_web', help='Root web URL for the Jenkins server.')
parser.add_argument('-o', '--output', default='-', metavar='OUTPUT_FILE', dest='output', help='Write output to file instead of stdout. Default: - for stdout.')
parser.add_argument('-v', '--verbosity', action="count", dest='verbosity', help="Increase output verbosity.")
parser.add_argument('--raw-response', action='store_true', dest='raw_response', help='By default, script console responses are stripped of leading and trailing spaces. This option disables the behavior and renders the literal response from Jenkins.')
parser.add_argument('JENKINS_URL', help='The URL to a Jenkins endpoint to call.')
#prepend additional arguments from environment
additional_args = os.getenv('JENKINS_CALL_ARGS', '')
if len(additional_args) > 0:
if '|' in list(additional_args):
sys.argv = [sys.argv[0]] + additional_args.strip().split('|') + sys.argv[1:]
else:
sys.argv = [sys.argv[0]] + additional_args.strip().split() + sys.argv[1:]
args = parser.parse_args()
if args.verbosity == None:
args.verbosity = 0
def trim_url_slash(url):
return url[:-1] if url[-1:] == '/' else url
def printCurl(settings, url=trim_url_slash(args.jenkins_web)):
curl='curl'
headers = settings['headers']
if 'socks_proxy' in settings and len(settings['socks_proxy']) > 0:
curl += ' --proxy socks5h://%s' % settings['socks_proxy']
if args.http_method != 'GET' and url != trim_url_slash(args.jenkins_web):
if args.http_method == 'HEAD':
curl += " --head"
else:
curl += " -X%s" % args.http_method
if len(args.http_data_files) > 0 and url != trim_url_slash(args.jenkins_web):
curl += ' --data-urlencode "'
if len(args.http_data_strings) > 0:
for s in args.http_data_strings:
curl += '%s' % s
for f in args.http_data_files:
if f == '-':
curl += sys.stdin.read()
else:
curl += '$(<%s)' % f
curl += '"'
if args.pinned_cert:
curl += ' --cacert %s' % args.pinned_cert
for k,v in headers.items():
curl += " -H '%s:%s'" % (k, v)
print(curl, url)
sys.exit(1)
def getHost(url):
return url.split('/')[2].split(':')[0]
#print to stderr
def printErr(message=''):
sys.stderr.write(message + '\n')
sys.stderr.flush()
if not args.http_method in ['GET', 'HEAD', 'POST']:
printErr("Invalid --http-method specified: %s" % args.http_method)
parser.print_help()
sys.exit(1)
#build credentials
username = os.getenv('JENKINS_USER')
password = os.getenv('JENKINS_PASSWORD')
if len(args.load_headers_file) > 0 and os.path.exists(args.load_headers_file) and os.path.getsize(args.load_headers_file) > 0:
if args.verbosity >= 2:
printErr("Loading HTTP headers from JSON file.")
with open(args.load_headers_file) as f:
settings = json.load(f)
else:
settings = {}
if 'headers' in settings:
headers = settings['headers']
else:
headers = {
'Accept-Encoding': '',
'Connection': 'close',
'Host': getHost(args.jenkins_web),
}
if args.auto_jenkins_web:
headers['Host'] = getHost(trim_url_slash(args.JENKINS_URL))
#always set the User-Agent
headers['User-Agent'] = 'jenkins-call-url %s' % version
if 'Authorization' in headers:
if args.verbosity >= 2:
printErr("Reusing Authorization from HTTP headers file.")
else:
if not username == None:
if args.verbosity >= 2:
printErr("Logging in as user %s." % username)
headers['Authorization'] = "Basic %s" % base64.b64encode("%s:%s" % (username, password)).decode('ascii')
#configure SOCKS5 proxy
if args.socks_proxy != None:
proxy = args.socks_proxy
elif 'socks_proxy' in settings:
if args.verbosity >= 2:
printErr("Reusing proxy configuration from headers file.")
proxy = str(settings['socks_proxy'])
else:
proxy = ''
if proxy and not re.match(r'[-0-9a-zA-Z.]+:[0-9]+', proxy):
printErr("Invalid --proxy specified: %s" % args.socks_proxy)
parser.print_help()
sys.exit(1)
if proxy:
if args.verbosity >= 1:
printErr("Using SOCKS5 proxy: %s" % proxy)
settings['socks_proxy'] = proxy
proxy_host = proxy.split(':')[0]
proxy_port = int(proxy.split(':')[1])
if not socks_supported:
printErr("WARNING: Python socks module not installed so socks is not supported")
else:
socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5, proxy_host, proxy_port)
socket.socket = socks.socksocket
else:
if args.verbosity >= 3:
printErr("Removing proxy configuration from headers file.")
settings.pop('socks_proxy', None)
if args.print_curl_crumb:
settings['headers'] = headers
args.http_method = 'POST'
if args.auto_jenkins_web:
printCurl(settings, args.JENKINS_URL + '/crumbIssuer/api/json?pretty=true')
else:
printCurl(settings, args.jenkins_web + '/crumbIssuer/api/json?pretty=true')
if args.print_curl_init:
settings['headers'] = headers
if args.auto_jenkins_web:
printCurl(settings, args.JENKINS_URL)
else:
printCurl(settings)
#
### POST-PROXY SETTINGS using urllib and urllib2
# See: https://stackoverflow.com/questions/2317849/how-can-i-use-a-socks-4-5-proxy-with-urllib2
# Note: SOCKS proxy must always be configured before importing urllib and urllib2
import urllib
try:
import urllib2
except:
from urllib import request as urllib2
def getUrl(url, headers, data=None, method='GET'):
if args.verbosity >= 3:
printErr('%s %s' % (method, url))
responseCode = -1
responseString = ""
responseErrorReason = None
try:
if url.startswith('https'):
#https://docs.python.org/3/library/ssl.html#ssl-security
context = ssl.create_default_context()
if args.pinned_cert:
context.load_verify_locations(cafile=args.pinned_cert)
else:
context.load_default_certs()
req = urllib2.Request(url, data=data, headers=headers)
if method != 'GET':
req.get_method = lambda: method
urlconn = urllib2.urlopen(req, context=context)
else:
req = urllib2.Request(url=url, data=data, headers=headers)
if method != 'GET':
req.get_method = lambda: method
urlconn = urllib2.urlopen(req)
responseString = urlconn.read()
if url.endswith('crumbIssuer/api/json'):
if 'Cookie' in headers:
headers.pop('Cookie')
if 'Set-Cookie' in urlconn.info():
headers['Cookie'] = urlconn.info()['Set-Cookie']
if args.verbosity >= 2:
printErr('Set a new session cookie ' + headers['Cookie'])
responseCode = urlconn.getcode()
urlconn.close()
except urllib2.HTTPError as e:
responseCode = e.code
responseErrorReason = e.reason
return (responseCode, responseString, responseErrorReason)
def getJSONUrl(url, headers):
if not url.endswith('/api/json'):
url = url + '/api/json'
code, response, reason = getUrl(url, headers)
if reason:
printErr("HTTP ERROR %s: %s\n%s" % (str(code), reason, url))
sys.exit(1)
parsed = json.loads(response)
return parsed
#
### END POST-PROXY SETTINGS using urllib and urllib2
#
#complete arguments for use
jenkins_url = trim_url_slash(args.JENKINS_URL)
jenkins_web = ''
jenkins_root_api_response = {}
if args.auto_jenkins_web:
#force a crumb reload since we're resolving JENKINS_WEB
args.force_crumb = True
if args.verbosity >= 2:
printErr("Automatically resolving JENKINS_WEB.")
jenkins_web = jenkins_url
if jenkins_web[-8:] == 'api/json':
jenkins_web = jenkins_web[:-8]
if jenkins_web[-1:] != '/':
jenkins_web += '/'
while len(jenkins_web.split('/')) > 3:
jenkins_web = '/'.join(jenkins_web.split('/')[:-1])
if jenkins_web.split('/')[-1] in ['job', 'view']:
continue
jenkins_root_api_response = getJSONUrl(jenkins_web + '/api/json', headers)
if "useSecurity" in jenkins_root_api_response:
break
if len(args.load_headers_file) == 0 or args.force_crumb or not os.path.exists(args.load_headers_file):
if not args.auto_jenkins_web:
jenkins_web = trim_url_slash(args.jenkins_web)
if not jenkins_url.startswith(jenkins_web):
printErr("ERROR: JENKINS_URL does not start with JENKINS_WEB. See --help for -s or -a.")
sys.exit(2)
jenkins_root_api_response = getJSONUrl(jenkins_web + '/api/json', headers)
#detect CSRF protection
if jenkins_root_api_response['useCrumbs']:
if args.verbosity >= 2:
printErr("CSRF protection enabled.")
csrf_crumb = getJSONUrl(jenkins_web + '/crumbIssuer', headers)
headers[csrf_crumb["crumbRequestField"]] = csrf_crumb["crumb"]
else:
if args.verbosity >= 2:
printErr("CSRF protection disabled.")
else:
if args.verbosity >= 2:
printErr("Reusing CSRF crumb from HTTP headers file.")
#include the full operation
if args.print_curl:
settings['headers'] = headers
printCurl(settings, url=jenkins_url)
if args.verbosity >= 1:
printErr("%s %s" % (args.http_method, jenkins_url))
#prepare the HTTP message body payload
data=None
if len(args.http_data_files) > 0:
data = ""
for string in args.http_data_strings:
data += string
for path in args.http_data_files:
if path == '-':
data += urllib.quote(sys.stdin.read())
else:
with open(path) as f:
data += urllib.quote(f.read())
#Call the URL
code, response, reason = getUrl(jenkins_url, headers, method=args.http_method, data=data)
if args.verbosity >= 1:
printErr("Response (HTTP %s):" % str(code))
#print without newline at end
if len(response) > 0:
if args.output == '-':
if args.raw_response:
print(response)
else:
print(str(response).strip() + '\n')
else:
with open(args.output, 'w') as f:
f.write(response)
f.flush()
if len(args.save_headers_file) > 0:
if args.verbosity >= 2:
printErr("Saving HTTP headers to file.")
with open(args.save_headers_file, 'w') as f:
settings['headers'] = headers
json.dump(settings,f)
#credentials are stored in the headers file so don't want just anybody to read it
os.chmod(args.save_headers_file, 0o600)
STATUS = 0
if reason:
STATUS=1
if args.verbosity >= 1:
printErr(reason)
sys.exit(STATUS)
# CHANGELOG
# 0.9
# - Bugfix backwards compatibility for older Jenkins when fixing security
# issues in version 0.8.
# 0.8
# - Bugfix: A security fix in newer versions of Jenkins broke jenkins-call-url
# from being able to post to the Jenkins API with CSRF crumbs.
# Jenkins issues SECURITY-1491, SECURITY-626
# See https://jenkins.io/security/advisory/2019-08-28/#SECURITY-1491
# - Feature, new option --curl-crumb which will output a curl command for
# querying the crumbIssuer URL. This will simplify development debugging in
# the future related to this feature of Jenkins..
# 0.7
# - Feature, new option --raw-response which outputs the raw non-stripped
# output of the Jenkins response.
# 0.6
# - Feature, support reading Jenkins scripts from stdin when -d file path is
# a single hyphen (-). e.g. "-d -"
# - Bugfix Jenkins scriptText endpoint returning many spaces. It now strips
# leading and trailing spaces from the Jenkins response by default. This
# hack is a workaround for the following issue.
# https://issues.jenkins-ci.org/browse/JENKINS-58548
# 0.5
# - Bugfix --automatic-jenkins-server not working when Jenkins is under a URL
# path. e.g. https://example.com/jenkins/
# 0.4
# - Bugfix incorrect host header being set when --automatic-jenkins-server.
# - Bugfix --curl and --curl-init options rendering wrong HEAD method.
# - Bugfix --curl and --curl-init renedering wrong URLs for troubleshooting.
# 0.3
# - Bugfix empty JSON file decoding with an error.
# 0.2
# - SECURITY: fix insecure permissions for headers file.
# - SOCKS5 proxy support.
# - Add a CHANGELOG.
# 0.1 - Jun 16, 2017
# - A generic script which reads Jenkins API and can make calls to arbitrary
# Jenkins URL endpoints. Supported HTTP methods include: GET, HEAD, POST.
# - Print debugging output to stderr.
# - Auto-resolving JENKINS_WEB.
# - Support save/loading HTTP session headers.
# - Support --data-string in jenkins-call-url.
# - Non-zero exit code on HTTP errors with a human readable format.
# - Support for specifying a TLS certificate authority bundle.
# - JENKINS_CALL_ARGS is a more advanced environment variable which gives the
# user flexibility to define arguments they would normally put on the
# command line in an environment variable, instead.