using System; using System.Dynamic; using System.IO; using System.Linq; using System.Net; using System.Reflection; using System.Text; using System.Threading; using System.Web; using Newtonsoft.Json.Linq; namespace RedditSharp { public sealed class WebAgent : IWebAgent { /// /// Additional values to append to the default RedditSharp user agent. /// public static string UserAgent { get; set; } /// /// It is strongly advised that you leave this enabled. Reddit bans excessive /// requests with extreme predjudice. /// public static bool EnableRateLimit { get; set; } public static string Protocol { get; set; } /// /// It is strongly advised that you leave this set to Burst or Pace. Reddit bans excessive /// requests with extreme predjudice. /// public static RateLimitMode RateLimit { get; set; } /// /// The method by which the WebAgent will limit request rate /// public enum RateLimitMode { /// /// Limits requests to one every two seconds /// Pace, /// /// Restricts requests to thirty per minute /// Burst, /// /// Does not restrict request rate. ***NOT RECOMMENDED*** /// None } /// /// The root domain RedditSharp uses to address Reddit. /// www.reddit.com by default /// public static string RootDomain { get; set; } /// /// Used to make calls against Reddit's API using OAuth23 /// public string AccessToken { get; set; } public CookieContainer Cookies { get; set; } public string AuthCookie { get; set; } private static DateTime _lastRequest; private static DateTime _burstStart; private static int _requestsThisBurst; public JToken CreateAndExecuteRequest(string url) { Uri uri; if (!Uri.TryCreate(url, UriKind.Absolute, out uri)) { if (!Uri.TryCreate(String.Format("{0}://{1}{2}", Protocol, RootDomain, url), UriKind.Absolute, out uri)) throw new Exception("Could not parse Uri"); } var request = CreateGet(uri); try { return ExecuteRequest(request); } catch (Exception) { var tempProtocol = Protocol; var tempRootDomain = RootDomain; Protocol = "http"; RootDomain = "www.reddit.com"; var retval = CreateAndExecuteRequest(url); Protocol = tempProtocol; RootDomain = tempRootDomain; return retval; } } /// /// Executes the web request and handles errors in the response /// /// /// public JToken ExecuteRequest(HttpWebRequest request) { EnforceRateLimit(); var response = request.GetResponse(); var result = GetResponseString(response.GetResponseStream()); var json = JToken.Parse(result); try { if (json["json"] != null) { json = json["json"]; //get json object if there is a root node } if (json["error"] != null) { switch (json["error"].ToString()) { case "404": throw new Exception("File Not Found"); case "403": throw new Exception("Restricted"); case "invalid_grant": //Refresh authtoken //AccessToken = authProvider.GetRefreshToken(); //ExecuteRequest(request); break; } } } catch { } return json; } private static void EnforceRateLimit() { switch (RateLimit) { case RateLimitMode.Pace: while ((DateTime.UtcNow - _lastRequest).TotalSeconds < 2)// Rate limiting Thread.Sleep(250); _lastRequest = DateTime.UtcNow; break; case RateLimitMode.Burst: if (_requestsThisBurst == 0)//this is first request _burstStart = DateTime.UtcNow; if (_requestsThisBurst >= 30) //limit has been reached { while ((DateTime.UtcNow - _burstStart).TotalSeconds < 60) Thread.Sleep(250); _burstStart = DateTime.UtcNow; } _requestsThisBurst++; break; } } public HttpWebRequest CreateRequest(string url, string method) { EnforceRateLimit(); bool prependDomain; // IsWellFormedUriString returns true on Mono for some reason when using a string like "/api/me" if (Type.GetType("Mono.Runtime") != null) prependDomain = !url.StartsWith("http://") && !url.StartsWith("https://"); else prependDomain = !Uri.IsWellFormedUriString(url, UriKind.Absolute); HttpWebRequest request; if (prependDomain) request = (HttpWebRequest)WebRequest.Create(String.Format("{0}://{1}{2}", Protocol, RootDomain, url)); else request = (HttpWebRequest)WebRequest.Create(url); request.CookieContainer = Cookies; if (Type.GetType("Mono.Runtime") != null) { var cookieHeader = Cookies.GetCookieHeader(new Uri("http://reddit.com")); request.Headers.Set("Cookie", cookieHeader); } if (RootDomain == "oauth.reddit.com")// use OAuth { request.Headers.Set("Authorization", "bearer " + AccessToken);//Must be included in OAuth calls } request.Method = method; request.UserAgent = UserAgent + " - with RedditSharp by /u/sircmpwn"; return request; } private HttpWebRequest CreateRequest(Uri uri, string method) { EnforceRateLimit(); var request = (HttpWebRequest)WebRequest.Create(uri); request.CookieContainer = Cookies; if (Type.GetType("Mono.Runtime") != null) { var cookieHeader = Cookies.GetCookieHeader(new Uri("http://reddit.com")); request.Headers.Set("Cookie", cookieHeader); } if (RootDomain == "oauth.reddit.com")// use OAuth { request.Headers.Set("Authorization", "bearer " + AccessToken);//Must be included in OAuth calls } request.Method = method; request.UserAgent = UserAgent + " - with RedditSharp by /u/sircmpwn"; return request; } public HttpWebRequest CreateGet(string url) { return CreateRequest(url, "GET"); } private HttpWebRequest CreateGet(Uri url) { return CreateRequest(url, "GET"); } public HttpWebRequest CreatePost(string url) { var request = CreateRequest(url, "POST"); request.ContentType = "application/x-www-form-urlencoded"; return request; } public string GetResponseString(Stream stream) { var data = new StreamReader(stream).ReadToEnd(); stream.Close(); return data; } public void WritePostBody(Stream stream, object data, params string[] additionalFields) { var type = data.GetType(); var properties = type.GetProperties(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); string value = ""; foreach (var property in properties) { var attr = property.GetCustomAttributes(typeof(RedditAPINameAttribute), false).FirstOrDefault() as RedditAPINameAttribute; string name = attr == null ? property.Name : attr.Name; var entry = Convert.ToString(property.GetValue(data, null)); value += name + "=" + HttpUtility.UrlEncode(entry).Replace(";", "%3B").Replace("&", "%26") + "&"; } for (int i = 0; i < additionalFields.Length; i += 2) { var entry = Convert.ToString(additionalFields[i + 1]) ?? string.Empty; value += additionalFields[i] + "=" + HttpUtility.UrlEncode(entry).Replace(";", "%3B").Replace("&", "%26") + "&"; } value = value.Remove(value.Length - 1); // Remove trailing & var raw = Encoding.UTF8.GetBytes(value); stream.Write(raw, 0, raw.Length); stream.Close(); } } }