TapLogin-Unity/Standalone/Runtime/Internal/QRCodeWindow.cs

772 lines
30 KiB
C#
Raw Normal View History

2023-09-15 16:12:13 +08:00
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using TapTap.Common;
using UnityEngine;
using UnityEngine.UI;
using ZXing;
using ZXing.Common;
using ZXing.QrCode.Internal;
using Debug = UnityEngine.Debug;
using Random = System.Random;
namespace TapTap.Login.Internal
{
internal class QRCodeWindow : UIElement
{
// // \ue90f\ue91c\ue911\ue91a\ue901\ue902\ue903\ue90c 您已取消此次登录
// private static readonly string TEXT_CANCEL_LOGIN = "";
//
// // \ue910\ue91f\ue91b\ue914\ue90d 请重新扫码
// private static readonly string TEXT_PLEASE_RESCRAN = "";
//
// // \ue914\ue90d\ue900\ue907 扫码成功
// private static readonly string TEXT_SCAN_SUCCESS = "";
//
// // \ue910\ue91e\ue917\ue90a\ue915\ue912\ue913 请在手机上确认
// private static readonly string TEXT_CONFIRM_ON_PHONE = "";
public RawImage QRCodeRawImage;
public Text StatusText;
public Text SubStatusText;
public Image RefreshImage;
public Button RefreshButton;
public Button CloseButton;
public Button WebLoginButton;
private string clientId;
private string[] permissions;
private string deviceCode;
private long expireAt = 0;
private long lastCheckAt = 0;
private long interval = 5;
private Net net;
private HttpListener _httpListener;
private long lastWebRequestTime = 0;
private string webRequestMode = "";
public override Dictionary<string, object> Extra
{
get => extra;
set
{
extra = value;
if (extra != null)
{
if (extra.ContainsKey("client_id"))
{
clientId = extra["client_id"] as string;
permissions = extra["permissions"] as string[];
}
}
}
}
private void SetObjectText(string parentName, string objectName, string text)
{
try
{
var targetTransform = transform;
if (!string.IsNullOrEmpty(parentName))
{
targetTransform = transform.Find(parentName).gameObject.transform;
}
var targetObject = targetTransform.Find(objectName).gameObject;
var gameText = targetObject.GetComponent<Text>();
gameText.text = text;
}
catch (Exception e)
{
Debug.Log("SetObjectText fail" + parentName + " " + objectName);
Debug.Log("SetObjectText fail\n" + e);
}
}
void Awake()
{
var qrImageObject = transform.Find("QRImage").gameObject;
QRCodeRawImage = qrImageObject.GetComponent<RawImage>();
var statusObject = transform.Find("Status").gameObject;
StatusText = statusObject.GetComponent<Text>();
var subStatusObject = transform.Find("SubStatus").gameObject;
SubStatusText = subStatusObject.GetComponent<Text>();
var refreshObject = transform.Find("Refresh").gameObject;
RefreshImage = refreshObject.GetComponent<Image>();
var refreshButtonObject = refreshObject.transform.Find("RefreshButton").gameObject;
RefreshButton = refreshButtonObject.GetComponent<Button>();
var closeObject = transform.Find("Close").gameObject;
CloseButton = closeObject.GetComponent<Button>();
var webLoginButtonObject = transform.Find("WebLoginButton").gameObject;
WebLoginButton = webLoginButtonObject.GetComponent<Button>();
WebLoginButton.onClick.AddListener(() =>
{
var now = GetCurrentTime();
if (now - lastWebRequestTime > 1)
{
StartWebLogin();
}
lastWebRequestTime = now;
});
RefreshImage.gameObject.SetActive(false);
RefreshButton.onClick.AddListener(RefreshCode);
SubStatusText.gameObject.SetActive(false);
StatusText.gameObject.SetActive(false);
CloseButton.onClick.AddListener(Close);
SetObjectText("TitleContent", "TitleUse", LoginLanguage.GetCurrentLang().TitleUse());
SetObjectText("TitleContent", "TitleAccount", LoginLanguage.GetCurrentLang().TitleLogin());
SetObjectText("", "ScanLogin", LoginLanguage.GetCurrentLang().QrTitleLogin());
SetObjectText("", "ScanLogin", LoginLanguage.GetCurrentLang().QrTitleLogin());
SetObjectText("Notice", "Notice1", LoginLanguage.GetCurrentLang().QrNoticeUse());
SetObjectText("Notice", "Notice2", LoginLanguage.GetCurrentLang().QrNoticeClient());
SetObjectText("", "Notice3", LoginLanguage.GetCurrentLang().QrNoticeScanToLogin());
SetObjectText("", "WebLogin", LoginLanguage.GetCurrentLang().WebLogin());
SetObjectText("", "WebDescription", LoginLanguage.GetCurrentLang().WebNotice());
SetObjectText("WebLoginButton", "Text", LoginLanguage.GetCurrentLang().WebButtonJumpToWeb());
refreshButtonObject.transform.Find("Text").gameObject.GetComponent<Text>().text =
LoginLanguage.GetCurrentLang().QrRefresh();
if (string.IsNullOrEmpty(LoginLanguage.GetCurrentLang().QrNoticeUse()) &&
string.IsNullOrEmpty(LoginLanguage.GetCurrentLang().QrNoticeClient())) {
GameObject logoGO = transform.Find("Notice/Logo").gameObject;
logoGO.SetActive(false);
}
}
public override void OnEnter()
{
base.OnEnter();
gameObject.AddComponent<Net>();
net = gameObject.GetComponent<Net>();
GetCode();
WebLoginRequestManager.Instance.CreateNewLoginRequest(permissions);
StartHttpServer();
}
public override void OnExit()
{
base.OnExit();
Destroy(net.gameObject);
}
private void Close()
{
if (_httpListener != null)
{
try
{
_httpListener.Stop();
}
catch (Exception)
{
// ignored
}
}
OnCallback(UIManager.RESULT_CLOSE, "Close button clicked.");
GetUIManager().Pop();
}
private void StartHttpServer()
{
if (_httpListener == null)
{
_httpListener = new HttpListener();
_httpListener.Prefixes.Add(WebLoginRequestManager.Instance.GetCurrentRequest().GetRedirectHost());
}
try
{
_httpListener.Start();
}
catch (Exception)
{
Debug.Log("Http listener start fail, maybe is already started");
}
}
private void StartWebLogin()
{
ShowStatus("", "");
Application.OpenURL(WebLoginRequestManager.Instance.GetCurrentRequest().GetWebLoginUrl());
// Waits for the OAuth authorization response.
_ = StartWaitForRequest();
// _httpListener.BeginGetContext(HandleHttpRequest, _httpListener);
}
private async Task StartWaitForRequest()
{
HttpListenerContext context = await _httpListener.GetContextAsync();
Debug.Log(context.Request.RawUrl);
if (context.Request.HttpMethod == "OPTIONS") {
// 跨域请求
context.Response.StatusCode = 200;
context.Response.Close();
} else {
webRequestMode = "";
var resultCode = UrlCheck(context.Request.RawUrl);
CodeToTokenFromWeb(resultCode);
var response = context.Response;
var buffer = Array.Empty<byte>();
if (!string.IsNullOrEmpty(resultCode)) {
switch (webRequestMode) {
case "gif": {
response.StatusCode = 200;
response.AppendHeader("Access-Control-Allow-Origin", "*");
response.ContentType = "image/gif";
const string gif = "R0lGODlhAQABAIABAAAAAP///yH5BAEAAAEALAAAAAABAAEAAAICTAEAOw==";
try {
buffer = Convert.FromBase64String(gif);
} catch (Exception) {
buffer = Encoding.UTF8.GetBytes("ok");
}
break;
}
case "redirect":
response.Redirect(CodeUtil.GetTapTapOAuthRedirectUri());
break;
default:
response.StatusCode = 200;
response.AppendHeader("Access-Control-Allow-Origin", "*");
response.ContentType = "text/plain";
buffer = Encoding.UTF8.GetBytes("ok");
break;
}
} else {
response.StatusCode = 404;
}
response.ContentLength64 = buffer.Length;
var responseOutput = response.OutputStream;
await responseOutput.WriteAsync(buffer, 0, buffer.Length)
.ContinueWith((task) => { responseOutput.Close(); });
}
_ = StartWaitForRequest();
}
private string UrlCheck(string url)
{
// check url
if (string.IsNullOrEmpty(url) || !url.Contains(CodeUtil.GetTapTapOAuthPrefix()))
{
return null;
}
var urlParams = url.Split('?');
if (urlParams.Length <= 1)
{
return null;
}
var args = urlParams[1].Split('&');
var dic = args.Select(arg => arg.Split('=')).ToDictionary(kv => kv[0], kv => kv[1]);
var code = dic.ContainsKey("code") ? dic["code"] : "";
var state = dic.ContainsKey("state") ? dic["state"] : "";
webRequestMode = dic.ContainsKey("mode") ? dic["mode"] : "";
if (string.IsNullOrEmpty(code) || string.IsNullOrEmpty(state))
{
return null;
}
return !state.Equals(WebLoginRequestManager.Instance.GetCurrentRequest().GetState()) ? null : code;
}
private void CodeToTokenFromWeb(string code)
{
if (string.IsNullOrEmpty(code)) return;
ShowStatus(LoginLanguage.GetCurrentLang().WebNoticeLogin(), "");
// url is ok, go to get token
var formParams = new Dictionary<string, string>
{
{ "client_id", clientId },
{ "grant_type", "authorization_code" },
{ "secret_type", "hmac-sha-1" },
{ "code", code },
{ "redirect_uri", WebLoginRequestManager.Instance.GetCurrentRequest().GetRedirectUri() },
{ "code_verifier", WebLoginRequestManager.Instance.GetCodeVerifier() }
};
net.PostAsync(TapTapSdk.CurrentRegion.TokenUrl(), null, formParams, (result =>
{
if (Json.Deserialize(result) is Dictionary<string, object> resultDict &&
resultDict.ContainsKey("success") && (bool)resultDict["success"])
{
if (resultDict["data"] is Dictionary<string, object> dataDict)
{
var token = new AccessToken
{
kid = dataDict["kid"] as string,
macKey = dataDict["mac_key"] as string,
accessToken = dataDict["access_token"] as string,
tokenType = dataDict["token_type"] as string,
macAlgorithm = dataDict["mac_algorithm"] as string
};
// stop http server when success get token
_httpListener.Stop();
Debug.Log("HTTP server stopped.");
GetProfile(token);
return;
}
}
ShowStatus(LoginLanguage.GetCurrentLang().WebNoticeFail(),
LoginLanguage.GetCurrentLang().WebNoticeFail2());
Debug.Log("CodeToTokenFromWeb success with fail data \n" + result);
}), ((error, msg) =>
{
Debug.Log("CodeToTokenFromWeb fail \n" + msg);
if (Json.Deserialize(msg) is Dictionary<string, object> resultDict &&
resultDict.ContainsKey("data"))
{
if (resultDict["data"] is Dictionary<string, object> dataDict &&
dataDict.ContainsKey("error"))
{
var errorMsg = dataDict["error"] as string;
var errorDesc = dataDict["error_description"] as string;
ShowStatus(LoginLanguage.GetCurrentLang().WebNoticeFail(),
LoginLanguage.GetCurrentLang().WebNoticeFail2());
return;
}
}
ShowStatus(LoginLanguage.GetCurrentLang().WebNoticeFail(),
LoginLanguage.GetCurrentLang().WebNoticeFail2());
}));
}
private void RefreshCode()
{
StatusText.gameObject.SetActive(false);
SubStatusText.gameObject.SetActive(false);
GetCode();
}
private void GetCode()
{
RefreshImage.gameObject.SetActive(false);
var formParams = new Dictionary<string, string>
{
{ "client_id", clientId },
{ "response_type", "device_code" },
{ "scope", string.Join(",", permissions) },
{ "version", TapTapSdk.Version },
{ "platform", "unity" },
{ "info", "{\"device_id\":\"" + SystemInfo.deviceModel + "\"}" }
};
net.PostAsync(TapTapSdk.CurrentRegion.CodeUrl(),
null,
formParams,
(result) =>
{
if (Json.Deserialize(result) is Dictionary<string, object> resultDict &&
resultDict.ContainsKey("success") && (bool)resultDict["success"])
{
if (resultDict["data"] is Dictionary<string, object> dataDict &&
dataDict.ContainsKey("qrcode_url"))
{
var qrCodeUrl = dataDict["qrcode_url"] as string;
EncodeQrImage(qrCodeUrl, 309, 309);
if (dataDict.ContainsKey("device_code"))
{
deviceCode = dataDict["device_code"] as string;
}
if (dataDict.ContainsKey("expires_in"))
{
var expireIn = (long)dataDict["expires_in"];
var ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0);
expireAt = Convert.ToInt64(ts.TotalSeconds) + expireIn;
}
if (dataDict.ContainsKey("interval"))
{
interval = (long)dataDict["interval"];
}
StartCheck();
Debug.Log("QRCODE Get");
}
else
{
//StatusText.text = "获取二维码失败";
ShowQrRefresh();
Debug.Log("QRCODE Get Fail 1");
}
}
else
{
//StatusText.text = "获取二维码失败";
ShowQrRefresh();
Debug.Log("QRCODE Get Fail 2");
}
},
(error, msg) =>
{
//StatusText.text = "获取二维码失败";
ShowQrRefresh();
Debug.Log("QRCODE Get Fail 3");
}
);
}
private void EncodeQrImage(string content, int width, int height)
{
var writer = new MultiFormatWriter();
var hints = new Dictionary<EncodeHintType, object>
{
{ EncodeHintType.CHARACTER_SET, "UTF-8" },
//hints.Add(EncodeHintType.MARGIN, 0);
{ EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M }
};
var bitMatrix = writer.encode(content, BarcodeFormat.QR_CODE, width, height, hints);
bitMatrix = DeleteWhite(bitMatrix);
var w = bitMatrix.Width;
var h = bitMatrix.Height;
var texture = new Texture2D(w, h);
for (var x = 0; x < h; x++)
{
for (var y = 0; y < w; y++)
{
texture.SetPixel(y, x, bitMatrix[x, y] ? Color.black : Color.white);
}
}
texture.Apply();
texture.filterMode = FilterMode.Point;
QRCodeRawImage.texture = texture;
}
private static BitMatrix DeleteWhite(BitMatrix matrix)
{
var rec = matrix.getEnclosingRectangle();
var resWidth = rec[2];
var resHeight = rec[3];
var resMatrix = new BitMatrix(resWidth, resHeight);
resMatrix.clear();
for (var i = 0; i < resWidth; i++)
{
for (var j = 0; j < resHeight; j++)
{
if (matrix[i + rec[0], j + rec[1]])
{
resMatrix.flip(i, j);
}
}
}
return resMatrix;
}
private void StartCheck()
{
StartCoroutine(nameof(AutoCheck));
}
private void StopCheck()
{
StopCoroutine(nameof(AutoCheck));
}
private IEnumerator AutoCheck()
{
while (true)
{
yield return new WaitForSeconds(0.5f);
var ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0);
var now = Convert.ToInt64(ts.TotalSeconds);
//Debug.Log("Now: " + now + " ExpireAt: " + expireAt + " Remain: " + (expireAt - now));
if (now > expireAt)
{
ShowQrRefresh();
StopCheck();
break;
}
if (now <= lastCheckAt + interval) continue;
var wait = true;
var stop = false;
var formParams = new Dictionary<string, string>
{
{ "grant_type", "device_token" },
{ "client_id", clientId },
{ "secret_type", "hmac-sha-1" },
{ "code", deviceCode },
{ "version", "1.0" },
{ "platform", "unity" },
{ "info", "{\"device_id\":\"" + SystemInfo.deviceModel + "\"}" }
};
net.PostAsync(TapTapSdk.CurrentRegion.TokenUrl(),
null,
formParams,
(result) =>
{
//StatusText.text = "";
if (Json.Deserialize(result) is Dictionary<string, object> resultDict &&
resultDict.ContainsKey("success") && (bool)resultDict["success"])
{
//StatusText.text = "扫码成功";
//SubStatusText.gameObject.SetActive(false);
if (resultDict["data"] is Dictionary<string, object> dataDict)
{
var token = new AccessToken
{
kid = dataDict["kid"] as string,
macKey = dataDict["mac_key"] as string,
accessToken = dataDict["access_token"] as string,
tokenType = dataDict["token_type"] as string,
macAlgorithm = dataDict["mac_algorithm"] as string
};
GetProfile(token);
}
else
{
ShowQrRefresh();
}
}
else
{
//StatusText.text = "扫描二维码失败";
ShowQrRefresh();
}
wait = false;
stop = true;
StopCheck();
},
(error, msg) =>
{
wait = false;
if (Json.Deserialize(msg) is Dictionary<string, object> resultDict &&
resultDict.ContainsKey("data"))
{
if (resultDict["data"] is Dictionary<string, object> dataDict &&
dataDict.ContainsKey("error"))
{
var errorMsg = dataDict["error"] as string;
// Debug.Log("QRCODE FAIL " + errorMsg);
switch (errorMsg)
{
case "authorization_pending":
//StatusText.text = "扫码二维码登录";
break;
case "authorization_waiting":
ShowStatus(LoginLanguage.GetCurrentLang().QrnNoticeSuccess(),
LoginLanguage.GetCurrentLang().QrnNoticeSuccess2());
break;
case "access_denied":
ShowStatus(LoginLanguage.GetCurrentLang().QrNoticeCancel(),
LoginLanguage.GetCurrentLang().QrNoticeCancel2());
stop = true;
StopCheck();
GetCode();
break;
case "invalid_grant":
ShowQrRefresh();
stop = true;
StopCheck();
break;
case "slow_down":
break;
default:
ShowQrRefresh();
stop = true;
StopCheck();
break;
}
return;
}
}
// if false then refresh
ShowQrRefresh();
stop = true;
StopCheck();
}
);
while (wait)
{
yield return new WaitForSeconds(0.5f);
}
ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0);
lastCheckAt = Convert.ToInt64(ts.TotalSeconds);
if (stop) break;
}
}
private void GetProfile(AccessToken accessToken, int timestamp = 0)
{
var url = TapTapSdk.CurrentRegion.ProfileUrl() + clientId;
var ts = timestamp;
if (ts == 0)
{
var dt = DateTime.UtcNow - new DateTime(1970, 1, 1);
ts = (int)dt.TotalSeconds;
}
var uri = new Uri(url);
var sign = "MAC " + GetAuthorizationHeader(accessToken.kid,
accessToken.macKey,
accessToken.macAlgorithm,
"GET",
uri.PathAndQuery,
uri.Host,
"443", ts);
net.GetAsync(url,
sign,
null,
(result) =>
{
try
{
var tokenDict = new Dictionary<string, string>
{
{ "kid", accessToken.kid },
{ "mac_key", accessToken.macKey },
{ "access_token", accessToken.accessToken },
{ "token_type", accessToken.tokenType },
{ "mac_algorithm", accessToken.macAlgorithm }
};
if (Json.Deserialize(result) is Dictionary<string, object> resultDict &&
resultDict.ContainsKey("success") && (bool)resultDict["success"])
{
var dataDict = resultDict["data"] as Dictionary<string, object>;
DataStorage.SaveString("taptapsdk_accesstoken", Json.Serialize(tokenDict));
DataStorage.SaveString("taptapsdk_profile", Json.Serialize(dataDict));
OnCallback(UIManager.RESULT_SUCCESS, accessToken);
GetUIManager().Pop();
}
else
{
OnCallback(UIManager.RESULT_FAILED, "Get profile error");
GetUIManager().Pop();
}
}
catch (Exception)
{
OnCallback(UIManager.RESULT_FAILED, "Get profile error");
GetUIManager().Pop();
}
},
(error, msg) =>
{
if (timestamp == 0 && !string.IsNullOrEmpty(msg) && msg.Contains("invalid_time") &&
msg.Contains("now"))
{
try
{
if (!(Json.Deserialize(msg) is Dictionary<string, object> json)) return;
var now = (int)(long)json["now"];
GetProfile(accessToken, now);
return;
}
catch (Exception)
{
// ignored
}
}
OnCallback(UIManager.RESULT_FAILED, msg);
GetUIManager().Pop();
});
}
private static string GetAuthorizationHeader(string kid,
string macKey,
string macAlgorithm,
string method,
string uri,
string host,
string port,
int timestamp)
{
var nonce = new Random().Next().ToString();
var normalizedString = $"{timestamp}\n{nonce}\n{method}\n{uri}\n{host}\n{port}\n\n";
HashAlgorithm hashGenerator;
switch (macAlgorithm)
{
case "hmac-sha-256":
hashGenerator = new HMACSHA256(Encoding.ASCII.GetBytes(macKey));
break;
case "hmac-sha-1":
hashGenerator = new HMACSHA1(Encoding.ASCII.GetBytes(macKey));
break;
default:
throw new InvalidOperationException("Unsupported MAC algorithm");
}
var hash = Convert.ToBase64String(hashGenerator.ComputeHash(Encoding.ASCII.GetBytes(normalizedString)));
var authorizationHeader = new StringBuilder();
authorizationHeader.AppendFormat(@"id=""{0}"",ts=""{1}"",nonce=""{2}"",mac=""{3}""",
kid, timestamp, nonce, hash);
return authorizationHeader.ToString();
}
private void ShowQrRefresh()
{
RefreshImage.gameObject.SetActive(true);
SubStatusText.gameObject.SetActive(false);
StatusText.gameObject.SetActive(false);
}
private void ShowStatus(string statusText, string subStatusText)
{
StatusText.text = statusText;
SubStatusText.text = subStatusText;
if (!string.IsNullOrEmpty(statusText))
{
StatusText.gameObject.SetActive(true);
}
if (!string.IsNullOrEmpty(subStatusText))
{
SubStatusText.gameObject.SetActive(true);
}
}
private static long GetCurrentTime()
{
var ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0);
return Convert.ToInt64(ts.TotalSeconds);
}
}
}