Skip to content

Commit 7504501

Browse files
authored
Allow special characters in credentials (LT-20157, LT-21275) (#316)
* Escape credentials used in URLs Some users use their email addresses as their logins. When we overhauled the Get from Internet UX, we inadvertently stopped escaping credentials. * Allow users to type spaces in the password field * Properly encode spaces in passwords (not usernames) We are not allowing spaces in usernames, although we might could. I am unaware of any other services that allow spaces in usernames * Use WebUtility instead of HttpUtility (in some places) * Clean up HgServeRunner (indentation, change HttpUtility to WebUtility) * Test that spaces in passwords are encoded properly * Run more tests (some had been needlessly excluded) * Foolproof a test against developer machines set to use the QA server Addresses https://jira.sil.org/browse/LT-20157 and https://jira.sil.org/browse/LT-21275
1 parent 9547608 commit 7504501

File tree

11 files changed

+129
-87
lines changed

11 files changed

+129
-87
lines changed

src/Chorus/UI/Misc/ServerSettingsControl.Designer.cs

Lines changed: 0 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Chorus/UI/Misc/ServerSettingsControl.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ private void _textBox_KeyPress(object sender, KeyPressEventArgs e)
110110

111111
private void _password_TextChanged(object sender, EventArgs e)
112112
{
113-
Model.Password = _password.Text.Trim();
113+
Model.Password = _password.Text;
114114
UpdateDisplay();
115115
}
116116

src/ChorusHub/HgServeRunner.cs

Lines changed: 28 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
using System;
22
using System.Diagnostics;
33
using System.IO;
4+
using System.Net;
45
using System.Text;
56
using System.Threading;
6-
using System.Web;
77
using Chorus.VcsDrivers.Mercurial;
88
using SIL.CommandLineProcessing;
99
using SIL.Progress;
@@ -140,40 +140,45 @@ public void CheckForFailedPushes()
140140
try
141141
{
142142
if (!File.Exists(AccessLogPath))
143+
{
143144
return;
145+
}
144146

145147
using (var stream = File.Open(AccessLogPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
146148
using (TextReader reader = new StreamReader(stream))
147149
{
148150
while (true)
149151
{
150-
var line = reader.ReadLine();
151-
if (line == null)
152-
return;
152+
var line = reader.ReadLine();
153+
if (line == null)
154+
{
155+
return;
156+
}
153157

154-
var start = line.IndexOf("GET /") + 5;
155-
var end = line.IndexOf("?");
156-
if (line.Contains("404") && start > 9 & end > 0)
158+
var start = line.IndexOf("GET /", StringComparison.Ordinal) + 5;
159+
var end = line.IndexOf("?", StringComparison.Ordinal);
160+
if (line.Contains("404") && start > 9 & end > 0)
161+
{
162+
var name = line.Substring(start, end - start);
163+
string directory = Path.Combine(_rootFolder, name);
164+
165+
directory = WebUtility.UrlDecode(directory); // convert %20 --> space
166+
if (!Directory.Exists(directory))
157167
{
158-
var name = line.Substring(start, end - start);
159-
string directory = Path.Combine(_rootFolder, name);
160-
161-
directory = HttpUtility.UrlDecode(directory); // convert %20 --> space
162-
if (!Directory.Exists(directory))
163-
{
164-
//Progress.WriteMessage("Creating new folder '" + name + "'");
165-
Directory.CreateDirectory(directory);
166-
}
167-
if (!Directory.Exists(Path.Combine(directory, ".hg")))
168-
{
169-
//Progress.WriteMessage("Initializing blank repository: " + name +
170-
// ". Try Sending again in a few minutes, when hg notices the new directory.");
171-
HgRepository.CreateRepositoryInExistingDir(directory, new ConsoleProgress());
172-
}
168+
//Progress.WriteMessage("Creating new folder '" + name + "'");
169+
Directory.CreateDirectory(directory);
170+
}
171+
172+
if (!Directory.Exists(Path.Combine(directory, ".hg")))
173+
{
174+
//Progress.WriteMessage("Initializing blank repository: " + name +
175+
// ". Try Sending again in a few minutes, when hg notices the new directory.");
176+
HgRepository.CreateRepositoryInExistingDir(directory, new ConsoleProgress());
173177
}
174-
}
175178
}
176179
}
180+
}
181+
}
177182
catch
178183
{
179184
//EventLog.WriteEntry("Application", error.Message, EventLogEntryType.Error);

src/LibChorus/Model/ServerSettingsModel.cs

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
using System.Net;
55
using System.Security.Cryptography;
66
using System.Text;
7-
using System.Web;
7+
using Chorus.Properties;
88
using Chorus.Utilities;
99
using Chorus.VcsDrivers;
1010
using Chorus.VcsDrivers.Mercurial;
@@ -91,8 +91,8 @@ static ServerSettingsModel()
9191

9292
public ServerSettingsModel()
9393
{
94-
Username = Properties.Settings.Default.LanguageForgeUser;
95-
Password = DecryptPassword(Properties.Settings.Default.LanguageForgePass);
94+
Username = Settings.Default.LanguageForgeUser;
95+
Password = DecryptPassword(Settings.Default.LanguageForgePass);
9696
RememberPassword = !string.IsNullOrEmpty(Password) || string.IsNullOrEmpty(Username);
9797
}
9898

@@ -118,13 +118,13 @@ public virtual void InitFromProjectPath(string path)
118118

119119
public virtual void InitFromUri(string url)
120120
{
121-
var urlUsername = HttpUtility.UrlDecode(UrlHelper.GetUserName(url));
121+
var urlUsername = WebUtility.UrlDecode(UrlHelper.GetUserName(url));
122122
if (!string.IsNullOrEmpty(urlUsername))
123123
{
124124
Username = urlUsername;
125-
Password = HttpUtility.UrlDecode(UrlHelper.GetPassword(url));
125+
Password = WebUtility.UrlDecode(UrlHelper.GetPassword(url));
126126
}
127-
ProjectId = HttpUtility.UrlDecode(UrlHelper.GetPathAfterHost(url));
127+
ProjectId = WebUtility.UrlDecode(UrlHelper.GetPathAfterHost(url));
128128
HasLoggedIn = !string.IsNullOrEmpty(ProjectId);
129129
Bandwidth = new BandwidthItem(RepositoryAddress.IsKnownResumableRepository(url) ? BandwidthEnum.Low : BandwidthEnum.High);
130130

@@ -146,7 +146,7 @@ public string URL
146146
return CustomUrl;
147147
}
148148

149-
return $"https://{Host}/{HttpUtility.UrlEncode(ProjectId)}";
149+
return $"https://{Host}/{WebUtility.UrlEncode(ProjectId)}";
150150
}
151151
}
152152

@@ -264,10 +264,10 @@ protected RepositoryAddress CreateRepositoryAddress(string name)
264264

265265
public void SaveUserSettings()
266266
{
267-
Properties.Settings.Default.LanguageForgeUser = Username;
268-
Properties.Settings.Default.LanguageForgePass = RememberPassword ? EncryptPassword(Password) : null;
267+
Settings.Default.LanguageForgeUser = Username;
268+
Settings.Default.LanguageForgePass = RememberPassword ? EncryptPassword(Password) : null;
269269
PasswordForSession = Password;
270-
Properties.Settings.Default.Save();
270+
Settings.Default.Save();
271271
}
272272

273273
public void LogIn(out string error)
@@ -377,6 +377,19 @@ internal static string DecryptPassword(string decryptMe)
377377
return Encoding.Unicode.GetString(decryptedData);
378378
}
379379

