Skip to content

Commit

Permalink
Add ftp support
Browse files Browse the repository at this point in the history
  • Loading branch information
hallipr committed Sep 25, 2019
1 parent e4055b7 commit a8d5d47
Show file tree
Hide file tree
Showing 13 changed files with 818 additions and 3 deletions.
24 changes: 24 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 @@ -116,6 +117,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="mods\HandleUnknownMods.cs" />
<Compile Include="mods\ModInfo.cs" />
Expand Down Expand Up @@ -146,8 +149,20 @@
<Compile Include="settings\ATImportFileLocationDialog.Designer.cs">
<DependentUpon>ATImportFileLocationDialog.cs</DependentUpon>
</Compile>
<Compile Include="settings\FtpProgress.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="settings\FtpProgress.Designer.cs">
<DependentUpon>FtpProgress.cs</DependentUpon>
</Compile>
<Compile Include="species\ARKColor.cs" />
<Compile Include="species\ARKColors.cs" />
<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 @@ -582,6 +597,12 @@
<EmbeddedResource Include="settings\customSoundChooser.resx">
<DependentUpon>customSoundChooser.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Include="settings\FtpProgress.resx">
<DependentUpon>FtpProgress.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 @@ -747,6 +768,9 @@
</ProjectReference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="FluentFTP">
<Version>27.1.2</Version>
</PackageReference>
<PackageReference Include="Newtonsoft.Json">
<Version>11.0.2</Version>
</PackageReference>
Expand Down
178 changes: 176 additions & 2 deletions ARKBreedingStats/Form1.cs
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
using ARKBreedingStats.duplicates;
using ARKBreedingStats.Library;
using ARKBreedingStats.miscClasses;
using ARKBreedingStats.ocr;
using ARKBreedingStats.settings;
using ARKBreedingStats.species;
using ARKBreedingStats.uiControls;
using ARKBreedingStats.values;

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;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Xml.Serialization;

Expand Down Expand Up @@ -802,9 +808,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, atImportFileLocation.ConvenientName, 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 @@ -841,6 +865,156 @@ private async void RunSavegameImport(object sender, EventArgs e)
}
}

private async Task<string> CopyFtpFileAsync(Uri ftpUri, string serverName, string workingCopyFolder)
{
var credentialsByServerName = LoadSavedCredentials();
credentialsByServerName.TryGetValue(serverName, out var credentials);

var dialogText = $"Ftp Credentials for {serverName}";

while (true)
{
if (credentials == null)
{
// get new credentials
using (var dialog = new FtpCredentialsForm { Text = dialogText })
{
if (dialog.ShowDialog(this) == DialogResult.Cancel)
{
return null;
}

credentials = dialog.Credentials;

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

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

var cancellationTokenSource = new CancellationTokenSource();
using (var progressDialog = new FtpProgressForm(cancellationTokenSource))
{
try
{
progressDialog.StatusText = $"Authenticating";
progressDialog.Show(this);

await client.ConnectAsync();

progressDialog.StatusText = $"Finding most recent file";
await Task.Yield();

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

ftpPath = mostRecentlyModifiedMatch.FullName;
}

var fileName = Path.GetFileName(ftpPath);

progressDialog.FileName = fileName;
progressDialog.StatusText = $"Downloading {fileName}";
await Task.Yield();

var filePath = Path.Combine(workingCopyFolder, Path.GetFileName(ftpPath));
await client.DownloadFileAsync(filePath, ftpPath, FtpLocalExists.Overwrite, FtpVerify.Retry, progressDialog, token: cancellationTokenSource.Token);
await Task.Delay(500);

if (filePath.EndsWith(".gz"))
{
progressDialog.StatusText = $"Decompressing {fileName}";
await Task.Yield();

filePath = await DecompressGZippedFileAsync(filePath, cancellationTokenSource.Token);
}

return filePath;
}
catch (FtpAuthenticationException ex)
{
// if auth fails, clear credentials, alert the user and loop until the either auth succeeds or the user cancels
progressDialog.StatusText = $"Authentication failed: {ex.Message}";
credentials = null;
await Task.Delay(1000);
}
catch(OperationCanceledException)
{
return null;
}
catch(Exception ex)
{
progressDialog.StatusText = $"Unexpected error: {ex.Message}";
}
}
}
}

/// <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 async Task<string> DecompressGZippedFileAsync(string filePath, CancellationToken cancellationToken)
{
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))
{
await decompressionStream.CopyToAsync(decompressedFileStream, 81920, cancellationToken);
}

return newFileName;
}

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

// 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));
}

/// <summary>
/// Checks each creature for tags and saves them in a list.
/// </summary>
Expand Down
3 changes: 2 additions & 1 deletion ARKBreedingStats/ImportSavegame.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ private static void importCollection(CreatureCollection creatureCollection, List
if (creatureCollection.changeCreatureStatusOnSavegameImport)
{
// mark creatures that are no longer present as unavailable
var removedCreatures = creatureCollection.creatures.Where(c => c.status == CreatureStatus.Available).Except(newCreatures);
var removedCreatures = creatureCollection.creatures.Where(c => c.status == CreatureStatus.Available && c.server == serverName).Except(newCreatures);
foreach (var c in removedCreatures)
c.status = CreatureStatus.Unavailable;

Expand All @@ -130,6 +130,7 @@ private static void importCollection(CreatureCollection creatureCollection, List
{
creature.server = serverName;
});

creatureCollection.mergeCreatureList(newCreatures, true);
}

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.

2 changes: 2 additions & 0 deletions ARKBreedingStats/Properties/Settings.settings
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,8 @@
</Setting>
<Setting Name="WarnWhenImportingMoreCreaturesThan" Type="System.Int32" Scope="User">
<Value Profile="(Default)">50</Value>
<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 a8d5d47

Please sign in to comment.