/
diev
/
winauth
Обзор
Документация
Войти
/
diev
/
winauth
Код
Запросы
0
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
Authenticator/SteamAuthenticator.cs
887 строк
28 KB
WinAuth
Merge remote-tracking branch 'remotes/origin/pr/435'
08 авг 2017, 06:19
08 авг 2017, 06:19
e40bfae
Код
Авторство
О чём код?
/* * Copyright (C) 2015 Colin Mackie. * This software is distributed under the terms of the GNU General Public License. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ using System; using System.Collections.Specialized; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Net.Cache; using System.Security.Cryptography; using System.Runtime.InteropServices; using System.Text; using System.Text.RegularExpressions; using System.Web; using System.Xml; using System.Xml.Serialization; using System.Xml.XPath; using Org.BouncyCastle.Crypto; using Org.BouncyCastle.Crypto.Engines; using Org.BouncyCastle.Crypto.Macs; using Org.BouncyCastle.Crypto.Parameters; using Org.BouncyCastle.Crypto.Paddings; using Org.BouncyCastle.Crypto.Digests; using Org.BouncyCastle.Crypto.Generators; using NLog; using Newtonsoft.Json; using Newtonsoft.Json.Linq; #if NUNIT using NUnit.Framework; #endif #if NETCF using OpenNETCF.Security.Cryptography; #endif namespace WinAuth { /// <summary> /// Class that implements Steam's SteamGuadrd version of the RFC6238 Authenticator /// </summary> public class SteamAuthenticator : Authenticator { /// <summary> /// Number of characters in code /// </summary> private const int CODE_DIGITS = 5; /// <summary> /// Number of minutes to ignore syncing if network error /// </summary> private const int SYNC_ERROR_MINUTES = 60; /// <summary> /// Number of attempts to activate /// </summary> private const int ENROLL_ACTIVATE_RETRIES = 30; /// <summary> /// Incorrect activation code /// </summary> private const int INVALID_ACTIVATION_CODE = 89; /// <summary> /// Time for http request when calling Sync in ms /// </summary> private const int SYNC_TIMEOUT = 5000; /// <summary> /// Steam issuer for KeyUri /// </summary> private const string STEAM_ISSUER = "Steam"; /// <summary> /// URLs for all mobile services /// </summary> private static string COMMUNITY_BASE = "https://steamcommunity.com"; private static string WEBAPI_BASE = "https://api.steampowered.com"; private static string SYNC_URL = "https://api.steampowered.com:443/ITwoFactorService/QueryTime/v0001"; /// <summary> /// Character set for authenticator code /// </summary> private static char[] STEAMCHARS = new char[] { '2', '3', '4', '5', '6', '7', '8', '9', 'B', 'C', 'D', 'F', 'G', 'H', 'J', 'K', 'M', 'N', 'P', 'Q', 'R', 'T', 'V', 'W', 'X', 'Y'}; /// <summary> /// Create logger /// </summary> private static ILogger Logger = LogManager.GetCurrentClassLogger(); /// <summary> /// Enrolling state /// </summary> public class EnrollState { public string Username { get; set; } public string Password { get; set; } public string CaptchaId { get; set; } public string CaptchaUrl { get; set; } public string CaptchaText { get; set; } public string EmailDomain { get; set; } public string EmailAuthText { get; set; } public string ActivationCode { get; set; } public CookieContainer Cookies { get; set; } public string SteamId { get; set; } public string OAuthToken { get; set; } public bool RequiresLogin { get; set; } public bool RequiresCaptcha { get; set; } public bool Requires2FA { get; set; } public bool RequiresEmailAuth { get; set; } public bool RequiresActivation { get; set; } public string RevocationCode { get; set; } public string SecretKey { get; set; } public bool Success { get; set; } public string Error { get; set; } } #region Authenticator data /// <summary> /// Time of last Sync error /// </summary> private static DateTime _lastSyncError = DateTime.MinValue; /// <summary> /// Returned serial number of authenticator /// </summary> public string Serial { get; set; } /// <summary> /// Random device ID we created and registered /// </summary> public string DeviceId { get; set; } /// <summary> /// JSON steam data /// </summary> public string SteamData { get; set; } /// <summary> /// JSON session data /// </summary> public string SessionData { get; set; } /// <summary> /// /// </summary> public string PollData { get; set; } /// <summary> /// Current Steam client instance /// </summary> public SteamClient Client { get; protected set; } #endregion /// <summary> /// Expanding offsets to retry when creating first code /// </summary> private int[] ENROLL_OFFSETS = new int[] { 0, -30, 30, -60, 60, -90, 90, -120, 120}; /// <summary> /// Create a new Authenticator object /// </summary> public SteamAuthenticator() : base(CODE_DIGITS) { Issuer = STEAM_ISSUER; } /// <summary> /// Get/set the combined secret data value /// </summary> public override string SecretData { get { if (this.Client != null && this.Client.Session != null) { this.SessionData = this.Client.Session.ToString(); } //if (Logger != null) //{ // Logger.Debug("Get Steam data: {0}, Session:{1}", (SteamData ?? string.Empty).Replace("\n"," ").Replace("\r",""), (SessionData ?? string.Empty).Replace("\n", " ").Replace("\r", "")); //} // this is the key | serial | deviceid return base.SecretData + "|" + Authenticator.ByteArrayToString(Encoding.UTF8.GetBytes(Serial)) + "|" + Authenticator.ByteArrayToString(Encoding.UTF8.GetBytes(DeviceId)) + "|" + Authenticator.ByteArrayToString(Encoding.UTF8.GetBytes(SteamData)) + "|" + (string.IsNullOrEmpty(SessionData) == false ? Authenticator.ByteArrayToString(Encoding.UTF8.GetBytes(SessionData)) : string.Empty); } set { // extract key + serial + deviceid if (string.IsNullOrEmpty(value) == false) { string[] parts = value.Split('|'); base.SecretData = value; Serial = (parts.Length > 1 ? Encoding.UTF8.GetString(Authenticator.StringToByteArray(parts[1])) : null); DeviceId = (parts.Length > 2 ? Encoding.UTF8.GetString(Authenticator.StringToByteArray(parts[2])) : null); SteamData = (parts.Length > 3 ? Encoding.UTF8.GetString(Authenticator.StringToByteArray(parts[3])) : string.Empty); if (string.IsNullOrEmpty(SteamData) == false && SteamData[0] != '{') { // convert old recovation code into SteamData json SteamData = "{\"revocation_code\":\"" + SteamData + "\"}"; } string session = (parts.Length > 4 ? Encoding.UTF8.GetString(Authenticator.StringToByteArray(parts[4])) : null); //if (Logger != null) //{ // Logger.Debug("Set Steam data: {0}, Session:{1}", (SteamData ?? string.Empty).Replace("\n", " ").Replace("\r", ""), (SessionData ?? string.Empty).Replace("\n", " ").Replace("\r", "")); //} if (string.IsNullOrEmpty(session) == false) { SessionData = session; } } else { SecretKey = null; Serial = null; DeviceId = null; SteamData = null; SessionData = null; } } } /// <summary> /// Get (or create) the current Steam client for this Authenticator /// </summary> /// <returns>current or new SteamClient</returns> public SteamClient GetClient() { lock (this) { if (this.Client == null) { this.Client = new SteamClient(this, this.SessionData); } return this.Client; } } /// <summary> /// Perform a request to the Steam WebAPI service /// </summary> /// <param name="url">API url</param> /// <param name="method">GET or POST</param> /// <param name="data">Name-data pairs</param> /// <param name="cookies">current cookie container</param> /// <returns>response body</returns> private string Request(string url, string method, NameValueCollection data = null, CookieContainer cookies = null, NameValueCollection headers = null, int timeout = 0) { // create form-encoded data for query or body string query = (data == null ? string.Empty : string.Join("&", Array.ConvertAll(data.AllKeys, key => String.Format("{0}={1}", HttpUtility.UrlEncode(key), HttpUtility.UrlEncode(data[key]))))); if (string.Compare(method, "GET", true) == 0) { url += (url.IndexOf("?") == -1 ? "?" : "&") + query; } // call the server HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url); request.Method = method; request.Accept = "text/javascript, text/html, application/xml, text/xml, */*"; request.ServicePoint.Expect100Continue = false; request.UserAgent = "Mozilla/5.0 (Linux; U; Android 4.1.1; en-us; Google Nexus 4 - 4.1.1 - API 16 - 768x1280 Build/JRO03S) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30"; request.AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip; request.Referer = COMMUNITY_BASE; // + "/mobilelogin?oauth_client_id=DE45CD61&oauth_scope=read_profile%20write_profile%20read_client%20write_client"; if (headers != null) { request.Headers.Add(headers); } if (cookies != null) { request.CookieContainer = cookies; } if (timeout != 0) { request.Timeout = timeout; } if (string.Compare(method, "POST", true) == 0) { request.ContentType = "application/x-www-form-urlencoded; charset=UTF-8"; request.ContentLength = query.Length; StreamWriter requestStream = new StreamWriter(request.GetRequestStream()); requestStream.Write(query); requestStream.Close(); } try { using (HttpWebResponse response = (HttpWebResponse)request.GetResponse()) { LogRequest(method, url, cookies, data, response.StatusCode.ToString() + " " + response.StatusDescription); // OK? if (response.StatusCode != HttpStatusCode.OK) { throw new InvalidRequestException(string.Format("{0}: {1}", (int)response.StatusCode, response.StatusDescription)); } // load the response using (StreamReader responseStream = new StreamReader(response.GetResponseStream())) { string responseData = responseStream.ReadToEnd(); LogRequest(method, url, cookies, data, responseData); return responseData; } } } catch (Exception ex) { LogException(method, url, cookies, data, ex); if (ex is WebException && ((WebException)ex).Response != null && ((HttpWebResponse)((WebException)ex).Response).StatusCode == HttpStatusCode.Forbidden) { throw new UnauthorisedRequestException(ex); } throw new InvalidRequestException(ex.Message, ex); } } /// <summary> /// Enroll the authenticator with the server /// </summary> public bool Enroll(EnrollState state) { // clear error state.Error = null; try { var data = new NameValueCollection(); var cookies = state.Cookies = state.Cookies ?? new CookieContainer(); string response; if (string.IsNullOrEmpty(state.OAuthToken) == true) { // get session if (cookies.Count == 0) { cookies.Add(new Uri(COMMUNITY_BASE + "/"), new Cookie("mobileClientVersion", "3067969+%282.1.3%29")); cookies.Add(new Uri(COMMUNITY_BASE + "/"), new Cookie("mobileClient", "android")); cookies.Add(new Uri(COMMUNITY_BASE + "/"), new Cookie("steamid", "")); cookies.Add(new Uri(COMMUNITY_BASE + "/"), new Cookie("steamLogin", "")); cookies.Add(new Uri(COMMUNITY_BASE + "/"), new Cookie("Steam_Language", "english")); cookies.Add(new Uri(COMMUNITY_BASE + "/"), new Cookie("dob", "")); NameValueCollection headers = new NameValueCollection(); headers.Add("X-Requested-With", "com.valvesoftware.android.steam.community"); response = Request("https://steamcommunity.com/mobilelogin?oauth_client_id=DE45CD61&oauth_scope=read_profile%20write_profile%20read_client%20write_client", "GET", null, cookies, headers); } // Steam strips any non-ascii chars from username and password state.Username = Regex.Replace(state.Username, @"[^\u0000-\u007F]", string.Empty); state.Password = Regex.Replace(state.Password, @"[^\u0000-\u007F]", string.Empty); // get the user's RSA key data.Add("username", state.Username); response = Request(COMMUNITY_BASE + "/mobilelogin/getrsakey", "POST", data, cookies); var rsaresponse = JObject.Parse(response); if (rsaresponse.SelectToken("success").Value<bool>() != true) { throw new InvalidEnrollResponseException("Cannot get steam information for user: " + state.Username); } // encrypt password with RSA key RNGCryptoServiceProvider random = new RNGCryptoServiceProvider(); byte[] encryptedPassword; using (var rsa = new RSACryptoServiceProvider()) { var passwordBytes = Encoding.ASCII.GetBytes(state.Password); var p = rsa.ExportParameters(false); p.Exponent = Authenticator.StringToByteArray(rsaresponse.SelectToken("publickey_exp").Value<string>()); p.Modulus = Authenticator.StringToByteArray(rsaresponse.SelectToken("publickey_mod").Value<string>()); rsa.ImportParameters(p); encryptedPassword = rsa.Encrypt(passwordBytes, false); } // login request data = new NameValueCollection(); data.Add("password", Convert.ToBase64String(encryptedPassword)); data.Add("username", state.Username); data.Add("twofactorcode", ""); data.Add("emailauth", (state.EmailAuthText != null ? state.EmailAuthText : string.Empty)); data.Add("loginfriendlyname", "#login_emailauth_friendlyname_mobile"); data.Add("captchagid", (state.CaptchaId != null ? state.CaptchaId : "-1")); data.Add("captcha_text", (state.CaptchaText != null ? state.CaptchaText : "enter above characters")); data.Add("emailsteamid", (state.EmailAuthText != null ? state.SteamId ?? string.Empty : string.Empty)); data.Add("rsatimestamp", rsaresponse.SelectToken("timestamp").Value<string>()); data.Add("remember_login", "false"); data.Add("oauth_client_id", "DE45CD61"); data.Add("oauth_scope", "read_profile write_profile read_client write_client"); data.Add("donotache", new DateTime().ToUniversalTime().Subtract(new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalMilliseconds.ToString()); response = Request(COMMUNITY_BASE + "/mobilelogin/dologin/", "POST", data, cookies); Dictionary<string, object> loginresponse = JsonConvert.DeserializeObject<Dictionary<string, object>>(response); if (loginresponse.ContainsKey("emailsteamid") == true) { state.SteamId = loginresponse["emailsteamid"] as string; } // require captcha if (loginresponse.ContainsKey("captcha_needed") == true && (bool)loginresponse["captcha_needed"] == true) { state.RequiresCaptcha = true; state.CaptchaId = (string)loginresponse["captcha_gid"]; state.CaptchaUrl = COMMUNITY_BASE + "/public/captcha.php?gid=" + state.CaptchaId; } else { state.RequiresCaptcha = false; state.CaptchaId = null; state.CaptchaUrl = null; state.CaptchaText = null; } // require email auth if (loginresponse.ContainsKey("emailauth_needed") == true && (bool)loginresponse["emailauth_needed"] == true) { if (loginresponse.ContainsKey("emaildomain") == true) { var emaildomain = (string)loginresponse["emaildomain"]; if (string.IsNullOrEmpty(emaildomain) == false) { state.EmailDomain = emaildomain; } } state.RequiresEmailAuth = true; } else { state.EmailDomain = null; state.RequiresEmailAuth = false; } // require email auth if (loginresponse.ContainsKey("requires_twofactor") == true && (bool)loginresponse["requires_twofactor"] == true) { state.Requires2FA = true; } else { state.Requires2FA = false; } // if we didn't login, return the result if (loginresponse.ContainsKey("login_complete") == false || (bool)loginresponse["login_complete"] == false || loginresponse.ContainsKey("oauth") == false) { if (loginresponse.ContainsKey("oauth") == false) { state.Error = "Invalid response from Steam (No OAuth token)"; } if (loginresponse.ContainsKey("message") == true) { state.Error = (string)loginresponse["message"]; } return false; } // get the OAuth token - is stringified json string oauth = (string)loginresponse["oauth"]; var oauthjson = JObject.Parse(oauth); state.OAuthToken = oauthjson.SelectToken("oauth_token").Value<string>(); if (oauthjson.SelectToken("steamid") != null) { state.SteamId = oauthjson.SelectToken("steamid").Value<string>(); } } // login to webapi data.Clear(); data.Add("access_token", state.OAuthToken); response = Request(WEBAPI_BASE + "/ISteamWebUserPresenceOAuth/Logon/v0001", "POST", data); var sessionid = cookies.GetCookies(new Uri(COMMUNITY_BASE + "/"))["sessionid"].Value; if (state.RequiresActivation == false) { data.Clear(); data.Add("op", "has_phone"); data.Add("arg", "null"); data.Add("sessionid", sessionid); response = Request(COMMUNITY_BASE + "/steamguard/phoneajax", "POST", data, cookies); var jsonresponse = JObject.Parse(response); bool hasPhone = jsonresponse.SelectToken("has_phone").Value<Boolean>(); if (hasPhone == false) { state.OAuthToken = null; // force new login state.RequiresLogin = true; state.Cookies = null; state.Error = "Your Steam account must have a SMS-capable phone number attached. Go into Account Details of the Steam client or Steam website and click Add a Phone Number."; return false; } //response = Request(COMMUNITY_BASE + "/steamguard/phone_checksms?bForTwoFactor=1&bRevoke2fOnCancel=", "GET", null, cookies); // add a new authenticator data.Clear(); string deviceId = BuildRandomId(); data.Add("access_token", state.OAuthToken); data.Add("steamid", state.SteamId); data.Add("authenticator_type", "1"); data.Add("device_identifier", deviceId); data.Add("sms_phone_id", "1"); response = Request(WEBAPI_BASE + "/ITwoFactorService/AddAuthenticator/v0001", "POST", data); var tfaresponse = JObject.Parse(response); if (response.IndexOf("status") == -1 && tfaresponse.SelectToken("response.status").Value<int>() == 84) { // invalid response state.OAuthToken = null; // force new login state.RequiresLogin = true; state.Cookies = null; state.Error = "Unable to send SMS. Check your phone is registered on your Steam account."; return false; } if (response.IndexOf("shared_secret") == -1) { // invalid response state.OAuthToken = null; // force new login state.RequiresLogin = true; state.Cookies = null; state.Error = "Invalid response from Steam: " + response; return false; } // save data into this authenticator var secret = tfaresponse.SelectToken("response.shared_secret").Value<string>(); this.SecretKey = Convert.FromBase64String(secret); this.Serial = tfaresponse.SelectToken("response.serial_number").Value<string>(); this.DeviceId = deviceId; state.RevocationCode = tfaresponse.SelectToken("response.revocation_code").Value<string>(); // add the steamid into the data var steamdata = JObject.Parse(tfaresponse.SelectToken("response").ToString()); if (steamdata.SelectToken("steamid") == null) { steamdata.Add("steamid", state.SteamId); } if (steamdata.SelectToken("steamguard_scheme") == null) { steamdata.Add("steamguard_scheme", "2"); } this.SteamData = steamdata.ToString(Newtonsoft.Json.Formatting.None); // calculate server drift long servertime = tfaresponse.SelectToken("response.server_time").Value<long>() * 1000; ServerTimeDiff = servertime - CurrentTime; LastServerTime = DateTime.Now.Ticks; state.RequiresActivation = true; return false; } // finalize adding the authenticator data.Clear(); data.Add("access_token", state.OAuthToken); data.Add("steamid", state.SteamId); data.Add("activation_code", state.ActivationCode); // try and authorise var retries = 0; while (state.RequiresActivation == true && retries < ENROLL_ACTIVATE_RETRIES) { data.Add("authenticator_code", this.CalculateCode(false)); data.Add("authenticator_time", this.ServerTime.ToString()); response = Request(WEBAPI_BASE + "/ITwoFactorService/FinalizeAddAuthenticator/v0001", "POST", data); var finalizeresponse = JObject.Parse(response); if (response.IndexOf("status") != -1 && finalizeresponse.SelectToken("response.status").Value<int>() == INVALID_ACTIVATION_CODE) { state.Error = "Invalid activation code"; return false; } // reset our time if (response.IndexOf("server_time") != -1) { long servertime = finalizeresponse.SelectToken("response.server_time").Value<long>() * 1000; ServerTimeDiff = servertime - CurrentTime; LastServerTime = DateTime.Now.Ticks; } // check success if (finalizeresponse.SelectToken("response.success").Value<bool>() == true) { if (response.IndexOf("want_more") != -1 && finalizeresponse.SelectToken("response.want_more").Value<bool>() == true) { ServerTimeDiff += ((long)this.Period * 1000L); retries++; continue; } state.RequiresActivation = false; break; } ServerTimeDiff += ((long)this.Period * 1000L); retries++; } if (state.RequiresActivation == true) { state.Error = "There was a problem activating. There might be an issue with the Steam servers. Please try again later."; return false; } // mark and successful and return key state.Success = true; state.SecretKey = Authenticator.ByteArrayToString(this.SecretKey); // send confirmation email data.Clear(); data.Add("access_token", state.OAuthToken); data.Add("steamid", state.SteamId); data.Add("email_type", "2"); response = Request(WEBAPI_BASE + "/ITwoFactorService/SendEmail/v0001", "POST", data); return true; } catch (UnauthorisedRequestException ex) { throw new InvalidEnrollResponseException("You are not allowed to add an authenticator. Have you enabled 'community-generated content' in Family View?", ex); } catch (InvalidRequestException ex) { throw new InvalidEnrollResponseException("Error enrolling new authenticator", ex); } } /// <summary> /// Synchronise this authenticator's time with Steam. /// </summary> public override void Sync() { // check if data is protected if (this.SecretKey == null && this.EncryptedData != null) { throw new EncryptedSecretDataException(); } // don't retry for 5 minutes if (_lastSyncError >= DateTime.Now.AddMinutes(0 - SYNC_ERROR_MINUTES)) { return; } try { var response = Request(SYNC_URL, "POST", null, null, null, SYNC_TIMEOUT); var json = JObject.Parse(response); // get servertime in ms long servertime = json.SelectToken("response.server_time").Value<long>() * 1000; // get the difference between the server time and our current time ServerTimeDiff = servertime - CurrentTime; LastServerTime = DateTime.Now.Ticks; // clear any sync error _lastSyncError = DateTime.MinValue; } catch (Exception ) { // don't retry for a while after error _lastSyncError = DateTime.Now; // set to zero to force reset //ServerTimeDiff = 0; } } /// <summary> /// Calculate the current code for the authenticator. /// </summary> /// <param name="resyncTime">flag to resync time</param> /// <returns>authenticator code</returns> protected override string CalculateCode(bool resyncTime = false, long interval = -1) { // sync time if required if (resyncTime == true || ServerTimeDiff == 0) { if (interval > 0) { ServerTimeDiff = (interval * ((long)this.Period * 1000L)) - CurrentTime; } else { Sync(); } } HMac hmac = new HMac(new Sha1Digest()); hmac.Init(new KeyParameter(SecretKey)); byte[] codeIntervalArray = BitConverter.GetBytes(CodeInterval); if (BitConverter.IsLittleEndian) { Array.Reverse(codeIntervalArray); } hmac.BlockUpdate(codeIntervalArray, 0, codeIntervalArray.Length); byte[] mac = new byte[hmac.GetMacSize()]; hmac.DoFinal(mac, 0); // the last 4 bits of the mac say where the code starts (e.g. if last 4 bit are 1100, we start at byte 12) int start = mac[19] & 0x0f; // extract those 4 bytes byte[] bytes = new byte[4]; Array.Copy(mac, start, bytes, 0, 4); if (BitConverter.IsLittleEndian) { Array.Reverse(bytes); } uint fullcode = BitConverter.ToUInt32(bytes, 0) & 0x7fffffff; // build the alphanumeric code StringBuilder code = new StringBuilder(); for (var i=0; i<CODE_DIGITS; i++) { code.Append(STEAMCHARS[fullcode % STEAMCHARS.Length]); fullcode /= (uint)STEAMCHARS.Length; } return code.ToString(); } /// <summary> /// Create a random Device ID string for Enrolling /// </summary> /// <returns>Random string</returns> private static string BuildRandomId() { return "android:" + Guid.NewGuid().ToString(); } /// <summary> /// Log an exception from a Request /// </summary> /// <param name="method">Get or POST</param> /// <param name="url">Request URL</param> /// <param name="cookies">cookie container</param> /// <param name="request">Request data</param> /// <param name="ex">Thrown exception</param> private static void LogException(string method, string url, CookieContainer cookies, NameValueCollection request, Exception ex) { StringBuilder data = new StringBuilder(); if (cookies != null) { foreach (Cookie cookie in cookies.GetCookies(new Uri(url))) { if (data.Length == 0) { data.Append("Cookies:"); } else { data.Append("&"); } data.Append(cookie.Name + "=" + cookie.Value); } data.Append(" "); } if (request != null) { foreach (var key in request.AllKeys) { if (data.Length == 0) { data.Append("Req:"); } else { data.Append("&"); } data.Append(key + "=" + request[key]); } data.Append(" "); } Logger.Error(ex, "{0}\t{1}\t{2}", method, url, data.ToString()); } /// <summary> /// Log a normal response /// </summary> /// <param name="method">Get or POST</param> /// <param name="url">Request URL</param> /// <param name="cookies">cookie container</param> /// <param name="request">Request data</param> /// <param name="response">response body</param> private static void LogRequest(string method, string url, CookieContainer cookies, NameValueCollection request, string response) { StringBuilder data = new StringBuilder(); if (cookies != null) { foreach (Cookie cookie in cookies.GetCookies(new Uri(url))) { if (data.Length == 0) { data.Append("Cookies:"); } else { data.Append("&"); } data.Append(cookie.Name + "=" + cookie.Value); } data.Append(" "); } if (request != null) { foreach (var key in request.AllKeys) { if (data.Length == 0) { data.Append("Req:"); } else { data.Append("&"); } data.Append(key + "=" + request[key]); } data.Append(" "); } Logger.Info("{0}\t{1}\t{2}\t{3}", method, url, data.ToString(), (response != null ? response.Replace("\n", "\\n").Replace("\r", "") : string.Empty)); } /// <summary> /// Our custom exception for the internal Http Request /// </summary> class InvalidRequestException : ApplicationException { public InvalidRequestException(string msg = null, Exception ex = null) : base(msg, ex) { } } /// <summary> /// 403 forbidden responses /// </summary> class UnauthorisedRequestException : InvalidRequestException { public UnauthorisedRequestException(Exception ex = null) : base("Unauthorised", ex) { } } } }