380+
/// <summary>
381+
/// URL-encoded password to use for the current Send and Receive session. <see cref="PasswordForSession"/>
382+
/// </summary>
383+
/// <remarks>
384+
/// UrlEncode encodes spaces as "+" and "+" as "%2b". LanguageDepot fails to decode plus-encoded spaces. Encode spaces as "%20"
385+
/// </remarks>
386+
public static string EncodedPasswordForSession => WebUtility.UrlEncode(PasswordForSession)?.Replace("+", "%20");
387+
388+
/// <summary>
389+
/// URL-encoded language forge username
390+
/// </summary>
391+
public static string EncodedLanguageForgeUser => WebUtility.UrlEncode(Settings.Default.LanguageForgeUser);
392+
380393
private static string _passwordForSession;
381394

382395
/// <summary>
@@ -387,7 +400,7 @@ internal static string DecryptPassword(string decryptMe)
387400
/// </summary>
388401
public static string PasswordForSession
389402
{
390-
internal get { return _passwordForSession ?? DecryptPassword(Properties.Settings.Default.LanguageForgePass); }
403+
internal get { return _passwordForSession ?? DecryptPassword(Settings.Default.LanguageForgePass); }
391404
set { _passwordForSession = value; }
392405
}
393406

@@ -402,7 +415,7 @@ public static string PasswordForSession
402415
/// <param name="clearString">Any string containing a URL with the <see cref="PasswordForSession"/> in clear text.</param>
403416
internal static string RemovePasswordForLog(string clearString)
404417
{
405-
return clearString?.Replace($":{PasswordForSession}@", $":{PasswordAsterisks}@");
418+
return clearString?.Replace($":{EncodedPasswordForSession}@", $":{PasswordAsterisks}@");
406419
}
407420

