| 
									
										
										
										
											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.
 | 
					
						
							|  |  |  |  */
 | 
					
						
							|  |  |  |     
 | 
					
						
							| 
									
										
										
										
											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;
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 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;
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-08-21 07:44:50 +02:00
										 |  |  | @SuppressWarnings("UnstableApiUsage") // guava hash is marked as unstable
 | 
					
						
							| 
									
										
										
										
											2019-08-20 14:43:48 +02:00
										 |  |  | class PluginInstaller {
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   private final SCMContextProvider context;
 | 
					
						
							|  |  |  |   private final AdvancedHttpClient client;
 | 
					
						
							| 
									
										
										
										
											2020-01-24 12:01:21 +01:00
										 |  |  |   private final SmpDescriptorExtractor smpDescriptorExtractor;
 | 
					
						
							| 
									
										
										
										
											2019-08-20 14:43:48 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |   @Inject
 | 
					
						
							| 
									
										
										
										
											2020-01-24 12:01:21 +01:00
										 |  |  |   public PluginInstaller(SCMContextProvider context, AdvancedHttpClient client, SmpDescriptorExtractor smpDescriptorExtractor) {
 | 
					
						
							| 
									
										
										
										
											2019-08-20 14:43:48 +02:00
										 |  |  |     this.context = context;
 | 
					
						
							|  |  |  |     this.client = client;
 | 
					
						
							| 
									
										
										
										
											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
 | 
					
						
							| 
									
										
										
										
											2019-08-20 16:38:29 +02:00
										 |  |  |   public PendingPluginInstallation install(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-01-23 17:05:55 +01:00
										 |  |  |       verifyConditions(plugin, file);
 | 
					
						
							| 
									
										
										
										
											2019-08-21 12:49:15 +02:00
										 |  |  |       return new PendingPluginInstallation(plugin.install(), file);
 | 
					
						
							| 
									
										
										
										
											2019-08-20 14:43:48 +02:00
										 |  |  |     } catch (IOException ex) {
 | 
					
						
							| 
									
										
										
										
											2019-08-21 07:56:52 +02:00
										 |  |  |       cleanup(file);
 | 
					
						
							| 
									
										
										
										
											2019-08-21 07:44:50 +02:00
										 |  |  |       throw new PluginDownloadException("failed to download plugin", ex);
 | 
					
						
							| 
									
										
										
										
											2019-08-20 14:43: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) {
 | 
					
						
							|  |  |  |       throw new PluginInstallException("failed to cleanup, after broken installation");
 | 
					
						
							|  |  |  |     }
 | 
					
						
							|  |  |  |   }
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   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);
 | 
					
						
							| 
									
										
										
										
											2019-08-20 14:43:48 +02:00
										 |  |  |         throw new PluginChecksumMismatchException(
 | 
					
						
							|  |  |  |           String.format("downloaded plugin checksum %s does not match expected %s", calculatedChecksum, checksum.get())
 | 
					
						
							|  |  |  |         );
 | 
					
						
							|  |  |  |       }
 | 
					
						
							|  |  |  |     }
 | 
					
						
							|  |  |  |   }
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-01-23 17:05:55 +01:00
										 |  |  |   private void verifyConditions(AvailablePlugin plugin, Path file) throws IOException {
 | 
					
						
							| 
									
										
										
										
											2020-01-24 12:01:21 +01:00
										 |  |  |     InstalledPluginDescriptor pluginDescriptor = smpDescriptorExtractor.extractPluginDescriptor(file);
 | 
					
						
							| 
									
										
										
										
											2020-01-23 17:05:55 +01:00
										 |  |  |     if (!pluginDescriptor.getCondition().isSupported()) {
 | 
					
						
							|  |  |  |       cleanup(file);
 | 
					
						
							|  |  |  |       throw new PluginConditionFailedException(
 | 
					
						
							|  |  |  |         pluginDescriptor.getCondition(),
 | 
					
						
							|  |  |  |         String.format(
 | 
					
						
							|  |  |  |           "could not load plugin %s, the plugin condition does not match",
 | 
					
						
							|  |  |  |           plugin.getDescriptor().getInformation().getName()
 | 
					
						
							|  |  |  |         )
 | 
					
						
							|  |  |  |       );
 | 
					
						
							|  |  |  |     }
 | 
					
						
							|  |  |  |   }
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-08-20 14:43:48 +02:00
										 |  |  |   private InputStream download(AvailablePlugin plugin) throws IOException {
 | 
					
						
							|  |  |  |     return client.get(plugin.getDescriptor().getUrl()).request().contentAsStream();
 | 
					
						
							|  |  |  |   }
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-08-21 07:44:50 +02:00
										 |  |  |   private Path createFile(AvailablePlugin plugin) throws IOException {
 | 
					
						
							|  |  |  |     Path directory = context.resolve(Paths.get("plugins"));
 | 
					
						
							|  |  |  |     Files.createDirectories(directory);
 | 
					
						
							|  |  |  |     return directory.resolve(plugin.getDescriptor().getInformation().getName() + ".smp");
 | 
					
						
							| 
									
										
										
										
											2019-08-20 14:43:48 +02:00
										 |  |  |   }
 | 
					
						
							|  |  |  | }
 |