websitepanel/WebsitePanel/Sources/WebsitePanel.Providers.Base/Web/HtaccessFolder.cs

709 lines
17 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using WebsitePanel.Providers.Common;
using System.Linq;
namespace WebsitePanel.Providers.Web
{
public class HtaccessFolder : IComparable
{
#region Constants
public const string HTTPD_CONF_FILE = "httpd.conf";
public const string HTACCESS_FILE = ".htaccess";
public const string HTPASSWDS_FILE = ".htpasswds";
public const string HTGROUPS_FILE = ".htgroups";
public const string AUTH_NAME_DIRECTIVE = "AuthName";
public const string AUTH_TYPE_DIRECTIVE = "AuthType";
public const string AUTH_TYPE_BASIC = "Basic";
public const string AUTH_TYPE_DIGEST = "Digest";
public const string DEFAULT_AUTH_TYPE = AUTH_TYPE_BASIC;
public static readonly string[] PASSWORD_ENCODING_TYPES = new string[]
{
"Apache MD5",
"Unix Crypt",
"SHA1"
};
public static readonly string[] AUTH_TYPES = new string[]
{
AUTH_TYPE_BASIC,
AUTH_TYPE_DIGEST
};
public const string REQUIRE_DIRECTIVE = "Require";
public const string AUTH_BASIC_PROVIDER_FILE = "AuthBasicProvider file";
public const string AUTH_DIGEST_PROVIDER_FILE = "AuthDigestProvider file";
public const string VALID_USER = "valid-user";
public const string REQUIRE_USER = "user";
public const string REQUIRE_GROUP = "group";
public const string AUTH_USER_FILE_DIRECTIVE = "AuthUserFile";
public const string AUTH_GROUP_FILE_DIRECTIVE = "AuthGroupFile";
protected static string LinesSeparator = System.Environment.NewLine;
#endregion
#region parsing regexps
protected static readonly Regex RE_AUTH_NAME = new Regex("^\\s*AuthName\\s+\"?([^\"]+)\"?\\s*$", RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.Compiled);
protected static readonly Regex RE_AUTH_TYPE = new Regex(@"^\s*AuthType\s+(basic|digest)\s*$", RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.Compiled);
protected static readonly Regex RE_REQUIRE = new Regex(@"^\s*Require\s+(.+)\s*$", RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.Compiled);
protected static readonly Regex RE_AUTH_PROVIDER = new Regex(@"^\s*Auth(Basic|Digest)Provider\s+(file)\s*$", RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.Compiled);
protected static readonly Regex RE_AUTH_USER_FILE = new Regex(@"^\s*AuthUserFile\s+(.+)\s*$", RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.Compiled);
protected static readonly Regex RE_AUTH_GROUP_FILE = new Regex(@"^\s*AuthGroupFile\s+(.+)\s*$", RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.Compiled);
public static readonly Regex RE_DIGEST_PASSWORD = new Regex(@"^[^:]*:[0-9a-f]{32}$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
#endregion
#region private fields
private string siteRootPath;
private string path;
private string contentPath;
private string htaccessContent;
private string authName;
private string authType = DEFAULT_AUTH_TYPE;
private List<string> users;
private List<string> groups;
private bool validUser = false;
private string authUserFile;
private string authGroupFile;
private bool doAuthUpdate = false;
private string filename = HTACCESS_FILE;
#endregion
#region public properties
public string Path
{
get { return path; }
set
{
path = value;
if (path.EndsWith("\\") && !path.Equals("\\"))
{
path = path.Substring(0, path.Length - 1);
}
}
}
public string ContentPath
{
get { return contentPath; }
set { contentPath = value; }
}
public string HtaccessContent
{
get { return htaccessContent; }
set { htaccessContent = value; }
}
public string AuthName
{
get { return authName; }
set { authName = value; }
}
public string AuthType
{
get { return authType; }
set { authType = value; }
}
public List<string> Users
{
get { return users; }
set { users = value; }
}
public List<string> Groups
{
get { return groups; }
set { groups = value; }
}
public bool ValidUser
{
get { return validUser; }
set { validUser = value; }
}
public bool DoAuthUpdate
{
get { return doAuthUpdate; }
set { doAuthUpdate = value; }
}
public string AuthUserFile
{
get { return authUserFile; }
set { authUserFile = value; }
}
public string AuthGroupFile
{
get { return authGroupFile; }
set { authGroupFile = value; }
}
public string SiteRootPath
{
get { return siteRootPath; }
set { siteRootPath = value; }
}
#endregion
public HtaccessFolder()
{
Users = new List<string>();
Groups = new List<string>();
}
public HtaccessFolder(string siteRootPath, string path, string contentPath)
: this()
{
this.SiteRootPath = siteRootPath;
this.Path = path;
this.ContentPath = contentPath;
ReadHtaccess();
}
private void ReadHttpdConf()
{
filename = HTTPD_CONF_FILE;
string htpath = System.IO.Path.Combine(ContentPath, filename);
HtaccessContent = ReadFile(htpath);
}
public void ReadHtaccess()
{
filename = HTACCESS_FILE;
string htpath = System.IO.Path.Combine(ContentPath, filename);
HtaccessContent = ReadFile(htpath);
ParseHtaccess();
}
private void ParseHtaccess()
{
validUser = false;
Match mAuthName = RE_AUTH_NAME.Match(HtaccessContent);
if (mAuthName.Success)
{
AuthName = mAuthName.Groups[1].Value;
}
Match mAuthType = RE_AUTH_TYPE.Match(HtaccessContent);
if (mAuthType.Success)
{
AuthType = mAuthType.Groups[1].Value;
}
Match mRequire = RE_REQUIRE.Match(HtaccessContent);
if (mRequire.Success)
{
ParseRequirements(mRequire.Groups[1].Value);
}
Match mAuthUserFile = RE_AUTH_USER_FILE.Match(HtaccessContent);
if (mAuthUserFile.Success)
{
AuthUserFile = mAuthUserFile.Groups[1].Value;
}
else
{
string authUserFilePath = System.IO.Path.Combine(SiteRootPath, HTPASSWDS_FILE);
if (File.Exists(authUserFilePath))
{
AuthUserFile = authUserFilePath;
}
}
Match mAuthGroupFile = RE_AUTH_GROUP_FILE.Match(HtaccessContent);
if (mAuthGroupFile.Success)
{
AuthGroupFile = mAuthGroupFile.Groups[1].Value;
}
else
{
string authGroupFilePath = System.IO.Path.Combine(SiteRootPath, HTGROUPS_FILE);
if (File.Exists(authGroupFilePath))
{
AuthGroupFile = authGroupFilePath;
}
}
}
private void ParseRequirements(string requirementsLine)
{
bool acceptUsers = false, acceptGroups = false;
foreach (string requirement in requirementsLine.Split(new char[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries))
{
string req = requirement.Trim();
if (req.Equals(VALID_USER))
{
ValidUser = true;
acceptUsers = acceptGroups = false;
}
else if (req.Equals(REQUIRE_USER))
{
acceptUsers = true;
acceptGroups = false;
}
else if (req.Equals(REQUIRE_GROUP))
{
acceptUsers = false;
acceptGroups = true;
}
else
{
if (acceptUsers)
{
Users.Add(req);
}
else if (acceptGroups)
{
Groups.Add(req);
}
}
}
}
public void Update()
{
if (Directory.Exists(ContentPath))
{
if (doAuthUpdate)
{
UpdateAuthDirectives();
}
string htaccessPath = System.IO.Path.Combine(ContentPath, filename);
WriteFile(htaccessPath, HtaccessContent);
}
}
private void UpdateAuthDirectives()
{
if (Users.Contains(VALID_USER))
{
ValidUser = true;
Users.Remove(VALID_USER);
}
else
{
ValidUser = false;
}
// update AuthName
Match mAuthName = RE_AUTH_NAME.Match(HtaccessContent);
if (!string.IsNullOrEmpty(AuthName))
{
string s = string.Format("{0} \"{1}\"", AUTH_NAME_DIRECTIVE, AuthName);
if (mAuthName.Success)
{
HtaccessContent = RE_AUTH_NAME.Replace(HtaccessContent, s);
}
else
{
HtaccessContent += s + Environment.NewLine;
}
}
else
{
if (mAuthName.Success)
{
HtaccessContent = RE_AUTH_NAME.Replace(HtaccessContent, "");
}
}
// update AuthType
Match mAuthType = RE_AUTH_TYPE.Match(HtaccessContent);
if (!string.IsNullOrEmpty(AuthType))
{
string s = string.Format("{0} {1}", AUTH_TYPE_DIRECTIVE, AuthType);
if (mAuthType.Success)
{
HtaccessContent = RE_AUTH_TYPE.Replace(HtaccessContent, s);
}
else
{
HtaccessContent += s + Environment.NewLine;
}
}
else
{
if (mAuthType.Success)
{
HtaccessContent = RE_AUTH_TYPE.Replace(HtaccessContent, "");
}
}
// update Auth(Basic|Digest)Provider
Match mAuthProvider = RE_AUTH_PROVIDER.Match(HtaccessContent);
string prov = AuthType == "Basic" ? AUTH_BASIC_PROVIDER_FILE : AUTH_DIGEST_PROVIDER_FILE;
if (mAuthProvider.Success)
{
HtaccessContent = RE_AUTH_PROVIDER.Replace(HtaccessContent, prov);
}
else
{
HtaccessContent += prov + Environment.NewLine;
}
// update AuthUserFile
Match mAuthUserFile = RE_AUTH_USER_FILE.Match(HtaccessContent);
if (!string.IsNullOrEmpty(AuthUserFile))
{
string s = string.Format("{0} {1}", AUTH_USER_FILE_DIRECTIVE, AuthUserFile);
if (mAuthUserFile.Success)
{
HtaccessContent = RE_AUTH_USER_FILE.Replace(HtaccessContent, s);
}
else
{
HtaccessContent += s + Environment.NewLine;
}
}
else
{
if (mAuthUserFile.Success)
{
HtaccessContent = RE_AUTH_USER_FILE.Replace(HtaccessContent, "");
}
}
// update AuthUserFile
Match mAuthGroupFile = RE_AUTH_GROUP_FILE.Match(HtaccessContent);
if (!string.IsNullOrEmpty(AuthGroupFile))
{
string s = string.Format("{0} {1}", AUTH_GROUP_FILE_DIRECTIVE, AuthGroupFile);
if (mAuthGroupFile.Success)
{
HtaccessContent = RE_AUTH_GROUP_FILE.Replace(HtaccessContent, s);
}
else
{
HtaccessContent += s + Environment.NewLine;
}
}
else
{
if (mAuthGroupFile.Success)
{
HtaccessContent = RE_AUTH_GROUP_FILE.Replace(HtaccessContent, "");
}
}
// update Require
Match mRequire = RE_REQUIRE.Match(HtaccessContent);
if (ValidUser || Users.Count > 0 || Groups.Count > 0)
{
string s = GenerateReqiure();
if (mRequire.Success)
{
HtaccessContent = RE_REQUIRE.Replace(HtaccessContent, s);
}
else
{
HtaccessContent += s + Environment.NewLine;
}
}
else
{
if (mRequire.Success)
{
HtaccessContent = RE_AUTH_TYPE.Replace(HtaccessContent, "");
}
}
}
private string GenerateReqiure()
{
StringBuilder sb = new StringBuilder();
sb.Append(REQUIRE_DIRECTIVE);
if (ValidUser)
{
sb.Append(" ").Append(VALID_USER);
}
if (Users.Count > 0)
{
sb.Append(" ").Append(REQUIRE_USER);
foreach (string user in Users)
{
sb.AppendFormat(" {0}", user);
}
}
if (Groups.Count > 0)
{
sb.Append(" ").Append(REQUIRE_GROUP);
foreach (string group in Groups)
{
sb.AppendFormat(" {0}", group);
}
}
return sb.ToString();
}
#region Implementation of IComparable
public int CompareTo(object obj)
{
HtaccessFolder folder = obj as HtaccessFolder;
if (folder != null)
{
return String.CompareOrdinal(this.Path.ToLower(), folder.Path.ToLower());
}
return 0;
}
#endregion
#region static helper members
public static void GetDirectoriesWithHtaccess(string siteRootPath, string virtualRoot, string rootPath, string subPath, List<HtaccessFolder> folders)
{
if (Directory.Exists(subPath))
{
if (HasHtaccessInDirectory(subPath))
{
// Check this subPath is in folders list already
bool included = folders.Any(x => String.Equals(subPath, x.ContentPath, StringComparison.OrdinalIgnoreCase));
// Add only if it is not included
if (included == false)
{
//
folders.Add(new HtaccessFolder
{
SiteRootPath = siteRootPath,
Path = virtualRoot + RelativePath(rootPath, subPath),
ContentPath = subPath
});
}
}
// Process nested virtual directories if any
Array.ForEach(Directory.GetDirectories(subPath), (x) =>
{
GetDirectoriesWithHtaccess(siteRootPath, virtualRoot, rootPath, x, folders);
});
}
}
private static bool HasHtaccessInDirectory(string path)
{
return File.Exists(System.IO.Path.Combine(path, HTACCESS_FILE));
}
private static string RelativePath(string basePath, string subPath)
{
if (string.Equals(basePath, subPath, StringComparison.OrdinalIgnoreCase))
{
return "\\";
}
if (subPath.StartsWith(subPath, StringComparison.OrdinalIgnoreCase))
{
return subPath.Substring(basePath.Length);
}
throw new ArgumentException("Paths do not have a common base");
}
public static HtaccessFolder CreateHtaccessFolder(string siteRootPath, string folderPath)
{
HtaccessFolder folder = new HtaccessFolder
{
SiteRootPath = siteRootPath,
Path = folderPath,
ContentPath = System.IO.Path.Combine(siteRootPath, folderPath)
};
folder.ReadHtaccess();
return folder;
}
public static HtaccessFolder CreateHttpdConfFolder(string path)
{
HtaccessFolder folder = new HtaccessFolder
{
ContentPath = path,
Path = HTTPD_CONF_FILE
};
folder.ReadHttpdConf();
return folder;
}
public static string ReadFile(string path)
{
string result = string.Empty;
if (!File.Exists(path))
return result;
using (StreamReader reader = new StreamReader(path))
{
result = reader.ReadToEnd();
reader.Close();
}
return result;
}
public static List<string> ReadLinesFile(string path)
{
List<string> lines = new List<string>();
string content = ReadFile(path);
if (!string.IsNullOrEmpty(content))
{
foreach (string line in Regex.Split(content, LinesSeparator))
{
lines.Add(line);
}
}
return lines;
}
public static void WriteFile(string path, string content)
{
// remove 'hidden' attribute
FileAttributes fileAttributes = FileAttributes.Normal;
if (File.Exists(path))
{
fileAttributes = File.GetAttributes(path);
if ((fileAttributes & FileAttributes.Hidden) == FileAttributes.Hidden)
{
fileAttributes &= ~FileAttributes.Hidden;
File.SetAttributes(path, fileAttributes);
}
}
// check if folder exists
string folder = System.IO.Path.GetDirectoryName(path);
if (!Directory.Exists(folder))
Directory.CreateDirectory(folder);
// write file
using (StreamWriter writer = new StreamWriter(path))
{
writer.WriteLine(content);
writer.Close();
}
// set 'hidden' attribute
fileAttributes = File.GetAttributes(path);
fileAttributes |= FileAttributes.Hidden;
File.SetAttributes(path, fileAttributes);
}
public static void WriteLinesFile(string path, List<string> lines)
{
StringBuilder content = new StringBuilder();
foreach (string line in lines)
{
if (!string.IsNullOrEmpty(line))
{
content.AppendLine(line);
}
}
WriteFile(path, content.ToString());
}
#endregion
}
public class HtaccessUser : WebUser
{
public const string ENCODING_TYPE_APACHE_MD5 = "Apache MD5";
public const string ENCODING_TYPE_UNIX_CRYPT = "Unix Crypt";
public const string ENCODING_TYPE_SHA1 = "SHA1";
public static readonly string[] ENCODING_TYPES = new string[]
{
ENCODING_TYPE_APACHE_MD5, ENCODING_TYPE_UNIX_CRYPT, ENCODING_TYPE_SHA1
};
private string authType;
private string encType;
private string realm;
public string AuthType
{
get
{
if (string.IsNullOrEmpty(authType))
{
if (!string.IsNullOrEmpty(Password))
{
authType = HtaccessFolder.RE_DIGEST_PASSWORD.IsMatch(Password) ? HtaccessFolder.AUTH_TYPE_DIGEST : HtaccessFolder.AUTH_TYPE_BASIC;
}
}
return authType;
}
set { authType = value; }
}
public string EncType
{
get
{
if (string.IsNullOrEmpty(encType))
{
if (HtaccessFolder.AUTH_TYPE_BASIC != AuthType)
{
encType = string.Empty;
}
else if (Password.StartsWith("{SHA"))
{
encType = ENCODING_TYPE_SHA1;
}
else if (Password.StartsWith("$apr1$"))
{
encType = ENCODING_TYPE_APACHE_MD5;
}
else
{
encType = ENCODING_TYPE_UNIX_CRYPT;
}
}
return encType;
}
set { encType = value; }
}
public string Realm
{
get { return realm; }
set { realm = value; }
}
}
}