Skip to content

Commit 99be3cd

Browse files
Lenrys29Lenrys29
authored andcommitted
(Feature) Add authentication + private Nexus oss repository access
Author: Lenrys29
1 parent 5413165 commit 99be3cd

1 file changed

Lines changed: 168 additions & 57 deletions

File tree

docker_pull.py

Lines changed: 168 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -8,51 +8,79 @@
88
import requests
99
import tarfile
1010
import urllib3
11+
import re
1112
urllib3.disable_warnings()
1213

13-
if len(sys.argv) != 2 :
14-
print('Usage:\n\tdocker_pull.py [registry/][repository/]image[:tag|@digest]\n')
15-
exit(1)
14+
# Graphical needs for drawing full line
15+
console_rows, console_columns = os.popen('stty size', 'r').read().split()
1616

17-
# Look for the Docker image to download
18-
repo = 'library'
19-
tag = 'latest'
20-
imgparts = sys.argv[1].split('/')
21-
try:
22-
img,tag = imgparts[-1].split('@')
23-
except ValueError:
24-
try:
25-
img,tag = imgparts[-1].split(':')
26-
except ValueError:
27-
img = imgparts[-1]
28-
# Docker client doesn't seem to consider the first element as a potential registry unless there is a '.' or ':'
29-
if len(imgparts) > 1 and ('.' in imgparts[0] or ':' in imgparts[0]):
30-
registry = imgparts[0]
31-
repo = '/'.join(imgparts[1:-1])
32-
else:
33-
registry = 'registry-1.docker.io'
34-
if len(imgparts[:-1]) != 0:
35-
repo = '/'.join(imgparts[:-1])
17+
############# DEFAULTs VAR
18+
19+
DOCKER_DEFAULT_auth_url='auth.docker.io/token'
20+
DOCKER_DEFAULT_server_url='registry-1.docker.io'
21+
DOCKER_DEFAULT_repo = 'library'
22+
DOCKER_DEFAULT_tag = 'latest'
23+
24+
username = ""
25+
password = ""
26+
27+
json_manifest_type='application/vnd.docker.distribution.manifest.v2+json'
28+
json_manifest_type_bis='application/vnd.docker.distribution.manifest.list.v2+json'
29+
30+
############################################ FUNCTION ######################################################
31+
32+
# Get endpoint registry from url
33+
def get_endpoint_registry(url,repository):
34+
resp = requests.get('https://{}/v2/'.format(url), verify=False)
35+
server_auth_url=""
36+
37+
# If we get 401, we need to authenticate, so get server_auth_url
38+
if resp.status_code == 401:
39+
try:
40+
realm_address = re.search('realm="([^"]*)"',resp.headers['WWW-Authenticate'])
41+
42+
# If Repository is on NEXUS OSS
43+
if realm_address.group(1) == "Sonatype Nexus Repository Manager":
44+
server_auth_url = "https://" + url + "/v2/"
45+
print ("Nexus OSS repository type")
46+
47+
# If Repository is on DockerHub like
48+
if realm_address.group(1) != url and "http" in realm_address.group(1) :
49+
service = re.search('service="([^"]*)"',resp.headers['WWW-Authenticate'])
50+
server_auth_url = realm_address.group(1) + "?service=" + service.group(1) + "&scope=repository:" + repository + ":pull"
51+
print ("Docker Hub repository type")
52+
53+
except IndexError:
54+
server_auth_url = "https://" + url + "/v2/"
55+
print ("failed !")
56+
57+
return server_auth_url
58+
59+
# Get authentication headers
60+
def get_auth_head(registry_endpoint,type):
61+
62+
# Get authentication header from endpoint
63+
if len(username) != 0 and len(password) != 0:
64+
resp = requests.get('{}'.format(registry_endpoint), auth=(username, password),verify=False)
3665
else:
37-
repo = 'library'
38-
repository = '{}/{}'.format(repo, img)
39-
40-
# Get Docker authentication endpoint when it is required
41-
auth_url='https://auth.docker.io/token'
42-
reg_service='registry.docker.io'
43-
resp = requests.get('https://{}/v2/'.format(registry), verify=False)
44-
if resp.status_code == 401:
45-
auth_url = resp.headers['WWW-Authenticate'].split('"')[1]
66+
resp = requests.get('{}'.format(registry_endpoint), verify=False)
67+
68+
# Check if status is 401 => auth fail
69+
if resp.status_code == 401:
70+
if len(username) != 0 and len(password) != 0:
71+
print ("Authentication failed !")
72+
else:
73+
print ("Authentication needed !")
74+
exit (1)
75+
76+
# Generate authentication header from response
4677
try:
47-
reg_service = resp.headers['WWW-Authenticate'].split('"')[3]
48-
except IndexError:
49-
reg_service = ""
50-
51-
# Get Docker token (this function is useless for unauthenticated registries like Microsoft)
52-
def get_auth_head(type):
53-
resp = requests.get('{}?service={}&scope=repository:{}:pull'.format(auth_url, reg_service, repository), verify=False)
54-
access_token = resp.json()['token']
55-
auth_head = {'Authorization':'Bearer '+ access_token, 'Accept': type}
78+
access_token = resp.json()['token']
79+
auth_head = {'Authorization':'Bearer '+ access_token, 'Accept': type}
80+
except ValueError:
81+
access_token = resp.request.headers['Authorization'].split("Basic ")[1]
82+
auth_head = {'Authorization':'Basic '+ access_token, 'Accept': type}
83+
5684
return auth_head
5785

