/// <summary> /// Execute a request for the given requestModel asynchronously, executing the given callback upon completions /// other than cancellations (gui updates for cancellation should be done at point of cancellation). /// </summary> /// <param name="requestModel"></param> /// <param name="callback"></param> /// <returns></returns> public CancellationTokenSource ExecuteAsync(RequestModel requestModel, Action<ResponseModel> callback) { //todo: add using statements for disposible objects var hasCookies = requestModel.RequestHeaders.ContainsKey("cookie"); var hasExpect100ContinueHeader = requestModel.RequestHeaders.ContainsKey("expect") && requestModel.RequestHeaders["expect"].Equals("100-continue", StringComparison.OrdinalIgnoreCase); var handler = new HttpClientHandler { AllowAutoRedirect = followRedirects, UseCookies = hasCookies }; if (!proxyServer.IsBlank()) { handler.Proxy = new WebProxy(proxyServer, false); //make second arg a config option. handler.UseProxy = true; } if (enableAutomaticContentDecompression) { foreach (var enc in requestModel.AcceptEncodings) { switch (enc) { case "gzip": handler.AutomaticDecompression |= DecompressionMethods.GZip; break; case "deflate": handler.AutomaticDecompression |= DecompressionMethods.Deflate; break; default: log.Warn("Automatic decompression of '{0}' content specified by the Accept-Encoding request header is not supported", enc); break; } } } var client = new HttpClient(handler); var request = new HttpRequestMessage { RequestUri = requestModel.Url, Method = requestModel.Method }; foreach (var header in requestModel.RequestHeaders) { if(header.Key.Equals("cookie", StringComparison.OrdinalIgnoreCase)) handler.CookieContainer.SetCookies(requestModel.Url, header.Value.Replace("; ", ", ")); else request.Headers.Add(header.Key, header.Value); } request.Headers.ExpectContinue = hasExpect100ContinueHeader; if (!requestModel.Url.UserInfo.IsBlank()) { var userInfoBase64Text = Base64EncodeUrlUserInfo(requestModel.Url); request.Headers.Authorization = new AuthenticationHeaderValue("Basic", userInfoBase64Text); } if (requestModel.Method != HttpMethod.Get && requestModel.Method != HttpMethod.Head && !requestModel.Body.IsBlank()) { //default content-type: http://mattryall.net/blog/2008/03/default-content-type string textCt; requestModel.ContentHeaders.TryGetValue("Content-Type", out textCt); textCt = textCt.IsBlank() ? defaultRequestContentType : textCt; //then try settings supplied textCt = textCt.IsBlank() ? "application/octet-stream" : textCt; // then try w3 spec default //get encoding var ct = new ContentType(textCt); //write content w/ BOM if needed var contentBytes = GetEncodedBytes(requestModel.Body, ct.CharSet, includeUtf8Bom); var content = new ByteArrayContent(contentBytes); foreach (var header in requestModel.ContentHeaders) { content.Headers.Remove(header.Key); //remove defaults if (String.Equals(header.Key, "content-type", StringComparison.OrdinalIgnoreCase)) //treat special w/ defaults, etc. content.Headers.Add(header.Key, textCt); else content.Headers.Add(header.Key, header.Value); } request.Content = content; } var start = DateTime.Now; var ctokenSource = new CancellationTokenSource(); var ctoken = ctokenSource.Token; client.SendAsync(request,ctoken).ContinueWith(responseTask => { try { var end = DateTime.Now; switch (responseTask.Status) { case TaskStatus.RanToCompletion: { var response = responseTask.Result; var responseModel = new ResponseModel(response, start, end); callback(responseModel); break; } case TaskStatus.Canceled: { log.Info("request canceled by user"); break; } case TaskStatus.Faulted: { var aggException = responseTask.Exception.Flatten(); foreach (var exception in aggException.InnerExceptions) log.Error("request terminated with an error", exception); string errMessage = String.Join(Environment.NewLine, aggException.InnerExceptions); var responseModel = new ResponseModel(errMessage, start, end); callback(responseModel); break; } default: { var errMessage = String.Format("The request terminated with an unexpected status={0}", responseTask.Status); log.Warn(errMessage); var responseModel = new ResponseModel(errMessage, start, end); callback(responseModel); break; } } } catch(Exception ex) { log.Error("exception raised in request continuation, application will proceed in a corrupt state until Task is disposed, at which point the application will shut down with a fatal exception", ex); throw; } }); return ctokenSource; }
public static List<string> TryCreate(RequestViewModel vm, out RequestModel requestModel) { var validationErrors = new List<string>(); Uri url = null; if(vm.Url.IsBlank()) validationErrors.Add("Request URL may not be empty"); else { var forgivingUrl = vm.Url.Contains("://") ? vm.Url : "http://" + vm.Url; if(!Uri.TryCreate(forgivingUrl, UriKind.Absolute, out url)) validationErrors.Add("Request URL is invalid"); } var requestHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); var contentHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); var acceptEncodings = new HashSet<string>(); foreach (var line in vm.Headers) { if (line.IsBlank()) continue; //allow empty lines var match = Regex.Match(line, @"^([0-9a-zA-Z-]+)\s*\:(.*)$", RegexOptions.Compiled); if (!match.Success) validationErrors.Add("Invalid header line (format incorrect): " + line); else { var key = match.Groups[1].Value.Trim(); var value = match.Groups[2].Value.Trim(); if (requestHeaders.ContainsKey(key) || contentHeaders.ContainsKey(key)) validationErrors.Add("Invalid header line (duplicate key, comma-separate multiple values for one key): " + line); else if (String.Equals(key, "authorization", StringComparison.OrdinalIgnoreCase) && !url.UserInfo.IsBlank()) { validationErrors.Add("Invalid header line (Authorization header cannot be specified when user information is given in the url): " + line); } else { //var values = value.Split(',').Select(x => x.Trim()).ToList().AsReadOnly(); //some ugliness to leverage system.net.http request and content header validation var hrhValidator = (HttpRequestHeaders)Activator.CreateInstance(typeof(HttpRequestHeaders), true); try { hrhValidator.Add(key, value); requestHeaders.Add(key, value); if(key.Equals("accept-encoding", StringComparison.OrdinalIgnoreCase)) { var encodings = value.Split(',').Select(x => x.Trim().ToLower()).Where(x => x != ""); foreach(var enc in encodings) acceptEncodings.Add(enc); } } catch (InvalidOperationException) { //i.e. header belongs in content headers var hchValidator = (HttpContentHeaders)Activator.CreateInstance(typeof(HttpContentHeaders), BindingFlags.Instance | BindingFlags.CreateInstance | BindingFlags.NonPublic, null, new[] { (object)(Func<long?>)(() => (long?)null) }, CultureInfo.CurrentCulture); try { hchValidator.Add(key, value); contentHeaders.Add(key, value); } catch (Exception e) { validationErrors.Add(string.Format("Invalid header line ({0}): {1}", e.Message, line)); } } catch (Exception e) { validationErrors.Add(string.Format("Invalid header line ({0}): {1}", e.Message, line)); } } } } if (validationErrors.Count > 0) requestModel = null; else { requestModel = new RequestModel() { Url = url, Method = new HttpMethod(vm.Method), RequestHeaders = requestHeaders, ContentHeaders = contentHeaders, AcceptEncodings = acceptEncodings, Body = vm.Body }; } return validationErrors; }
/// <summary> /// Execute a request for the given requestModel asynchronously, executing the given callback upon completions /// other than cancellations (gui updates for cancellation should be done at point of cancellation). /// </summary> /// <param name="requestModel"></param> /// <param name="callback"></param> /// <returns></returns> public CancellationTokenSource ExecuteAsync(RequestModel requestModel, Action <ResponseModel> callback) { //todo: add using statements for disposible objects var handler = new HttpClientHandler { AllowAutoRedirect = followRedirects }; if (!proxyServer.IsBlank()) { handler.Proxy = new WebProxy(proxyServer, false); //make second arg a config option. handler.UseProxy = true; } var client = new HttpClient(handler); var request = new HttpRequestMessage { RequestUri = requestModel.Url, Method = requestModel.Method }; foreach (var header in requestModel.RequestHeaders) { request.Headers.Add(header.Key, header.Value); } if (requestModel.Method != HttpMethod.Get && requestModel.Method != HttpMethod.Head) { //default content-type: http://mattryall.net/blog/2008/03/default-content-type string textCt; requestModel.ContentHeaders.TryGetValue("Content-Type", out textCt); textCt = textCt.IsBlank() ? defaultRequestContentType : textCt; //then try settings supplied textCt = textCt.IsBlank() ? "application/octet-stream" : textCt; // then try w3 spec default //get encoding var ct = new ContentType(textCt); //write content w/ BOM if needed var contentBytes = GetEncodedBytes(requestModel.Body, ct.CharSet, includeUtf8Bom); var content = new ByteArrayContent(contentBytes); foreach (var header in requestModel.ContentHeaders) { content.Headers.Remove(header.Key); //remove defaults if (String.Equals(header.Key, "content-type", StringComparison.OrdinalIgnoreCase)) //treat special w/ defaults, etc. { content.Headers.Add(header.Key, textCt); } else { content.Headers.Add(header.Key, header.Value); } } request.Content = content; } var start = DateTime.Now; var ctokenSource = new CancellationTokenSource(); var ctoken = ctokenSource.Token; client.SendAsync(request, ctoken).ContinueWith(responseTask => { try { var end = DateTime.Now; switch (responseTask.Status) { case TaskStatus.RanToCompletion: { var response = responseTask.Result; var responseModel = new ResponseModel(response, start, end); callback(responseModel); break; } case TaskStatus.Canceled: { log.Info("request canceled by user"); break; } case TaskStatus.Faulted: { var aggException = responseTask.Exception.Flatten(); foreach (var exception in aggException.InnerExceptions) { log.ErrorException("request terminated with an error", exception); } string errMessage = String.Join(Environment.NewLine, aggException.InnerExceptions); var responseModel = new ResponseModel(errMessage, start, end); callback(responseModel); break; } default: { var errMessage = String.Format("The request terminated with an unexpected status={0}", responseTask.Status); log.Warn(errMessage); var responseModel = new ResponseModel(errMessage, start, end); callback(responseModel); break; } } } catch (Exception ex) { log.ErrorException("exception raised in request continuation, application will proceed in a corrupt state until Task is disposed, at which point the application will shut down with a fatal exception", ex); throw; } }); return(ctokenSource); }
public static List <string> TryCreate(RequestViewModel vm, out RequestModel requestModel) { var validationErrors = new List <string>(); Uri url = null; if (vm.Url.IsBlank()) { validationErrors.Add("Request URL may not be empty"); } else { var forgivingUrl = vm.Url.Contains("://") ? vm.Url : "http://" + vm.Url; if (!Uri.TryCreate(forgivingUrl, UriKind.Absolute, out url)) { validationErrors.Add("Request URL is invalid"); } } var requestHeaders = new Dictionary <string, string>(StringComparer.OrdinalIgnoreCase); var contentHeaders = new Dictionary <string, string>(StringComparer.OrdinalIgnoreCase); var acceptEncodings = new HashSet <string>(); foreach (var line in vm.Headers) { if (line.IsBlank()) { continue; //allow empty lines } var match = Regex.Match(line, @"^([0-9a-zA-Z-]+)\s*\:(.*)$", RegexOptions.Compiled); if (!match.Success) { validationErrors.Add("Invalid header line (format incorrect): " + line); } else { var key = match.Groups[1].Value.Trim(); var value = match.Groups[2].Value.Trim(); if (requestHeaders.ContainsKey(key) || contentHeaders.ContainsKey(key)) { validationErrors.Add("Invalid header line (duplicate key, comma-separate multiple values for one key): " + line); } else if (String.Equals(key, "authorization", StringComparison.OrdinalIgnoreCase) && !url.UserInfo.IsBlank()) { validationErrors.Add("Invalid header line (Authorization header cannot be specified when user information is given in the url): " + line); } else { //var values = value.Split(',').Select(x => x.Trim()).ToList().AsReadOnly(); //some ugliness to leverage system.net.http request and content header validation var hrhValidator = (HttpRequestHeaders)Activator.CreateInstance(typeof(HttpRequestHeaders), true); try { hrhValidator.Add(key, value); requestHeaders.Add(key, value); if (key.Equals("accept-encoding", StringComparison.OrdinalIgnoreCase)) { var encodings = value.Split(',').Select(x => x.Trim().ToLower()).Where(x => x != ""); foreach (var enc in encodings) { acceptEncodings.Add(enc); } } } catch (InvalidOperationException) { //i.e. header belongs in content headers var hchValidator = (HttpContentHeaders)Activator.CreateInstance(typeof(HttpContentHeaders), BindingFlags.Instance | BindingFlags.CreateInstance | BindingFlags.NonPublic, null, new[] { (object)(Func <long?>)(() => (long?)null) }, CultureInfo.CurrentCulture); try { hchValidator.Add(key, value); contentHeaders.Add(key, value); } catch (Exception e) { validationErrors.Add(string.Format("Invalid header line ({0}): {1}", e.Message, line)); } } catch (Exception e) { validationErrors.Add(string.Format("Invalid header line ({0}): {1}", e.Message, line)); } } } } if (validationErrors.Count > 0) { requestModel = null; } else { requestModel = new RequestModel() { Url = url, Method = new HttpMethod(vm.Method), RequestHeaders = requestHeaders, ContentHeaders = contentHeaders, AcceptEncodings = acceptEncodings, Body = vm.Body }; } return(validationErrors); }
public static List <string> TryCreate(RequestViewModel vm, out RequestModel requestModel) { var validationErrors = new List <string>(); Uri url = null; if (vm.Url.IsBlank()) { validationErrors.Add("Request URL may not be empty"); } else { var knownProtocals = new[] { "http://", "https://", "ftp://", "ftps://", "file:///" }; var forgivingUrl = knownProtocals.Any(x => vm.Url.StartsWith(x)) ? vm.Url : "http://" + vm.Url; if (!Uri.TryCreate(forgivingUrl, UriKind.Absolute, out url)) { validationErrors.Add("Request URL is invalid"); } } var requestHeaders = new Dictionary <string, string>(StringComparer.OrdinalIgnoreCase); var contentHeaders = new Dictionary <string, string>(StringComparer.OrdinalIgnoreCase); foreach (var line in vm.Headers) { if (line.IsBlank()) { continue; //allow empty lines } var match = Regex.Match(line, @"^([^\:]+)\:(.+)$", RegexOptions.Compiled); if (!match.Success) { validationErrors.Add("Invalid header line (format incorrect): " + line); } else { var key = match.Groups[1].Value.Trim(); var value = match.Groups[2].Value.Trim(); if (key.IsBlank() || value.IsBlank()) { validationErrors.Add("Invalid header line (key or value is blank): " + line); } else if (requestHeaders.ContainsKey(key) || contentHeaders.ContainsKey(key)) { validationErrors.Add("Invalid header line (duplicate key, comma-separate multiple values for one key): " + line); } else { //var values = value.Split(',').Select(x => x.Trim()).ToList().AsReadOnly(); //some ugliness to leverage system.net.http request and content header validation var hrhValidator = (HttpRequestHeaders)Activator.CreateInstance(typeof(HttpRequestHeaders), true); try { hrhValidator.Add(key, value); requestHeaders.Add(key, value); } catch (InvalidOperationException) { //i.e. header belongs in content headers var hchValidator = (HttpContentHeaders)Activator.CreateInstance(typeof(HttpContentHeaders), BindingFlags.Instance | BindingFlags.CreateInstance | BindingFlags.NonPublic, null, new[] { (object)(Func <long?>)(() => (long?)null) }, CultureInfo.CurrentCulture); try { hchValidator.Add(key, value); contentHeaders.Add(key, value); } catch (Exception e) { validationErrors.Add(string.Format("Invalid header line ({0}): {1}", e.Message, line)); } } catch (Exception e) { validationErrors.Add(string.Format("Invalid header line ({0}): {1}", e.Message, line)); } } } } if (validationErrors.Count > 0) { requestModel = null; } else { requestModel = new RequestModel() { Url = url, Method = new HttpMethod(vm.Method), RequestHeaders = requestHeaders, ContentHeaders = contentHeaders, Body = vm.Body }; } return(validationErrors); }
/// <summary> /// Execute a request for the given requestModel asynchronously, executing the given callback upon completions /// other than cancellations (gui updates for cancellation should be done at point of cancellation). /// </summary> /// <param name="requestModel"></param> /// <param name="callback"></param> /// <returns></returns> public CancellationTokenSource ExecuteAsync(RequestModel requestModel, Action <ResponseModel> callback) { //todo: add using statements for disposible objects var hasCookies = requestModel.RequestHeaders.ContainsKey("cookie"); var hasExpect100ContinueHeader = requestModel.RequestHeaders.ContainsKey("expect") && requestModel.RequestHeaders["expect"].Equals("100-continue", StringComparison.OrdinalIgnoreCase); var handler = new HttpClientHandler { AllowAutoRedirect = followRedirects, UseCookies = hasCookies, UseDefaultCredentials = true }; if (!proxyServer.IsBlank()) { handler.Proxy = new WebProxy(proxyServer, false); //make second arg a config option. handler.UseProxy = true; } if (enableAutomaticContentDecompression) { foreach (var enc in requestModel.AcceptEncodings) { switch (enc) { case "gzip": handler.AutomaticDecompression |= DecompressionMethods.GZip; break; case "deflate": handler.AutomaticDecompression |= DecompressionMethods.Deflate; break; default: log.Warn("Automatic decompression of '{0}' content specified by the Accept-Encoding request header is not supported", enc); break; } } } var client = new HttpClient(handler); var request = new HttpRequestMessage { RequestUri = requestModel.Url, Method = requestModel.Method }; foreach (var header in requestModel.RequestHeaders) { if (header.Key.Equals("cookie", StringComparison.OrdinalIgnoreCase)) { handler.CookieContainer.SetCookies(requestModel.Url, header.Value.Replace("; ", ", ")); } else { request.Headers.Add(header.Key, header.Value); } } request.Headers.ExpectContinue = hasExpect100ContinueHeader; if (!requestModel.Url.UserInfo.IsBlank()) { var userInfoBase64Text = Base64EncodeUrlUserInfo(requestModel.Url); request.Headers.Authorization = new AuthenticationHeaderValue("Basic", userInfoBase64Text); } if (requestModel.Method != HttpMethod.Get && requestModel.Method != HttpMethod.Head && !requestModel.Body.IsBlank()) { //default content-type: http://mattryall.net/blog/2008/03/default-content-type string textCt; requestModel.ContentHeaders.TryGetValue("Content-Type", out textCt); textCt = textCt.IsBlank() ? defaultRequestContentType : textCt; //then try settings supplied textCt = textCt.IsBlank() ? "application/octet-stream" : textCt; // then try w3 spec default //get encoding var ct = new ContentType(textCt); //write content w/ BOM if needed var contentBytes = GetEncodedBytes(requestModel.Body, ct.CharSet, includeUtf8Bom); var content = new ByteArrayContent(contentBytes); foreach (var header in requestModel.ContentHeaders) { content.Headers.Remove(header.Key); //remove defaults if (String.Equals(header.Key, "content-type", StringComparison.OrdinalIgnoreCase)) //treat special w/ defaults, etc. { content.Headers.Add(header.Key, textCt); } else { content.Headers.Add(header.Key, header.Value); } } request.Content = content; } var start = DateTime.Now; var ctokenSource = new CancellationTokenSource(); var ctoken = ctokenSource.Token; client.SendAsync(request, ctoken).ContinueWith(responseTask => { try { var end = DateTime.Now; switch (responseTask.Status) { case TaskStatus.RanToCompletion: { var response = responseTask.Result; var responseModel = new ResponseModel(response, start, end); callback(responseModel); break; } case TaskStatus.Canceled: { log.Info("request canceled by user"); break; } case TaskStatus.Faulted: { var aggException = responseTask.Exception.Flatten(); foreach (var exception in aggException.InnerExceptions) { log.Error(exception, "request terminated with an error"); } string errMessage = String.Join(Environment.NewLine, aggException.InnerExceptions); var responseModel = new ResponseModel(errMessage, start, end); callback(responseModel); break; } default: { var errMessage = String.Format("The request terminated with an unexpected status={0}", responseTask.Status); log.Warn(errMessage); var responseModel = new ResponseModel(errMessage, start, end); callback(responseModel); break; } } } catch (Exception ex) { log.Error(ex, "exception raised in request continuation, application will proceed in a corrupt state until Task is disposed, at which point the application will shut down with a fatal exception"); throw; } }); return(ctokenSource); }