using Duplicati.Library.Interface; using Duplicati.Library.Logging; using Duplicati.Library.Utility; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Text; using System.Text.RegularExpressions; namespace Duplicati.Library.Modules.Builtin { public class SendHttpMessage : Interface.IGenericCallbackModule { #region Option names /// /// Option used to specify server URL /// private const string OPTION_URL = "send-http-url"; /// /// Option used to specify report body /// private const string OPTION_MESSAGE = "send-http-message"; /// /// Option used to specify the parameter name for the message /// private const string OPTION_MESSAGE_PARAMETER_NAME = "send-http-message-parameter-name"; /// /// Option used to specify extra parameters /// private const string OPTION_EXTRA_PARAMETERS = "send-http-extra-parameters"; /// /// Option used to specify report level /// private const string OPTION_SENDLEVEL = "send-http-level"; /// /// Option used to specify if reports are sent for other operations than backups /// private const string OPTION_SENDALL = "send-http-any-operation"; #endregion #region Option defaults /// /// The default message parameter name /// private const string DEFAULT_MESSAGE_PARAMETER_NAME = "message"; /// /// The default mail level /// private const string DEFAULT_LEVEL = "all"; /// /// The default message body /// private readonly string DEFAULT_MESSAGE = string.Format("Duplicati %OPERATIONNAME% report for %backup-name%{0}{0}%RESULT%", Environment.NewLine); #endregion #region Private variables /// /// The cached name of the operation /// private string m_operationname; /// /// The cached remote url /// private string m_remoteurl; /// /// The cached local path /// private string[] m_localpath; /// /// The cached set of options /// private IDictionary m_options; /// /// The parsed result level /// private string m_parsedresultlevel = string.Empty; /// /// The HTTP report URL /// private string m_url; /// /// The message parameter name /// private string m_messageParameterName; /// /// The message parameter name /// private string m_extraParameters; /// /// The HTTP message /// private string m_body; /// /// The HTTP send level /// private string[] m_levels; /// /// True to send all operations /// private bool m_sendAll; #endregion #region Implementation of IGenericModule /// /// The module key, used to activate or deactivate the module on the commandline /// public string Key { get { return "sendhttp"; } } /// /// A localized string describing the module with a friendly name /// public string DisplayName { get { return Strings.SendHttpMessage.DisplayName;} } /// /// A localized description of the module /// public string Description { get { return Strings.SendHttpMessage.Description; } } /// /// A boolean value that indicates if the module should always be loaded. /// If true, the user can choose to not load the module by entering the appropriate commandline option. /// If false, the user can choose to load the module by entering the appropriate commandline option. /// public bool LoadAsDefault { get { return true; } } /// /// Gets a list of supported commandline arguments /// public IList SupportedCommands { get { return new List(new ICommandLineArgument[] { new CommandLineArgument(OPTION_URL, CommandLineArgument.ArgumentType.String, Strings.SendHttpMessage.SendhttpurlShort, Strings.SendHttpMessage.SendhttpurlLong), new CommandLineArgument(OPTION_MESSAGE, CommandLineArgument.ArgumentType.String, Strings.SendHttpMessage.SendhttpmessageShort, Strings.SendHttpMessage.SendhttpmessageLong, DEFAULT_MESSAGE), new CommandLineArgument(OPTION_MESSAGE_PARAMETER_NAME, CommandLineArgument.ArgumentType.String, Strings.SendHttpMessage.SendhttpmessageparameternameShort, Strings.SendHttpMessage.SendhttpmessageparameternameLong, DEFAULT_MESSAGE_PARAMETER_NAME), new CommandLineArgument(OPTION_EXTRA_PARAMETERS, CommandLineArgument.ArgumentType.String, Strings.SendHttpMessage.SendhttpextraparametersShort, Strings.SendHttpMessage.SendhttpextraparametersLong), new CommandLineArgument(OPTION_SENDLEVEL, CommandLineArgument.ArgumentType.Enumeration, Strings.SendHttpMessage.SendhttplevelShort, Strings.SendHttpMessage.SendhttplevelLong(ParsedResultType.Success.ToString(), ParsedResultType.Warning.ToString(), ParsedResultType.Error.ToString(), ParsedResultType.Fatal.ToString(), "All"), DEFAULT_LEVEL, null, Enum.GetNames(typeof(ParsedResultType)).Union(new string[] { "All" } ).ToArray()), new CommandLineArgument(OPTION_SENDALL, CommandLineArgument.ArgumentType.Boolean, Strings.SendHttpMessage.SendhttpanyoperationShort, Strings.SendHttpMessage.SendhttpanyoperationLong) }); } } /// /// This method is the interception where the module can interact with the execution environment and modify the settings. /// /// A set of commandline options passed to Duplicati public void Configure(IDictionary commandlineOptions) { //We need a URL to report to commandlineOptions.TryGetValue(OPTION_URL, out m_url); if (string.IsNullOrEmpty(m_url)) return; commandlineOptions.TryGetValue(OPTION_MESSAGE_PARAMETER_NAME, out m_messageParameterName); if (string.IsNullOrEmpty(m_messageParameterName)) m_messageParameterName = DEFAULT_MESSAGE_PARAMETER_NAME; commandlineOptions.TryGetValue(OPTION_EXTRA_PARAMETERS, out m_extraParameters); m_options = commandlineOptions; string tmp; commandlineOptions.TryGetValue(OPTION_SENDLEVEL, out tmp); if (!string.IsNullOrEmpty(tmp)) m_levels = tmp .Split(new string[] { "," }, StringSplitOptions.RemoveEmptyEntries) .Where(x => !string.IsNullOrWhiteSpace(x)) .Select(x => x.Trim()) .ToArray(); if (m_levels == null || m_levels.Length == 0) m_levels = DEFAULT_LEVEL .Split(new string[] { "," }, StringSplitOptions.RemoveEmptyEntries) .Where(x => !string.IsNullOrWhiteSpace(x)) .Select(x => x.Trim()) .ToArray(); m_sendAll = Utility.Utility.ParseBoolOption(commandlineOptions, OPTION_SENDALL); commandlineOptions.TryGetValue(OPTION_MESSAGE, out m_body); if (string.IsNullOrEmpty(m_body)) m_body = DEFAULT_MESSAGE; } #endregion #region Implementation of IGenericCallbackModule /// /// Called when the operation starts /// /// The full name of the operation /// The remote backend url /// The local path, if required public void OnStart(string operationname, ref string remoteurl, ref string[] localpath) { m_operationname = operationname; m_remoteurl = remoteurl; m_localpath = localpath; } /// /// Called when the operation finishes /// /// The result object, if this derives from an exception, the operation failed public void OnFinish(object result) { //If no URL is supplied, then skip if (string.IsNullOrEmpty(m_url)) return; //If we do not report this action, then skip if (!m_sendAll && !string.Equals(m_operationname, "Backup", StringComparison.OrdinalIgnoreCase)) return; ParsedResultType level; if (result is Exception) level = ParsedResultType.Fatal; else if (result != null && result is Library.Interface.IBasicResults) level = ((IBasicResults)result).ParsedResult; else level = ParsedResultType.Error; m_parsedresultlevel = level.ToString(); if (string.Equals(m_operationname, "Backup", StringComparison.OrdinalIgnoreCase)) { if (!m_levels.Any(x => string.Equals(x, "all", StringComparison.OrdinalIgnoreCase))) { //Check if this level should send mail if (!m_levels.Any(x => string.Equals(x, level.ToString(), StringComparison.OrdinalIgnoreCase))) return; } } try { string body = m_body; if (body != DEFAULT_MESSAGE && System.IO.File.Exists(body)) body = System.IO.File.ReadAllText(body); body = ReplaceTemplate(body, result); SendMessages(body); } catch (Exception ex) { Exception top = ex; var sb = new StringBuilder(); while (top != null) { if (sb.Length != 0) sb.Append("--> "); sb.AppendFormat("{0}: {1}{2}", top.GetType().FullName, top.Message, Environment.NewLine); top = top.InnerException; } Logging.Log.WriteMessage(Strings.SendHttpMessage.SendMessageError(sb.ToString()), LogMessageType.Warning, ex); } } #endregion private void SendMessages(string message) { Exception ex = null; var request = (HttpWebRequest)WebRequest.Create(m_url); var postData = $"{m_messageParameterName}={System.Uri.EscapeDataString(message)}"; if (!string.IsNullOrEmpty(m_extraParameters)) { postData += $"&{System.Uri.EscapeUriString(m_extraParameters)}"; } var data = Encoding.UTF8.GetBytes(postData); request.Method = "POST"; request.ContentType = "application/x-www-form-urlencoded"; request.ContentLength = data.Length; try { using (var stream = request.GetRequestStream()) { stream.Write(data, 0, data.Length); } var response = (HttpWebResponse)request.GetResponse(); var responseString = new StreamReader(response.GetResponseStream()).ReadToEnd(); } catch (Exception e) { ex = e; } if (ex != null) throw ex; } private string ReplaceTemplate(string input, object result) { input = Regex.Replace(input, "\\%OPERATIONNAME\\%", m_operationname ?? "", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); input = Regex.Replace(input, "\\%REMOTEURL\\%", m_remoteurl ?? "", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); input = Regex.Replace(input, "\\%LOCALPATH\\%", m_localpath == null ? "" : string.Join(System.IO.Path.PathSeparator.ToString(), m_localpath), RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); input = Regex.Replace(input, "\\%PARSEDRESULT\\%", m_parsedresultlevel ?? "", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); if (input.IndexOf("%RESULT%", StringComparison.OrdinalIgnoreCase) >= 0) using (TempFile tf = new TempFile()) { RunScript.SerializeResult(tf, result); input = Regex.Replace(input, "\\%RESULT\\%", System.IO.File.ReadAllText(tf), RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); } foreach (KeyValuePair kv in m_options) input = Regex.Replace(input, "\\%" + kv.Key + "\\%", kv.Value ?? "", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); if (!m_options.ContainsKey("backup-name")) input = Regex.Replace(input, "\\%backup-name\\%", System.IO.Path.GetFileNameWithoutExtension(Duplicati.Library.Utility.Utility.getEntryAssembly().Location) ?? "", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); input = Regex.Replace(input, "\\%[^\\%]+\\%", ""); return input; } } }