@@ -2,76 +2,163 @@ import parseLink from 'parse-link-header';
2
2
3
3
import { registryAPI , repositoriesPerPage , tagsPerPage , usePortusExplore } from '@/options' ;
4
4
5
+ function parseWWWAuthenticate ( text ) {
6
+ const result = { } ;
7
+ // extract auth-scheme
8
+ const schemeParts = text . split ( ' ' ) ;
9
+ [ result . scheme ] = schemeParts ;
10
+ // extract auth-params
11
+ const remain = schemeParts . slice ( 1 ) . join ( ' ' ) ;
12
+ const parts = remain . split ( / ? , ? / ) ;
13
+ parts . forEach ( ( part ) => {
14
+ // parse auth-param
15
+ const kv = part . split ( '=' ) ;
16
+ if ( kv . length === 2 ) {
17
+ const key = kv [ 0 ] . trim ( ) ;
18
+ const value = kv [ 1 ] . trim ( ) ;
19
+ if ( value . startsWith ( '"' ) && value . endsWith ( '"' ) ) {
20
+ result [ key ] = value . substring ( 1 , value . length - 1 ) ;
21
+ } else {
22
+ result [ key ] = value ;
23
+ }
24
+ } else {
25
+ result [ parts ] = true ;
26
+ }
27
+ } ) ;
28
+ return result ;
29
+ }
30
+
31
+ let cachedToken = null ;
32
+ async function doAuth ( ) {
33
+ if ( cachedToken !== null ) {
34
+ return cachedToken ;
35
+ }
36
+
37
+ // try accessing registry API
38
+ const response = await fetch ( `${ await registryAPI ( ) } /v2/` ) ;
39
+ if ( response . ok ) {
40
+ // token not needed for registry
41
+ cachedToken = false ;
42
+ return cachedToken ;
43
+ }
44
+ if ( response . status !== 401 ) {
45
+ throw new Error ( response . statusText ) ;
46
+ }
47
+
48
+ // solve authentication challenge
49
+ const { headers } = response ;
50
+ if ( ! headers . has ( 'Www-Authenticate' ) ) {
51
+ throw new Error ( 'no challenge presented' ) ;
52
+ }
53
+ const chal = parseWWWAuthenticate ( headers . get ( 'Www-Authenticate' ) ) ;
54
+ if ( chal . scheme !== 'Bearer' ) {
55
+ throw new Error ( 'unsupported scheme' ) ;
56
+ }
57
+
58
+ const tokURL = new URL ( chal . realm ) ;
59
+ tokURL . searchParams . append ( 'client_id' , 'dri-client' ) ;
60
+ tokURL . searchParams . append ( 'service' , chal . service ) ;
61
+ tokURL . searchParams . append ( 'scope' , 'repository:pwd/migrate:pull' ) ;
62
+ const tokResponse = await fetch ( tokURL ) ;
63
+ const tok = await tokResponse . json ( ) ;
64
+ cachedToken = tok . token ;
65
+ return tok . token ;
66
+ }
67
+
5
68
async function paginatable ( path , n , last = null ) {
6
- const url = new URL ( `${ await registryAPI ( ) } ${ path } ` ) ;
7
- if ( n ) url . searchParams . append ( 'n' , n ) ;
8
- if ( last ) url . searchParams . append ( 'last' , last ) ;
9
-
10
- const response = await fetch ( url ) ;
11
- let nextLast = null ;
12
- if ( response . headers . has ( 'Link' ) ) {
13
- const links = parseLink ( response . headers . get ( 'Link' ) ) ;
14
- if ( links && links . next ) {
15
- nextLast = links . next . last ;
16
- }
17
- }
18
- return Object . assign ( await response . json ( ) , { nextLast } ) ;
69
+ const token = await doAuth ( ) ;
70
+ const url = new URL ( `${ await registryAPI ( ) } ${ path } ` ) ;
71
+ if ( n ) url . searchParams . append ( 'n' , n ) ;
72
+ if ( last ) url . searchParams . append ( 'last' , last ) ;
73
+
74
+ const headers = { } ;
75
+ if ( token ) headers . Authorization = `Bearer ${ token } ` ;
76
+ const response = await fetch ( url , { headers } ) ;
77
+ let nextLast = null ;
78
+ if ( response . headers . has ( 'Link' ) ) {
79
+ const links = parseLink ( response . headers . get ( 'Link' ) ) ;
80
+ if ( links && links . next ) {
81
+ nextLast = links . next . last ;
82
+ }
83
+ }
84
+ return Object . assign ( await response . json ( ) , { nextLast } ) ;
19
85
}
20
86
21
87
async function get ( path ) {
22
- const url = new URL ( `${ await registryAPI ( ) } ${ path } ` ) ;
23
- const response = await fetch ( url ) ;
24
- return response . json ( ) ;
88
+ const token = await doAuth ( ) ;
89
+ const url = new URL ( `${ await registryAPI ( ) } ${ path } ` ) ;
90
+ const headers = { } ;
91
+ if ( token ) headers . Authorization = `Bearer ${ token } ` ;
92
+ const response = await fetch ( url , { headers } ) ;
93
+ return response . json ( ) ;
25
94
}
26
95
27
96
async function head ( path ) {
28
- const url = new URL ( `${ await registryAPI ( ) } ${ path } ` ) ;
29
- const response = await fetch ( url , { method : 'HEAD' } ) ;
30
- return response . headers ;
97
+ const token = await doAuth ( ) ;
98
+ const url = new URL ( `${ await registryAPI ( ) } ${ path } ` ) ;
99
+ const headers = { } ;
100
+ if ( token ) headers . Authorization = `Bearer ${ token } ` ;
101
+ const response = await fetch ( url , { method : 'HEAD' , headers } ) ;
102
+ return response . headers ;
31
103
}
32
104
33
105
106
+ async function portus ( ) {
107
+ // TODO: Use the Portus API when it enables anonymous access
108
+ const response = await fetch ( `${ await registryAPI ( ) } /explore?explore%5Bsearch%5D=` ) ;
109
+ const html = await response . text ( ) ;
110
+
111
+ // unconventionally parse out JSON from HTML
112
+ const startString = 'window.repositories = ' ;
113
+ const endString = ';</script>' ;
114
+ const string = html . substring (
115
+ html . lastIndexOf ( startString ) + startString . length ,
116
+ html . indexOf ( endString ) ,
117
+ ) ;
118
+ const object = JSON . parse ( string ) ;
119
+ return object ;
120
+ }
121
+
34
122
async function repos ( last = null ) {
35
- if ( await usePortusExplore ( ) ) {
36
- // TODO: Use the Portus API when it enables anonymous access
37
- const response = await fetch ( `${ await registryAPI ( ) } /explore?explore%5Bsearch%5D=` ) ;
38
- const html = await response . text ( ) ;
39
-
40
- // unconventionally parse out JSON from HTML
41
- const startString = 'window.repositories = ' ;
42
- const endString = ';</script>' ;
43
- const string = html . substring (
44
- html . lastIndexOf ( startString ) + startString . length ,
45
- html . indexOf ( endString ) ,
46
- ) ;
47
- return JSON . parse ( string ) ;
48
- }
49
- return paginatable ( '/v2/_catalog' , await repositoriesPerPage ( ) , last ) ;
123
+ if ( await usePortusExplore ( ) ) {
124
+ const p = await portus ( ) ;
125
+ return {
126
+ repositories : p . map ( r => r . full_name ) ,
127
+ } ;
128
+ }
129
+ return paginatable ( '/v2/_catalog' , await repositoriesPerPage ( ) , last ) ;
50
130
}
51
131
52
132
async function repo ( name ) {
53
- return get ( `/v2/${ name } ` ) ;
133
+ return get ( `/v2/${ name } ` ) ;
54
134
}
55
135
56
136
async function tags ( name , last = null ) {
57
- return paginatable ( `/v2/${ name } /tags/list` , await tagsPerPage ( ) , last ) ;
137
+ if ( await usePortusExplore ( ) ) {
138
+ const p = await portus ( ) ;
139
+ return {
140
+ tags : p . find ( r => r . full_name === name )
141
+ . tags . map ( t => t [ 0 ] . name ) ,
142
+ } ;
143
+ }
144
+ return paginatable ( `/v2/${ name } /tags/list` , await tagsPerPage ( ) , last ) ;
58
145
}
59
146
60
147
async function tag ( name , ref ) {
61
- return get ( `/v2/${ name } /manifests/${ ref } ` ) ;
148
+ return get ( `/v2/${ name } /manifests/${ ref } ` ) ;
62
149
}
63
150
64
151
async function blob ( name , digest ) {
65
- const headers = await head ( `/v2/${ name } /blobs/${ digest } ` ) ;
66
- return {
67
- contentLength : parseInt ( headers . get ( 'Content-Length' ) , 10 ) ,
68
- } ;
152
+ const headers = await head ( `/v2/${ name } /blobs/${ digest } ` ) ;
153
+ return {
154
+ contentLength : parseInt ( headers . get ( 'Content-Length' ) , 10 ) ,
155
+ } ;
69
156
}
70
157
71
158
export {
72
- repos ,
73
- repo ,
74
- tags ,
75
- tag ,
76
- blob ,
159
+ repos ,
160
+ repo ,
161
+ tags ,
162
+ tag ,
163
+ blob ,
77
164
} ;
0 commit comments