Перевести код из Node JS в C#

166
28 июля 2019, 22:10

Код на Node JS

import * as request from 'request'
import * as crypto from 'crypto'
import * as querystring from 'querystring'
import * as url from 'url'
import 'es6-shim'
const W_BASEURL = "https://api.com"
const W_DEFAULT_API_VERSION = "2"
const W_DEFAULT_API_FORMAT = "json"
export class WClient {
    constructor(private config: any, private masqueradeTarget?: string) {
        if(!config.secretKey) throw new Error('config.secretKey is missing');
        if(!config.apiKey)  throw new Error('config.apiKey is missing');
        this.config.options = this.config.options || {};
    }
    public get(path: string, params?: any, options?: any): Promise<any> {
        return this.request("GET", path, params, options)
    }
    public post(path: string, body?: any, options?: any): Promise<any> {
        return this.request("POST", path, body, options)
    }
    public put(path: string, body?: any, options?: any): Promise<any> {
        return this.request("PUT", path, body, options)
    }
    public delete(path: string, body?: any, options?: any): Promise<any> {
        return this.request("DELETE", path, body, options)
    }
    /**
     * returns a new W client which is ostensibly authenticated as the supplied
     * target
     *
     * @param target an account ID or an SRN
     */
    public masqueraded(target: string): WClient {
        return new WClient(this.config, target);
    }
    private request(method: string, path: string, params: any = {}, options: any = {}): Promise<any> {
        if(!path)
            throw "path required"
        let requestOptions = this.buildRequestOptions(method, path, params, options)
        return new Promise((resolve, reject) => {
            request(requestOptions, (err, res) => {
                if(err)
                    throw err;
                else if(res.statusCode >= 200 && res.statusCode < 300)
                    resolve(res.body || {})
                else
                    reject(res.body || { statusCode: res.statusCode })
            })
        })
    }
    private buildRequestOptions(method: string, path: string, params: any, options: any): request.UrlOptions & request.CoreOptions {
        options = options || {};
        let parsedUrl = url.parse(url.resolve(this.config.baseUrl || W_BASEURL, path), true)
        let json = !(options.headers || {}).hasOwnProperty('Content-Type') || options.headers['Content-Type'] == 'application/json';
        let requestOptions: request.UrlOptions & request.CoreOptions = {
            ...this.config.options,
            ...options,
            url: parsedUrl.protocol + "//" + parsedUrl.host + parsedUrl.pathname, // no querystring here!
            method: method,
            headers: {
                ...this.config.options.headers,
                ...options.headers,
                "X-Api-Version": this.config.apiVersion || W_DEFAULT_API_VERSION,
                "X-Api-Key": this.config.apiKey
            },
            qs: {
                ...this.config.qs,
                ...options.qs,
                timestamp: new Date().getTime(),
                format: this.config.format || W_DEFAULT_API_FORMAT
            },
            json: json
        };
        if(requestOptions.method == "GET")
            requestOptions.qs = Object.assign(requestOptions.qs, params)
        else
            requestOptions.body = params
        Object.assign(requestOptions.qs, parsedUrl.query);
        if(this.masqueradeTarget && !('masqueradeAs' in requestOptions))
            requestOptions.qs.masqueradeAs = this.masqueradeTarget;
        requestOptions.headers["X-Api-Signature"] = this.buildSignature(requestOptions);
        return requestOptions
    }
    private buildSignature(requestOptions: request.UrlOptions & request.CoreOptions): string {
        let buffers: Buffer[] = [];
        const encoding = 'utf8';
        buffers.push(Buffer.from(requestOptions.url.toString(), encoding));
        buffers.push(Buffer.from(requestOptions.url.toString().indexOf('?') < 0 ? "?" : "&", encoding));
        buffers.push(Buffer.from(querystring.stringify(requestOptions.qs), encoding));
        if(requestOptions.body) {
            if(typeof requestOptions.body == 'string')
                buffers.push(Buffer.from(requestOptions.body, encoding));
            else if(requestOptions.body instanceof Buffer)
                buffers.push(requestOptions.body);
            else
                buffers.push(Buffer.from(JSON.stringify(requestOptions.body), encoding));
        }
        return crypto.createHmac("sha256", this.config.secretKey)
            .update(Buffer.concat(buffers))
            .digest("hex")
    }
}

Код на JS

