88import requests
99import tarfile
1010import urllib3
11+ import re
1112urllib3 .disable_warnings ()
1213
13- if len (sys .argv ) != 2 :
14- print ('Usage:\n \t docker_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 \t docker_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 ("\n Docker 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)
74163if (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
87183layers = 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+
91191os .mkdir (imgdir )
92192print ('Creating image structure in: ' + imgdir )
93193
194+ # Get SHA256 ID image
94195config = 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
96201file = open ('{}/{}.json' .format (imgdir , config [7 :]), 'wb' )
97202file .write (confresp .content )
98203file .close ()
99204
205+ # Prepare content args for json
100206content = [{
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
110216empty_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
115221parentid = ''
116222for 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 ('\r ERROR: 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):
194305file .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'
198309sys .stdout .write ("Creating archive..." )
199310sys .stdout .flush ()
200311tar = tarfile .open (docker_tar , "w" )
201312tar .add (imgdir , arcname = os .path .sep )
202313tar .close ()
203314shutil .rmtree (imgdir )
204- print ('\r Docker image pulled: ' + docker_tar )
315+ print ('\r Docker image pulled: ' + docker_tar )
0 commit comments