1616import static com .google .common .collect .ImmutableList .toImmutableList ;
1717import static java .nio .charset .StandardCharsets .ISO_8859_1 ;
1818
19+ import com .google .auth .Credentials ;
20+ import com .google .auto .value .AutoValue ;
1921import com .google .common .annotations .VisibleForTesting ;
2022import com .google .common .base .Ascii ;
2123import com .google .common .base .Preconditions ;
2224import com .google .common .base .Strings ;
2325import com .google .common .collect .ImmutableList ;
2426import com .google .common .collect .ImmutableMap ;
2527import com .google .common .collect .ImmutableSet ;
28+ import com .google .devtools .build .lib .authandtls .Netrc ;
29+ import com .google .devtools .build .lib .authandtls .NetrcCredentials ;
30+ import com .google .devtools .build .lib .authandtls .NetrcParser ;
2631import com .google .devtools .build .lib .events .Event ;
2732import com .google .devtools .build .lib .events .Reporter ;
33+ import com .google .devtools .build .lib .util .OS ;
34+ import com .google .devtools .build .lib .vfs .FileSystem ;
35+ import com .google .devtools .build .lib .vfs .Path ;
2836import java .io .BufferedReader ;
2937import java .io .IOException ;
3038import java .io .Reader ;
3846import java .nio .file .Paths ;
3947import java .util .Base64 ;
4048import java .util .Collection ;
49+ import java .util .HashMap ;
4150import java .util .List ;
4251import java .util .Map ;
4352import java .util .Objects ;
53+ import java .util .Optional ;
4454import java .util .function .Consumer ;
4555import java .util .function .Function ;
4656import java .util .regex .Matcher ;
4757import java .util .regex .Pattern ;
4858import javax .annotation .Nullable ;
59+ import net .starlark .java .syntax .Location ;
4960
5061/**
5162 * Helper class for taking URLs and converting them according to an optional config specified by
@@ -59,13 +70,19 @@ public class UrlRewriter {
5970 private static final ImmutableSet <String > REWRITABLE_SCHEMES = ImmutableSet .of ("http" , "https" );
6071
6172 private final UrlRewriterConfig config ;
62- private final Function <URL , List <URL >> rewriter ;
73+ private final Function <URL , List <RewrittenURL >> rewriter ;
74+ @ Nullable private final Credentials netrcCreds ;
6375
6476 @ VisibleForTesting
65- UrlRewriter (Consumer <String > log , String filePathForErrorReporting , Reader reader )
77+ UrlRewriter (
78+ Consumer <String > log ,
79+ String filePathForErrorReporting ,
80+ Reader reader ,
81+ @ Nullable Credentials netrcCreds )
6682 throws UrlRewriterParseException {
6783 Preconditions .checkNotNull (reader , "UrlRewriterConfig source must be set" );
6884 this .config = new UrlRewriterConfig (filePathForErrorReporting , reader );
85+ this .netrcCreds = netrcCreds ;
6986
7087 this .rewriter = this ::rewrite ;
7188 }
@@ -75,89 +92,124 @@ public class UrlRewriter {
7592 *
7693 * @param configPath Path to the config file to use. May be null.
7794 * @param reporter Used for logging when URLs are rewritten.
95+ * @param clientEnv a map of the current Bazel command's environment
96+ * @param fileSystem the Blaze file system
7897 */
79- public static UrlRewriter getDownloaderUrlRewriter (String configPath , Reporter reporter )
98+ public static UrlRewriter getDownloaderUrlRewriter (
99+ String configPath ,
100+ Reporter reporter ,
101+ ImmutableMap <String , String > clientEnv ,
102+ FileSystem fileSystem )
80103 throws UrlRewriterParseException {
81104 Consumer <String > log = str -> reporter .handle (Event .info (str ));
82105
106+ // "empty" UrlRewriter shouldn't alter auth headers
83107 if (Strings .isNullOrEmpty (configPath )) {
84- return new UrlRewriter (log , "" , new StringReader ("" ));
108+ return new UrlRewriter (log , "" , new StringReader ("" ), null );
85109 }
86110
111+ Credentials creds = null ;
112+ try {
113+ creds = newCredentialsFromNetrc (clientEnv , fileSystem );
114+ } catch (UrlRewriterParseException e ) {
115+ // If the credentials extraction failed, we're letting bazel try without credentials.
116+ }
87117 try (BufferedReader reader = Files .newBufferedReader (Paths .get (configPath ))) {
88- return new UrlRewriter (log , configPath , reader );
118+ return new UrlRewriter (log , configPath , reader , creds );
89119 } catch (IOException e ) {
90120 throw new UncheckedIOException (e );
91121 }
92122 }
93123
94124 /**
95125 * Rewrites {@code urls} using the configuration provided to {@link
96- * #getDownloaderUrlRewriter(String, Reporter)}. The returned list of URLs may be empty if the
97- * configuration used blocks all the input URLs.
126+ * #getDownloaderUrlRewriter(String, Reporter, ImmutableMap, FileSystem )}. The returned list of
127+ * URLs may be empty if the configuration used blocks all the input URLs.
98128 *
99129 * @param urls The input list of {@link URL}s. May be empty.
100130 * @return The amended lists of URLs.
101131 */
102- public List < URL > amend (List <URL > urls ) {
132+ public ImmutableList < RewrittenURL > amend (List <URL > urls ) {
103133 Objects .requireNonNull (urls , "URLS to check must be set but may be empty" );
104134
105- ImmutableList <URL > rewritten =
106- urls .stream ().map (rewriter ).flatMap (Collection ::stream ).collect (toImmutableList ());
107-
108- return rewritten ;
135+ return urls .stream ().map (rewriter ).flatMap (Collection ::stream ).collect (toImmutableList ());
109136 }
110137
111138 /**
112- * Updates {@code authHeaders} using the userInfo available in the provided {@code urls}.
139+ * Updates {@code authHeaders} using the userInfo available in the provided {@code urls}. Note
140+ * that if the same url is present in both {@code authHeaders} and <b>download config</b> then it
141+ * will be overridden with the value from <b>download config</b>.
113142 *
114143 * @param urls The input list of {@link URL}s. May be empty.
115144 * @param authHeaders A map of the URLs and their corresponding auth tokens.
116145 * @return A map of the updated authentication headers.
117146 */
118147 public Map <URI , Map <String , String >> updateAuthHeaders (
119- List <URL > urls , Map <URI , Map <String , String >> authHeaders ) {
120- ImmutableMap .Builder <URI , Map <String , String >> authHeadersBuilder =
121- ImmutableMap .<URI , Map <String , String >>builder ().putAll (authHeaders );
148+ List <RewrittenURL > urls , Map <URI , Map <String , String >> authHeaders ) {
149+ Map <URI , Map <String , String >> updatedAuthHeaders = new HashMap <>(authHeaders );
122150
123- for (URL url : urls ) {
124- String userInfo = url .getUserInfo ();
151+ for (RewrittenURL url : urls ) {
152+ // if URL was not re-written by UrlRewriter in first place, we should not attach auth headers
153+ // to it
154+ if (!url .rewritten ()) {
155+ continue ;
156+ }
157+
158+ String userInfo = url .url ().getUserInfo ();
125159 if (userInfo != null ) {
126160 try {
127161 String token =
128162 "Basic " + Base64 .getEncoder ().encodeToString (userInfo .getBytes (ISO_8859_1 ));
129- authHeadersBuilder .put (url .toURI (), ImmutableMap .of ("Authorization" , token ));
163+ updatedAuthHeaders .put (url . url () .toURI (), ImmutableMap .of ("Authorization" , token ));
130164 } catch (URISyntaxException e ) {
131165 // If the credentials extraction failed, we're letting bazel try without credentials.
132166 }
167+ } else if (this .netrcCreds != null ) {
168+ try {
169+ Map <String , List <String >> urlAuthHeaders =
170+ this .netrcCreds .getRequestMetadata (url .url ().toURI ());
171+ if (urlAuthHeaders == null || urlAuthHeaders .isEmpty ()) {
172+ continue ;
173+ }
174+ // there could be multiple Auth headers, take the first one
175+ Map .Entry <String , List <String >> firstAuthHeader =
176+ urlAuthHeaders .entrySet ().stream ().findFirst ().get ();
177+ if (firstAuthHeader .getValue () != null && !firstAuthHeader .getValue ().isEmpty ()) {
178+ updatedAuthHeaders .put (
179+ url .url ().toURI (),
180+ ImmutableMap .of (firstAuthHeader .getKey (), firstAuthHeader .getValue ().get (0 )));
181+ }
182+ } catch (URISyntaxException | IOException e ) {
183+ // If the credentials extraction failed, we're letting bazel try without credentials.
184+ }
133185 }
134186 }
135187
136- return authHeadersBuilder . build ( );
188+ return ImmutableMap . copyOf ( updatedAuthHeaders );
137189 }
138190
139- private ImmutableList <URL > rewrite (URL url ) {
191+ private ImmutableList <RewrittenURL > rewrite (URL url ) {
140192 Preconditions .checkNotNull (url );
141193
142194 // Cowardly refuse to rewrite non-HTTP(S) urls
143195 if (REWRITABLE_SCHEMES .stream ()
144196 .noneMatch (scheme -> Ascii .equalsIgnoreCase (scheme , url .getProtocol ()))) {
145- return ImmutableList .of (url );
197+ return ImmutableList .of (RewrittenURL . create ( url , false ) );
146198 }
147199
148- List < URL > rewrittenUrls = applyRewriteRules (url );
200+ ImmutableList < RewrittenURL > rewrittenUrls = applyRewriteRules (url );
149201
150- ImmutableList .Builder <URL > toReturn = ImmutableList .builder ();
202+ ImmutableList .Builder <RewrittenURL > toReturn = ImmutableList .builder ();
151203 // Now iterate over the URLs
152- for (URL consider : rewrittenUrls ) {
204+ for (RewrittenURL consider : rewrittenUrls ) {
153205 // If there's an allow entry, add it to the set to return and continue
154- if (isAllowMatched (consider )) {
206+ if (isAllowMatched (consider . url () )) {
155207 toReturn .add (consider );
156208 continue ;
157209 }
158210
159211 // If there's no block that matches the domain, add it to the set to return and continue
160- if (!isBlockMatched (consider )) {
212+ if (!isBlockMatched (consider . url () )) {
161213 toReturn .add (consider );
162214 }
163215 }
@@ -192,7 +244,7 @@ private static boolean isMatchingHostName(URL url, String host) {
192244 return host .equals (url .getHost ()) || url .getHost ().endsWith ("." + host );
193245 }
194246
195- private ImmutableList <URL > applyRewriteRules (URL url ) {
247+ private ImmutableList <RewrittenURL > applyRewriteRules (URL url ) {
196248 String withoutScheme = url .toString ().substring (url .getProtocol ().length () + 3 );
197249
198250 ImmutableSet .Builder <String > rewrittenUrls = ImmutableSet .builder ();
@@ -210,11 +262,12 @@ private ImmutableList<URL> applyRewriteRules(URL url) {
210262 }
211263
212264 if (!matchMade ) {
213- return ImmutableList .of (url );
265+ return ImmutableList .of (RewrittenURL . create ( url , false ) );
214266 }
215267
216268 return rewrittenUrls .build ().stream ()
217269 .map (urlString -> prefixWithProtocol (urlString , url .getProtocol ()))
270+ .map (plainUrl -> RewrittenURL .create (plainUrl , true ))
218271 .collect (toImmutableList ());
219272 }
220273
@@ -232,8 +285,64 @@ private static URL prefixWithProtocol(String url, String protocol) {
232285 }
233286 }
234287
288+ /**
289+ * Create a new {@link Credentials} object by parsing the .netrc file with following order to
290+ * search it:
291+ *
292+ * <ol>
293+ * <li>If environment variable $NETRC exists, use it as the path to the .netrc file
294+ * <li>Fallback to $HOME/.netrc or $USERPROFILE/.netrc
295+ * </ol>
296+ *
297+ * @return the {@link Credentials} object or {@code null} if there is no .netrc file.
298+ * @throws UrlRewriterParseException in case the credentials can't be constructed.
299+ */
300+ // TODO : consider re-using RemoteModule.newCredentialsFromNetrc
301+ @ VisibleForTesting
302+ static Credentials newCredentialsFromNetrc (Map <String , String > clientEnv , FileSystem fileSystem )
303+ throws UrlRewriterParseException {
304+ final Optional <String > homeDir ;
305+ if (OS .getCurrent () == OS .WINDOWS ) {
306+ homeDir = Optional .ofNullable (clientEnv .get ("USERPROFILE" ));
307+ } else {
308+ homeDir = Optional .ofNullable (clientEnv .get ("HOME" ));
309+ }
310+ String netrcFileString =
311+ Optional .ofNullable (clientEnv .get ("NETRC" ))
312+ .orElseGet (() -> homeDir .map (home -> home + "/.netrc" ).orElse (null ));
313+ if (netrcFileString == null ) {
314+ return null ;
315+ }
316+ Location location = Location .fromFileLineColumn (netrcFileString , 0 , 0 );
317+
318+ Path netrcFile = fileSystem .getPath (netrcFileString );
319+ if (netrcFile .exists ()) {
320+ try {
321+ Netrc netrc = NetrcParser .parseAndClose (netrcFile .getInputStream ());
322+ return new NetrcCredentials (netrc );
323+ } catch (IOException e ) {
324+ throw new UrlRewriterParseException (
325+ "Failed to parse " + netrcFile .getPathString () + ": " + e .getMessage (), location );
326+ }
327+ } else {
328+ return null ;
329+ }
330+ }
331+
235332 @ Nullable
236333 public String getAllBlockedMessage () {
237334 return config .getAllBlockedMessage ();
238335 }
336+
337+ /** Holds the URL along with meta-info, such as whether URL was re-written or not. */
338+ @ AutoValue
339+ public abstract static class RewrittenURL {
340+ static RewrittenURL create (URL url , boolean rewritten ) {
341+ return new AutoValue_UrlRewriter_RewrittenURL (url , rewritten );
342+ }
343+
344+ abstract URL url ();
345+
346+ abstract boolean rewritten ();
347+ }
239348}
0 commit comments