"use strict";
var __assign = (this && this.__assign) || Object.assign || function(t) {
    for (var s, i = 1, n = arguments.length; i < n; i++) {
        s = arguments[i];
        for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
            t[p] = s[p];
    }
    return t;
};
Object.defineProperty(exports, "__esModule", { value: true });
var request = require("request");
var crypto = require("crypto");
var querystring = require("querystring");
var url = require("url");
require("es6-shim");
var W_BASEURL = "https://api.com";
var W_DEFAULT_API_VERSION = "2";
var W_DEFAULT_API_FORMAT = "json";
var Client = (function () {
    function Client(config, masqueradeTarget) {
        this.config = config;
        this.masqueradeTarget = masqueradeTarget;
        if (!config.secretKey)
            throw new Error('config.secretKey is missing');
        if (!config.apiKey)
            throw new Error('config.apiKey is missing');
        this.config.options = this.config.options || {};
    }
    Client.prototype.get = function (path, params, options) {
        return this.request("GET", path, params, options);
    };
    Client.prototype.post = function (path, body, options) {
        return this.request("POST", path, body, options);
    };
    Client.prototype.put = function (path, body, options) {
        return this.request("PUT", path, body, options);
    };
    Client.prototype.delete = function (path, body, options) {
        return this.request("DELETE", path, body, options);
    };
    Client.prototype.masqueraded = function (target) {
        return new Client(this.config, target);
    };
    Client.prototype.request = function (method, path, params, options) {
        if (params === void 0) { params = {}; }
        if (options === void 0) { options = {}; }
        if (!path)
            throw "path required";
        var requestOptions = this.buildRequestOptions(method, path, params, options);
        return new Promise(function (resolve, reject) {
            request(requestOptions, function (err, res) {
                if (err)
                    throw err;
                else if (res.statusCode >= 200 && res.statusCode < 300)
                    resolve(res.body || {});
                else
                    reject(res.body || { statusCode: res.statusCode });
            });
        });
    };
    Client.prototype.buildRequestOptions = function (method, path, params, options) {
        options = options || {};
        var parsedUrl = url.parse(url.resolve(this.config.baseUrl || W_BASEURL, path), true);
        var json = !(options.headers || {}).hasOwnProperty('Content-Type') || options.headers['Content-Type'] == 'application/json';
        var requestOptions = __assign({}, this.config.options, options, { 
            url: parsedUrl.protocol + "//" + parsedUrl.host + parsedUrl.pathname, 
            method: method, 
            headers: __assign({}, 
                this.config.options.headers, 
                options.headers, 
                { "X-Api-Version": this.config.apiVersion || W_DEFAULT_API_VERSION, 
                "X-Api-Key": this.config.apiKey }), 
            qs: __assign({}, this.config.qs, options.qs, 
                { timestamp: new Date().getTime(), 
                    format: this.config.format || W_DEFAULT_API_FORMAT }), json: json });
        if (requestOptions.method == "GET")
            requestOptions.qs = Object.assign(requestOptions.qs, params);
        else
            requestOptions.body = params;
        Object.assign(requestOptions.qs, parsedUrl.query);
        if (this.masqueradeTarget && !('masqueradeAs' in requestOptions))
            requestOptions.qs.masqueradeAs = this.masqueradeTarget;
        requestOptions.headers["X-Api-Signature"] = this.buildSignature(requestOptions);
        return requestOptions;
    };
    Client.prototype.buildSignature = function (requestOptions) {
        var buffers = [];
        var encoding = 'utf8';
        buffers.push(Buffer.from(requestOptions.url.toString(), encoding));
        buffers.push(Buffer.from(requestOptions.url.toString().indexOf('?') < 0 ? "?" : "&", encoding));
        buffers.push(Buffer.from(querystring.stringify(requestOptions.qs), encoding));
        if (requestOptions.body) {
            if (typeof requestOptions.body == 'string')
                buffers.push(Buffer.from(requestOptions.body, encoding));
            else if (requestOptions.body instanceof Buffer){
                console.log("BUFFER");
                console.log(requestOptions.body);
                buffers.push(requestOptions.body);
            }
            else
                buffers.push(Buffer.from(JSON.stringify(requestOptions.body), encoding));
        }
        return crypto.createHmac("sha256", this.config.secretKey)
            .update(Buffer.concat(buffers))
            .digest("hex");
    };
    return Client;
}());
exports.Client = Client;

