View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.wrapper;
20  
21  import java.io.IOException;
22  import java.io.InputStream;
23  import java.lang.reflect.Method;
24  import java.net.Authenticator;
25  import java.net.PasswordAuthentication;
26  import java.net.URI;
27  import java.net.URL;
28  import java.net.URLConnection;
29  import java.nio.charset.StandardCharsets;
30  import java.nio.file.Files;
31  import java.nio.file.Path;
32  import java.nio.file.StandardCopyOption;
33  import java.util.Locale;
34  import java.util.concurrent.ThreadLocalRandom;
35  
36  import static org.apache.maven.wrapper.MavenWrapperMain.MVNW_PASSWORD;
37  import static org.apache.maven.wrapper.MavenWrapperMain.MVNW_USERNAME;
38  
39  /**
40   * @author Hans Dockter
41   */
42  public class DefaultDownloader implements Downloader {
43      private final String applicationName;
44  
45      private final String applicationVersion;
46  
47      public DefaultDownloader(String applicationName, String applicationVersion) {
48          this.applicationName = applicationName;
49          this.applicationVersion = applicationVersion;
50          configureProxyAuthentication();
51          configureAuthentication();
52      }
53  
54      private void configureProxyAuthentication() {
55          if (System.getProperty("http.proxyUser") != null) {
56              Authenticator.setDefault(new SystemPropertiesProxyAuthenticator());
57          }
58      }
59  
60      private void configureAuthentication() {
61          if (System.getenv(MVNW_USERNAME) != null
62                  && System.getenv(MVNW_PASSWORD) != null
63                  && System.getProperty("http.proxyUser") == null) {
64              Authenticator.setDefault(new Authenticator() {
65                  @Override
66                  protected PasswordAuthentication getPasswordAuthentication() {
67                      return new PasswordAuthentication(
68                              System.getenv(MVNW_USERNAME),
69                              System.getenv(MVNW_PASSWORD).toCharArray());
70                  }
71              });
72          }
73      }
74  
75      @Override
76      public void download(URI address, Path destination) throws Exception {
77          if (Files.exists(destination)) {
78              return;
79          }
80          Files.createDirectories(destination.getParent());
81  
82          if (!"https".equals(address.getScheme())) {
83              Logger.warn("Using an insecure connection to download the Maven distribution."
84                      + " Please consider using HTTPS.");
85          }
86          downloadInternal(address, destination);
87      }
88  
89      private void downloadInternal(URI address, Path destination) throws IOException {
90          URL url = address.toURL();
91          URLConnection conn = url.openConnection();
92          addBasicAuthentication(address, conn);
93          final String userAgentValue = calculateUserAgent();
94          conn.setRequestProperty("User-Agent", userAgentValue);
95  
96          Path temp = destination
97                  .getParent()
98                  .resolve(destination.getFileName() + "."
99                          + Long.toUnsignedString(ThreadLocalRandom.current().nextLong()) + ".tmp");
100         try (InputStream inStream = conn.getInputStream()) {
101             Files.copy(inStream, temp, StandardCopyOption.REPLACE_EXISTING);
102             Files.move(temp, destination, StandardCopyOption.REPLACE_EXISTING);
103         } finally {
104             Files.deleteIfExists(temp);
105         }
106     }
107 
108     private void addBasicAuthentication(URI address, URLConnection connection) {
109         String userInfo = calculateUserInfo(address);
110         if (userInfo == null) {
111             return;
112         }
113         connection.setRequestProperty("Authorization", "Basic " + base64Encode(userInfo));
114     }
115 
116     /**
117      * Base64 encode user info for HTTP Basic Authentication. Try to use {@literal java.util.Base64} encoder which is
118      * available starting with Java 8. Fallback to {@literal javax.xml.bind.DatatypeConverter} from JAXB which is
119      * available starting with Java 6 but is not anymore in Java 9. Fortunately, both of these two Base64 encoders
120      * implement the right Base64 flavor, the one that does not split the output in multiple lines.
121      *
122      * @param userInfo user info
123      * @return Base64 encoded user info
124      * @throws RuntimeException if no public Base64 encoder is available on this JVM
125      */
126     private String base64Encode(String userInfo) {
127         ClassLoader loader = getClass().getClassLoader();
128         try {
129             Method getEncoderMethod = loader.loadClass("java.util.Base64").getMethod("getEncoder");
130             Method encodeMethod =
131                     loader.loadClass("java.util.Base64$Encoder").getMethod("encodeToString", byte[].class);
132             Object encoder = getEncoderMethod.invoke(null);
133             return (String) encodeMethod.invoke(encoder, new Object[] {userInfo.getBytes(StandardCharsets.UTF_8)});
134         } catch (Exception java7OrEarlier) {
135             try {
136                 Method encodeMethod = loader.loadClass("javax.xml.bind.DatatypeConverter")
137                         .getMethod("printBase64Binary", byte[].class);
138                 return (String) encodeMethod.invoke(null, new Object[] {userInfo.getBytes(StandardCharsets.UTF_8)});
139             } catch (Exception java5OrEarlier) {
140                 throw new RuntimeException(
141                         "Downloading Maven distributions with HTTP Basic Authentication"
142                                 + " is not supported on your JVM.",
143                         java5OrEarlier);
144             }
145         }
146     }
147 
148     private String calculateUserInfo(URI uri) {
149         String username = System.getenv(MVNW_USERNAME);
150         String password = System.getenv(MVNW_PASSWORD);
151         if (username != null && password != null) {
152             return username + ':' + password;
153         }
154         return uri.getUserInfo();
155     }
156 
157     private String calculateUserAgent() {
158         String appVersion = applicationVersion;
159 
160         String javaVendor = System.getProperty("java.vendor");
161         String javaVersion = System.getProperty("java.version");
162         String javaVendorVersion = System.getProperty("java.vm.version");
163         String osName = System.getProperty("os.name");
164         String osVersion = System.getProperty("os.version");
165         String osArch = System.getProperty("os.arch");
166         return String.format(
167                 Locale.ROOT,
168                 "%s/%s (%s;%s;%s) (%s;%s;%s)",
169                 applicationName,
170                 appVersion,
171                 osName,
172                 osVersion,
173                 osArch,
174                 javaVendor,
175                 javaVersion,
176                 javaVendorVersion);
177     }
178 
179     private static class SystemPropertiesProxyAuthenticator extends Authenticator {
180         @Override
181         protected PasswordAuthentication getPasswordAuthentication() {
182             return new PasswordAuthentication(
183                     System.getProperty("http.proxyUser"),
184                     System.getProperty("http.proxyPassword", "").toCharArray());
185         }
186     }
187 }