Removed the title monitoring, as it was dependent on to many factory (Edge cutting the title).

This commit is contained in:
Robin Krom 2020-10-26 20:48:50 +01:00
commit 2cacd80992
6 changed files with 1095 additions and 921 deletions

View file

@ -21,6 +21,7 @@
using System.Windows.Forms;
using Greenshot.Helpers;
using GreenshotPlugin.Core;
namespace Greenshot.Forms {
partial class AboutForm {

View file

@ -169,11 +169,11 @@ namespace GreenshotImgurPlugin {
{
AuthUrlPattern = "https://api.imgur.com/oauth2/authorize?response_type=token&client_id={ClientId}&state={State}",
TokenUrl = "https://api.imgur.com/oauth2/token",
RedirectUrl = "https://getgreenshot.org/oauth/imgur",
RedirectUrl = "https://getgreenshot.org/authorize/imgur",
CloudServiceName = "Imgur",
ClientId = ImgurCredentials.CONSUMER_KEY,
ClientSecret = ImgurCredentials.CONSUMER_SECRET,
AuthorizeMode = OAuth2AuthorizeMode.MonitorTitle,
AuthorizeMode = OAuth2AuthorizeMode.JsonReceiver,
RefreshToken = Config.RefreshToken,
AccessToken = Config.AccessToken,
AccessTokenExpires = Config.AccessTokenExpires

View file

@ -0,0 +1,191 @@
/*
* Greenshot - a free and open source screenshot tool
* Copyright (C) 2007-2020 Thomas Braun, Jens Klingen, Robin Krom
*
* For more information see: http://getgreenshot.org/
* The Greenshot project is hosted on GitHub https://github.com/greenshot/greenshot
*
* 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 1 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.Generic;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using log4net;
using Newtonsoft.Json;
namespace GreenshotPlugin.Core.OAuth
{
/// <summary>
/// OAuth 2.0 verification code receiver that runs a local server on a free port
/// and waits for a call with the authorization verification code.
/// </summary>
public class LocalJsonReceiver
{
private static readonly ILog Log = LogManager.GetLogger(typeof(LocalJsonReceiver));
private readonly ManualResetEvent _ready = new ManualResetEvent(true);
private IDictionary<string, string> _returnValues;
/// <summary>
/// The url format for the website to post to. Expects one port parameter.
/// Default: http://localhost:{0}/authorize/
/// </summary>
public string ListeningUrlFormat { get; set; } = "http://localhost:{0}/authorize/";
private string _listeningUri;
/// <summary>
/// The URL where the server is listening
/// </summary>
public string ListeningUri {
get {
if (string.IsNullOrEmpty(_listeningUri))
{
_listeningUri = string.Format(ListeningUrlFormat, GetRandomUnusedPort());
}
return _listeningUri;
}
set => _listeningUri = value;
}
/// <summary>
/// This action is called when the URI must be opened, default is just to run Process.Start
/// </summary>
public Action<string> OpenUriAction
{
set;
get;
} = authorizationUrl =>
{
Log.DebugFormat("Open a browser with: {0}", authorizationUrl);
using var process = Process.Start(authorizationUrl);
};
/// <summary>
/// Timeout for waiting for the website to respond
/// </summary>
public TimeSpan Timeout { get; set; } = TimeSpan.FromMinutes(4);
/// <summary>
/// The OAuth code receiver
/// </summary>
/// <param name="oauth2Settings">OAuth2Settings</param>
/// <returns>Dictionary with values</returns>
public IDictionary<string, string> ReceiveCode(OAuth2Settings oauth2Settings) {
using var listener = new HttpListener();
// Make sure the port is stored in the state, so the website can process this.
oauth2Settings.State = new Uri(ListeningUri).Port.ToString();
listener.Prefixes.Add(ListeningUri);
try {
listener.Start();
_ready.Reset();
listener.BeginGetContext(ListenerCallback, listener);
OpenUriAction(oauth2Settings.FormattedAuthUrl);
_ready.WaitOne(Timeout, true);
} catch (Exception) {
// Make sure we can clean up, also if the thead is aborted
_ready.Set();
throw;
} finally {
listener.Close();
}
return _returnValues;
}
/// <summary>
/// Handle a connection async, this allows us to break the waiting
/// </summary>
/// <param name="result">IAsyncResult</param>
private void ListenerCallback(IAsyncResult result) {
HttpListener listener = (HttpListener)result.AsyncState;
//If not listening return immediately as this method is called one last time after Close()
if (!listener.IsListening) {
return;
}
// Use EndGetContext to complete the asynchronous operation.
HttpListenerContext context = listener.EndGetContext(result);
// Handle request
HttpListenerRequest request = context.Request;
if (request.HasEntityBody)
{
// Process the body
using var body = request.InputStream;
using var reader = new StreamReader(body, request.ContentEncoding);
using var jsonTextReader = new JsonTextReader(reader);
var serializer = new JsonSerializer();
_returnValues = serializer.Deserialize<Dictionary<string, string>>(jsonTextReader);
}
// Create the response.
using (HttpListenerResponse response = context.Response)
{
if (request.HttpMethod == "OPTIONS")
{
response.AddHeader("Access-Control-Allow-Headers", "Content-Type, Accept, X-Requested-With");
response.AddHeader("Access-Control-Allow-Methods", "POST");
response.AddHeader("Access-Control-Max-Age", "1728000");
}
response.AppendHeader("Access-Control-Allow-Origin", "*");
if (request.HasEntityBody)
{
response.ContentType = "application/json";
// currently only return the version, more can be added later
string jsonContent = "{\"version\": \"" + EnvironmentInfo.GetGreenshotVersion(true) + "\"}";
// Write a "close" response.
byte[] buffer = Encoding.UTF8.GetBytes(jsonContent);
// Write to response stream.
response.ContentLength64 = buffer.Length;
using var stream = response.OutputStream;
stream.Write(buffer, 0, buffer.Length);
}
}
if (_returnValues != null)
{
_ready.Set();
}
else
{
// Make sure the next request is processed
listener.BeginGetContext(ListenerCallback, listener);
}
}
/// <summary>
/// Returns a random, unused port.
/// </summary>
/// <returns>port to use</returns>
private static int GetRandomUnusedPort() {
var listener = new TcpListener(IPAddress.Loopback, 0);
try {
listener.Start();
return ((IPEndPoint)listener.LocalEndpoint).Port;
} finally {
listener.Stop();
}
}
}
}

View file

@ -27,8 +27,7 @@ namespace GreenshotPlugin.Core.OAuth
public enum OAuth2AuthorizeMode {
Unknown, // Will give an exception, caller needs to specify another value
LocalServer, // Will specify a redirect URL to http://localhost:port/authorize, while having a HttpListener
MonitorTitle, // Will monitor for title changes, the title needs the status and query params
Pin, // Not implemented yet: Will ask the user to enter the shown PIN
JsonReceiver, // Will start a local HttpListener and wait for a Json post
EmbeddedBrowser // Will open into an embedded _browser (OAuthLoginForm), and catch the redirect
}
}

View file

@ -21,12 +21,9 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Net;
using System.Windows.Forms;
using GreenshotPlugin.Controls;
using GreenshotPlugin.Hooking;
namespace GreenshotPlugin.Core.OAuth {
/// <summary>
@ -134,7 +131,7 @@ namespace GreenshotPlugin.Core.OAuth {
/// <summary>
/// Go out and retrieve a new access token via refresh-token with the TokenUrl in the settings
/// Will upate the access token, refresh token, expire date
/// Will update the access token, refresh token, expire date
/// </summary>
/// <param name="settings"></param>
public static void GenerateAccessToken(OAuth2Settings settings) {
@ -159,22 +156,23 @@ namespace GreenshotPlugin.Core.OAuth {
// "token_type":"Bearer",
IDictionary<string, object> accessTokenResult = JSONHelper.JsonDecode(accessTokenJsonResult);
if (accessTokenResult.ContainsKey("error")) {
if ("invalid_grant" == (string)accessTokenResult["error"]) {
if (accessTokenResult.ContainsKey("error"))
{
if ("invalid_grant" == (string)accessTokenResult["error"]) {
// Refresh token has also expired, we need a new one!
settings.RefreshToken = null;
settings.AccessToken = null;
settings.AccessTokenExpires = DateTimeOffset.MinValue;
settings.Code = null;
return;
} else {
if (accessTokenResult.ContainsKey("error_description")) {
throw new Exception($"{accessTokenResult["error"]} - {accessTokenResult["error_description"]}");
} else {
throw new Exception((string)accessTokenResult["error"]);
}
}
}
if (accessTokenResult.ContainsKey("error_description")) {
throw new Exception($"{accessTokenResult["error"]} - {accessTokenResult["error_description"]}");
}
throw new Exception((string)accessTokenResult["error"]);
}
if (accessTokenResult.ContainsKey(AccessToken))
{
@ -205,76 +203,62 @@ namespace GreenshotPlugin.Core.OAuth {
{
OAuth2AuthorizeMode.LocalServer => AuthenticateViaLocalServer(settings),
OAuth2AuthorizeMode.EmbeddedBrowser => AuthenticateViaEmbeddedBrowser(settings),
OAuth2AuthorizeMode.MonitorTitle => AuthenticateViaDefaultBrowser(settings),
OAuth2AuthorizeMode.JsonReceiver => AuthenticateViaDefaultBrowser(settings),
_ => throw new NotImplementedException($"Authorize mode '{settings.AuthorizeMode}' is not 'yet' implemented."),
};
return completed;
}
/// <summary>
/// Authenticate via the default browser, using the browser title.
/// Authenticate via the default browser, via the Greenshot website.
/// It will wait for a Json post.
/// If this works, return the code
/// </summary>
/// <param name="settings">OAuth2Settings with the Auth / Token url etc</param>
/// <returns>true if completed, false if canceled</returns>
private static bool AuthenticateViaDefaultBrowser(OAuth2Settings settings)
{
var monitor = new WindowsTitleMonitor();
var codeReceiver = new LocalJsonReceiver();
IDictionary<string, string> result = codeReceiver.ReceiveCode(settings);
string error = null;
var fields = new HashSet<string>();
int nrOfFields = 100;
monitor.TitleChangeEvent += args =>
foreach (var key in result.Keys)
{
if (!args.Title.Contains(settings.State))
switch (key)
{
return;
case AccessToken:
settings.AccessToken = result[key];
break;
case ExpiresIn:
if (int.TryParse(result[key], out var seconds))
{
settings.AccessTokenExpires = DateTimeOffset.Now.AddSeconds(seconds);
}
break;
case RefreshToken:
settings.RefreshToken = result[key];
break;
}
}
var title = args.Title;
title = title.Substring(0,title.IndexOf(' '));
var parameters = NetworkHelper.ParseQueryString(title);
if (parameters.ContainsKey("nr"))
if (result.TryGetValue("error", out var error))
{
if (result.TryGetValue("error_description", out var errorDescription))
{
nrOfFields = int.Parse(parameters["nr"]);
throw new Exception(errorDescription);
}
if ("access_denied" == error)
{
throw new UnauthorizedAccessException("Access denied");
}
throw new Exception(error);
}
if (result.TryGetValue(Code, out var code) && !string.IsNullOrEmpty(code))
{
settings.Code = code;
GenerateRefreshToken(settings);
}
foreach (var key in parameters.Keys)
{
fields.Add(key);
switch (key)
{
case AccessToken:
settings.AccessToken = parameters[key];
break;
case ExpiresIn:
if (int.TryParse(parameters[key], out var seconds))
{
settings.AccessTokenExpires = DateTimeOffset.Now.AddSeconds(seconds);
}
break;
case RefreshToken:
settings.RefreshToken = parameters[key];
break;
case Error:
error = parameters[key];
break;
}
}
};
using (var process = Process.Start(settings.FormattedAuthUrl))
{
while (nrOfFields > fields.Count)
{
// Have the thread process Forms events
Application.DoEvents();
}
};
monitor.Dispose();
return error == null;
return true;
}
/// <summary>