Skip to content

Commit

Permalink
Add FTP import support
Browse files Browse the repository at this point in the history
- Add ProtectedData based encryption utility
- Add PackageReference to FluentFTP library (MIT License)
- Add FTP credentials collection form
- Add logic to Form1.cs to conditionally display FTP credentials popup
  • Loading branch information
hallipr committed Mar 26, 2019
1 parent 5e4466f commit ea89a95
Show file tree
Hide file tree
Showing 9 changed files with 519 additions and 2 deletions.
15 changes: 15 additions & 0 deletions ARKBreedingStats/ARKBreedingStats.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
<Reference Include="System.Configuration" />
<Reference Include="System.Core" />
<Reference Include="System.Runtime.Serialization" />
<Reference Include="System.Security" />
<Reference Include="System.Speech" />
<Reference Include="System.Windows.Forms.DataVisualization" />
<Reference Include="System.Xml.Linq" />
Expand Down Expand Up @@ -109,6 +110,8 @@
<DesignTime>True</DesignTime>
<DependentUpon>strings.resx</DependentUpon>
</Compile>
<Compile Include="miscClasses\Encryption.cs" />
<Compile Include="miscClasses\FtpCredentials.cs" />
<Compile Include="miscClasses\IssueNotes.cs" />
<Compile Include="multiplierTesting\StatMultiplierTestingControl.cs">
<SubType>UserControl</SubType>
Expand All @@ -131,6 +134,12 @@
<Compile Include="settings\ATImportFileLocationDialog.Designer.cs">
<DependentUpon>ATImportFileLocationDialog.cs</DependentUpon>
</Compile>
<Compile Include="settings\FtpCredentials.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="settings\FtpCredentials.Designer.cs">
<DependentUpon>FtpCredentials.cs</DependentUpon>
</Compile>
<Compile Include="species\CreatureColors.cs" />
<Compile Include="multiplierTesting\StatsMultiplierTesting.cs">
<SubType>UserControl</SubType>
Expand Down Expand Up @@ -526,6 +535,9 @@
<EmbeddedResource Include="settings\customSoundChooser.resx">
<DependentUpon>customSoundChooser.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Include="settings\FtpCredentials.resx">
<DependentUpon>FtpCredentials.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Include="SpeciesSelector.resx">
<DependentUpon>SpeciesSelector.cs</DependentUpon>
</EmbeddedResource>
Expand Down Expand Up @@ -694,6 +706,9 @@
</ProjectReference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="FluentFTP">
<Version>19.2.2</Version>
</PackageReference>
<PackageReference Include="Newtonsoft.Json">
<Version>11.0.2</Version>
</PackageReference>
Expand Down
136 changes: 134 additions & 2 deletions ARKBreedingStats/Form1.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@
using ARKBreedingStats.settings;
using ARKBreedingStats.species;
using ARKBreedingStats.uiControls;
using FluentFTP;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
Expand Down Expand Up @@ -1530,9 +1534,27 @@ private async void runSavegameImport(object sender, EventArgs e)
{
workingCopyfilename = Path.GetTempPath();
}
workingCopyfilename = Path.Combine(workingCopyfilename, Path.GetFileName(atImportFileLocation.FileLocation));

File.Copy(atImportFileLocation.FileLocation, workingCopyfilename, true);

if (Uri.TryCreate(atImportFileLocation.FileLocation, UriKind.Absolute, out var uri))
{
switch(uri.Scheme)
{
case "ftp":
workingCopyfilename = await CopyFtpFileAsync(uri, workingCopyfilename);
if (workingCopyfilename == null)
// the user didn't enter credentials
return;
break;
default:
throw new Exception($"Unsuppoerted uri scheme: {uri.Scheme}");
}
}
else
{
workingCopyfilename = Path.Combine(workingCopyfilename, Path.GetFileName(atImportFileLocation.FileLocation));
File.Copy(atImportFileLocation.FileLocation, workingCopyfilename, true);
}

await ImportSavegame.ImportCollectionFromSavegame(creatureCollection, workingCopyfilename, atImportFileLocation.ServerName);

Expand Down Expand Up @@ -1567,6 +1589,116 @@ private async void runSavegameImport(object sender, EventArgs e)
}
}

private async Task<string> CopyFtpFileAsync(Uri ftpUri, string workingCopyFolder)
{
Dictionary<string, FtpCredentials> credentialsByHostname = LoadSavedCredentials();

credentialsByHostname.TryGetValue(ftpUri.Host, out var credentials);

while (true)
{
if (credentials == null)
{
using (var dialog = new FtpCredentialsForm { Text = $"Ftp Credentials for {ftpUri.Host}" })
{
if (dialog.ShowDialog(this) == DialogResult.Cancel)
{
return null;
}

credentials = dialog.Credentials;

if (dialog.SaveCredentials)
{
credentialsByHostname[ftpUri.Host] = credentials;
Properties.Settings.Default.SavedFtpCredentials = Encryption.Protect(JsonConvert.SerializeObject(credentialsByHostname));
Properties.Settings.Default.Save();
}
}
}

var client = new FtpClient(ftpUri.Host, ftpUri.Port, credentials.Username, credentials.Password);
await client.ConnectAsync();

var ftpPath = ftpUri.AbsolutePath;

var lastSegment = ftpUri.Segments.Last();
if (lastSegment.Contains("*"))
{
var mostRecentlyModifiedMatch = await GetLastModifiedFileAsync(client, ftpUri);
if (mostRecentlyModifiedMatch == null)
{
throw new Exception($"No file found matching pattern '{lastSegment}'");
}

ftpPath = mostRecentlyModifiedMatch.FullName;
}

var filePath = Path.Combine(workingCopyFolder, Path.GetFileName(ftpPath));

await client.DownloadFileAsync(filePath, ftpPath);

MessageBox.Show($"Downloaded { Path.GetFileName(filePath) }", "FTP Download", MessageBoxButtons.OK, MessageBoxIcon.Information);

return filePath.EndsWith(".gz")
? DecompressGZippedFile(filePath)
: filePath;
}
}

