From dbe8d65639a45bd766e1ddfd566edd22cc357aa8 Mon Sep 17 00:00:00 2001 From: William Dibbern Date: Wed, 17 Jun 2015 09:11:50 -0500 Subject: [PATCH] Multiple Profile Support --- Imposter.Fiddler/Helpers/FileWatcher.cs | 60 +++++ Imposter.Fiddler/Helpers/PathHelper.cs | 53 +++++ Imposter.Fiddler/Imposter.Fiddler.csproj | 2 + Imposter.Fiddler/Imposter.cs | 229 +++++++++---------- Imposter.Fiddler/Models/Profile.cs | 64 +++++- Imposter.Fiddler/Models/Settings.cs | 93 ++++++-- Imposter.Fiddler/Properties/AssemblyInfo.cs | 4 +- Imposter.Fiddler/Views/ProfileEditor.xaml.cs | 9 +- Imposter.Fiddler/imposter.js | 21 +- 9 files changed, 376 insertions(+), 159 deletions(-) create mode 100644 Imposter.Fiddler/Helpers/FileWatcher.cs create mode 100644 Imposter.Fiddler/Helpers/PathHelper.cs diff --git a/Imposter.Fiddler/Helpers/FileWatcher.cs b/Imposter.Fiddler/Helpers/FileWatcher.cs new file mode 100644 index 0000000..97ebef0 --- /dev/null +++ b/Imposter.Fiddler/Helpers/FileWatcher.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace Imposter.Fiddler.Helpers +{ + public class FileWatcher : IDisposable + { + private FileSystemWatcher _watcher; + + public bool EnableRaisingEvents + { + get { return _watcher.EnableRaisingEvents; } + set { _watcher.EnableRaisingEvents = value; } + } + + public FileSystemEventHandler Handler + { + set + { + _watcher.Changed += value; + _watcher.Created += value; + _watcher.Deleted += value; + } + } + + public FileWatcher(string path, bool enabled) + { + if (_watcher == null) + { + _watcher = new FileSystemWatcher(); + _watcher.IncludeSubdirectories = true; + _watcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.DirectoryName | NotifyFilters.CreationTime; + _watcher.Filter = "*.*"; + } + _watcher.Path = path; + _watcher.EnableRaisingEvents = enabled; + } + + public void Start() + { + _watcher.EnableRaisingEvents = true; + } + + public void Stop() + { + _watcher.EnableRaisingEvents = false; + } + + public void Dispose() + { + if (_watcher != null) + { + _watcher.Dispose(); + } + } + } +} diff --git a/Imposter.Fiddler/Helpers/PathHelper.cs b/Imposter.Fiddler/Helpers/PathHelper.cs new file mode 100644 index 0000000..7805463 --- /dev/null +++ b/Imposter.Fiddler/Helpers/PathHelper.cs @@ -0,0 +1,53 @@ +using Imposter.Fiddler.Model; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Windows; + +namespace Imposter.Fiddler.Helpers +{ + public static class PathHelper + { + public static string GetStringAfterSubString(string fullString, string subString) + { + int index = fullString.IndexOf(subString); + return fullString.Substring(index + subString.Length); + } + + public static string GetLocalFilePath(string urlFragment, string localDirectory, List overrides) + { + var path = localDirectory + @"\" + urlFragment.Replace("/", @"\"); + + if (File.Exists(path)) + { + return path; + } + + foreach (var ovr in overrides) + { + if (urlFragment.Contains(ovr.RemoteFile.ToLower()) && CheckIfFileExists(ovr.LocalFile)) + { + return ovr.LocalFile; + } + } + + return null; + } + + public static bool CheckIfFileExists(string file) + { + var result = true; + + result = result && File.Exists(file); + + if (!result) + { + MessageBox.Show(string.Format("Imposter says: Override file \"{0}\" does not appear to exist on disk.", file)); + } + + return result; + } + } +} diff --git a/Imposter.Fiddler/Imposter.Fiddler.csproj b/Imposter.Fiddler/Imposter.Fiddler.csproj index 489e1ef..18be904 100644 --- a/Imposter.Fiddler/Imposter.Fiddler.csproj +++ b/Imposter.Fiddler/Imposter.Fiddler.csproj @@ -67,6 +67,8 @@ + + diff --git a/Imposter.Fiddler/Imposter.cs b/Imposter.Fiddler/Imposter.cs index 72e336a..292642b 100644 --- a/Imposter.Fiddler/Imposter.cs +++ b/Imposter.Fiddler/Imposter.cs @@ -18,12 +18,7 @@ public class Imposter : IAutoTamper public bool EnableAutoReload { get; set; } private ImposterSettings _settings = null; - // TODO: switch to List, need to set watcher on the profile maybe? then switch poll for changes to have a parameter - private Profile _currentProfile = null; - private ToolStripMenuItem _currentMenuItem = null; - - private FileSystemWatcher _watcher = null; - private bool _hasChanges = false; + private List _enabledProfiles = null; private ToolStripMenuItem _imposterMenu; private ToolStripMenuItem _profiles; @@ -32,6 +27,7 @@ public class Imposter : IAutoTamper public Imposter() { + _enabledProfiles = new List(); _settings = ImposterSettings.Load(); InitializeMenu(); } @@ -49,37 +45,28 @@ public void OnLoad() private void Start() { - if (_currentProfile == null) + if (_enabledProfiles == null || _enabledProfiles.Count == 0) { MessageBox.Show("In order to start the proxy, you must first select a profile."); return; } - if (!Directory.Exists(_currentProfile.LocalDirectory)) - { - MessageBox.Show(string.Format("The folder located at '{0}' does not exist. Please correct this error before continuing.", _currentProfile.LocalDirectory)); - return; - } - - if (_watcher == null) + else { - _watcher = new FileSystemWatcher(); - _watcher.IncludeSubdirectories = true; - _watcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.DirectoryName | NotifyFilters.CreationTime; - _watcher.Filter = "*.*"; - _watcher.Changed += FileWatchUpdate; - _watcher.Created += FileWatchUpdate; - _watcher.Deleted += FileWatchUpdate; - _watcher.Renamed += FileWatchUpdate; + foreach (var profile in _enabledProfiles) + { + if (!profile.IsRunning) + { + profile.Start(EnableAutoReload); + } + } } - _watcher.Path = _currentProfile.LocalDirectory; - _watcher.EnableRaisingEvents = EnableAutoReload; } private void Stop() { - if (_watcher != null) + foreach (var profile in _enabledProfiles) { - _watcher.EnableRaisingEvents = false; + profile.Stop(); } } @@ -121,6 +108,7 @@ private void LoadProfileItems(string checkedProfile = null) foreach (var profile in _settings.Profiles) { var item = new ToolStripMenuItem(profile.Name); + item.Tag = profile.ProfileId; var itemEnable = new ToolStripMenuItem("&Enable"); itemEnable.Click += ProfileEnable_Click; @@ -162,7 +150,10 @@ private void AutoReload_Click(object sender, EventArgs e) { EnableAutoReload = _autoReload.Checked = !_autoReload.Checked; - _watcher.EnableRaisingEvents = EnableAutoReload; + foreach (var profile in _enabledProfiles) + { + profile.EnableWatcher = EnableAutoReload; + } } private void AddNew_Click(object sender, EventArgs e) @@ -193,32 +184,26 @@ private void ProfileEnable_Click(object sender, EventArgs e) _isEnabled.Enabled = true; _autoReload.Enabled = true; - _currentProfile = _settings.Profiles.Where(p => p.Name == parent.Text).First(); - - if (_currentMenuItem != null && _currentMenuItem != item) - { - // Uncheck previously enabled profile - _currentMenuItem.Checked = false; - parent = _currentMenuItem.OwnerItem as ToolStripMenuItem; - parent.Checked = false; - } - - // Track currently enabled profile for unchecking later - _currentMenuItem = item; + _enabledProfiles.Add(_settings.Profiles.Where(p => p.ProfileId == (Guid)parent.Tag).First()); Start(); } else { - IsEnabled = false; - _isEnabled.Checked = false; - _isEnabled.Enabled = false; - _autoReload.Enabled = false; + var profile = _enabledProfiles.Where(p => p.ProfileId == (Guid)parent.Tag).First(); + profile.Stop(); - _currentProfile = null; - _currentMenuItem = null; + _enabledProfiles.Remove(profile); - Stop(); + if (_enabledProfiles.Count == 0) + { + IsEnabled = false; + _isEnabled.Checked = false; + _isEnabled.Enabled = false; + _autoReload.Enabled = false; + + Stop(); + } } } @@ -240,7 +225,8 @@ private void ProfileEdit_Click(object sender, EventArgs e) if (IsEnabled && item.Checked) { - _currentProfile = profileEditor.Profile; + _enabledProfiles.RemoveAll(p => p.ProfileId == (Guid)parent.Tag); + _enabledProfiles.Add(profileEditor.Profile); } LoadProfileItems(profileEditor.Profile.Name); @@ -252,10 +238,37 @@ private void ProfileDelete_Click(object sender, EventArgs e) var item = (ToolStripMenuItem)sender; var parent = item.OwnerItem as ToolStripMenuItem; - _settings.Profiles = _settings.Profiles.Where(p => p.Name != parent.Text).ToList(); - _settings.Save(); + var profile = _settings.Profiles.Where(p => p.ProfileId == (Guid)parent.Tag).First(); - LoadProfileItems(); + // If the profile is enabled + if (_enabledProfiles.Contains(profile)) + { + // Stop it if it is running + if (profile.IsRunning) + { + profile.Stop(); + } + + // Remove it from the active list + _enabledProfiles.Remove(profile); + + // If there are no other active profiles, call it a day + if (_enabledProfiles.Count == 0) + { + IsEnabled = false; + _isEnabled.Checked = false; + _isEnabled.Enabled = false; + _autoReload.Enabled = false; + + Stop(); + } + } + + // Remove the item from the menu + parent.Dispose(); + + _settings.Profiles = _settings.Profiles.Where(p => p.ProfileId != (Guid)parent.Tag).ToList(); + _settings.Save(); } #endregion @@ -268,21 +281,7 @@ public void AutoTamperRequestBefore(Session oSession) } string fullString = oSession.fullUrl.ToLower(); - if (fullString.Contains(_currentProfile.RemoteUrl.ToLower())) - { - fullString = GetStringAfterSubString(fullString, _currentProfile.RemoteUrl.ToLower()).Split(new char[] { '?' })[0]; - var path = GetLocalFilePath(fullString); - if (path != null) - { - oSession.utilCreateResponseAndBypassServer(); - oSession.LoadResponseFromFile(path); - oSession.ResponseHeaders.Add("x-imposter", path); - if (oSession.ViewItem != null) - { - oSession.ViewItem.BackColor = Color.SkyBlue; - } - } - } + if (fullString.EndsWith("imposter.js") && EnableAutoReload) { oSession.utilCreateResponseAndBypassServer(); @@ -290,20 +289,48 @@ public void AutoTamperRequestBefore(Session oSession) oSession.LoadResponseFromFile(js); oSession.ResponseHeaders.Add("x-imposter", js); } - if (fullString.Contains("/imposter-poll-for-changes") && EnableAutoReload) + + if (fullString.ToLower().Contains("/imposter-poll-for-changes?profileid=") && EnableAutoReload) { + var profileIdIndex = fullString.ToLower().IndexOf("/imposter-poll-for-changes?profileid="); + var profileIdFragment = fullString.Substring(profileIdIndex + "/imposter-poll-for-changes?profileid=".Length); + + Guid profileId; + var success = Guid.TryParse(profileIdFragment, out profileId); + oSession.utilCreateResponseAndBypassServer(); oSession.ResponseHeaders.Add("x-imposter", "AUTO RELOAD"); - if (_hasChanges) + + if (success && _enabledProfiles.Any(p => p.ProfileId == profileId && p.HasChanges)) { oSession.utilSetResponseBody("true"); - _hasChanges = false; + _enabledProfiles.ForEach(p => p.HasChanges = false); } else { oSession.utilSetResponseBody("false"); } } + + foreach (var profile in _enabledProfiles) + { + var path = profile.GetFileMatch(fullString); + + if (path == null) + { + continue; + } + + oSession.utilCreateResponseAndBypassServer(); + oSession.LoadResponseFromFile(path); + oSession.ResponseHeaders.Add("x-imposter", path); + if (oSession.ViewItem != null) + { + oSession.ViewItem.BackColor = Color.SkyBlue; + } + // Only swap for the first match + break; + } } private Stream GetResponseStream(string path) @@ -320,16 +347,25 @@ private Stream GetResponseStream(string path) public void AutoTamperResponseBefore(Session oSession) { - if (!IsEnabled) + if (!IsEnabled || !EnableAutoReload) { return; } var fullString = oSession.fullUrl.ToLower(); - if (fullString.Contains(_currentProfile.RemoteUrl.ToLower()) && EnableAutoReload) + + foreach (var profile in _enabledProfiles) { - oSession.utilDecodeResponse(); - bool replaced = oSession.utilReplaceInResponse("", ""); + if (fullString.Contains(profile.RemoteUrl.ToLower())) + { + oSession.utilDecodeResponse(); + bool replaced = oSession.utilReplaceInResponse("", string.Format( + @" + + ", profile.ProfileId)); + + break; + } } } @@ -342,55 +378,6 @@ private string GetFileNames(string[] paths) return string.Join(", ", paths); } - private void FileWatchUpdate(object sender, FileSystemEventArgs e) - { - _hasChanges = true; - } - - #region Helpers - - private string GetStringAfterSubString(string fullString, string subString) - { - int index = fullString.IndexOf(subString); - return fullString.Substring(index + subString.Length); - } - - private string GetLocalFilePath(string urlFragment) - { - var path = _currentProfile.LocalDirectory + @"\" + urlFragment.Replace("/", @"\"); - - if (File.Exists(path)) - { - return path; - } - - foreach (var ovr in _currentProfile.Overrides) - { - if (urlFragment.Contains(ovr.RemoteFile.ToLower()) && CheckIfFileExists(ovr.LocalFile)) - { - return ovr.LocalFile; - } - } - - return null; - } - - private bool CheckIfFileExists(string file) - { - var result = true; - - result = result && File.Exists(file); - - if (!result) - { - MessageBox.Show(string.Format("File \"{0}\" does not appear to exist on disk.", file)); - } - - return result; - } - - #endregion Helpers - #region Not Implemented public void AutoTamperRequestAfter(Session oSession) diff --git a/Imposter.Fiddler/Models/Profile.cs b/Imposter.Fiddler/Models/Profile.cs index 8d77e31..e184a2e 100644 --- a/Imposter.Fiddler/Models/Profile.cs +++ b/Imposter.Fiddler/Models/Profile.cs @@ -1,12 +1,33 @@ -using System; -using System.Collections.Generic; -using System.Runtime.Serialization; +using Imposter.Fiddler.Helpers; +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.Serialization; +using System.Windows.Forms; namespace Imposter.Fiddler.Model { [DataContract] public class Profile - { + { + private FileWatcher _watcher; + + [IgnoreDataMember] + public bool EnableWatcher + { + get { return _watcher.EnableRaisingEvents; } + set { _watcher.EnableRaisingEvents = value; } + } + + [IgnoreDataMember] + public bool HasChanges { get; set; } + + [IgnoreDataMember] + public bool IsRunning { get; set; } + + [DataMember(Name = "profileId")] + public Guid ProfileId { get; set; } + [DataMember(Name = "name")] public string Name { get; set; } @@ -22,6 +43,41 @@ public class Profile public override string ToString() { return Name.ToString(); + } + + public void Start(bool enableAutoReload) + { + IsRunning = true; + + _watcher = new FileWatcher(LocalDirectory, enableAutoReload); + _watcher.Handler = FileWatchUpdate; + } + + public void Stop() + { + IsRunning = false; + + _watcher.Dispose(); + _watcher = null; + } + + public string GetFileMatch(string url) + { + if (url.Contains(RemoteUrl.ToLower())) + { + url = PathHelper.GetStringAfterSubString(url, RemoteUrl.ToLower()).Split(new char[] { '?' })[0]; + + var path = PathHelper.GetLocalFilePath(url, LocalDirectory, Overrides); + + return path; + } + + return null; + } + + private void FileWatchUpdate(object sender, FileSystemEventArgs e) + { + HasChanges = true; } } } diff --git a/Imposter.Fiddler/Models/Settings.cs b/Imposter.Fiddler/Models/Settings.cs index a7d9b06..9aa9349 100644 --- a/Imposter.Fiddler/Models/Settings.cs +++ b/Imposter.Fiddler/Models/Settings.cs @@ -1,18 +1,24 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Runtime.Serialization; -using System.Runtime.Serialization.Json; -using System.Text; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.Serialization; +using System.Runtime.Serialization.Json; +using System.Text; using System.Windows; namespace Imposter.Fiddler.Model { [DataContract] public class ImposterSettings - { - public static string FolderPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Imposter.Fiddler"); + { + private const string FILE_NAME = "settings.json"; + public static string FOLDER_PATH = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Imposter.Fiddler"); + private static string FILE_PATH = Path.Combine(FOLDER_PATH, FILE_NAME); + + [DataMember(Name = "version")] + public string Version { get; set; } [DataMember(Name = "profiles")] public List Profiles { get; set; } @@ -21,25 +27,26 @@ public static ImposterSettings Load() { try { - var filePath = Path.Combine(FolderPath, "settings.json"); - - if (!Directory.Exists(FolderPath)) + if (!Directory.Exists(FOLDER_PATH)) { - Directory.CreateDirectory(FolderPath); + Directory.CreateDirectory(FOLDER_PATH); } - if (File.Exists(filePath)) + if (File.Exists(FILE_PATH)) { - var settingsJson = File.ReadAllText(filePath); - var json = new DataContractJsonSerializer(typeof(ImposterSettings)); - var stream = new MemoryStream(Encoding.UTF8.GetBytes(settingsJson)); - return (Model.ImposterSettings)json.ReadObject(stream); + return Read(); } else { try { - File.WriteAllText(filePath, "{ \"profiles\": [] }"); + var profileStub = string.Join(string.Empty, + "{ \"profiles\": [], \"version\": \"", + Assembly.GetCallingAssembly().GetName().Version.ToString(), + "\" }"); + + File.WriteAllText(FILE_PATH, profileStub); + return Load(); } catch (Exception ex) @@ -54,21 +61,61 @@ public static ImposterSettings Load() } return null; + } + + private static ImposterSettings Read() + { + var settingsJson = File.ReadAllText(FILE_PATH); + + var json = new DataContractJsonSerializer(typeof(ImposterSettings)); + + using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(settingsJson))) + { + var settings = (Model.ImposterSettings)json.ReadObject(stream); + + return Upgrade(settings); + } + } + + private static ImposterSettings Upgrade(ImposterSettings settings) + { + if (settings == null) + { + return settings; + } + + if (string.IsNullOrEmpty(settings.Version)) + { + var result = MessageBox.Show("Imposter says: Invalid version found in settings.json. Drop all settings and recreate?", + string.Empty, MessageBoxButton.YesNo); + + if (result == MessageBoxResult.Yes) + { + File.Delete(FILE_PATH); + + return Load(); + } + + return null; + } + else + { + // We're up to date + return settings; + } } public void Save() { try - { - var path = Path.Combine(FolderPath, "settings.json"); - + { var serializer = new DataContractJsonSerializer(this.GetType()); using (var ms = new MemoryStream()) { serializer.WriteObject(ms, this); byte[] json = ms.ToArray(); - File.WriteAllText(path, Encoding.UTF8.GetString(json, 0, json.Length)); + File.WriteAllText(FILE_PATH, Encoding.UTF8.GetString(json, 0, json.Length)); } } catch (Exception ex) diff --git a/Imposter.Fiddler/Properties/AssemblyInfo.cs b/Imposter.Fiddler/Properties/AssemblyInfo.cs index f897bf5..c83d2a9 100644 --- a/Imposter.Fiddler/Properties/AssemblyInfo.cs +++ b/Imposter.Fiddler/Properties/AssemblyInfo.cs @@ -32,6 +32,6 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: AssemblyVersion("1.0.0.1")] +[assembly: AssemblyFileVersion("1.0.0.1")] [assembly: Fiddler.RequiredVersion("2.2.8.6")] diff --git a/Imposter.Fiddler/Views/ProfileEditor.xaml.cs b/Imposter.Fiddler/Views/ProfileEditor.xaml.cs index 37c1fe6..03ed2d7 100644 --- a/Imposter.Fiddler/Views/ProfileEditor.xaml.cs +++ b/Imposter.Fiddler/Views/ProfileEditor.xaml.cs @@ -11,13 +11,16 @@ namespace Imposter.Fiddler.Views /// Interaction logic for ProfileEditor.xaml /// public partial class ProfileEditor : MetroWindow - { + { + public Guid ProfileId { get; set; } + public Profile Profile { get { return new Profile { + ProfileId = ProfileId, Name = Name.Text, LocalDirectory = Local.Text, RemoteUrl = Remote.Text, @@ -25,7 +28,8 @@ public Profile Profile }; } set - { + { + ProfileId = value.ProfileId; Name.Text = value.Name == "[Dat One Unknown Profile Doe]" ? string.Empty : value.Name; Local.Text = value.LocalDirectory; Remote.Text = value.RemoteUrl; @@ -43,6 +47,7 @@ public ProfileEditor(Profile profile = null) } else { + ProfileId = Guid.NewGuid(); Overrides.ItemsSource = new List(); } diff --git a/Imposter.Fiddler/imposter.js b/Imposter.Fiddler/imposter.js index 9103656..3d468c5 100644 --- a/Imposter.Fiddler/imposter.js +++ b/Imposter.Fiddler/imposter.js @@ -4,13 +4,20 @@ function _pollForChanges() { var req = new XMLHttpRequest(), - url = [ - window.location.protocol, - '//', - window.location.host, - '/', - 'imposter-poll-for-changes' - ].join(''); + url; + + if (!exports.__IMPOSTER.profileId) { + return; + } + + url = [ + window.location.protocol, + '//', + window.location.host, + '/', + 'imposter-poll-for-changes?profileId=', + exports.__IMPOSTER.profileId + ].join(''); req.open('POST', url, true); req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');