summaryrefslogtreecommitdiffstats
path: root/src/DotNetOpenId/MessageEncoder.cs
blob: 2de2c0e27a049442c16a6ac648d2cce0030f2b6f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
using System;
using System.Collections.Specialized;
using System.Text;
using System.Net;
using System.Diagnostics;
using DotNetOpenId.Provider;
using System.IO;
using System.Web;

namespace DotNetOpenId {
	/// <summary>
	/// Encodes <see cref="IEncodable"/> messages into <see cref="Response"/> instances
	/// that can be interpreted by the host web site.
	/// </summary>
	internal class MessageEncoder {
		/// <summary>
		/// The HTTP Content-Type to use in Key-Value Form responses.
		/// </summary>
		/// <remarks>
		/// OpenID 2.0 section 5.1.2 says this SHOULD be text/plain.  But this value 
		/// does not prevent free hosters like GoDaddy from tacking on their ads
		/// to the end of the direct response, corrupting the data.  So we deviate
		/// from the spec a bit here to improve the story for free Providers.
		/// </remarks>
		const string KeyValueFormContentType = "application/x-openid-kvf";
		/// <summary>
		/// The maximum allowable size for a 301 Redirect response before we send
		/// a 200 OK response with a scripted form POST with the parameters instead
		/// in order to ensure successfully sending a large payload to another server
		/// that might have a maximum allowable size restriction on its GET request.
		/// </summary>
		internal static int GetToPostThreshold = 2 * 1024; // 2KB, per OpenID group and http://support.microsoft.com/kb/q208427/
		// We are intentionally using " instead of the html single quote ' below because
		// the HtmlEncode'd values that we inject will only escape the double quote, so
		// only the double-quote used around these values is safe.
		const string FormPostFormat = @"
<html>
<body onload=""var btn = document.getElementById('submit_button'); btn.disabled = true; btn.value = 'Login in progress'; document.getElementById('openid_message').submit()"">
<form id=""openid_message"" action=""{0}"" method=""post"" accept-charset=""UTF-8"" enctype=""application/x-www-form-urlencoded"" onSubmit=""var btn = document.getElementById('submit_button'); btn.disabled = true; btn.value = 'Login in progress'; return true;"">
{1}
	<input id=""submit_button"" type=""submit"" value=""Continue"" />
</form>
</body>
</html>
";
		/// <summary>
		/// Encodes messages into <see cref="Response"/> instances.
		/// </summary>
		public virtual Response Encode(IEncodable message) {
			if (message == null) throw new ArgumentNullException("message");

			EncodingType encode_as = message.EncodingType;
			Response wr;

			WebHeaderCollection headers = new WebHeaderCollection();
			switch (encode_as) {
				case EncodingType.DirectResponse:
					Logger.DebugFormat("Sending direct message response:{0}{1}",
						Environment.NewLine, Util.ToString(message.EncodedFields));
					HttpStatusCode code = (message is Exception) ?
						HttpStatusCode.BadRequest : HttpStatusCode.OK;
					// Key-Value Encoding is how response bodies are sent.
					// Setting the content-type to something other than text/html or text/plain
					// prevents free hosted sites like GoDaddy's from automatically appending
					// the <script/> at the end that adds a banner, and invalidating our response.
					headers.Add(HttpResponseHeader.ContentType, KeyValueFormContentType);
					wr = new Response(code, headers, ProtocolMessages.KeyValueForm.GetBytes(message.EncodedFields), message);
					break;
				case EncodingType.IndirectMessage:
					Logger.DebugFormat("Sending indirect message:{0}{1}",
						Environment.NewLine, Util.ToString(message.EncodedFields));
					// TODO: either redirect or do a form POST depending on payload size.
					Debug.Assert(message.RedirectUrl != null);
					if (getSizeOfPayload(message) <= GetToPostThreshold)
						wr = Create301RedirectResponse(message);
					else
						wr = CreateFormPostResponse(message);
					break;
				default:
					Logger.ErrorFormat("Cannot encode response: {0}", message);
					wr = new Response(HttpStatusCode.BadRequest, headers, new byte[0], message);
					break;
			}
			return wr;
		}

		/// <summary>
		/// Gets the length of the URL that would be used for a simple redirect to carry
		/// this indirect message to its recipient.
		/// </summary>
		/// <param name="message">The message.</param>
		/// <returns>The number of characters in the redirect URL.</returns>
		static int getSizeOfPayload(IEncodable message) {
			Debug.Assert(message != null);
			UriBuilder redirect = new UriBuilder(message.RedirectUrl);
			UriUtil.AppendQueryArgs(redirect, message.EncodedFields);
			return redirect.Uri.AbsoluteUri.Length;
		}
		protected virtual Response Create301RedirectResponse(IEncodable message) {
			WebHeaderCollection headers = new WebHeaderCollection();
			UriBuilder builder = new UriBuilder(message.RedirectUrl);
			UriUtil.AppendQueryArgs(builder, message.EncodedFields);
			headers.Add(HttpResponseHeader.Location, builder.Uri.AbsoluteUri);
			Logger.DebugFormat("Redirecting to {0}", builder.Uri.AbsoluteUri);
			return new Response(HttpStatusCode.Redirect, headers, new byte[0], message);
		}
		protected virtual Response CreateFormPostResponse(IEncodable message) {
			WebHeaderCollection headers = new WebHeaderCollection();
			MemoryStream body = new MemoryStream();
			StreamWriter bodyWriter = new StreamWriter(body);
			StringBuilder hiddenFields = new StringBuilder();
			foreach (var field in message.EncodedFields) {
				hiddenFields.AppendFormat("\t<input type=\"hidden\" name=\"{0}\" value=\"{1}\" />\r\n",
					HttpUtility.HtmlEncode(field.Key), HttpUtility.HtmlEncode(field.Value));
			}
			bodyWriter.WriteLine(FormPostFormat,
				HttpUtility.HtmlEncode(message.RedirectUrl.AbsoluteUri), hiddenFields);
			bodyWriter.Flush();
			return new Response(HttpStatusCode.OK, headers, body.ToArray(), message);
		}
	}

	internal class EncodeEventArgs : EventArgs {
		public EncodeEventArgs(IEncodable encodable) {
			Message = encodable;
		}
		public IEncodable Message { get; private set; }
	}
}