//----------------------------------------------------------------------- // // Copyright (c) Outercurve Foundation. All rights reserved. // //----------------------------------------------------------------------- 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; /// /// Indicates the level of strictness to require when decoding a /// Key-Value Form encoded dictionary. /// public enum KeyValueFormConformanceLevel { /// /// Be as forgiving as possible to errors made while encoding. /// Loose, /// /// Allow for certain errors in encoding attributable to ambiguities /// in the OpenID 1.1 spec's description of the encoding. /// OpenId11, /// /// The strictest mode. The decoder requires the encoded dictionary /// to be in strict compliance with OpenID 2.0's description of /// the encoding. /// OpenId20, } /// /// 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 /// /// /// This class is thread safe and immutable. /// internal class KeyValueFormEncoding { /// /// Characters that must not appear in parameter names. /// private static readonly char[] IllegalKeyCharacters = { '\n', ':' }; /// /// Characters that must not appaer in parameter values. /// private static readonly char[] IllegalValueCharacters = { '\n' }; /// /// The newline character sequence to use. /// private const string NewLineCharacters = "\n"; /// /// The character encoding to use. /// private static readonly Encoding textEncoding = new UTF8Encoding(false); /// /// Initializes a new instance of the class. /// public KeyValueFormEncoding() { this.ConformanceLevel = KeyValueFormConformanceLevel.Loose; } /// /// Initializes a new instance of the class. /// /// How strictly an incoming Key-Value Form message will be held to the spec. public KeyValueFormEncoding(KeyValueFormConformanceLevel conformanceLevel) { this.ConformanceLevel = conformanceLevel; } /// /// Gets a value controlling how strictly an incoming Key-Value Form message will be held to the spec. /// public KeyValueFormConformanceLevel ConformanceLevel { get; private set; } /// /// Encodes key/value pairs to Key-Value Form. /// /// /// The dictionary of key/value pairs to convert to a byte stream. /// /// The UTF8 byte array. /// /// Enumerating a Dictionary<TKey, TValue> has undeterministic ordering. /// If ordering of the key=value pairs is important, a deterministic enumerator must /// be used. /// [SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification = "Not a problem for this type.")] public static byte[] GetBytes(IEnumerable> 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(); } } /// /// Decodes bytes in Key-Value Form to key/value pairs. /// /// The stream of Key-Value Form encoded bytes. /// The deserialized dictionary. /// Thrown when the data is not in the expected format. public IDictionary GetDictionary(Stream data) { using (StreamReader reader = new StreamReader(data, textEncoding)) { var dict = new Dictionary(); 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; } } } }