From c983567827911089e9129fc5582ae210d7281f03 Mon Sep 17 00:00:00 2001 From: RKrom Date: Mon, 8 Oct 2012 06:26:29 +0000 Subject: [PATCH] Added Multipart/form-data support, this should improve memory usage and upload performance. Is also needed for Flickr. git-svn-id: http://svn.code.sf.net/p/greenshot/code/trunk@2124 7dccd23d-a4a3-4e1f-8c07-b4c1b4018ab4 --- GreenshotImgurPlugin/ImgurUtils.cs | 67 ++++--- .../PhotobucketUtils.cs | 2 +- GreenshotPlugin/Core/NetworkHelper.cs | 24 +-- GreenshotPlugin/Core/OAuthHelper.cs | 181 ++++++++++++++---- 4 files changed, 197 insertions(+), 77 deletions(-) diff --git a/GreenshotImgurPlugin/ImgurUtils.cs b/GreenshotImgurPlugin/ImgurUtils.cs index ca3d7ed6b..9d5b2014e 100644 --- a/GreenshotImgurPlugin/ImgurUtils.cs +++ b/GreenshotImgurPlugin/ImgurUtils.cs @@ -100,35 +100,30 @@ namespace GreenshotImgurPlugin { /// byte[] with image data /// ImgurResponse public static ImgurInfo UploadToImgur(byte[] imageData, int dataLength, string title, string filename) { - IDictionary uploadParameters = new Dictionary(); - // Add image - uploadParameters.Add("image", System.Convert.ToBase64String(imageData, 0, dataLength)); - // add type - uploadParameters.Add("type", "base64"); - - // add title - if (title != null) { - uploadParameters.Add("title", title); - } - // add filename - if (filename != null) { - uploadParameters.Add("name", filename); - } + IDictionary uploadParameters = new Dictionary(); string responseString = null; if (config.AnonymousAccess) { + // add title + if (title != null) { + uploadParameters.Add("title", title); + } + // add filename + if (filename != null) { + uploadParameters.Add("name", filename); + } + // add key uploadParameters.Add("key", IMGUR_ANONYMOUS_API_KEY); - HttpWebRequest webRequest = (HttpWebRequest)NetworkHelper.CreateWebRequest(config.ImgurApiUrl + "/upload"); + HttpWebRequest webRequest = (HttpWebRequest)NetworkHelper.CreateWebRequest(config.ImgurApiUrl + "/upload.xml?" + NetworkHelper.GenerateQueryParameters(uploadParameters)); webRequest.Method = "POST"; - webRequest.ContentType = "application/x-www-form-urlencoded"; + webRequest.ContentType = "image/png"; webRequest.ServicePoint.Expect100Continue = false; - - using(StreamWriter streamWriter = new StreamWriter(webRequest.GetRequestStream())) { - string urloadText = NetworkHelper.GenerateQueryParameters(uploadParameters); - streamWriter.Write(urloadText); + using (var requestStream = webRequest.GetRequestStream()) { + requestStream.Write(imageData, 0, dataLength); } + using (WebResponse response = webRequest.GetResponse()) { LogCredits(response); Stream responseStream = response.GetResponseStream(); @@ -144,12 +139,34 @@ namespace GreenshotImgurPlugin { oAuth.AuthorizeUrl = "http://api.imgur.com/oauth/authorize"; oAuth.RequestTokenUrl = "http://api.imgur.com/oauth/request_token"; oAuth.LoginTitle = "Imgur authorization"; - //oAuth.UseHTTPHeadersForAuthorization = false; oAuth.Token = config.ImgurToken; oAuth.TokenSecret = config.ImgurTokenSecret; + if (string.IsNullOrEmpty(oAuth.Token)) { + if (!oAuth.Authorize()) { + return null; + } + if (!string.IsNullOrEmpty(oAuth.Token)) { + config.ImgurToken = oAuth.Token; + } + if (!string.IsNullOrEmpty(oAuth.TokenSecret)) { + config.ImgurTokenSecret = oAuth.TokenSecret; + } + IniConfig.Save(); + } try { - LOG.DebugFormat("Test: {0}", oAuth.MakeOAuthRequest(HTTPMethod.GET, "http://api.imgur.com/2/account", null)); - responseString = oAuth.MakeOAuthRequest(HTTPMethod.POST, "http://api.imgur.com/2/account/images.xml", uploadParameters); + string apiUrl = "http://api.imgur.com/2/account/images.xml"; + // sign without parameters + oAuth.Sign(HTTPMethod.POST, apiUrl, uploadParameters); + // add title + if (title != null) { + uploadParameters.Add("title", title); + } + // add filename + if (filename != null) { + uploadParameters.Add("name", filename); + } + uploadParameters.Add("image", new FileParameter(imageData, filename, "image/png", dataLength)); + responseString = oAuth.MakeRequest(HTTPMethod.POST, apiUrl, uploadParameters, null, null); } catch (Exception ex) { LOG.Error("Upload to imgur gave an exeption: ", ex); throw ex; @@ -163,9 +180,7 @@ namespace GreenshotImgurPlugin { IniConfig.Save(); } } - LOG.Info(responseString); - ImgurInfo imgurInfo = ImgurInfo.ParseResponse(responseString); - return imgurInfo; + return ImgurInfo.ParseResponse(responseString); } public static void RetrieveImgurThumbnail(ImgurInfo imgurInfo) { diff --git a/GreenshotPhotobucketPlugin/PhotobucketUtils.cs b/GreenshotPhotobucketPlugin/PhotobucketUtils.cs index 5d58fe118..4c15241dd 100644 --- a/GreenshotPhotobucketPlugin/PhotobucketUtils.cs +++ b/GreenshotPhotobucketPlugin/PhotobucketUtils.cs @@ -95,7 +95,7 @@ namespace GreenshotPhotobucketPlugin { oAuth.Sign(HTTPMethod.POST, apiUrl, parameters); apiUrl = apiUrl.Replace("api.photobucket.com", config.SubDomain); // Add image - parameters.Add("uploadfile", System.Convert.ToBase64String(imageData, 0, dataLength)); + parameters.Add("uploadfile", new FileParameter(imageData, filename, "image/png", dataLength)); responseString = oAuth.MakeRequest(HTTPMethod.POST, apiUrl, parameters, null, null); } catch (Exception ex) { LOG.Error("Error uploading to Photobucket.", ex); diff --git a/GreenshotPlugin/Core/NetworkHelper.cs b/GreenshotPlugin/Core/NetworkHelper.cs index bf2b3db00..69cd4f885 100644 --- a/GreenshotPlugin/Core/NetworkHelper.cs +++ b/GreenshotPlugin/Core/NetworkHelper.cs @@ -222,24 +222,24 @@ namespace GreenshotPlugin.Core { } /// - /// Generate the query paramters - /// - /// the list of query parameters - /// a string with the query parameters - public static string GenerateQueryParameters(IDictionary queryParameters) { - if (queryParameters == null || queryParameters.Count == 0) { - return string.Empty; - } - - queryParameters = new SortedDictionary(queryParameters); + /// Generate the query paramters + /// + /// the list of query parameters + /// a string with the query parameters + public static string GenerateQueryParameters(IDictionary queryParameters) { + if (queryParameters == null || queryParameters.Count == 0) { + return string.Empty; + } + + queryParameters = new SortedDictionary(queryParameters); StringBuilder sb = new StringBuilder(); foreach(string key in queryParameters.Keys) { - sb.AppendFormat(System.Globalization.CultureInfo.InvariantCulture, "{0}={1}&", key, UrlEncode(queryParameters[key])); + sb.AppendFormat(System.Globalization.CultureInfo.InvariantCulture, "{0}={1}&", key, UrlEncode(string.Format("{0}",queryParameters[key]))); } sb.Remove(sb.Length-1,1); return sb.ToString(); - } + } } } diff --git a/GreenshotPlugin/Core/OAuthHelper.cs b/GreenshotPlugin/Core/OAuthHelper.cs index a059dc489..4a0735e5a 100644 --- a/GreenshotPlugin/Core/OAuthHelper.cs +++ b/GreenshotPlugin/Core/OAuthHelper.cs @@ -41,6 +41,22 @@ namespace GreenshotPlugin.Core { public enum HTTPMethod { GET, POST, PUT, DELETE }; + public class FileParameter { + public byte[] File { get; set; } + public string FileName { get; set; } + public string ContentType { get; set; } + public int FileSize {get; set; } + public FileParameter(byte[] file) : this(file, null) { } + public FileParameter(byte[] file, string filename) : this(file, filename, null) { } + public FileParameter(byte[] file, string filename, string contenttype) : this(file, filename, contenttype, 0) { } + public FileParameter(byte[] file, string filename, string contenttype, int filesize) { + File = file; + FileName = filename; + ContentType = contenttype; + FileSize = filesize; + } + } + public class OAuthSession { private static readonly log4net.ILog LOG = log4net.LogManager.GetLogger(typeof(OAuthSession)); protected const string OAUTH_VERSION = "1.0"; @@ -100,11 +116,31 @@ namespace GreenshotPlugin.Core { private string loginTitle = "Authorize Greenshot access"; #region PublicPropertiies - public string UserAgent { get { return _userAgent; } set { _userAgent = value; } } public string RequestTokenUrl { get; set; } public string AuthorizeUrl { get; set; } public string AccessTokenUrl { get; set; } - public string CallbackUrl { get { return _callbackUrl;} set { _callbackUrl = value; } } + + public string Token { get; set; } + public string TokenSecret { get; set; } + public string Verifier { get; set; } + + public bool UseMultipartFormData { get; set; } + public string UserAgent { + get { + return _userAgent; + } + set { + _userAgent = value; + } + } + public string CallbackUrl { + get { + return _callbackUrl; + } + set { + _callbackUrl = value; + } + } public bool CheckVerifier { get { return checkVerifier; @@ -123,12 +159,6 @@ namespace GreenshotPlugin.Core { } } - public string Token { - get; - set; - } - public string TokenSecret { get; set; } - public string LoginTitle { get { return loginTitle; @@ -137,10 +167,6 @@ namespace GreenshotPlugin.Core { loginTitle = value; } } - public string Verifier { - get; - set; - } public bool UseHTTPHeadersForAuthorization { get { return useHTTPHeadersForAuthorization; @@ -155,6 +181,7 @@ namespace GreenshotPlugin.Core { public OAuthSession(string consumerKey, string consumerSecret) { this.consumerKey = consumerKey; this.consumerSecret = consumerSecret; + this.UseMultipartFormData = true; } /// @@ -183,16 +210,18 @@ namespace GreenshotPlugin.Core { /// /// the list of query parameters /// a string with the normalized query parameters - private static string GenerateNormalizedParametersString(IDictionary queryParameters) { + private static string GenerateNormalizedParametersString(IDictionary queryParameters) { if (queryParameters == null || queryParameters.Count == 0) { return string.Empty; } - queryParameters = new SortedDictionary(queryParameters); + queryParameters = new SortedDictionary(queryParameters); StringBuilder sb = new StringBuilder(); foreach (string key in queryParameters.Keys) { - sb.AppendFormat(System.Globalization.CultureInfo.InvariantCulture, "{0}={1}&", key, UrlEncode3986(queryParameters[key])); + if (queryParameters[key] is string) { + sb.AppendFormat(System.Globalization.CultureInfo.InvariantCulture, "{0}={1}&", key, UrlEncode3986(string.Format("{0}",queryParameters[key]))); + } } sb.Remove(sb.Length - 1, 1); @@ -245,7 +274,7 @@ namespace GreenshotPlugin.Core { /// The request token. private String getRequestToken() { string ret = null; - IDictionary parameters = new Dictionary(); + IDictionary parameters = new Dictionary(); Sign(HTTPMethod.POST, RequestTokenUrl, parameters); string response = MakeRequest(HTTPMethod.POST, RequestTokenUrl, parameters, null, null); LOG.DebugFormat("Request token response: {0}", response); @@ -295,7 +324,7 @@ namespace GreenshotPlugin.Core { throw e; } - IDictionary parameters = new Dictionary(); + IDictionary parameters = new Dictionary(); Sign(HTTPMethod.POST, AccessTokenUrl, parameters); string response = MakeRequest(HTTPMethod.POST, AccessTokenUrl, parameters, null, null); LOG.DebugFormat("Access token response: {0}", response); @@ -351,7 +380,7 @@ namespace GreenshotPlugin.Core { /// /// /// - public string MakeOAuthRequest(HTTPMethod method, string requestURL, IDictionary parameters) { + public string MakeOAuthRequest(HTTPMethod method, string requestURL, IDictionary parameters) { return MakeOAuthRequest(method, requestURL, parameters, null, null); } @@ -364,9 +393,9 @@ namespace GreenshotPlugin.Core { /// contenttype for the postdata /// Data to post (MemoryStream) /// The web server response. - public string MakeOAuthRequest(HTTPMethod method, string requestURL, IDictionary parameters, string contentType, MemoryStream postData) { + public string MakeOAuthRequest(HTTPMethod method, string requestURL, IDictionary parameters, string contentType, MemoryStream postData) { if (parameters == null) { - parameters = new Dictionary(); + parameters = new Dictionary(); } int retries = 2; Exception lastException = null; @@ -389,7 +418,7 @@ namespace GreenshotPlugin.Core { TokenSecret = null; // Remove oauth keys, so they aren't added double - IDictionary newParameters = new Dictionary(); + IDictionary newParameters = new Dictionary(); foreach (string parameterKey in parameters.Keys) { if (!parameterKey.StartsWith(OAUTH_PARAMETER_PREFIX)) { newParameters.Add(parameterKey, parameters[parameterKey]); @@ -415,7 +444,7 @@ namespace GreenshotPlugin.Core { /// Method (POST,PUT,GET) /// Url to call /// IDictionary - public void Sign(HTTPMethod method, string requestURL, IDictionary parameters) { + public void Sign(HTTPMethod method, string requestURL, IDictionary parameters) { if (parameters == null) { throw new ArgumentNullException("parameters"); } @@ -450,6 +479,7 @@ namespace GreenshotPlugin.Core { parameters.Add(OAUTH_TOKEN_KEY, Token); } signatureBase.Append(UrlEncode3986(GenerateNormalizedParametersString(parameters))); + LOG.DebugFormat("Signature base: {0}", signatureBase); // Generate Signature and add it to the parameters HMACSHA1 hmacsha1 = new HMACSHA1(); hmacsha1.Key = Encoding.UTF8.GetBytes(string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0}&{1}", UrlEncode3986(consumerSecret), string.IsNullOrEmpty(TokenSecret) ? string.Empty : UrlEncode3986(TokenSecret))); @@ -467,19 +497,19 @@ namespace GreenshotPlugin.Core { /// /// /// Response from server - public string MakeRequest(HTTPMethod method, string requestURL, IDictionary parameters, string contentType, MemoryStream postData) { + public string MakeRequest(HTTPMethod method, string requestURL, IDictionary parameters, string contentType, MemoryStream postData) { if (parameters == null) { throw new ArgumentNullException("parameters"); } - IDictionary requestParameters = null; + IDictionary requestParameters = null; // Add oAuth values as HTTP headers, if this is allowed StringBuilder authHeader = null; if (HTTPMethod.POST == method && UseHTTPHeadersForAuthorization) { authHeader = new StringBuilder(); - requestParameters = new Dictionary(); + requestParameters = new Dictionary(); foreach (string parameterKey in parameters.Keys) { if (parameterKey.StartsWith(OAUTH_PARAMETER_PREFIX)) { - authHeader.AppendFormat(System.Globalization.CultureInfo.InvariantCulture, "{0}=\"{1}\", ", parameterKey, UrlEncode3986(parameters[parameterKey])); + authHeader.AppendFormat(System.Globalization.CultureInfo.InvariantCulture, "{0}=\"{1}\", ", parameterKey, UrlEncode3986(string.Format("{0}",parameters[parameterKey]))); } else if (!requestParameters.ContainsKey(parameterKey)) { requestParameters.Add(parameterKey, parameters[parameterKey]); } @@ -509,18 +539,32 @@ namespace GreenshotPlugin.Core { } if (HTTPMethod.POST == method && postData == null && requestParameters != null && requestParameters.Count > 0) { - StringBuilder form = new StringBuilder(); - foreach (string parameterKey in requestParameters.Keys) { - form.AppendFormat(System.Globalization.CultureInfo.InvariantCulture, "{0}={1}&", UrlEncode3986(parameterKey), UrlEncode3986(parameters[parameterKey])); - } - // Remove trailing & - if (form.Length > 0) { - form.Remove(form.Length - 1, 1); - } - webRequest.ContentType = "application/x-www-form-urlencoded"; - byte[] data = Encoding.UTF8.GetBytes(form.ToString()); - using (var requestStream = webRequest.GetRequestStream()) { - requestStream.Write(data, 0, data.Length); + + if (UseMultipartFormData) { + byte [] data = GetMultipartFormData(requestParameters, out contentType); + webRequest.ContentType = contentType; + using (var requestStream = webRequest.GetRequestStream()) { + requestStream.Write(data, 0, data.Length); + } + } else { + StringBuilder form = new StringBuilder(); + foreach (string parameterKey in requestParameters.Keys) { + if (parameters[parameterKey] is FileParameter) { + FileParameter fileParameter = parameters[parameterKey] as FileParameter; + form.AppendFormat(System.Globalization.CultureInfo.InvariantCulture, "{0}={1}&", UrlEncode3986(parameterKey), UrlEncode3986(System.Convert.ToBase64String(fileParameter.File, 0, fileParameter.FileSize != 0 ? fileParameter.FileSize : fileParameter.File.Length))); + } else { + form.AppendFormat(System.Globalization.CultureInfo.InvariantCulture, "{0}={1}&", UrlEncode3986(parameterKey), UrlEncode3986(string.Format("{0}",parameters[parameterKey]))); + } + } + // Remove trailing & + if (form.Length > 0) { + form.Remove(form.Length - 1, 1); + } + webRequest.ContentType = "application/x-www-form-urlencoded"; + byte[] data = Encoding.UTF8.GetBytes(form.ToString()); + using (var requestStream = webRequest.GetRequestStream()) { + requestStream.Write(data, 0, data.Length); + } } } else { webRequest.ContentType = contentType; @@ -538,6 +582,67 @@ namespace GreenshotPlugin.Core { return responseData; } + + /// + /// Create a Multipart Form Data as byte[] + /// + /// Parameters to include in the multipart form data + /// out parameter for contenttype + /// byte[] with Multipart Form Data which can be used to upload + private static byte[] GetMultipartFormData(IDictionary postParameters, out string contentType) { + string boundary = String.Format("----------{0:N}", Guid.NewGuid()); + contentType = "multipart/form-data; boundary=" + boundary; + Stream formDataStream = new MemoryStream(); + bool needsCLRF = false; + + foreach (var param in postParameters) { + // Thanks to feedback from commenters, add a CRLF to allow multiple parameters to be added. + // Skip it on the first parameter, add it to subsequent parameters. + if (needsCLRF) { + formDataStream.Write(Encoding.UTF8.GetBytes("\r\n"), 0, Encoding.UTF8.GetByteCount("\r\n")); + } + + needsCLRF = true; + + if (param.Value is FileParameter) { + FileParameter fileToUpload = (FileParameter)param.Value; + + // Add just the first part of this param, since we will write the file data directly to the Stream + string header = string.Format("--{0}\r\nContent-Disposition: form-data; name=\"{1}\"; filename=\"{2}\";\r\nContent-Type: {3}\r\n\r\n", + boundary, + param.Key, + fileToUpload.FileName ?? param.Key, + fileToUpload.ContentType ?? "application/octet-stream"); + + formDataStream.Write(Encoding.UTF8.GetBytes(header), 0, Encoding.UTF8.GetByteCount(header)); + + // Write the file data directly to the Stream, rather than serializing it to a string. + if (fileToUpload.FileSize > 0) { + formDataStream.Write(fileToUpload.File, 0, fileToUpload.FileSize); + } else { + formDataStream.Write(fileToUpload.File, 0, fileToUpload.File.Length); + } + } else { + string postData = string.Format("--{0}\r\nContent-Disposition: form-data; name=\"{1}\"\r\n\r\n{2}", + boundary, + param.Key, + param.Value); + formDataStream.Write(Encoding.UTF8.GetBytes(postData), 0, Encoding.UTF8.GetByteCount(postData)); + } + } + + // Add the end of the request. Start with a newline + string footer = "\r\n--" + boundary + "--\r\n"; + formDataStream.Write(Encoding.UTF8.GetBytes(footer), 0, Encoding.UTF8.GetByteCount(footer)); + + // Dump the Stream into a byte[] + formDataStream.Position = 0; + byte[] formData = new byte[formDataStream.Length]; + formDataStream.Read(formData, 0, formData.Length); + formDataStream.Close(); + + return formData; + } /// /// Process the web response.