// (c) Copyright Ascensio System SIA 2010-2022 // // This program is a free software product. // You can redistribute it and/or modify it under the terms // of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software // Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended // to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of // any third-party rights. // // This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty // of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see // the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html // // You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021. // // The interactive user interfaces in modified source and object code versions of the Program must // display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3. // // Pursuant to Section 7(b) of the License you must retain the original Product logo when // distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under // trademark law for use of our trademarks. // // All the Product's GUI elements, including illustrations and icon sets, as well as technical writing // content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0 // International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode namespace ASC.Web.Core.Files { /// /// Class service connector /// public static class DocumentService { /// /// Timeout to request conversion /// public static readonly int Timeout = 120000; //public static int Timeout = Convert.ToInt32(ConfigurationManagerExtension.AppSettings["files.docservice.timeout"] ?? "120000"); /// /// Number of tries request conversion /// public static readonly int MaxTry = 3; /// /// Translation key to a supported form. /// /// Expected key /// Supported key public static string GenerateRevisionId(string expectedKey) { expectedKey ??= ""; const int maxLength = 128; using var sha256 = SHA256.Create(); if (expectedKey.Length > maxLength) expectedKey = Convert.ToBase64String(sha256.ComputeHash(Encoding.UTF8.GetBytes(expectedKey))); var key = Regex.Replace(expectedKey, "[^0-9a-zA-Z_]", "_"); return key.Substring(key.Length - Math.Min(key.Length, maxLength)); } /// /// The method is to convert the file to the required format /// /// Url to the service of conversion /// Uri for the document to convert /// Document extension /// Extension to which to convert /// Key for caching on service /// Password /// Thumbnail settings /// Perform conversions asynchronously /// Secret key to generate the token /// Uri to the converted document /// The percentage of completion of conversion /// /// string convertedDocumentUri; /// GetConvertedUri("http://helpcenter.teamlab.com/content/GettingStarted.pdf", ".pdf", ".docx", "469971047", false, out convertedDocumentUri); /// /// /// public static Task<(int ResultPercent, string ConvertedDocumentUri)> GetConvertedUriAsync( FileUtility fileUtility, string documentConverterUrl, string documentUri, string fromExtension, string toExtension, string documentRevisionId, string password, ThumbnailData thumbnail, SpreadsheetLayout spreadsheetLayout, bool isAsync, string signatureSecret, IHttpClientFactory clientFactory) { fromExtension = string.IsNullOrEmpty(fromExtension) ? Path.GetExtension(documentUri) : fromExtension; if (string.IsNullOrEmpty(fromExtension)) throw new ArgumentNullException(nameof(fromExtension), "Document's extension for conversion is not known"); if (string.IsNullOrEmpty(toExtension)) throw new ArgumentNullException(nameof(toExtension), "Extension for conversion is not known"); return InternalGetConvertedUriAsync(fileUtility, documentConverterUrl, documentUri, fromExtension, toExtension, documentRevisionId, password, thumbnail, spreadsheetLayout, isAsync, signatureSecret, clientFactory); } private static async Task<(int ResultPercent, string ConvertedDocumentUri)> InternalGetConvertedUriAsync( FileUtility fileUtility, string documentConverterUrl, string documentUri, string fromExtension, string toExtension, string documentRevisionId, string password, ThumbnailData thumbnail, SpreadsheetLayout spreadsheetLayout, bool isAsync, string signatureSecret, IHttpClientFactory clientFactory) { var title = Path.GetFileName(documentUri ?? ""); title = string.IsNullOrEmpty(title) || title.Contains('?') ? Guid.NewGuid().ToString() : title; documentRevisionId = string.IsNullOrEmpty(documentRevisionId) ? documentUri : documentRevisionId; documentRevisionId = GenerateRevisionId(documentRevisionId); var request = new HttpRequestMessage(); request.RequestUri = new Uri(documentConverterUrl); request.Method = HttpMethod.Post; request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); var httpClient = clientFactory.CreateClient(); httpClient.Timeout = TimeSpan.FromMilliseconds(Timeout); var body = new ConvertionBody { Async = isAsync, FileType = fromExtension.Trim('.'), Key = documentRevisionId, OutputType = toExtension.Trim('.'), Title = title, Thumbnail = thumbnail, SpreadsheetLayout = spreadsheetLayout, Url = documentUri, }; if (!string.IsNullOrEmpty(password)) { body.Password = password; } if (!string.IsNullOrEmpty(signatureSecret)) { var payload = new Dictionary { { "payload", body } }; var token = JsonWebToken.Encode(payload, signatureSecret); //todo: remove old scheme request.Headers.Add(fileUtility.SignatureHeader, "Bearer " + token); token = JsonWebToken.Encode(body, signatureSecret); body.Token = token; } var bodyString = System.Text.Json.JsonSerializer.Serialize(body, new System.Text.Json.JsonSerializerOptions() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }); request.Content = new StringContent(bodyString, Encoding.UTF8, "application/json"); string dataResponse; HttpResponseMessage response = null; Stream responseStream = null; try { var countTry = 0; while (countTry < MaxTry) { try { countTry++; response = await httpClient.SendAsync(request); responseStream = await response.Content.ReadAsStreamAsync(); break; } catch (HttpRequestException ex) { throw new HttpException((int)HttpStatusCode.BadRequest, ex.Message, ex); } } if (countTry == MaxTry) { throw new HttpRequestException("Timeout"); } if (responseStream == null) throw new WebException("Could not get an answer"); using var reader = new StreamReader(responseStream); dataResponse = await reader.ReadToEndAsync(); } finally { if (responseStream != null) responseStream.Dispose(); if (response != null) response.Dispose(); } return GetResponseUri(dataResponse); } /// /// Request to Document Server with command /// /// Url to the command service /// Name of method /// Key for caching on service, whose used in editor /// Url to the callback handler /// users id for drop /// file meta data for update /// Secret key to generate the token /// server version /// Response public static async Task CommandRequestAsync(FileUtility fileUtility, string documentTrackerUrl, CommandMethod method, string documentRevisionId, string callbackUrl, string[] users, MetaData meta, string signatureSecret, IHttpClientFactory clientFactory) { var request = new HttpRequestMessage(); request.RequestUri = new Uri(documentTrackerUrl); request.Method = HttpMethod.Post; var httpClient = clientFactory.CreateClient(); httpClient.Timeout = TimeSpan.FromMilliseconds(Timeout); var body = new CommandBody { Command = method, Key = documentRevisionId, }; if (!string.IsNullOrEmpty(callbackUrl)) body.Callback = callbackUrl; if (users != null && users.Length > 0) body.Users = users; if (meta != null) body.Meta = meta; if (!string.IsNullOrEmpty(signatureSecret)) { var payload = new Dictionary { { "payload", body } }; var token = JsonWebToken.Encode(payload, signatureSecret); //todo: remove old scheme request.Headers.Add(fileUtility.SignatureHeader, "Bearer " + token); token = JsonWebToken.Encode(body, signatureSecret); body.Token = token; } var bodyString = System.Text.Json.JsonSerializer.Serialize(body, new System.Text.Json.JsonSerializerOptions() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }); request.Content = new StringContent(bodyString, Encoding.UTF8, "application/json"); string dataResponse; using (var response = await httpClient.SendAsync(request)) using (var stream = await response.Content.ReadAsStreamAsync()) { if (stream == null) throw new Exception("Response is null"); using var reader = new StreamReader(stream); dataResponse = await reader.ReadToEndAsync(); } try { var commandResponse = JsonConvert.DeserializeObject(dataResponse); return commandResponse; } catch (Exception ex) { return new CommandResponse { Error = CommandResponse.ErrorTypes.ParseError, ErrorString = ex.Message }; } } public static Task<(string DocBuilderKey, Dictionary Urls)> DocbuilderRequestAsync( FileUtility fileUtility, string docbuilderUrl, string requestKey, string scriptUrl, bool isAsync, string signatureSecret, IHttpClientFactory clientFactory) { ArgumentNullOrEmptyException.ThrowIfNullOrEmpty(docbuilderUrl); if (string.IsNullOrEmpty(requestKey) && string.IsNullOrEmpty(scriptUrl)) throw new ArgumentException("requestKey or inputScript is empty"); return InternalDocbuilderRequestAsync(fileUtility, docbuilderUrl, requestKey, scriptUrl, isAsync, signatureSecret, clientFactory); } private static async Task<(string DocBuilderKey, Dictionary Urls)> InternalDocbuilderRequestAsync( FileUtility fileUtility, string docbuilderUrl, string requestKey, string scriptUrl, bool isAsync, string signatureSecret, IHttpClientFactory clientFactory) { var request = new HttpRequestMessage(); request.RequestUri = new Uri(docbuilderUrl); request.Method = HttpMethod.Post; var httpClient = clientFactory.CreateClient(); httpClient.Timeout = TimeSpan.FromMilliseconds(Timeout); var body = new BuilderBody { Async = isAsync, Key = requestKey, Url = scriptUrl }; if (!string.IsNullOrEmpty(signatureSecret)) { var payload = new Dictionary { { "payload", body } }; var token = JsonWebToken.Encode(payload, signatureSecret); //todo: remove old scheme request.Headers.Add(fileUtility.SignatureHeader, "Bearer " + token); token = JsonWebToken.Encode(body, signatureSecret); body.Token = token; } var bodyString = System.Text.Json.JsonSerializer.Serialize(body, new System.Text.Json.JsonSerializerOptions() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }); request.Content = new StringContent(bodyString, Encoding.UTF8, "application/json"); string dataResponse = null; using (var response = await httpClient.SendAsync(request)) using (var responseStream = await response.Content.ReadAsStreamAsync()) { if (responseStream != null) { using var reader = new StreamReader(responseStream); dataResponse = await reader.ReadToEndAsync(); } } if (string.IsNullOrEmpty(dataResponse)) throw new Exception("Invalid response"); var responseFromService = JObject.Parse(dataResponse); if (responseFromService == null) throw new Exception("Invalid answer format"); var errorElement = responseFromService.Value("error"); if (!string.IsNullOrEmpty(errorElement)) { DocumentServiceException.ProcessResponseError(errorElement); } var isEnd = responseFromService.Value("end"); Dictionary urls = null; if (isEnd) { IDictionary rates = (JObject)responseFromService["urls"]; urls = rates.ToDictionary(pair => pair.Key, pair => pair.Value.ToString()); } return (responseFromService.Value("key"), urls); } public static Task HealthcheckRequestAsync(string healthcheckUrl, IHttpClientFactory clientFactory) { ArgumentNullOrEmptyException.ThrowIfNullOrEmpty(healthcheckUrl); return InternalHealthcheckRequestAsync(healthcheckUrl, clientFactory); } private static async Task InternalHealthcheckRequestAsync(string healthcheckUrl, IHttpClientFactory clientFactory) { var request = new HttpRequestMessage(); request.RequestUri = new Uri(healthcheckUrl); var httpClient = clientFactory.CreateClient(); httpClient.Timeout = TimeSpan.FromMilliseconds(Timeout); using var response = await httpClient.SendAsync(request); using var responseStream = await response.Content.ReadAsStreamAsync(); if (responseStream == null) { throw new Exception("Empty response"); } using var reader = new StreamReader(responseStream); var dataResponse = await reader.ReadToEndAsync(); return dataResponse.Equals("true", StringComparison.InvariantCultureIgnoreCase); } public enum CommandMethod { Info, Drop, Saved, //not used Version, ForceSave, //not used Meta, License } [Serializable] [DebuggerDisplay("{Key}")] public class CommandResponse { [JsonPropertyName("error")] public ErrorTypes Error { get; set; } [JsonPropertyName("errorString")] public string ErrorString { get; set; } [JsonPropertyName("key")] public string Key { get; set; } [JsonPropertyName("license")] public License License { get; set; } [JsonPropertyName("server")] public ServerInfo Server { get; set; } [JsonPropertyName("quota")] public QuotaInfo Quota { get; set; } [JsonPropertyName("version")] public string Version { get; set; } public enum ErrorTypes { NoError = 0, DocumentIdError = 1, ParseError = 2, UnknownError = 3, NotModify = 4, UnknownCommand = 5, Token = 6, TokenExpire = 7, } [Serializable] [DebuggerDisplay("{BuildVersion}")] public class ServerInfo { [JsonPropertyName("buildDate")] public DateTime BuildDate { get; set; } [JsonPropertyName("buildNumber")] public int buildNumber { get; set; } [JsonPropertyName("buildVersion")] public string BuildVersion { get; set; } [JsonPropertyName("packageType")] public PackageTypes PackageType { get; set; } [JsonPropertyName("resultType")] public ResultTypes ResultType { get; set; } [JsonPropertyName("workersCount")] public int WorkersCount { get; set; } public enum PackageTypes { OpenSource = 0, IntegrationEdition = 1, DeveloperEdition = 2 } public enum ResultTypes { Error = 1, Expired = 2, Success = 3, UnknownUser = 4, Connections = 5, ExpiredTrial = 6, SuccessLimit = 7, UsersCount = 8, ConnectionsOS = 9, UsersCountOS = 10, ExpiredLimited = 11 } } [Serializable] [DataContract(Name = "Quota", Namespace = "")] public class QuotaInfo { [JsonPropertyName("users")] public List Users { get; set; } [Serializable] [DebuggerDisplay("{UserId} ({Expire})")] public class User { [JsonPropertyName("userid")] public string UserId { get; set; } [JsonPropertyName("expire")] public DateTime Expire { get; set; } } } } [Serializable] [DebuggerDisplay("{Command} ({Key})")] private class CommandBody { [System.Text.Json.Serialization.JsonIgnore] public CommandMethod Command { get; set; } [JsonPropertyName("c")] public string C { get { return Command.ToString().ToLower(CultureInfo.InvariantCulture); } } [JsonPropertyName("callback")] public string Callback { get; set; } [JsonPropertyName("key")] public string Key { get; set; } [JsonPropertyName("meta")] public MetaData Meta { get; set; } [JsonPropertyName("users")] public string[] Users { get; set; } [JsonPropertyName("token")] public string Token { get; set; } //not used [JsonPropertyName("userdata")] public string UserData { get; set; } } [Serializable] [DebuggerDisplay("{Title}")] public class MetaData { [JsonPropertyName("title")] public string Title { get; set; } } [Serializable] [DebuggerDisplay("{Height}x{Width}")] public class ThumbnailData { [JsonPropertyName("aspect")] public int Aspect { get; set; } [JsonPropertyName("first")] public bool First { get; set; } [JsonPropertyName("height")] public int Height { get; set; } [JsonPropertyName("width")] public int Width { get; set; } } [Serializable] [DataContract(Name = "spreadsheetLayout", Namespace = "")] [DebuggerDisplay("SpreadsheetLayout {IgnorePrintArea} {Orientation} {FitToHeight} {FitToWidth} {Headings} {GridLines}")] public class SpreadsheetLayout { [JsonPropertyName("ignorePrintArea")] public bool IgnorePrintArea { get; set; } [JsonPropertyName("orientation")] public string Orientation { get; set; } [JsonPropertyName("fitToHeight")] public int FitToHeight { get; set; } [JsonPropertyName("fitToWidth")] public int FitToWidth { get; set; } [JsonPropertyName("headings")] public bool Headings { get; set; } [JsonPropertyName("gridLines")] public bool GridLines { get; set; } [JsonPropertyName("margins")] public LayoutMargins Margins { get; set; } [JsonPropertyName("pageSize")] public LayoutPageSize PageSize { get; set; } [Serializable] [DebuggerDisplay("Margins {Top} {Right} {Bottom} {Left}")] public class LayoutMargins { [JsonPropertyName("left")] public string Left { get; set; } [JsonPropertyName("right")] public string Right { get; set; } [JsonPropertyName("top")] public string Top { get; set; } [JsonPropertyName("bottom")] public string Bottom { get; set; } } [Serializable] [DebuggerDisplay("PageSize {Width} {Height}")] public class LayoutPageSize { [JsonPropertyName("height")] public string Height { get; set; } [JsonPropertyName("width")] public string Width { get; set; } } } [Serializable] [DebuggerDisplay("{Title} from {FileType} to {OutputType} ({Key})")] private class ConvertionBody { [JsonPropertyName("async")] public bool Async { get; set; } [JsonPropertyName("filetype")] public string FileType { get; set; } [JsonPropertyName("key")] public string Key { get; set; } [JsonPropertyName("outputtype")] public string OutputType { get; set; } [JsonPropertyName("password")] public string Password { get; set; } [JsonPropertyName("title")] public string Title { get; set; } [JsonPropertyName("thumbnail")] public ThumbnailData Thumbnail { get; set; } [JsonPropertyName("spreadsheetLayout")] public SpreadsheetLayout SpreadsheetLayout { get; set; } [JsonPropertyName("url")] public string Url { get; set; } [JsonPropertyName("token")] public string Token { get; set; } } [Serializable] [DebuggerDisplay("{Key}")] private class BuilderBody { [JsonPropertyName("async")] public bool Async { get; set; } [JsonPropertyName("key")] public string Key { get; set; } [JsonPropertyName("url")] public string Url { get; set; } [JsonPropertyName("token")] public string Token { get; set; } } [Serializable] public class FileLink { [JsonPropertyName("filetype")] public string FileType { get; set; } [JsonPropertyName("token")] public string Token { get; set; } [JsonPropertyName("url")] public string Url { get; set; } } [Serializable] public class DocumentServiceException : Exception { public ErrorCode Code { get; set; } public DocumentServiceException(ErrorCode errorCode, string message) : base(message) { Code = errorCode; } protected DocumentServiceException(SerializationInfo info, StreamingContext context) : base(info, context) { if (info != null) { Code = (ErrorCode)info.GetValue("Code", typeof(ErrorCode)); } } public override void GetObjectData(SerializationInfo info, StreamingContext context) { base.GetObjectData(info, context); if (info != null) { info.AddValue("Code", Code); } } public static void ProcessResponseError(string errorCode) { if (!Enum.TryParse(errorCode, true, out ErrorCode code)) { code = ErrorCode.Unknown; } var errorMessage = code switch { ErrorCode.VkeyUserCountExceed => "user count exceed", ErrorCode.VkeyKeyExpire => "signature expire", ErrorCode.VkeyEncrypt => "encrypt signature", ErrorCode.UploadCountFiles => "count files", ErrorCode.UploadExtension => "extension", ErrorCode.UploadContentLength => "upload length", ErrorCode.Vkey => "document signature", ErrorCode.TaskQueue => "database", ErrorCode.ConvertPassword => "password", ErrorCode.ConvertDownload => "download", ErrorCode.Convert => "convertation", ErrorCode.ConvertTimeout => "convertation timeout", ErrorCode.Unknown => "unknown error", _ => "errorCode = " + errorCode, }; throw new DocumentServiceException(code, errorMessage); } public enum ErrorCode { VkeyUserCountExceed = -22, VkeyKeyExpire = -21, VkeyEncrypt = -20, UploadCountFiles = -11, UploadExtension = -10, UploadContentLength = -9, Vkey = -8, TaskQueue = -6, ConvertPassword = -5, ConvertDownload = -4, Convert = -3, ConvertTimeout = -2, Unknown = -1 } } /// /// Processing document received from the editing service /// /// The resulting json from editing service /// Uri to the converted document /// The percentage of completion of conversion private static (int ResultPercent, string responseuri) GetResponseUri(string jsonDocumentResponse) { if (string.IsNullOrEmpty(jsonDocumentResponse)) throw new ArgumentException("Invalid param", nameof(jsonDocumentResponse)); var responseFromService = JObject.Parse(jsonDocumentResponse); if (responseFromService == null) throw new WebException("Invalid answer format"); var errorElement = responseFromService.Value("error"); if (!string.IsNullOrEmpty(errorElement)) DocumentServiceException.ProcessResponseError(errorElement); var isEndConvert = responseFromService.Value("endConvert"); int resultPercent; var responseUri = string.Empty; if (isEndConvert) { responseUri = responseFromService.Value("fileUrl"); resultPercent = 100; } else { resultPercent = responseFromService.Value("percent"); if (resultPercent >= 100) resultPercent = 99; } return (resultPercent, responseUri); } } }