Сделал так, но есть недоработки, такие как:

  1. Проверка на masqueradeAs
  2. Загрузка файлов
  3. Указание headers

Что есть:

using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Linq;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json;
using ConfigAccess;
using LogAccess;
using System.Collections.Specialized;
namespace WApiLib
{
    public class WApi
    {
        private static readonly string domain = Configuration.Get()["W:Domain"];
        private static readonly string apiKey = Configuration.Get()["W:ApiKey"];
        private static readonly string secKey = Configuration.Get()["W:SecKey"];
        public HttpWebResponse Get(string path)
        {
            return Get(path, new Dictionary<string, object>());
        }
        public HttpWebResponse Get(string path, Dictionary<string, object> queryParams)
        {
            return Request("GET", path, queryParams);
        }
        public HttpWebResponse Post(string path, Dictionary<string, object> body)
        {
            return Request("POST", path, body);
        }
        private HttpWebResponse Request(string method, string path, Dictionary<string, object> body)
        {
            Dictionary<string, object> queryParams = new Dictionary<string, object>();
            if (method.Equals("GET"))
                queryParams = body;
            queryParams.Add("timestamp", DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());
            string queryString = queryParams.Aggregate("", (previous, current) => previous + "&" + current.Key + "=" + current.Value).Remove(0, 1);
            string url = domain + path + "?" + queryString;
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
            request.Method = method;
            request.ContentType = "application/json";
            request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
            if (!method.Equals("GET"))
            {
                url += JsonConvert.SerializeObject(body);
                using (StreamWriter writer = new StreamWriter(request.GetRequestStream()))
                    writer.Write(JsonConvert.SerializeObject(body));
            }
            request.Headers["X-Api-Key"] = apiKey;
            request.Headers["X-Api-Signature"] = CalcAuthSigHash(secKey, url);
            request.Headers["X-Api-Version"] = Configuration.Get()["W:ApiVersion"];
            try
            {
                return (HttpWebResponse)request.GetResponse();
            }
            catch (WebException e)
            {
                string msg = new StreamReader(e.Response.GetResponseStream()).ReadToEnd();
                Log.Error(msg);
                throw new SystemException(msg);
            }
        }
        private static byte[] GetBytes(string str)
        {
            return Encoding.UTF8.GetBytes(str);
        }
        private static string GetString(byte[] bytes)
        {
            return BitConverter.ToString(bytes);
        }
        private static String CalcAuthSigHash(string key, string value)
        {
            HMACSHA256 hmac = new HMACSHA256(GetBytes(key));
            string hash = GetString(hmac.ComputeHash(GetBytes(value))).Replace("-", "");
            return hash;
        }
    }
}
Answer 1

Код с typescript достаточно хорошо переводится на C#, благодаря наличию типов. Однако могут возникнуть небольшие сложности со специальными возможностями, типа Object.assign, который позволяет добавлять в один объект поля из другого. Однако это может быть решено использованием нескольких Dictionary.

Конструктор

TypeScript позволяет объявлять поля и сопоставлять их с одноименными параметрами непосредственно в определении конструктора:

constructor(private config: any, private masqueradeTarget?: string) {
    if(!config.secretKey) throw new Error('config.secretKey is missing');
    if(!config.apiKey)  throw new Error('config.apiKey is missing');
    this.config.options = this.config.options || {};
}

Здесь создаются два приватных поля, которые инициализируются значениями параметров и устанавливается значение по умолчанию для config.options, если его не передали.

в C# это может выглядеть так:

private JObject config; // был использован JObject, из-за неизвестного формата. лучше использовать конкретный класс или интерфейс
private string masqueradeTarget;
public WClient(JObject config, string masqueradeTarget = null)
{
    if (!config["SecretKey"].HasValues) throw new ArgumentException("config.secretKey is missing");
    if (!config["ApiKey"].HasValues) throw new ArgumentException("config.apiKey is missing");
    this.config = config;
    this.config["Options"] = this.config["Options"].HasValues ? this.config["Options"] : new JObject();
    this.masqueradeTarget  = masqueradeTarget;
    /* остальная инициализация полей, например создание httpClient */
    client = new HttpClient();
}

Методы

Метод masqueraded возвращает новый объект этого класса, которому передается существующий config и устанавливается параметр masqueradeTarget

public WClient Masqueraded(string target)
{
    return new WClient(config, target);
}

Методы вызывающие Request вырождаются как и в TypeScript в одну строку:

