diff --git a/src/OneScript.StandardLibrary/Http/HttpConnectionContext.cs b/src/OneScript.StandardLibrary/Http/HttpConnectionContext.cs
index 78472bfbc..a55b77dfe 100644
--- a/src/OneScript.StandardLibrary/Http/HttpConnectionContext.cs
+++ b/src/OneScript.StandardLibrary/Http/HttpConnectionContext.cs
@@ -13,6 +13,7 @@ This Source Code Form is subject to the terms of the
using System.Text;
using OneScript.Contexts;
using OneScript.Exceptions;
+using OneScript.Execution;
using OneScript.StandardLibrary.Collections;
using OneScript.Types;
using RegExp = System.Text.RegularExpressions;
@@ -130,9 +131,9 @@ public int Timeout
/// Строка. Имя файла, в который нужно записать ответ. Необязательный параметр.
/// HTTPОтвет. Ответ сервера.
[ContextMethod("Получить", "Get")]
- public HttpResponseContext Get(HttpRequestContext request, string output = null)
+ public HttpResponseContext Get(IBslProcess process, HttpRequestContext request, string output = null)
{
- return GetResponse(request, "GET", output);
+ return GetResponse(process, request, "GET", output);
}
///
@@ -141,9 +142,9 @@ public HttpResponseContext Get(HttpRequestContext request, string output = null)
/// HTTPЗапрос. Данные и заголовки запроса http
/// HTTPОтвет. Ответ сервера.
[ContextMethod("Записать", "Put")]
- public HttpResponseContext Put(HttpRequestContext request)
+ public HttpResponseContext Put(IBslProcess process, HttpRequestContext request)
{
- return GetResponse(request, "PUT");
+ return GetResponse(process, request, "PUT", null);
}
///
@@ -153,9 +154,9 @@ public HttpResponseContext Put(HttpRequestContext request)
/// Строка. Имя файла, в который нужно записать ответ. Необязательный параметр.
/// HTTPОтвет. Ответ сервера.
[ContextMethod("ОтправитьДляОбработки", "Post")]
- public HttpResponseContext Post(HttpRequestContext request, string output = null)
+ public HttpResponseContext Post(IBslProcess process, HttpRequestContext request, string output = null)
{
- return GetResponse(request, "POST", output);
+ return GetResponse(process, request, "POST", output);
}
///
@@ -164,9 +165,9 @@ public HttpResponseContext Post(HttpRequestContext request, string output = null
/// HTTPЗапрос. Данные и заголовки запроса http
/// HTTPОтвет. Ответ сервера.
[ContextMethod("Удалить", "Delete")]
- public HttpResponseContext Delete(HttpRequestContext request)
+ public HttpResponseContext Delete(IBslProcess process, HttpRequestContext request)
{
- return GetResponse(request, "DELETE");
+ return GetResponse(process, request, "DELETE");
}
///
@@ -175,9 +176,9 @@ public HttpResponseContext Delete(HttpRequestContext request)
/// HTTPЗапрос. Данные и заголовки запроса http
/// HTTPОтвет. Ответ сервера.
[ContextMethod("Изменить", "Patch")]
- public HttpResponseContext Patch(HttpRequestContext request)
+ public HttpResponseContext Patch(IBslProcess process, HttpRequestContext request)
{
- return GetResponse(request, "PATCH");
+ return GetResponse(process, request, "PATCH");
}
///
@@ -186,9 +187,9 @@ public HttpResponseContext Patch(HttpRequestContext request)
/// HTTPЗапрос. Данные и заголовки запроса http
/// HTTPОтвет. Ответ сервера.
[ContextMethod("ПолучитьЗаголовки", "Head")]
- public HttpResponseContext Head(HttpRequestContext request)
+ public HttpResponseContext Head(IBslProcess process, HttpRequestContext request)
{
- return GetResponse(request, "HEAD");
+ return GetResponse(process, request, "HEAD");
}
///
@@ -199,9 +200,9 @@ public HttpResponseContext Head(HttpRequestContext request)
/// Строка. Имя выходного файла
/// HTTPОтвет. Ответ сервера.
[ContextMethod("ВызватьHTTPМетод", "CallHTTPMethod")]
- public HttpResponseContext Patch(string method, HttpRequestContext request, string output = null)
+ public HttpResponseContext CallHTTPMethod(IBslProcess process, string method, HttpRequestContext request, string output = null)
{
- return GetResponse(request, method, output);
+ return GetResponse(process, request, method, output);
}
private HttpWebRequest CreateRequest(string resource)
@@ -335,12 +336,12 @@ private static List ParseRange(string rangeHeader)
return range;
}
- private HttpResponseContext GetResponse(HttpRequestContext request, string method, string output = null)
+ private HttpResponseContext GetResponse(IBslProcess process, HttpRequestContext request, string method, string output = null)
{
var webRequest = CreateRequest(request.ResourceAddress);
webRequest.AllowAutoRedirect = AllowAutoRedirect;
webRequest.Method = method;
- SetRequestHeaders(request, webRequest);
+ SetRequestHeaders(request, webRequest, process);
if (ContentBodyAllowed(method))
SetRequestBody(request, webRequest);
@@ -385,7 +386,7 @@ private static void SetRequestBody(HttpRequestContext request, HttpWebRequest we
}
}
- private static void SetRequestHeaders(HttpRequestContext request, HttpWebRequest webRequest)
+ private static void SetRequestHeaders(HttpRequestContext request, HttpWebRequest webRequest, IBslProcess process)
{
foreach (var item in request.Headers.Select(x => x.GetRawValue() as KeyAndValueImpl))
{
@@ -483,15 +484,19 @@ private static void SetRequestHeaders(HttpRequestContext request, HttpWebRequest
default:
webRequest.Headers.Set(key, value);
break;
-
}
+ }
- // fix #1151
- if (webRequest.UserAgent == default)
- {
- webRequest.UserAgent = $"1Script v${Assembly.GetExecutingAssembly().GetName().Version}";
- }
+ // fix #1151
+ if (webRequest.UserAgent == default)
+ {
+ webRequest.UserAgent = $"1Script v${Assembly.GetExecutingAssembly().GetName().Version}";
+ }
+ if (request.AccessToken != null)
+ {
+ string token = request.AccessToken.ToString(process);
+ webRequest.Headers.Set(HttpRequestHeader.Authorization, $"Bearer {token}");
}
}
diff --git a/src/OneScript.StandardLibrary/Http/HttpRequestContext.cs b/src/OneScript.StandardLibrary/Http/HttpRequestContext.cs
index d28946b1a..a190ff265 100644
--- a/src/OneScript.StandardLibrary/Http/HttpRequestContext.cs
+++ b/src/OneScript.StandardLibrary/Http/HttpRequestContext.cs
@@ -11,6 +11,7 @@ This Source Code Form is subject to the terms of the
using OneScript.Exceptions;
using OneScript.StandardLibrary.Binary;
using OneScript.StandardLibrary.Collections;
+using OneScript.StandardLibrary.Security.Tokens;
using OneScript.StandardLibrary.Text;
using ScriptEngine.Machine;
using ScriptEngine.Machine.Contexts;
@@ -44,6 +45,8 @@ private void SetBody(IHttpRequestBody newBody)
public Stream Body => _body?.GetDataStream();
+ public AccessTokenContext AccessToken { get; private set; }
+
///
/// Относительный путь к ресурсу на сервере (не включает имя сервера)
///
@@ -115,6 +118,16 @@ public GenericStream GetBodyAsStream()
return new GenericStream(_body.GetDataStream());
}
+ ///
+ /// Добавляет токен доступа к HTTP-запросу. Устанавливает заголовок Authorization.
+ ///
+ /// Токен доступа
+ [ContextMethod("ДобавитьТокенДоступа", "AddAccessToken")]
+ public void AddAccessToken(AccessTokenContext accessToken)
+ {
+ AccessToken = accessToken;
+ }
+
[ScriptConstructor(Name = "Формирование неинициализированного объекта")]
public static HttpRequestContext Constructor()
{
diff --git a/src/OneScript.StandardLibrary/OneScript.StandardLibrary.csproj b/src/OneScript.StandardLibrary/OneScript.StandardLibrary.csproj
index 1c2bbf009..2191c573f 100644
--- a/src/OneScript.StandardLibrary/OneScript.StandardLibrary.csproj
+++ b/src/OneScript.StandardLibrary/OneScript.StandardLibrary.csproj
@@ -26,6 +26,7 @@
+
diff --git a/src/OneScript.StandardLibrary/Security/Tokens/AccessTokenContext.cs b/src/OneScript.StandardLibrary/Security/Tokens/AccessTokenContext.cs
new file mode 100644
index 000000000..f0dbc1395
--- /dev/null
+++ b/src/OneScript.StandardLibrary/Security/Tokens/AccessTokenContext.cs
@@ -0,0 +1,448 @@
+/*----------------------------------------------------------
+This Source Code Form is subject to the terms of the
+Mozilla Public License, v.2.0. If a copy of the MPL
+was not distributed with this file, You can obtain one
+at http://mozilla.org/MPL/2.0/.
+----------------------------------------------------------*/
+
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Security.Cryptography;
+using System.IdentityModel.Tokens.Jwt;
+using Microsoft.IdentityModel.Tokens;
+using OneScript.Contexts;
+using OneScript.Exceptions;
+using OneScript.Execution;
+using OneScript.StandardLibrary.Collections;
+using OneScript.Types;
+using ScriptEngine.Machine;
+using ScriptEngine.Machine.Contexts;
+
+namespace OneScript.StandardLibrary.Security.Tokens
+{
+ ///
+ /// Описывает структуру токена доступа в формате JSON Web Token.
+ ///
+ [ContextClass("ТокенДоступа", "AccessToken")]
+ public class AccessTokenContext : AutoContext, IDisposable
+ {
+ private int? _lifeTime;
+ private int? _creationTime;
+ private string _tokenId;
+ private string _userMatchingKey;
+ private string _issuer;
+ private string _token;
+ private bool _isSigned = false;
+ private RSA _rsa;
+ private ECDsa _ecdsa;
+ private bool _disposed = false;
+
+ ///
+ /// Определяет время жизни токена в секундах.
+ /// Устанавливает значение для ключа 'exp' равное сумме времени создания и времени жизни.
+ ///
+ /// Число (Number)
+ [ContextProperty("ВремяЖизни", "LifeTime")]
+ public int LifeTime
+ {
+ get => _lifeTime ?? 0;
+ set => _lifeTime = value;
+ }
+
+ ///
+ /// Числовое значение времени создания токена доступа в формате UnixTime (количество секунд, прошедших с
+ /// полуночи 01.01.1970). Соответствует ключам 'iat' и 'nbf'.
+ ///
+ /// Число (Number)
+ [ContextProperty("ВремяСоздания", "CreationTime")]
+ public int CreationTime
+ {
+ get => _creationTime ?? 0;
+ set => _creationTime = value;
+ }
+
+ ///
+ /// Заголовки токена доступа.
+ ///
+ /// Соответствие (Map)
+ [ContextProperty("Заголовки", "Headers")]
+ public MapImpl Headers { get; set; }
+
+ ///
+ /// Идентификатор токена.
+ ///
+ /// Строка (String)
+ [ContextProperty("Идентификатор", "ID")]
+ public string TokenId
+ {
+ get => _tokenId ?? "";
+ set => _tokenId = value;
+ }
+
+ ///
+ /// Ключ сопоставления пользователя. Соответствует ключу 'sub' токена доступа.
+ ///
+ /// Строка (String)
+ [ContextProperty("КлючСопоставленияПользователя", "UserMatchingKey")]
+ public string UserMatchingKey
+ {
+ get => _userMatchingKey ?? "";
+ set => _userMatchingKey = value;
+ }
+
+ ///
+ /// Полезная нагрузка токена доступа.
+ ///
+ /// Соответствие (Map)
+ [ContextProperty("ПолезнаяНагрузка", "Payload")]
+ public MapImpl Payload { get; set; }
+
+ ///
+ /// Массив строк, который содержит идентификаторы получателей токена. Соответствует ключу 'aud'.
+ ///
+ /// Массив (Array)
+ [ContextProperty("Получатели", "Recipients")]
+ public ArrayImpl Recipients { get; set; }
+
+ ///
+ /// Идентификатор эмитента, выпустившего токен. Соответствует ключу 'iss'.
+ ///
+ /// Строка (String)
+ [ContextProperty("Эмитент", "Issuer")]
+ public string Issuer
+ {
+ get => _issuer ?? "";
+ set => _issuer = value;
+ }
+
+ ///
+ /// Добавляет токену доступа подпись по указанному в параметрах алгоритму.
+ ///
+ /// Алгоритм подписи токена доступа.
+ /// Информация о ключе, который используется для формирования подписи в формате PEM.
+ /// Данный параметр является необязательным только, если не указан алгоритм подписи.
+ [ContextMethod("Подписать", "Sign")]
+ public void Sign(IBslProcess process, AccessTokenSignAlgorithmEnum algorithm, string secretKey = "")
+ {
+ CreateToken(process, algorithm, secretKey);
+ _isSigned = true;
+ }
+
+ private void CreateUnsignedToken(IBslProcess process)
+ {
+ CreateToken(process, AccessTokenSignAlgorithmEnum.None);
+ }
+
+ private void CreateToken(IBslProcess process, AccessTokenSignAlgorithmEnum algorithm, string secretKey = "")
+ {
+ try
+ {
+ var header = CreateJwtHeader(process, algorithm, secretKey);
+ var payload = CreateJwtPayload(process);
+ var jwtToken = new JwtSecurityToken(header, payload);
+
+ var tokenHandler = new JwtSecurityTokenHandler
+ {
+ SetDefaultTimesOnTokenCreation = false
+ };
+
+ _token = tokenHandler.WriteToken(jwtToken);
+ }
+ catch (Exception ex)
+ {
+ throw new SecurityTokenException($"Ошибка при создании токена: {ex.Message}", ex);
+ }
+ }
+
+ private JwtHeader CreateJwtHeader(IBslProcess process, AccessTokenSignAlgorithmEnum algorithm, string secretKey)
+ {
+ JwtHeader header;
+
+ if (algorithm == AccessTokenSignAlgorithmEnum.None)
+ {
+ header = new JwtHeader();
+ header["alg"] = "none";
+ }
+ else
+ {
+ var signingCredentials = GetSigningCredentials(algorithm, secretKey);
+ header = new JwtHeader(signingCredentials);
+ }
+
+ if (Headers != null)
+ {
+ foreach (var headerItem in Headers)
+ {
+ if (headerItem.Key.SystemType != BasicTypes.String)
+ throw RuntimeException.InvalidArgumentType();
+
+ var key = headerItem.Key.ToString();
+ var value = headerItem.Value.AsString(process);
+
+ if(!String.IsNullOrEmpty(key))
+ header[key] = value;
+ }
+ }
+
+ return header;
+ }
+
+ private JwtPayload CreateJwtPayload(IBslProcess process)
+ {
+ var payload = new JwtPayload();
+
+ AddStandardClaimsToPayload(payload);
+ AddAudienceToPayload(process, payload);
+ AddCustomClaimsToPayload(process, payload);
+
+ return payload;
+ }
+
+ private void AddStandardClaimsToPayload(JwtPayload payload)
+ {
+ if (_issuer != null)
+ payload[JwtRegisteredClaimNames.Iss] = _issuer;
+
+ if (CreationTime != 0)
+ {
+ payload[JwtRegisteredClaimNames.Iat] = CreationTime;
+ payload[JwtRegisteredClaimNames.Nbf] = CreationTime;
+ }
+
+ if (CreationTime != 0 || LifeTime != 0)
+ {
+ int expires = CreationTime + LifeTime;
+ payload[JwtRegisteredClaimNames.Exp] = expires;
+ }
+
+ if (_tokenId != null)
+ payload[JwtRegisteredClaimNames.Jti] = _tokenId;
+
+ if(_userMatchingKey != null)
+ payload[JwtRegisteredClaimNames.Sub] = _userMatchingKey;
+ }
+
+ private void AddAudienceToPayload(IBslProcess process, JwtPayload payload)
+ {
+ if (Recipients == null || Recipients.Count() == 0)
+ return;
+
+ if (Recipients.Count() == 1)
+ {
+ payload[JwtRegisteredClaimNames.Aud] = Recipients[0].AsString(process);
+ }
+ else
+ {
+ var recipientsStrings = new List();
+ foreach (var recipient in Recipients)
+ {
+ if (recipient.SystemType != BasicTypes.String)
+ throw RuntimeException.InvalidArgumentType();
+
+ recipientsStrings.Add(recipient.ToString());
+ }
+
+ payload[JwtRegisteredClaimNames.Aud] = recipientsStrings;
+ }
+ }
+
+ private void AddCustomClaimsToPayload(IBslProcess process, JwtPayload payload)
+ {
+ if (Payload == null || Payload.Count() == 0)
+ return;
+
+ foreach (var payloadItem in Payload)
+ {
+ if (payloadItem.Key.SystemType != BasicTypes.String)
+ throw RuntimeException.InvalidArgumentType();
+
+ var key = payloadItem.Key.ToString();
+ var value = ConvertToClrObject(process, payloadItem.Value);
+
+ if(!String.IsNullOrEmpty(key) && value != null)
+ payload[key] = value;
+ }
+ }
+
+ private SigningCredentials GetSigningCredentials(AccessTokenSignAlgorithmEnum algorithm, string secretKey)
+ {
+ if (algorithm == AccessTokenSignAlgorithmEnum.None)
+ return null;
+
+ if (string.IsNullOrEmpty(secretKey))
+ throw new ArgumentException("Ключ подписи не может быть пустым для выбранного алгоритма");
+
+ var key = GetSigningKey(algorithm, secretKey);
+ return new SigningCredentials(key, GetSecurityAlgorithm(algorithm));
+ }
+
+ private SecurityKey GetSigningKey(AccessTokenSignAlgorithmEnum algorithm, string secretKey)
+ {
+ return algorithm switch
+ {
+ // Симметричные алгоритмы (HMAC)
+ AccessTokenSignAlgorithmEnum.HS256 => new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey)),
+ AccessTokenSignAlgorithmEnum.HS384 => new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey)),
+ AccessTokenSignAlgorithmEnum.HS512 => new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey)),
+
+ // Асимметричные алгоритмы (RSA)
+ AccessTokenSignAlgorithmEnum.RS256 => CreateRsaSecurityKey(secretKey),
+ AccessTokenSignAlgorithmEnum.RS384 => CreateRsaSecurityKey(secretKey),
+ AccessTokenSignAlgorithmEnum.RS512 => CreateRsaSecurityKey(secretKey),
+ AccessTokenSignAlgorithmEnum.PS256 => CreateRsaSecurityKey(secretKey),
+ AccessTokenSignAlgorithmEnum.PS384 => CreateRsaSecurityKey(secretKey),
+ AccessTokenSignAlgorithmEnum.PS512 => CreateRsaSecurityKey(secretKey),
+
+ // Эллиптические кривые (ECDSA)
+ AccessTokenSignAlgorithmEnum.ES256 => CreateEcdsaSecurityKey(secretKey),
+ AccessTokenSignAlgorithmEnum.ES384 => CreateEcdsaSecurityKey(secretKey),
+ AccessTokenSignAlgorithmEnum.ES512 => CreateEcdsaSecurityKey(secretKey),
+
+ _ => throw new ArgumentException($"Неподдерживаемый алгоритм для ключа: {algorithm}")
+ };
+ }
+
+ private SecurityKey CreateRsaSecurityKey(string secretKey)
+ {
+ _rsa?.Dispose();
+ _rsa = RSA.Create();
+ try
+ {
+ _rsa.ImportFromPem(secretKey);
+ return new RsaSecurityKey(_rsa);
+ }
+ catch (Exception ex)
+ {
+ _rsa.Dispose();
+ _rsa = null;
+ throw new ArgumentException($"Ошибка при создании RSA ключа: {ex.Message}", ex);
+ }
+ }
+
+ private SecurityKey CreateEcdsaSecurityKey(string secretKey)
+ {
+ _ecdsa?.Dispose();
+ _ecdsa = ECDsa.Create();
+ try
+ {
+ _ecdsa.ImportFromPem(secretKey);
+ return new ECDsaSecurityKey(_ecdsa);
+ }
+ catch (Exception ex)
+ {
+ _ecdsa.Dispose();
+ _ecdsa = null;
+ throw new ArgumentException($"Ошибка при создании ECDSA ключа: {ex.Message}", ex);
+ }
+ }
+
+ private string GetSecurityAlgorithm(AccessTokenSignAlgorithmEnum algorithm)
+ {
+ return algorithm switch
+ {
+ // HMAC алгоритмы
+ AccessTokenSignAlgorithmEnum.HS256 => SecurityAlgorithms.HmacSha256,
+ AccessTokenSignAlgorithmEnum.HS384 => SecurityAlgorithms.HmacSha384,
+ AccessTokenSignAlgorithmEnum.HS512 => SecurityAlgorithms.HmacSha512,
+
+ // RSA алгоритмы
+ AccessTokenSignAlgorithmEnum.RS256 => SecurityAlgorithms.RsaSha256,
+ AccessTokenSignAlgorithmEnum.RS384 => SecurityAlgorithms.RsaSha384,
+ AccessTokenSignAlgorithmEnum.RS512 => SecurityAlgorithms.RsaSha512,
+
+ // RSA-PSS алгоритмы
+ AccessTokenSignAlgorithmEnum.PS256 => SecurityAlgorithms.RsaSsaPssSha256,
+ AccessTokenSignAlgorithmEnum.PS384 => SecurityAlgorithms.RsaSsaPssSha384,
+ AccessTokenSignAlgorithmEnum.PS512 => SecurityAlgorithms.RsaSsaPssSha512,
+
+ // ECDSA алгоритмы
+ AccessTokenSignAlgorithmEnum.ES256 => SecurityAlgorithms.EcdsaSha256,
+ AccessTokenSignAlgorithmEnum.ES384 => SecurityAlgorithms.EcdsaSha384,
+ AccessTokenSignAlgorithmEnum.ES512 => SecurityAlgorithms.EcdsaSha512,
+
+ _ => throw new ArgumentException($"Неподдерживаемый алгоритм: {algorithm}")
+ };
+ }
+
+ private object ConvertToClrObject(IBslProcess process, IValue value)
+ {
+ if (value == null)
+ return null;
+
+ switch (value)
+ {
+ case ArrayImpl array:
+ case FixedArrayImpl fixedArray:
+ var list = new List