5886
# Docker style progress bar
@@ -68,53 +96,134 @@ def progress_bar(ublob, nb_traits):
6896
sys.stdout.write(']')
6997
sys.stdout.flush()
7098

71-
# Fetch manifest v2 and get image layer digests
72-
auth_head = get_auth_head('application/vnd.docker.distribution.manifest.v2+json')
73-
resp = requests.get('https://{}/v2/{}/manifests/{}'.format(registry, repository, tag), headers=auth_head, verify=False)
99+
#/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\#
100+
101+
############################################## MAIN ########################################################
102+
103+
############## Check if args < 2
104+
105+
if len(sys.argv) < 2 :
106+
print('Usage:\n\tdocker_pull.py [registry/][repository/]image[:tag|@digest] [username] [password]\n')
107+
exit(1)
108+
109+
############## Get info from arg
110+
111+
imgparts = sys.argv[1].split('/')
112+
113+
############## Setup username & password
114+
if len(sys.argv) == 4:
115+
username = sys.argv[2]
116+
password = sys.argv[3]
117+
118+
############## Get repository url + registry url for auth
119+
120+
if len(imgparts) > 1 and ('.' in imgparts[0] or ':' in imgparts[0]):
121+
registry_url = imgparts[0]
122+
repository = imgparts[1]
123+
124+
if len(imgparts[:-2]) != 0:
125+
img = ('/'.join(imgparts[2:])).split(':')[0]
126+
tag = ('/'.join(imgparts[2:])).split(':')[1]
127+
else:
128+
img = (imgparts[2:]).split(':')[0]
129+
tag = (imgparts[2:]).split(':')[1]
130+
else:
131+
registry_url = DOCKER_DEFAULT_server_url
132+
133+
if len(imgparts[:-1]) != 0:
134+
img = "" # FIXME: Image name on docker hub is actually the repository url
135+
tag = ('/'.join(imgparts).split('/')[1].split(':')[1])
136+
repository = ('/'.join(imgparts).split(':')[0])
137+
else:
138+
img = "" # FIXME: Image name on docker hub is actually the repository url
139+
tag = (imgparts[0]).split(':')[1]
140+
repository = (imgparts[0]).split(':')[0]
141+
142+
############## Get Registry Authentication endpoint when it is required
143+
registry_endpoint = get_endpoint_registry(registry_url,repository)
144+
145+
# Printing vars
146+
147+
print('_'*int(console_columns))
148+
print ("\nDocker image :\t\t\t" + img)
149+
print ("Docker tag :\t\t\t" + tag)
150+
print ("Repository :\t\t\t" + repository )
151+
print ("Serveur_URL :\t\t\t" + "https://" + registry_url )
152+
print ( "Registry_endpoint :\t\t" + registry_endpoint)
153+
print('_'*int(console_columns))
154+
155+
############## Fetch manifest v2 and get image layer digests
156+
157+
# Get manifest v2
158+
auth_head=get_auth_head(registry_endpoint,json_manifest_type)
159+
160+
resp = requests.get('https://{}/v2/{}/{}/manifests/{}'.format(registry_url, repository, img, tag), headers=auth_head, verify=False)
161+
162+
# Check if error (not getting manifest)
74163
if (resp.status_code != 200):
75164
print('[-] Cannot fetch manifest for {} [HTTP {}]'.format(repository, resp.status_code))
76-
print(resp.content)
77-
auth_head = get_auth_head('application/vnd.docker.distribution.manifest.list.v2+json')
78-
resp = requests.get('https://{}/v2/{}/manifests/{}'.format(registry, repository, tag), headers=auth_head, verify=False)
165+
166+
# Retry with other json_manifest_type
167+
auth_head = get_auth_head(registry_endpoint,json_manifest_type_bis)
168+
resp = requests.get('https://{}/v2/{}/{}/manifests/{}'.format(registry_url, repository, img, tag), headers=auth_head, verify=False)
169+
79170
if (resp.status_code == 200):
80171
print('[+] Manifests found for this tag (use the @digest format to pull the corresponding image):')
81172
manifests = resp.json()['manifests']
82173
for manifest in manifests:
83174
for key, value in manifest["platform"].items():
84175
sys.stdout.write('{}: {}, '.format(key, value))
85176
print('digest: {}'.format(manifest["digest"]))
86-
exit(1)
177+
else:
178+
print ("Errors : ")
179+
print (resp.headers)
180+
exit(1)
181+
182+
# Get all layers from manifest
87183
layers = resp.json()['layers']
88184

