summaryrefslogtreecommitdiffstats
path: root/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/KeyValueFormEncoding.cs
blob: cdd108511f7986ee54cc213a62f6f46261df7409 (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
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
//-----------------------------------------------------------------------
// <copyright file="KeyValueFormEncoding.cs" company="Outercurve Foundation">
//     Copyright (c) Outercurve Foundation. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------

namespace DotNetOpenAuth.OpenId.ChannelElements {
	using System;
	using System.Collections.Generic;
	using System.Diagnostics.CodeAnalysis;
	using System.Diagnostics.Contracts;
	using System.Globalization;
	using System.IO;
	using System.Text;
	using DotNetOpenAuth.Messaging;

	/// <summary>
	/// Indicates the level of strictness to require when decoding a
	/// Key-Value Form encoded dictionary.
	/// </summary>
	public enum KeyValueFormConformanceLevel {
		/// <summary>
		/// Be as forgiving as possible to errors made while encoding.
		/// </summary>
		Loose,

		/// <summary>
		/// Allow for certain errors in encoding attributable to ambiguities
		/// in the OpenID 1.1 spec's description of the encoding.
		/// </summary>
		OpenId11,

		/// <summary>
		/// The strictest mode.  The decoder requires the encoded dictionary
		/// to be in strict compliance with OpenID 2.0's description of
		/// the encoding.
		/// </summary>
		OpenId20,
	}

	/// <summary>
	/// Performs conversion to and from the Key-Value Form Encoding defined by
	/// OpenID Authentication 2.0 section 4.1.1.
	/// http://openid.net/specs/openid-authentication-2_0.html#anchor4
	/// </summary>
	/// <remarks>
	/// This class is thread safe and immutable.
	/// </remarks>
	internal class KeyValueFormEncoding {
		/// <summary>
		/// Characters that must not appear in parameter names.
		/// </summary>
		private static readonly char[] IllegalKeyCharacters = { '\n', ':' };

		/// <summary>
		/// Characters that must not appaer in parameter values.
		/// </summary>
		private static readonly char[] IllegalValueCharacters = { '\n' };

		/// <summary>
		/// The newline character sequence to use.
		/// </summary>
		private const string NewLineCharacters = "\n";

		/// <summary>
		/// The character encoding to use.
		/// </summary>
		private static readonly Encoding textEncoding = new UTF8Encoding(false);

		/// <summary>
		/// Initializes a new instance of the <see cref="KeyValueFormEncoding"/> class.
		/// </summary>
		public KeyValueFormEncoding() {
			this.ConformanceLevel = KeyValueFormConformanceLevel.Loose;
		}

		/// <summary>
		/// Initializes a new instance of the <see cref="KeyValueFormEncoding"/> class.
		/// </summary>
		/// <param name="conformanceLevel">How strictly an incoming Key-Value Form message will be held to the spec.</param>
		public KeyValueFormEncoding(KeyValueFormConformanceLevel conformanceLevel) {
			this.ConformanceLevel = conformanceLevel;
		}

		/// <summary>
		/// Gets a value controlling how strictly an incoming Key-Value Form message will be held to the spec.
		/// </summary>
		public KeyValueFormConformanceLevel ConformanceLevel { get; private set; }

		/// <summary>
		/// Encodes key/value pairs to Key-Value Form.
		/// </summary>
		/// <param name="keysAndValues">
		/// The dictionary of key/value pairs to convert to a byte stream.
		/// </param>
		/// <returns>The UTF8 byte array.</returns>
		/// <remarks>
		/// Enumerating a Dictionary&lt;TKey, TValue&gt; has undeterministic ordering.
		/// If ordering of the key=value pairs is important, a deterministic enumerator must
		/// be used.
		/// </remarks>
		[SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification = "Not a problem for this type.")]
		public static byte[] GetBytes(IEnumerable<KeyValuePair<string, string>> keysAndValues) {
			Requires.NotNull(keysAndValues, "keysAndValues");

			using (MemoryStream ms = new MemoryStream()) {
				using (StreamWriter sw = new StreamWriter(ms, textEncoding)) {
					sw.NewLine = NewLineCharacters;
					foreach (var pair in keysAndValues) {
						if (pair.Key.IndexOfAny(IllegalKeyCharacters) >= 0) {
							throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, OpenIdStrings.InvalidCharacterInKeyValueFormInput, pair.Key));
						}
						if (pair.Value.IndexOfAny(IllegalValueCharacters) >= 0) {
							throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, OpenIdStrings.InvalidCharacterInKeyValueFormInput, pair.Value));
						}

						sw.Write(pair.Key);
						sw.Write(':');
						sw.Write(pair.Value);
						sw.WriteLine();
					}
				}

				return ms.ToArray();
			}
		}

		/// <summary>
		/// Decodes bytes in Key-Value Form to key/value pairs.
		/// </summary>
		/// <param name="data">The stream of Key-Value Form encoded bytes.</param>
		/// <returns>The deserialized dictionary.</returns>
		/// <exception cref="FormatException">Thrown when the data is not in the expected format.</exception>
		public IDictionary<string, string> GetDictionary(Stream data) {
			using (StreamReader reader = new StreamReader(data, textEncoding)) {
				var dict = new Dictionary<string, string>();
				int line_num = 0;
				string line;
				while ((line = reader.ReadLine()) != null) {
					line_num++;
					if (this.ConformanceLevel == KeyValueFormConformanceLevel.Loose) {
						line = line.Trim();
						if (line.Length == 0) {
							continue;
						}
					}
					string[] parts = line.Split(new[] { ':' }, 2);
					ErrorUtilities.VerifyFormat(parts.Length == 2, OpenIdStrings.InvalidKeyValueFormCharacterMissing, ':', line_num, line);
					if (this.ConformanceLevel > KeyValueFormConformanceLevel.Loose) {
						ErrorUtilities.VerifyFormat(!(char.IsWhiteSpace(parts[0], parts[0].Length - 1) || char.IsWhiteSpace(parts[1], 0)), OpenIdStrings.InvalidCharacterInKeyValueFormInput, ' ', line_num, line);
					}
					if (this.ConformanceLevel < KeyValueFormConformanceLevel.OpenId20) {
						parts[0] = parts[0].Trim();
						parts[1] = parts[1].Trim();
					}

					// calling Add method will throw if a key is encountered twice,
					// which we should do.
					dict.Add(parts[0], parts[1]);
				}
				if (this.ConformanceLevel > KeyValueFormConformanceLevel.Loose) {
					reader.BaseStream.Seek(-1, SeekOrigin.End);
					ErrorUtilities.VerifyFormat(reader.BaseStream.ReadByte() == '\n', OpenIdStrings.InvalidKeyValueFormCharacterMissing, "\\n", line_num, line);
				}
				return dict;
			}
		}
	}
}