408421
/// <summary>

src/LibChorus/VcsDrivers/RepositoryAddress.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public abstract class RepositoryAddress
3838
/// <summary>
3939
/// This message is displayed when the user tries to create a new repository with the same name as an
4040
/// unrelated, existing repository. Because of the way this string is used, two customizations are necessary:
41-
/// - replace <see cref="MediumVariable"/> with the name of the syncronization medium
41+
/// - replace <see cref="MediumVariable"/> with the name of the synchronization medium
4242
/// - replace {0} and {0} with the URI's of the existing and new repositories, respectively
4343
/// </summary>
4444
public const string DuplicateWarningMessage = "Warning: There is a project repository on the " + MediumVariable
@@ -163,7 +163,7 @@ public override string GetPotentialRepoUri(string repoIdentifier, string project
163163
// Our resumable API supports passing credentials in request headers; Mercurial requires them in the URL.
164164
if (!IsResumable && string.IsNullOrEmpty(UrlHelper.GetUserName(uri)))
165165
{
166-
uri = uri.Replace("://", $"://{Properties.Settings.Default.LanguageForgeUser}:{ServerSettingsModel.PasswordForSession}@");
166+
uri = uri.Replace("://", $"://{ServerSettingsModel.EncodedLanguageForgeUser}:{ServerSettingsModel.EncodedPasswordForSession}@");
167167
}
168168
return uri;
169169
}

src/LibChorusTests/Model/InternetCloneSettingsModelTests.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using NUnit.Framework;
33
using SIL.TestUtilities;
44
using Chorus.Model;
5+
using Chorus.Utilities;
56

67
namespace LibChorus.Tests.Model
78
{
@@ -26,7 +27,8 @@ public void InitFromUri_GivenCompleteUri_AllPropertiesCorrect()
2627
[Test]
2728
public void URL_AfterConstruction_GoodDefault()
2829
{
29-
using (var testFolder = new TemporaryFolder("clonetest"))
30+
using (var testFolder = new TemporaryFolder("cloneTest"))
31+
using (new ShortTermEnvironmentalVariable(ServerSettingsModel.ServerEnvVar, null))
3032
{
3133
var model = new InternetCloneSettingsModel(testFolder.Path);
3234
model.Username = "account";

0 commit comments

Comments
 (0)