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 users; private List 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 Users { get { return users; } set { users = value; } } public List 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(); Groups = new List(); } 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 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 ReadLinesFile(string path) { List lines = new List(); 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 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; } } } }