89185
# Create tmp folder that will hold the image
90-
imgdir = 'tmp_{}_{}'.format(img, tag.replace(':', '@'))
186+
imgdir = '/tmp/tmp_{}_{}'.format(img.replace('/', '.'), tag)
187+
188+
if os.path.exists(imgdir):
189+
shutil.rmtree(imgdir)
190+
91191
os.mkdir(imgdir)
92192
print('Creating image structure in: ' + imgdir)
93193

194+
# Get SHA256 ID image
94195
config = resp.json()['config']['digest']
95-
confresp = requests.get('https://{}/v2/{}/blobs/{}'.format(registry, repository, config), headers=auth_head, verify=False)
196+
197+
# Get manifest for SHA256 ID image
198+
confresp = requests.get('https://{}/v2/{}/blobs/{}'.format(registry_url, repository, config), headers=auth_head, verify=False)
199+
200+
# Write manifest inside file
96201
file = open('{}/{}.json'.format(imgdir, config[7:]), 'wb')
97202
file.write(confresp.content)
98203
file.close()
99204

205+
# Prepare content args for json
100206
content = [{
101207
'Config': config[7:] + '.json',
102208
'RepoTags': [ ],
103209
'Layers': [ ]
104210
}]
105-
if len(imgparts[:-1]) != 0:
106-
content[0]['RepoTags'].append('/'.join(imgparts[:-1]) + '/' + img + ':' + tag)
107-
else:
108-
content[0]['RepoTags'].append(img + ':' + tag)
109211

212+
# Set content tag
213+
content[0]['RepoTags'].append(sys.argv[1])
214+
215+
# Prepare template json
110216
empty_json = '{"created":"1970-01-01T00:00:00Z","container_config":{"Hostname":"","Domainname":"","User":"","AttachStdin":false, \
111217
"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false, "StdinOnce":false,"Env":null,"Cmd":null,"Image":"", \
112218
"Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":null}}'
113219

114220
# Build layer folders
115221
parentid=''
116222
for layer in layers:
223+
224+
#Get digest of layer
117225
ublob = layer['digest']
226+
118227
# FIXME: Creating fake layer ID. Don't know how Docker generates it
119228
fake_layerid = hashlib.sha256((parentid+'\n'+ublob+'\n').encode('utf-8')).hexdigest()
120229
layerdir = imgdir + '/' + fake_layerid
@@ -128,14 +237,16 @@ def progress_bar(ublob, nb_traits):
128237
# Creating layer.tar file
129238
sys.stdout.write(ublob[7:19] + ': Downloading...')
130239
sys.stdout.flush()
131-
auth_head = get_auth_head('application/vnd.docker.distribution.manifest.v2+json') # refreshing token to avoid its expiration
132-
bresp = requests.get('https://{}/v2/{}/blobs/{}'.format(registry, repository, ublob), headers=auth_head, stream=True, verify=False)
240+
auth_head = get_auth_head(registry_endpoint,json_manifest_type) # refreshing token to avoid its expiration
241+
242+
bresp = requests.get('https://{}/v2/{}/blobs/{}'.format(registry_url, repository, ublob), headers=auth_head, stream=True, verify=False)
133243
if (bresp.status_code != 200): # When the layer is located at a custom URL
134244
bresp = requests.get(layer['urls'][0], headers=auth_head, stream=True, verify=False)
135245
if (bresp.status_code != 200):
136246
print('\rERROR: Cannot download layer {} [HTTP {}]'.format(ublob[7:19], bresp.status_code, bresp.headers['Content-Length']))
137247
print(bresp.content)
138248
exit(1)
249+
139250
# Stream download and follow the progress
140251
bresp.raise_for_status()
141252
unit = int(bresp.headers['Content-Length']) / 50
@@ -194,11 +305,11 @@ def progress_bar(ublob, nb_traits):
194305
file.close()
195306

196307
# Create image tar and clean tmp folder
197-
docker_tar = repo.replace('/', '_') + '_' + img + '.tar'
308+
docker_tar = repository.replace('/', '_') + '_' + img.replace('/', '_') + '.tar'
198309
sys.stdout.write("Creating archive...")
199310
sys.stdout.flush()
200311
tar = tarfile.open(docker_tar, "w")
201312
tar.add(imgdir, arcname=os.path.sep)
202313
tar.close()
203314
shutil.rmtree(imgdir)
204-
print('\rDocker image pulled: ' + docker_tar)
315+
print('\rDocker image pulled: ' + docker_tar)

0 commit comments

Comments
 (0)