One of the neat things about the SIM tool was the ability to upload a package to a fresh install of Sitecore. This was great for installing things like a 301 redirect module, WFFM, or anything else that was boilerplate for your instance. With SIF, that functionality isn’t present out of the box. But… now it exists!
We need to address this problem in a couple steps:
- We need to get a Sitecore Context
- We need to call the Sitecore.Installer.Installer.Install method.
- We need this to be secure.
Believe it or not, SIM did something very similar to this.
The Process
Create an ASMX in the sitecore folder in our instance.
This file will have the ability to install a package into your sitecore instance, which is nothing to shake a stick at. We’ll need to secure this. I’ll use a two-step process:
- Put it at a random location, that only we know about
- Add an Access Key that will cause the process to silently fail if it doesn’t match
Here’s the file.
<%@ WebService Language="C#" Class="PackageInstaller" %> using System; using System.Configuration; using System.IO; using System.Web.Services; using System.Xml; using Sitecore.Data.Proxies; using Sitecore.Data.Engines; using Sitecore.Install.Files; using Sitecore.Install.Framework; using Sitecore.Install.Items; using Sitecore.SecurityModel; using Sitecore.Update; using Sitecore.Update.Installer; using Sitecore.Update.Installer.Utils; using Sitecore.Update.Utils; using log4net; using log4net.Config; /// <summary> /// Summary description for UpdatePackageInstaller /// </summary> [WebService(Namespace = "http://tempuri.org/")] [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)] [System.ComponentModel.ToolboxItem(false)] // To allow this Web Service to be called from script, using ASP.NET AJAX, uncomment the following line. // [System.Web.Script.Services.ScriptService] public class PackageInstaller : System.Web.Services.WebService { /// <summary> /// Installs a Sitecore Update Package. /// </summary> /// <param name="path">A path to a package that is reachable by the web server</param> [WebMethod(Description = "Installs a Sitecore Update Package.")] public void InstallUpdatePackage(string path, string token) { if(string.IsNullOrEmpty(token)) return; if(token != "[TOKEN]") return; // Use default logger var log = LogManager.GetLogger("root"); XmlConfigurator.Configure((XmlElement)ConfigurationManager.GetSection("log4net")); var file = new FileInfo(path); if (!file.Exists) throw new ApplicationException(string.Format("Cannot access path '{0}'.", path)); using (new SecurityDisabler()) { var installer = new DiffInstaller(UpgradeAction.Upgrade); var view = UpdateHelper.LoadMetadata(path); //Get the package entries bool hasPostAction; string historyPath; var entries = installer.InstallPackage(path, InstallMode.Install, log, out hasPostAction, out historyPath); installer.ExecutePostInstallationInstructions(path, historyPath, InstallMode.Install, view, log, ref entries); UpdateHelper.SaveInstallationMessages(entries, historyPath); } } /// <summary> /// Installs a Sitecore Zip Package. /// </summary> /// <param name="path">A path to a package that is reachable by the web server</param> [WebMethod(Description = "Installs a Sitecore Zip Package.")] public void InstallZipPackage(string path, string token) { if(string.IsNullOrEmpty(token)) return; if(token != "[TOKEN]") return; // Use default logger var log = LogManager.GetLogger("root"); XmlConfigurator.Configure((XmlElement)ConfigurationManager.GetSection("log4net")); var file = new FileInfo(path); if (!file.Exists) throw new ApplicationException(string.Format("Cannot access path '{0}'.", path)); Sitecore.Context.SetActiveSite("shell"); using (new SecurityDisabler()) { //using (new ProxyDisabler()) //{ using (new SyncOperationContext()) { IProcessingContext context = new SimpleProcessingContext(); // IItemInstallerEvents events = new DefaultItemInstallerEvents(new Sitecore.Install.Utils.BehaviourOptions(Sitecore.Install.Utils.InstallMode.Overwrite, Sitecore.Install.Utils.MergeMode.Undefined)); context.AddAspect(events); IFileInstallerEvents events1 = new DefaultFileInstallerEvents(true); context.AddAspect(events1); var installer = new Sitecore.Install.Installer(); installer.InstallPackage(Sitecore.MainUtil.MapPath(path), context); } //} } } }
Notice lines 39 and 74 where we check for the token. This bookmark is actually updated to a random value at creation, so there’s no way to guess it. If your attacker has file system access, you’ve likely got bigger problems than a rogue package.
Now that we have a way to install a package programmatically, we need to set this all up:
The SIF Module
This SIF Module is pretty straight forward. It does all of three things:
- Put the ASMX file in the sitecore folder
- Install the package
- Clean Up
Here’s the SIF module
Set-StrictMode -Version 2.0 Function Invoke-InstallPackageTask { [CmdletBinding(SupportsShouldProcess=$true)] param( [Parameter(Mandatory=$true)] [string]$SiteFolder, [Parameter(Mandatory=$true)] [string]$SiteUrl, [Parameter(Mandatory=$true)] [string]$PackagePath ) Write-TaskInfo "Installing Package $PackagePath" -Tag 'PackageInstall' #Generate a random 10 digit folder name. For security $folderKey = -join ((97..122) | Get-Random -Count 10 | % {[char]$_}) #Generate a Access Key (hi there TDS) $accessKey = New-Guid Write-TaskInfo "Folder Key = $folderKey" -Tag 'PackageInstall' Write-TaskInfo "Access Guid = $accessKey" -Tag 'PackageInstall' #The path to the source Agent. Should be in the same folder as I'm running $sourceAgentPath = Resolve-Path "PackageInstaller.asmx" #The folder on the Server where the Sitecore PackageInstaller folder is to be created $packageInstallPath = [IO.Path]::Combine($SiteFolder, 'sitecore', 'PackageInstaller') #The folder where the actuall install happens $destPath = [IO.Path]::Combine($SiteFolder, 'sitecore', 'PackageInstaller', $folderKey) #Full path including the installer name $fullFileDestPath = Join-Path $destPath "PackageInstaller.asmx" Write-TaskInfo "Source Agent [$sourceAgentPath]" -Tag 'PackageInstall' Write-TaskInfo "Dest AgentPath [$destPath]" -Tag 'PackageInstall' #Forcibly cread the folder New-Item -ItemType Directory -Force -Path $destPath #Read contents of the file, and embed the security token (Get-Content $sourceAgentPath).replace('[TOKEN]', $accessKey) | Set-Content $fullFileDestPath #How do we get to Sitecore? This URL! $webURI= "$siteURL/sitecore/PackageInstaller/$folderKey/packageinstaller.asmx?WSDL" Write-TaskInfo "Url $webURI" -Tag 'PackageInstall' #Do the install here $proxy = New-WebServiceProxy -uri $webURI $proxy.Timeout = 1800000 #Invoke our proxy $proxy.InstallZipPackage($PackagePath, $accessKey) #Remove the folderKey Remove-Item $packageInstallPath -Recurse } Register-SitecoreInstallExtension -Command Invoke-InstallPackageTask -As InstallPackage -Type Task
I’ll let the comments speak for themselves in there. If you have a question, shoot a note in the comments, or hit me on twitter, or slack
Now that our modules exists, let’s run SIF with it
The SIF Config
// -------------------------------------------------------------------------- // // Sitecore Install Framework - Sitecore Package Installation // // // // Run this configuration to install a package. Package is installed with // // 'Overwrite' flags // // // // NOTE: Only single line comments are accepted in configurations. // // -------------------------------------------------------------------------- // { "Parameters": { // Parameters are values that may be passed when Install-SitecoreConfiguration is called. // Parameters must declare a Type and may declare a DefaultValue and Description. // Parameters with no DefaultValue are required when Install-SitecoreConfiguration is called. "SiteName": { "Type": "string", "Description": "The name of the site to be deployed." }, "InstallDirectory": { "Type": "string", "Description": "The file path to the Solr instance." } }, "Variables": { // Variables are values calculated in a configuration. // They can reference Parameters, other Variables, and config functions. // The sites full path on disk "Site.PhysicalPath": "[joinpath(parameter('InstallDirectory'), parameter('SiteName'))]", "Site.Url": "[concat('http://', parameter('SiteName'))]" }, "Tasks": { // Tasks are separate units of work in a configuration. // Each task is an action that will be completed when Install-SitecoreConfiguration is called. // By default, tasks are applied in the order they are declared. // Tasks may reference Parameters, Variables, and config functions. "InstallPackages":{ "Type": "InstallPackage", "Params": [ { "SiteFolder": "[variable('Site.PhysicalPath')]", "SiteUrl": "[variable('Site.Url')]", "PackagePath": "D:\\SC9\\Configuration\\rob Package.zip" } ] } }, "Modules":[ ".\\Invoke-InstallPackageTask.psm1" ] }
The SiteName parameter is what you’d see in a normal SIF Config. I’ve added in the InstallDirectory, per another blog post. This allows for the flexibility of installing where you want to, rather than inetpub\wwwroot
When you run the following, you should see a flurry of activity and viola…your package is installed
Install-SitecoreConfiguration -Path .\install-sitecore-package.json -SiteName xp1.sc
You can grab all these files in a ZIP format here.
SUPER Props to Mike Skutta for putting something together that did a bulk of this lifting: https://community.sitecore.net/technical_blogs/b/mike_skutta/posts/sitecore-update-and-zip-package-installer-web-service