| 
									
										
										
										
											2020-03-23 15:35:58 +01:00
										 |  |  | /*
 | 
					
						
							|  |  |  |  * MIT License
 | 
					
						
							|  |  |  |  *
 | 
					
						
							|  |  |  |  * Copyright (c) 2020-present Cloudogu GmbH and Contributors
 | 
					
						
							|  |  |  |  *
 | 
					
						
							|  |  |  |  * Permission is hereby granted, free of charge, to any person obtaining a copy
 | 
					
						
							|  |  |  |  * of this software and associated documentation files (the "Software"), to deal
 | 
					
						
							|  |  |  |  * in the Software without restriction, including without limitation the rights
 | 
					
						
							|  |  |  |  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 | 
					
						
							|  |  |  |  * copies of the Software, and to permit persons to whom the Software is
 | 
					
						
							|  |  |  |  * furnished to do so, subject to the following conditions:
 | 
					
						
							|  |  |  |  *
 | 
					
						
							|  |  |  |  * The above copyright notice and this permission notice shall be included in all
 | 
					
						
							|  |  |  |  * copies or substantial portions of the Software.
 | 
					
						
							|  |  |  |  *
 | 
					
						
							|  |  |  |  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 | 
					
						
							|  |  |  |  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 | 
					
						
							|  |  |  |  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 | 
					
						
							|  |  |  |  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 | 
					
						
							|  |  |  |  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 | 
					
						
							|  |  |  |  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 | 
					
						
							|  |  |  |  * SOFTWARE.
 | 
					
						
							|  |  |  |  */
 | 
					
						
							| 
									
										
										
										
											2020-03-25 15:31:20 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-08-20 14:43:48 +02:00
										 |  |  | package sonia.scm.plugin;
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-08-21 07:44:50 +02:00
										 |  |  | import com.google.common.hash.HashCode;
 | 
					
						
							| 
									
										
										
										
											2019-08-20 14:43:48 +02:00
										 |  |  | import com.google.common.hash.Hashing;
 | 
					
						
							| 
									
										
										
										
											2019-08-21 07:44:50 +02:00
										 |  |  | import com.google.common.hash.HashingInputStream;
 | 
					
						
							| 
									
										
										
										
											2019-08-20 14:43:48 +02:00
										 |  |  | import sonia.scm.SCMContextProvider;
 | 
					
						
							|  |  |  | import sonia.scm.net.ahc.AdvancedHttpClient;
 | 
					
						
							| 
									
										
										
										
											2021-12-13 15:15:57 +01:00
										 |  |  | import sonia.scm.net.ahc.AdvancedHttpRequest;
 | 
					
						
							| 
									
										
										
										
											2019-08-20 14:43:48 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | import javax.inject.Inject;
 | 
					
						
							|  |  |  | import java.io.IOException;
 | 
					
						
							|  |  |  | import java.io.InputStream;
 | 
					
						
							| 
									
										
										
										
											2019-08-21 07:44:50 +02:00
										 |  |  | import java.nio.file.Files;
 | 
					
						
							|  |  |  | import java.nio.file.Path;
 | 
					
						
							|  |  |  | import java.nio.file.Paths;
 | 
					
						
							| 
									
										
										
										
											2019-08-20 14:43:48 +02:00
										 |  |  | import java.util.Optional;
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-11-04 09:37:24 +01:00
										 |  |  | import static sonia.scm.plugin.Tracing.SPAN_KIND;
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-12-13 15:15:57 +01:00
										 |  |  | @SuppressWarnings("UnstableApiUsage") // guava hash is marked as unstable
 | 
					
						
							| 
									
										
										
										
											2019-08-20 14:43:48 +02:00
										 |  |  | class PluginInstaller {
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-05 15:28:39 +02:00
										 |  |  |   private final SCMContextProvider scmContext;
 | 
					
						
							| 
									
										
										
										
											2019-08-20 14:43:48 +02:00
										 |  |  |   private final AdvancedHttpClient client;
 | 
					
						
							| 
									
										
										
										
											2021-12-13 15:15:57 +01:00
										 |  |  |   private final PluginCenterAuthenticator authenticator;
 | 
					
						
							| 
									
										
										
										
											2020-01-24 12:01:21 +01:00
										 |  |  |   private final SmpDescriptorExtractor smpDescriptorExtractor;
 | 
					
						
							| 
									
										
										
										
											2019-08-20 14:43:48 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |   @Inject
 | 
					
						
							| 
									
										
										
										
											2021-12-13 15:15:57 +01:00
										 |  |  |   public PluginInstaller(SCMContextProvider scmContext, AdvancedHttpClient client, PluginCenterAuthenticator authenticator, SmpDescriptorExtractor smpDescriptorExtractor) {
 | 
					
						
							| 
									
										
										
										
											2020-08-05 15:28:39 +02:00
										 |  |  |     this.scmContext = scmContext;
 | 
					
						
							| 
									
										
										
										
											2019-08-20 14:43:48 +02:00
										 |  |  |     this.client = client;
 | 
					
						
							| 
									
										
										
										
											2021-12-13 15:15:57 +01:00
										 |  |  |     this.authenticator = authenticator;
 | 
					
						
							| 
									
										
										
										
											2020-01-24 12:01:21 +01:00
										 |  |  |     this.smpDescriptorExtractor = smpDescriptorExtractor;
 | 
					
						
							| 
									
										
										
										
											2019-08-20 14:43:48 +02:00
										 |  |  |   }
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-08-21 16:46:50 +02:00
										 |  |  |   @SuppressWarnings("squid:S4790") // hashing should be safe
 | 
					
						
							| 
									
										
										
										
											2020-08-05 15:28:39 +02:00
										 |  |  |   public PendingPluginInstallation install(PluginInstallationContext context, AvailablePlugin plugin) {
 | 
					
						
							| 
									
										
										
										
											2019-08-21 07:56:52 +02:00
										 |  |  |     Path file = null;
 | 
					
						
							| 
									
										
										
										
											2019-08-21 07:44:50 +02:00
										 |  |  |     try (HashingInputStream input = new HashingInputStream(Hashing.sha256(), download(plugin))) {
 | 
					
						
							| 
									
										
										
										
											2019-08-21 07:56:52 +02:00
										 |  |  |       file = createFile(plugin);
 | 
					
						
							| 
									
										
										
										
											2019-08-21 07:44:50 +02:00
										 |  |  |       Files.copy(input, file);
 | 
					
						
							| 
									
										
										
										
											2019-08-20 14:43:48 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-08-21 07:56:52 +02:00
										 |  |  |       verifyChecksum(plugin, input.hash(), file);
 | 
					
						
							| 
									
										
										
										
											2020-08-05 16:54:48 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |       InstalledPluginDescriptor descriptor = smpDescriptorExtractor.extractPluginDescriptor(file);
 | 
					
						
							|  |  |  |       verifyInformation(plugin.getDescriptor(), descriptor);
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-10 13:59:30 +02:00
										 |  |  |       PluginInstallationVerifier.verify(context, descriptor);
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-08-21 12:49:15 +02:00
										 |  |  |       return new PendingPluginInstallation(plugin.install(), file);
 | 
					
						
							| 
									
										
										
										
											2020-08-05 16:54:48 +02:00
										 |  |  |     } catch (PluginException ex) {
 | 
					
						
							|  |  |  |       cleanup(file);
 | 
					
						
							|  |  |  |       throw ex;
 | 
					
						
							| 
									
										
										
										
											2019-08-20 14:43:48 +02:00
										 |  |  |     } catch (IOException ex) {
 | 
					
						
							| 
									
										
										
										
											2019-08-21 07:56:52 +02:00
										 |  |  |       cleanup(file);
 | 
					
						
							| 
									
										
										
										
											2020-03-25 15:31:20 +01:00
										 |  |  |       throw new PluginDownloadException(plugin, ex);
 | 
					
						
							| 
									
										
										
										
											2019-08-20 14:43:48 +02:00
										 |  |  |     }
 | 
					
						
							|  |  |  |   }
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-11 08:07:06 +02:00
										 |  |  |   private void verifyInformation(AvailablePluginDescriptor descriptorFromPluginCenter, InstalledPluginDescriptor downloadedDescriptor) {
 | 
					
						
							|  |  |  |     verifyInformation(descriptorFromPluginCenter.getInformation(), downloadedDescriptor.getInformation());
 | 
					
						
							| 
									
										
										
										
											2020-08-05 16:54:48 +02:00
										 |  |  |   }
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-11 08:07:06 +02:00
										 |  |  |   private void verifyInformation(PluginInformation informationFromPluginCenter, PluginInformation downloadedInformation) {
 | 
					
						
							|  |  |  |     if (!informationFromPluginCenter.getName().equals(downloadedInformation.getName())) {
 | 
					
						
							| 
									
										
										
										
											2020-08-05 16:54:48 +02:00
										 |  |  |       throw new PluginInformationMismatchException(
 | 
					
						
							| 
									
										
										
										
											2020-08-11 08:07:06 +02:00
										 |  |  |         informationFromPluginCenter, downloadedInformation,
 | 
					
						
							| 
									
										
										
										
											2020-08-05 16:54:48 +02:00
										 |  |  |         String.format(
 | 
					
						
							|  |  |  |           "downloaded plugin name \"%s\" does not match the expected name \"%s\" from plugin-center",
 | 
					
						
							| 
									
										
										
										
											2020-08-11 08:07:06 +02:00
										 |  |  |           downloadedInformation.getName(),
 | 
					
						
							|  |  |  |           informationFromPluginCenter.getName()
 | 
					
						
							| 
									
										
										
										
											2020-08-05 16:54:48 +02:00
										 |  |  |         )
 | 
					
						
							|  |  |  |       );
 | 
					
						
							|  |  |  |     }
 | 
					
						
							| 
									
										
										
										
											2020-08-11 08:07:06 +02:00
										 |  |  |     if (!informationFromPluginCenter.getVersion().equals(downloadedInformation.getVersion())) {
 | 
					
						
							| 
									
										
										
										
											2020-08-05 16:54:48 +02:00
										 |  |  |       throw new PluginInformationMismatchException(
 | 
					
						
							| 
									
										
										
										
											2020-08-11 08:07:06 +02:00
										 |  |  |         informationFromPluginCenter, downloadedInformation,
 | 
					
						
							| 
									
										
										
										
											2020-08-05 16:54:48 +02:00
										 |  |  |         String.format(
 | 
					
						
							|  |  |  |           "downloaded plugin version \"%s\" does not match the expected version \"%s\" from plugin-center",
 | 
					
						
							| 
									
										
										
										
											2020-08-11 08:07:06 +02:00
										 |  |  |           downloadedInformation.getVersion(),
 | 
					
						
							|  |  |  |           informationFromPluginCenter.getVersion()
 | 
					
						
							| 
									
										
										
										
											2020-08-05 16:54:48 +02:00
										 |  |  |         )
 | 
					
						
							|  |  |  |       );
 | 
					
						
							|  |  |  |     }
 | 
					
						
							|  |  |  |   }
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-08-21 07:56:52 +02:00
										 |  |  |   private void cleanup(Path file) {
 | 
					
						
							|  |  |  |     try {
 | 
					
						
							|  |  |  |       if (file != null) {
 | 
					
						
							|  |  |  |         Files.deleteIfExists(file);
 | 
					
						
							|  |  |  |       }
 | 
					
						
							|  |  |  |     } catch (IOException e) {
 | 
					
						
							| 
									
										
										
										
											2020-03-25 15:31:20 +01:00
										 |  |  |       throw new PluginCleanupException(file);
 | 
					
						
							| 
									
										
										
										
											2019-08-21 07:56:52 +02:00
										 |  |  |     }
 | 
					
						
							|  |  |  |   }
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   private void verifyChecksum(AvailablePlugin plugin, HashCode hash, Path file) {
 | 
					
						
							| 
									
										
										
										
											2019-08-20 14:43:48 +02:00
										 |  |  |     Optional<String> checksum = plugin.getDescriptor().getChecksum();
 | 
					
						
							|  |  |  |     if (checksum.isPresent()) {
 | 
					
						
							| 
									
										
										
										
											2019-08-21 07:44:50 +02:00
										 |  |  |       String calculatedChecksum = hash.toString();
 | 
					
						
							| 
									
										
										
										
											2019-08-20 14:43:48 +02:00
										 |  |  |       if (!checksum.get().equalsIgnoreCase(calculatedChecksum)) {
 | 
					
						
							| 
									
										
										
										
											2019-08-21 07:56:52 +02:00
										 |  |  |         cleanup(file);
 | 
					
						
							| 
									
										
										
										
											2020-03-25 15:31:20 +01:00
										 |  |  |         throw new PluginChecksumMismatchException(plugin, calculatedChecksum, checksum.get());
 | 
					
						
							| 
									
										
										
										
											2019-08-20 14:43:48 +02:00
										 |  |  |       }
 | 
					
						
							|  |  |  |     }
 | 
					
						
							|  |  |  |   }
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   private InputStream download(AvailablePlugin plugin) throws IOException {
 | 
					
						
							| 
									
										
										
										
											2021-12-13 15:15:57 +01:00
										 |  |  |     AdvancedHttpRequest request = client.get(plugin.getDescriptor().getUrl()).spanKind(SPAN_KIND);
 | 
					
						
							|  |  |  |     if (authenticator.isAuthenticated()) {
 | 
					
						
							|  |  |  |       request.bearerAuth(authenticator.fetchAccessToken());
 | 
					
						
							|  |  |  |     }
 | 
					
						
							|  |  |  |     return request.request().contentAsStream();
 | 
					
						
							| 
									
										
										
										
											2019-08-20 14:43:48 +02:00
										 |  |  |   }
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-08-21 07:44:50 +02:00
										 |  |  |   private Path createFile(AvailablePlugin plugin) throws IOException {
 | 
					
						
							| 
									
										
										
										
											2020-08-05 15:28:39 +02:00
										 |  |  |     Path directory = scmContext.resolve(Paths.get("plugins"));
 | 
					
						
							| 
									
										
										
										
											2019-08-21 07:44:50 +02:00
										 |  |  |     Files.createDirectories(directory);
 | 
					
						
							|  |  |  |     return directory.resolve(plugin.getDescriptor().getInformation().getName() + ".smp");
 | 
					
						
							| 
									
										
										
										
											2019-08-20 14:43:48 +02:00
										 |  |  |   }
 | 
					
						
							|  |  |  | }
 |