/*
* This file is part of Socialvoid.NET Project (https://github.com/Intellivoid/Socialvoid.NET).
* Copyright (c) 2021 Socialvoid.NET Authors.
*
* This library 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, version 3.
*
* This library 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 source code of library.
* If not, see .
*/
using System;
using System.Text;
using System.Net.Http;
using System.IO;
using Socialvoid.Security;
using Socialvoid.Security.Otp;
using Socialvoid.JObjects;
using Socialvoid.Errors.ServerErrors;
using Socialvoid.Errors.AuthenticationErrors;
using Socialvoid.Errors.ValidationErrors;
using MType = System.Net.Http.Headers.MediaTypeHeaderValue;
namespace Socialvoid.Client
{
///
/// Socialvoid client.
/// since: v0.0.0
///
public abstract class SocialvoidClient
{
//-------------------------------------------------
#region Constant's Region
///
/// the content type of all of our http requests.
/// since: v0.0.0
///
protected internal const string ContentType = "application/json-rpc";
///
/// the username key in jsonrpc request params.
/// since: v0.0.0
///
protected const string UsernameKey = "username";
///
/// the password key in jsonrpc request params.
/// since: v0.0.0
///
protected const string PasswordKey = "password";
///
/// the otp key in jsonrpc request params.
/// since: v0.0.0
///
protected const string OtpKey = "otp";
///
/// the public hash key in jsonrpc request params.
/// since: v0.0.0
///
protected const string PublicHashKey = "public_hash";
///
/// the private hash key in jsonrpc request params.
/// since: v0.0.0
///
protected const string PrivateHashKey = "private_hash";
///
/// the platform key in jsonrpc request params.
/// since: v0.0.0
///
protected const string PlatformKey = "platform";
///
/// the name key in jsonrpc request params.
/// since: v0.0.0
///
protected const string NameKey = "name";
///
/// the version key in jsonrpc request params.
/// since: v0.0.0
///
protected const string VersionKey = "version";
///
/// authenticate_user method value.
/// since: v0.0.0
///
protected const string AuthenticateUserMethod = "session.authenticate_user";
///
/// authenticate_user method value.
/// since: v0.0.0
///
protected const string CreateSessionMethod = "session.create";
///
/// authenticate_user method value.
/// since: v0.0.0
///
protected const string SessionIDKey = "session_identification";
#endregion
//-------------------------------------------------
#region static Properties Region
// some members here
#endregion
//-------------------------------------------------
#region Properties Region
///
/// The public hash of the client.
/// since: v0.0.0
///
public string PublicHash { get; protected set; }
///
/// The private hash of the client.
/// since: v0.0.0
///
public string PrivateHash { get; protected set; }
///
/// The platform of the client.
/// since: v0.0.0
///
public string Platform { get; protected set; }
///
/// The name of the client.
/// since: v0.0.0
///
public string ClientName { get; protected set; }
///
/// The version of the client.
/// since: v0.0.0
///
public string Version { get; protected set; }
///
///
/// since: v0.0.0
///
public HttpClient HttpClient { get; protected set; }
#endregion
//-------------------------------------------------
#region static field's Region
// some members here
#endregion
//-------------------------------------------------
#region field's Region
///
/// the endpoint url of socialvoid servers.
/// since: v0.0.0
///
protected internal string _endpoint = "http://socialvoid.qlg1.com:5601/";
///
/// Session object of this client.
/// since: v0.0.0
///
protected internal SessionEstablished _session;
///
/// if the client should send an otp answer in the next request,
/// this field should be set to true.
/// since: v0.0.0
///
protected bool _should_otp;
///
/// the last otp value of the client which should be sent in the
/// next jsonrpc request.
/// since: v0.0.0
///
protected string _otp;
///
/// since: v0.0.0
///
protected readonly MType _contentTypeValue = MType.Parse(ContentType);
#endregion
//-------------------------------------------------
#region static event field's Region
// some members here
#endregion
//-------------------------------------------------
#region event field's Region
// some members here
#endregion
//-------------------------------------------------
#region Constructor's Region
///
/// Creates a new instance of the class.
/// since: v0.0.0
///
///
/// the public hash of the client.
///
///
/// the private hash of the client.
///
///
/// the platform of the client.
///
///
/// the client name.
///
///
/// the version of the client.
///
protected SocialvoidClient(string publicHash, string privateHash,
string platform, string name, string version)
{
PublicHash = publicHash;
PrivateHash = privateHash;
Platform = platform;
ClientName = name;
Version = version;
HttpClient = new();
}
#endregion
//-------------------------------------------------
#region Destructor's Region
// some members here
#endregion
//-------------------------------------------------
#region Initialize Method's Region
// some methods here
#endregion
//-------------------------------------------------
#region Graphical Method's Region
// some methods here
#endregion
//-------------------------------------------------
#region event Method's Region
// some methods here
#endregion
//-------------------------------------------------
#region overrided Method's Region
// some methods here
#endregion
//-------------------------------------------------
#region ordinary Method's Region
///
/// CreateSession method (session.create), establishes a new session
/// to the network.
/// Please do notice that new and unauthenticated sessions
/// expires after 10 minutes of inactivity, authenticating to the session
/// will increase the expiration time to 72 hours of inactivity. This timer
/// is reset whenever the session is used in one way or another.
/// since: v0.0.0
///
///
/// Thrown if the server encounters an internal error.
///
///
/// Thrown if the parameter passed as client name is not valid.
///
///
/// Thrown if the parameter passed as public hash is not valid.
///
///
/// Thrown if the parameter passed as private hash is not valid.
///
///
/// Thrown if the parameter passed as platform is not valid.
///
///
/// Thrown if the parameter passed as version is not valid.
///
public virtual SessionEstablished CreateSession()
{
JArgs args = new(){
{PublicHashKey, PublicHash},
{PrivateHashKey, PrivateHash},
{PlatformKey, Platform},
{NameKey, ClientName},
{VersionKey, Version},
};
var request = GetRpcRequest(CreateSessionMethod, args);
var message = new HttpRequestMessage(HttpMethod.Post, _endpoint);
message.Content = SerializeContent(request);
message.Content.Headers.ContentType = _contentTypeValue;
var jresp = ParseContent(message);
if (!string.IsNullOrEmpty(jresp.Result.ChallengeSecret))
{
_should_otp = true;
_otp = GetChallengeAnswer(jresp.Result.ChallengeSecret);
// set challenege secret to null to avoid sending it again.
// this will avoid future conflicts in using old challenge secret.
jresp.Result.ChallengeSecret = null;
}
_session = jresp.Result;
return _session;
}
///
/// AuthenticateUser method (session.authenticate_user),
/// Authenticates a user via a Username and Password combination.
/// Optionally two-factor authentication if the account has enabled it.
/// Once authenticated, the session will be able to access methods that
/// requires authentication and preform operations as the authenticated
/// user.
/// since: v0.0.0
///
///
/// The Session Identification object.
///
///
/// The username of the user to authenticate to.
///
///
/// The password used to authenticate to this account.
///
///
/// The optional one-time password used to authenticate to this account.
/// It will be ignored by server if empty or larger than 64 characters.
///
///
/// Thrown if the server encounters an internal error.
///
///
/// Thrown if the authentication fails.
///
///
/// Thrown if the user is already authenticated.
///
///
/// Thrown if the user is not able to authenticate.
///
///
/// Thrown if the session challenge answer is invalid.
///
///
/// Thrown if the username or password is incorrect.
///
///
/// Thrown if the two-factor authentication code is incorrect.
///
///
/// Thrown if the user is not authenticated and the private access token is required.
///
///
/// Thrown if the session has expired.
///
///
/// Thrown if two-factor authentication is required.
///
public virtual void AuthenticateUser(string username, string password,
string otp = null, SessionIdentification sessionID = null)
{
if (sessionID == null && _session != null)
{
sessionID = new()
{
SessionID = _session.SessionID,
ClientPublicHash = PublicHash
};
}
JArgs args = new(){
{UsernameKey, username},
{PasswordKey, password},
{SessionIDKey, sessionID},
};
// check if the passed-by otp argument is valid or not.
// if yes, ignore _otp field and use user's specified otp value.
// otherwise check for _should_otp and see if we should send an
// otp answer or not.
if (IsOtpValid(otp))
{
args.Add(OtpKey, otp);
sessionID.ChallengeAnswer = otp;
}
else if (_should_otp && IsOtpValid(_otp))
{
// after adding otp answer to args, don't forget to set
// _should_otp to false (and _otp to null).
args.Add(OtpKey, _otp);
sessionID.ChallengeAnswer = _otp;
_should_otp = false;
_otp = null;
}
var request = GetRpcRequest(AuthenticateUserMethod, args);
var message = new HttpRequestMessage(HttpMethod.Post, _endpoint);
message.Content = SerializeContent(request);
message.Content.Headers.ContentType = _contentTypeValue;
var resp = HttpClient.Send(message);
var contentStr = ReadFromContent(resp.Content);
Console.WriteLine(contentStr);
}
///
/// returns a challenge's answer using the session's challenge secret.
/// since: v0.0.0
///
protected internal virtual string GetChallengeAnswer(string secret)
{
var otp = new Totp(Encoding.UTF8.GetBytes(secret));
return KeyGeneration.GetSha1(otp.ComputeTotp() + PrivateHash);;
}
#endregion
//-------------------------------------------------
#region Get Method's Region
///
/// Gets the session of this if and only
/// if you have already established a session (or has loaded it from a file);
/// oterwise it will just return a null object instead of throwing an exception.
/// since: v0.0.0
///
public virtual SessionEstablished GetSession()
{
return _session;
}
///
/// since: v0.0.0
///
///
/// the method.
///
///
/// the arguments.
///
///
/// the request ID.
///
///
/// set it to `true` if you want this request to have requestID parameter
/// set. (if the passed-by id paramater is null, this method will generate
/// a new id itself.)
///
protected internal JRequest GetRpcRequest(string method,
JArgs args = null,
Nullable id = null,
bool useID = true)
{
if (string.IsNullOrWhiteSpace(method))
{
throw new ArgumentException(
"method name in a rpc request cannot be null or empty",
nameof(method));
}
if (useID && (id == null))
{
id = DateTime.Now.Ticks;
}
return useID && id != null && id.HasValue ?
new()
{
Method = method,
Arguments = args,
ID = id.Value,
} :
new()
{
Method = method,
Arguments = args,
};
}
///
/// since: v0.0.0
///
protected StringContent SerializeContent(JRequest request)
{
return new(request.Serialize());
}
///
/// Parses the content of a as a
/// .
///
///
///
protected internal JResponse ParseContent(
HttpRequestMessage message,
bool ex = true)
where VType : class
{
if (HttpClient == null)
{
throw new InvalidOperationException("HttpClient wasn't initialized");
}
var resp = HttpClient.Send(message);
if (resp == null)
{
throw new InvalidOperationException("HttpClient.Send returned null");
}
return ParseContent(resp.Content, ex);
}
#endregion
//-------------------------------------------------
#region Set Method's Region
// some methods here
#endregion
//-------------------------------------------------
#region static Method's Region
///
/// Creates a new instance of the class.
/// since: v0.0.0
///
///
/// the public hash of the client.
///
///
/// the private hash of the client.
///
///
/// the platform of the client.
///
///
/// the client name.
///
///
/// the version of the client.
///
public static SocialvoidClient GetClient(string publicHash, string privateHash,
string platform, string name, string version)
{
if (string.IsNullOrWhiteSpace(publicHash) || publicHash.Length != 64)
{
throw new ArgumentException(
"publicHash parameter is invalid",
nameof(publicHash));
}
if (string.IsNullOrWhiteSpace(privateHash) || privateHash.Length != 64)
{
throw new ArgumentException(
"privateHash parameter is invalid",
nameof(privateHash));
}
if (string.IsNullOrWhiteSpace(platform))
{
throw new ArgumentException(
"platform cannot be null or empty",
nameof(platform));
}
if (string.IsNullOrWhiteSpace(version))
{
throw new ArgumentException(
"version cannot be null or empty",
nameof(version));
}
return new SvClient(publicHash, privateHash, platform, name, version);
}
///
/// Checks if a string can be sent as a TOTOP answer or not.
/// since: v0.0.0
///
///
/// the otp string to check.
///
///
/// true if the otp string is valid; otherwise false.
///
protected internal static bool IsOtpValid(string otp)
{
return !(string.IsNullOrWhiteSpace(otp) || otp.Length > 64);
}
///
/// reads the content of a as a string.
/// returns null if the stream cannot be read.
///
///
///
protected internal static string ReadFromContent(HttpContent content)
{
var stream = content.ReadAsStream();
if (stream == null || !stream.CanRead)
{
return null;
}
return new StreamReader(stream).ReadToEnd();
}
///
/// Parses the content of a as a
/// .
///
///
///
protected internal static JResponse ParseContent(
HttpContent content,
bool ex = true)
where VType : class
{
var jresp = JResponse.Deserialize(ReadFromContent(content));
if (ex && jresp.HasError())
{
throw jresp.GetException();
}
return jresp;
}
#endregion
//-------------------------------------------------
}
}