/// <summary>
/// Loads the encrypted ftp crednetials from settings, decrypts them, then returns them as a hostname to credentials dictionary
/// </summary>
private static Dictionary<string, FtpCredentials> LoadSavedCredentials()
{
try
{
var savedCredentials = Encryption.Unprotect(Properties.Settings.Default.SavedFtpCredentials);

if (!string.IsNullOrEmpty(savedCredentials))
{
var savedDictionary = JsonConvert.DeserializeObject<Dictionary<string, FtpCredentials>>(savedCredentials);

// Ensure that the resulting dictionary is case insensitive on hostname
return new Dictionary<string, FtpCredentials>(savedDictionary, StringComparer.OrdinalIgnoreCase);
}
}
catch (Exception ex)
{
MessageBox.Show($"An error occured while loading saved ftp credentials. Message: \n\n{ex.Message}",
"Settings Error", MessageBoxButtons.OK);
}

return new Dictionary<string, FtpCredentials>(StringComparer.OrdinalIgnoreCase);
}

private string DecompressGZippedFile(string filePath)
{
var newFileName = filePath.Remove(filePath.Length - 3);

using (var originalFileStream = File.OpenRead(filePath))
using (var decompressedFileStream = File.Create(newFileName))
using (var decompressionStream = new GZipStream(originalFileStream, CompressionMode.Decompress))
{
decompressionStream.CopyTo(decompressedFileStream);
}

return newFileName;
}

public async Task<FtpListItem> GetLastModifiedFileAsync(FtpClient client, Uri ftpUri)
{
var folderUri = new Uri(ftpUri, ".");
var listItems = await client.GetListingAsync(folderUri.AbsolutePath);

// Turn the wildcard into a regex pattern "super*.foo" -> "^super.*?\.foo$"
var nameRegex = new Regex("^" + Regex.Escape(ftpUri.Segments.Last()).Replace(@"\*", ".*?") + "$");

return listItems
.OrderByDescending(x => x.Modified)
.FirstOrDefault(x => nameRegex.IsMatch(x.Name));
}

private void createCreatureTagList()
{
creatureCollection.tags.Clear();
Expand Down
12 changes: 12 additions & 0 deletions ARKBreedingStats/Properties/Settings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions ARKBreedingStats/Properties/Settings.settings
Original file line number Diff line number Diff line change
Expand Up @@ -167,5 +167,8 @@
<Setting Name="UseOwnerFilterForBreedingPlan" Type="System.Boolean" Scope="User">
<Value Profile="(Default)">False</Value>
</Setting>
<Setting Name="SavedFtpCredentials" Type="System.String" Scope="User">
<Value Profile="(Default)" />
</Setting>
</Settings>
</SettingsFile>
63 changes: 63 additions & 0 deletions ARKBreedingStats/miscClasses/Encryption.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using System;
using System.Security.Cryptography;
using System.Text;

namespace ARKBreedingStats.miscClasses
{
class Encryption
{
private static byte[] additionalEntropy = Encoding.UTF8.GetBytes("Additional Entropy");

/// <summary>
/// Converts a string into a base64 encoded ProdectedData encrypted byte array
/// </summary>
/// <param name="value">The string to encrypt</param>
public static string Protect(string value)
{
try
{
var decryptedBytes = Encoding.UTF8.GetBytes(value);

// Encrypt the data using DataProtectionScope.CurrentUser. The result can be decrypted
// only by the same current user.
var encryptedBytes = ProtectedData.Protect(decryptedBytes, additionalEntropy, DataProtectionScope.CurrentUser);

return Convert.ToBase64String(encryptedBytes);
}
catch (CryptographicException e)
{
Console.WriteLine("Data was not encrypted. An error occurred.");
Console.WriteLine(e.ToString());
return null;
}
}

/// <summary>
/// Converts base64 encoded ProdectedData encrypted byte array into a string
/// </summary>
/// <param name="value">The string to decrypt</param>
public static string Unprotect(string value)
{
if (value == null)
{
return null;
}

try
{
var encryptedBytes = Convert.FromBase64String(value);

//Decrypt the data using DataProtectionScope.CurrentUser.
var decryptedBytes = ProtectedData.Unprotect(encryptedBytes, additionalEntropy, DataProtectionScope.CurrentUser);

return Encoding.UTF8.GetString(decryptedBytes);
}
catch (CryptographicException e)
{
Console.WriteLine("Data was not decrypted. An error occurred.");
Console.WriteLine(e.ToString());
return null;
}
}
}
}
9 changes: 9 additions & 0 deletions ARKBreedingStats/miscClasses/FtpCredentials.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace ARKBreedingStats.miscClasses
{
public class FtpCredentials
{
public string Username { get; set; }

public string Password { get; set; }
}
}
Loading

0 comments on commit ea89a95

Please sign in to comment.