//
// ClientAccessPolicy.cs
//
// Authors:
//	Atsushi Enomoto <atsushi@ximian.com>
//	Moonlight List (moonlight-list@lists.ximian.com)
//
// Copyright (C) 2009 Novell, Inc.  http://www.novell.com
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
// 
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//

#if NET_2_1

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;

namespace System.Windows.Browser.Net {

	partial class ClientAccessPolicy : BaseDomainPolicy {

		public class AccessPolicy {

			public const short MinPort = 4502;
			public const short MaxPort = 4534;

			public List<AllowFrom> AllowedServices { get; private set; }
			public List<GrantTo> GrantedResources { get; private set; }
			public long PortMask { get; set; }

			public AccessPolicy ()
			{
				AllowedServices = new List<AllowFrom> ();
				GrantedResources = new List<GrantTo> ();
			}

			public bool PortAllowed (int port)
			{
				if ((port < MinPort) || (port > MaxPort))
					return false;

				return (((PortMask >> (port - MinPort)) & 1) == 1);
			}
		}

		public ClientAccessPolicy ()
		{
			AccessPolicyList = new List<AccessPolicy> ();
		}

		public List<AccessPolicy> AccessPolicyList { get; private set; }

		public bool IsAllowed (IPEndPoint endpoint)
		{
			foreach (AccessPolicy policy in AccessPolicyList) {
				// does something allow our URI in this policy ?
				foreach (AllowFrom af in policy.AllowedServices) {
					if (af.IsAllowed (ApplicationUri, null)) {
						// if so, is our request port allowed ?
						if (policy.PortAllowed (endpoint.Port))
							return true;
					}
				}
			}
			// no policy allows this socket connection
			return false;
		}

		// note: tests show that it only applies to Silverlight policy (seems to work with Flash)
		// and only if we're not granting full access (i.e. '/' with all subpaths)
		// https://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=466043
		private bool CheckOriginalPath (Uri uri)
		{
			// Path Restriction for cross-domain requests
			// http://msdn.microsoft.com/en-us/library/cc838250(VS.95).aspx
			string original = uri.OriginalString;
			// applies to the *path* only (not the query part)
			int query = original.IndexOf ('?');
			if (query != -1)
				original = original.Substring (0, query);

			if (original.Contains ('%') || original.Contains ("./") || original.Contains ("..")) {
				// special case when no path restriction applies - i.e. the above characters are accepted by SL
				if (AccessPolicyList.Count != 1)
					return false;
				AccessPolicy policy = AccessPolicyList [0];
				if (policy.GrantedResources.Count != 1)
					return false;
				GrantTo gt = policy.GrantedResources [0];
				if (gt.Resources.Count != 1)
					return false;
				Resource r = gt.Resources [0];
				return (r.IncludeSubpaths && (r.Path == "/"));
			}
			return true;
		}

		public override bool IsAllowed (Uri uri, params string [] headerKeys)
		{
			// at this stage the URI has removed the "offending" characters so we need to look at the original
			if (!CheckOriginalPath (uri)) 
				return false;

			foreach (AccessPolicy policy in AccessPolicyList) {
				// does something allow our URI in this policy ?
				foreach (AllowFrom af in policy.AllowedServices) {
					// is the application (XAP) URI allowed by the policy ?
					if (af.IsAllowed (ApplicationUri, headerKeys)) {
						foreach (GrantTo gt in policy.GrantedResources) {
							// is the requested access to the Uri granted under this policy ?
							if (gt.IsGranted (uri))
								return true;
						}
					}
				}
			}
			// no policy allows this web connection
			return false;
		}

		public class AllowFrom {

			public AllowFrom ()
			{
				Domains = new List<string> ();
				HttpRequestHeaders = new Headers ();
				Scheme = String.Empty;
			}

			public bool AllowAnyDomain { get; set; }

			public List<string> Domains { get; private set; }

			public Headers HttpRequestHeaders { get; private set; }

			public string Scheme { get; internal set; }

			public bool IsAllowed (Uri uri, string [] headerKeys)
			{
				// check headers
				if (!HttpRequestHeaders.IsAllowed (headerKeys))
					return false;

				// check scheme
				if ((Scheme.Length > 0) && (Scheme == uri.Scheme)) {
					switch (Scheme) {
					case "http":
						return (uri.Port == 80);
					case "https":
						return (uri.Port == 443);
					case "file":
						return true;
					default:
						return false;
					}
				}
				// check domains
				if (AllowAnyDomain)
					return true;

				if (Domains.All (domain => !CheckDomainUri (domain)))
					return false;
				return true;
			}

			static bool CheckDomainUri (string policy)
			{
				Uri uri;
				if (Uri.TryCreate (policy, UriKind.Absolute, out uri)) {
					// if no local path is part of the policy domain then we compare to the root
					if (uri.LocalPath == "/")
						return (uri.ToString () == ApplicationRoot);
					// otherwise the path must match
					if (uri.LocalPath != ApplicationUri.LocalPath)
						return false;
					return (CrossDomainPolicyManager.GetRoot (uri) == ApplicationRoot);
				}

				// SL policies supports a * wildcard at the start of their host name (but not elsewhere)

				// check for matching protocol
				if (!policy.StartsWith (ApplicationUri.Scheme))
					return false;
				// check for the wirld card immediately after the scheme
				if (policy.IndexOf ("://*.", ApplicationUri.Scheme.Length) != ApplicationUri.Scheme.Length)
					return false;
				// remove *. from uri
				policy = policy.Remove (ApplicationUri.Scheme.Length + 3, 2);
				// create Uri - without the *. it should be a valid one
				if (!Uri.TryCreate (policy, UriKind.Absolute, out uri))
					return false;
				// path must be "empty" and query and fragment (really) empty
				if ((uri.LocalPath != "/") || !String.IsNullOrEmpty (uri.Query) || !String.IsNullOrEmpty (uri.Fragment))
					return false;
				// port must match
				if (ApplicationUri.Port != uri.Port)
					return false;
				// the application uri host must end with the policy host name
				return ApplicationUri.DnsSafeHost.EndsWith (uri.DnsSafeHost);
			}
		}

		public class GrantTo
		{
			public GrantTo ()
			{
				Resources = new List<Resource> ();
			}

			public List<Resource> Resources { get; private set; }

			public bool IsGranted (Uri uri)
			{
				foreach (var gr in Resources) {
					if (gr.IncludeSubpaths) {
						string granted = gr.Path;
						string local = uri.LocalPath;
						if (local.StartsWith (granted, StringComparison.Ordinal)) {
							// "/test" equals "/test" and "test/xyx" but not "/test2"
							// "/test/" equals "test/xyx" but not "/test" or "/test2"
							if (local.Length == granted.Length)
								return true;
							else if (granted [granted.Length - 1] == '/')
								return true;
							else if (local [granted.Length] == '/')
								return true;
						}
					} else {
						if (uri.LocalPath == gr.Path)
							return true;
					}
				}
				return false;
			}
		}

		public class Resource {
			private string path;

			public string Path { 
				get { return path; }
				set {
					// an empty Path Ressource makes the *whole* policy file invalid
					if (String.IsNullOrEmpty (value))
						throw new NotSupportedException ();
					path = value;
				}
			}

			public bool IncludeSubpaths { get; set; }
		}
	}
}

#endif