public Task<JObject> Get(string path, Dictionary<string, string> @params = null, JObject options = null) => Request(HttpMethod.Get, path, @params, options ?? new JObject());
public Task<JObject> Post(string path, object body = null, JObject options = null) => Request(HttpMethod.Post, path, body, options ?? new JObject());
public Task<JObject> Put(string path, object body = null, JObject options = null) => Request(HttpMethod.Put, path, body, options ?? new JObject());
public Task<JObject> Delete(string path, object body = null, JObject options = null) => Request(HttpMethod.Delete, path, body, options ?? new JObject());

Сам метод Request, за счет использования HttpClient становится довольно простым:

private HttpClient client;
private async Task<JObject> Request(HttpMethod method, string path, object @params = null, JObject options = null)
{
    if (path == null)
        throw new ArgumentNullException("path required");
    HttpRequestMessage requestOptions = BuildRequestOptions(method, path, @params, options); // собираем запрос
    var response = await client.SendAsync(requestOptions); // шлем запрос
    var content = await response.Content.ReadAsStringAsync(); // получаем результат
    return JObject.Parse(content); // разбираем результат и возвращаем
}

Далее начинается магия с построением запроса.

  1. собирается url адрес, на который пойдет запрос (устанавливаются из переданных и стандартных )
  2. в случае метода Get ему добавляются в QueryString переданные параметры
  3. в противном случае устанавливается body
  4. устанавливается параметр masqueradeAs, если соответствующее поле присутствует
  5. устанавливаются необходимые headers
  6. делается подпись и сохраняется в headers

За запрос в данном случае будет отвечать объект класса HttpRequestMessage

Данный класс имеет три основных свойства:

  1. Method - отвечающий за используемый HttpMethod
  2. RequestUri - полный Uri на который отправляется запрос
  3. Content - определяет тело запроса

Структура метода может выглядеть так:

private HttpRequestMessage BuildRequestOptions(HttpMethod method, string path, object @params, JObject options = null)
{
    var requestUri = /* собираем requesturi */;
    if(method == HttpMethod.Get)
    {
        /* Добавляем параметры в request uri */
    }
    if (!string.IsNullOrEmpty(masqueradeTarget))
    {
        /* Добавляем параметр masqueradeAs в requestUri со значением masqueradeTarget */
    }
    /* проверка что пришло в body и на основе проверки создание необходимого объекта Content, например: */
    HttpContent content;
    if(body.GetType() == typeof(string)){
        content = new StringContent(body.ToString());
    }else if(body.GetType() == typeof(byte[])){
        content = new ByteArrayContent((byte[])body);
    }else{ // и т.д. если в body несколько полей разных типов можно воспользоваться MultipartFormDataContent
    // иначе сериализуем в JSON
        content = new StringContent(JsonConvert.SerializeObject(body), Encoding.UTF8,  "application/json");
    }
    var rm = new HttpRequestMessage()
    {
        Method = method,
        RequestUri = requestUri,
        Content = content
    };
    /* заполнение Headers */
    rm.Headers.Add("X-Api-Version", /* value */ );
    /* далее получаем подпись на основе requestUri и body и записываем так же в headers */
    rm.Headers["X-Api-Signature"] = BuildSignature(requestUri, body);
    return rm;
}

Подпись

В текущей реализации подпись выполняется не совсем корректно: не учитывается тип body.

Нужно сделать по аналогии с созданием Content - проверить тип, и на основе этого получить byte[].

После этого объединить этот массив с тем что вернул GetBytes для url и только потом передать в ComputeHash

READ ALSO
Как правильно записать List&lt;string&gt; в БД с помощью Entity Framework?

Как правильно записать List<string> в БД с помощью Entity Framework?

Нужна база с такими двумерными списками, как ее правильно записать? Вряд ли получится сделать public DbSet<List<List<string>>> lst { get; set; } (я не пробовал)

154
Как открыть модальное окно ShowDialog() в FrameWork 4.0? Исключение in PresentationCore.dll

Как открыть модальное окно ShowDialog() в FrameWork 4.0? Исключение in PresentationCore.dll

Подскажите пожалуйста, разрабатываю приложение WPF под FrameWork 40, т

118
Фабрика для EF core в режиме dbcontext pool

Фабрика для EF core в режиме dbcontext pool

Необходимо сделать фабрику (проще лямбду конечно), которая бы выдавала свободный dbcontext из пула

155