2025年9月20日土曜日

JavaScript今さら入門(6)

今さらなんだけど、JavaScriptに挑戦してみるシリーズ。JavaScriptでgRPC Clientを作ってみる試み。Greeterから卒業。JavaScriptからDLLを呼び出せてるように見せるのを目指す。

何を題材にするか、、、まぁいつものやつです。
こういう構成でブラウザに平文と鍵を入力して暗号文を得るって感じにするってのにgPRCを使うわけです。
画面構成はこんな感じ

1. サーバー
サーバーはgRPCやってみる(4)をベースにして、 gRPCやってみる(6)gRPCやってみる(7)を参考にする。
プロジェクトフォルダーを作成してプロジェクトを生成
mkdir rijndael-srv
cd rijndael-srv
dotnet new grpc

Grpc.AspNetCore.Web パッケージへの参照を追加
dotnet add package Grpc.AspNetCore.Web

protoファイルをリネームして編集
mv Protos/greet.proto Protos/rijndael.proto
Protos/rijndael.proto
  1. syntax = "proto3";
  2.  
  3. option csharp_namespace = "rijndael_srv";
  4.  
  5. package rijndael_srv;
  6.  
  7. //rijndaelサービス定義
  8. service AES128EncSrv {
  9.   rpc AES128Enc (AES128EncRequest) returns (AES128EncReply);
  10. }
  11.  
  12. //リクエストメッセージはbytes型の平文とbytes型の暗号鍵を含む
  13. message AES128EncRequest{
  14.   bytes plain=1;
  15.   bytes key=2;
  16. }
  17.  
  18. //レスポンスメッセージはbytes型の暗号文
  19. message AES128EncReply{
  20.   bytes encoded=1;
  21. }

サービス(処理の実態)をリネームして編集
mv Services/GreeterService.cs Services/RijndaelService.cs
Services/RijndaelService.cs
  1. using Google.Protobuf;
  2. using Grpc.Core;
  3. using rijndael_srv;
  4. using System.Runtime.InteropServices;
  5.  
  6. namespace rijndael_srv.Services;
  7.  
  8. public class AES128EncSrvService : AES128EncSrv.AES128EncSrvBase
  9. {
  10.     private readonly ILogger<AES128EncSrvService> _logger;
  11.     public AES128EncSrvService(ILogger<AES128EncSrvService> logger)
  12.     {
  13.         _logger = logger;
  14.     }
  15.     //[DllImport("rijndael.dll", EntryPoint="AES128Encrypt")]
  16.     [DllImport("./librijndael.so", EntryPoint="AES128Encrypt")]
  17.     public static extern int AES128Encrypt(IntPtr plain,IntPtr key,IntPtr ciphered);
  18.     //[DllImport("rijndael.dll", EntryPoint="AES128Decrypt")]
  19.     [DllImport("./librijndael.so", EntryPoint="AES128Decrypt")]
  20.     public static extern int AES128Decrypt(IntPtr ciphered,IntPtr key,IntPtr plain);
  21.     
  22.     public override Task<AES128EncReply> AES128Enc(AES128EncRequest request, ServerCallContext context)
  23.     {
  24.         int ret;
  25.         byte[] encrypted={0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00};
  26.         byte[] plain=request.Plain.ToByteArray();
  27.         byte[] key=request.Key.ToByteArray();
  28.         int plain_size = Marshal.SizeOf(plain[0]) * plain.Length;
  29.         string txt="";
  30.         foreach(byte b in plain){
  31.             txt=txt+string.Format("{0,3:X2}",b);
  32.         }
  33.         Console.WriteLine(txt);
  34.         txt="";
  35.         foreach(byte b in key){
  36.             txt=txt+string.Format("{0,3:X2}",b);
  37.         }
  38.         Console.WriteLine(txt);
  39.  
  40.         if(plain_size<16)plain_size=16;
  41.         IntPtr plain_intPtr = Marshal.AllocHGlobal(plain_size);
  42.         int key_size = Marshal.SizeOf(key[0]) * key.Length;
  43.         if(key_size<16)key_size=16;
  44.         IntPtr key_intPtr = Marshal.AllocHGlobal(key_size);
  45.         int encrypted_size = Marshal.SizeOf(encrypted[0]) * encrypted.Length;
  46.         IntPtr encrypted_intPtr = Marshal.AllocHGlobal(encrypted_size);
  47.  
  48.         Marshal.Copy(plain, 0, plain_intPtr, plain_size);
  49.                 Marshal.Copy(key, 0, key_intPtr, key_size);
  50.         Marshal.Copy(encrypted, 0, encrypted_intPtr, encrypted_size);
  51.         ret=AES128Encrypt(plain_intPtr,key_intPtr,encrypted_intPtr);
  52.         Marshal.Copy(encrypted_intPtr, encrypted, 0, encrypted.Length);
  53.  
  54.         txt="";
  55.         foreach(byte b in encrypted){
  56.             txt=txt+string.Format("{0,3:X2}",b);
  57.         }
  58.         Console.WriteLine(txt);
  59.  
  60.         return Task.FromResult(new AES128EncReply
  61.         {
  62.             Encoded = ByteString.CopyFrom(encrypted)
  63.         });
  64.     }
  65. }

Program.csを編集
Program.cs
  1. using rijndael_srv.Services;
  2. var builder = WebApplication.CreateBuilder(args);
  3.  
  4. // gRPC-Webクライアントとの通信のために、KestrelをHTTP/1.1で動作させるよう設定します。
  5. builder.WebHost.ConfigureKestrel(options =>
  6. {
  7.     options.ListenLocalhost(5052, o => o.Protocols = Microsoft.AspNetCore.Server.Kestrel.Core.HttpProtocols.Http1);
  8. });
  9.  
  10. // gRPCサービスとCORSポリシーをアプリケーションに登録します。
  11. builder.Services.AddGrpc();
  12. builder.Services.AddCors(o => o.AddPolicy("AllowAll", builder =>
  13. {
  14.     builder.AllowAnyOrigin()
  15.             .AllowAnyMethod()
  16.             .AllowAnyHeader()
  17.             .WithExposedHeaders("Grpc-Status", "Grpc-Message",
  18.                 "Grpc-Encoding", "Grpc-Accept-Encoding",
  19.                 "Grpc-Status-Details-Bin");
  20. }));
  21.  
  22. var app = builder.Build();
  23.  
  24. // gRPC-Webミドルウェアを有効にします。
  25. app.UseGrpcWeb();
  26.  
  27. // CORSミドルウェアを有効にします。
  28. app.UseCors();
  29.  
  30. // Configure the HTTP request pipeline.
  31. app.MapGrpcService<AES128EncSrvService>().EnableGrpcWeb().RequireCors("AllowAll");
  32. app.MapGet("/", () => "Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909");
  33.  
  34. app.Run();
よくよく見ると、HTTP/1.1を強制する部分でポートを固定化してるんだね、、、

rijndael-srv.csprojを編集(protoファイルの部分)
rijndael-srv.csproj
  1. <Project Sdk="Microsoft.NET.Sdk.Web">
  2.   <PropertyGroup>
  3.     <TargetFramework>net8.0</TargetFramework>
  4.     <Nullable>enable</Nullable>
  5.     <ImplicitUsings>enable</ImplicitUsings>
  6.   </PropertyGroup>
  7.   <ItemGroup>
  8.     <Protobuf Include="Protos\rijndael.proto" GrpcServices="Server" />
  9.   </ItemGroup>
  10.   <ItemGroup>
  11.     <PackageReference Include="Grpc.AspNetCore" Version="2.57.0" />
  12.     <PackageReference Include="Grpc.AspNetCore.Web" Version="2.71.0" />
  13.   </ItemGroup>
  14. </Project>

dotnet buildからのdotnet run
dotnet build
dotnet run
、、、うまくいった
サーバーは実行時にDLLを参照するので、サーバーのプロジェクトルートフォルダにDLL(今回はLinuxなのでlibrijndael.so)を置いておく。

2. クライアント
クライアントはJavaScript今さら入門シリーズの集大成!

プロジェクトフォルダーを作成
mkdir rijndael-jscli
cd rijndael-jscli

もうすでにインストールされているけど、protocとgRPC-Webプラグインをインストールする。
tool-install.sh
  1. #!/bin/sh
  2. # プロトコルバッファコンパイラ(protoc)のインストール
  3. PROTOBUF_VERSION="32.1"
  4. wget "https://github.com/protocolbuffers/protobuf/releases/download/v${PROTOBUF_VERSION}/protoc-${PROTOBUF_VERSION}-linux-x86_64.zip"
  5. unzip "protoc-${PROTOBUF_VERSION}-linux-x86_64.zip" -d protoc_install
  6. sudo mv protoc_install/bin/protoc /usr/local/bin/
  7. sudo mv protoc_install/include /usr/local/include/
  8. rm -rf protoc_install*
  9.  
  10. # gRPC-Webプラグインのインストール
  11. GRPC_WEB_PLUGIN_VERSION="2.0.1"
  12. wget "https://github.com/grpc/grpc-web/releases/download/${GRPC_WEB_PLUGIN_VERSION}/protoc-gen-grpc-web-${GRPC_WEB_PLUGIN_VERSION}-linux-x86_64"
  13. sudo mv "protoc-gen-grpc-web-${GRPC_WEB_PLUGIN_VERSION}-linux-x86_64" /usr/local/bin/protoc-gen-grpc-web
  14. sudo chmod +x /usr/local/bin/protoc-gen-grpc-web
tool-install.shに実行属性を追加
chmod 755 tool-install.sh
tool-install.shを実行
./tool-install.sh

(↓これも前に(globalで)インストールしているから多分要らんけど)
protoc-gen-jsのインストール
sudo npm install -g protoc-gen-js

npmパッケージのインストール
npm init -y
npm install grpc-web google-protobuf


protoファイルの準備
mkdir Protos
cp ../rijndael-srv/Protos/* Protos

Protos/rijndael.proto
  1. syntax = "proto3";
  2. //option csharp_namespace = "rijndael_srv";
  3. package rijndael_srv;
  4. service AES128EncSrv {
  5.   rpc AES128Enc (AES128EncRequest) returns (AES128EncReply);
  6. }
  7. message AES128EncReply{
  8.   bytes encoded=1;
  9. }
  10.  
  11. message AES128EncRequest{
  12.   bytes plain=1;
  13.   bytes key=2;
  14. }

Protos/greet.protoファイルから、JavaScriptクライアントのコードを生成
生成先のディレクトリを作成
mkdir -p src/gen
protocを使ってコードを生成
-I: .protoファイルの場所を指定
--js_out: Protobufのメッセージクラスを生成
--grpc-web_out: gRPC-Webクライアントクラスを生成
protoc \
  -I=./Protos \
  --js_out=import_style=commonjs,binary:./src/gen \
  --grpc-web_out=import_style=commonjs,mode=grpcwebtext:./src/gen \
  ./Protos/rijndael.proto

src/gen/rijndael_grpc_web_pb.js

src/gen/rijndael_pb.js
ができる

ブラウザで開くhtmlファイルの作成(これがUIになる)
index.html
  1. <!DOCTYPE html>
  2. <html lang="ja">
  3. <head>
  4.   <meta charset="UTF-8">
  5.   <title>gRPC-Web Client</title>
  6. </head>
  7. <body>
  8.   <h1>gRPC-Web Client Example</h1>
  9.   <p>AES128 gRPCサーバーを使って暗号文を得る</p>
  10.   <input type="text" size=50 id="plain" placeholder="平文"><br>
  11.   <input type="text" size=50 id="key" placeholder="鍵"><br>
  12.   <button type="button" id="exec">実行</button><br>
  13.   <div id="out"></div><br>
  14.  
  15.   <script src="dist/bundle.js"></script>
  16. </body>
  17. </html>

JavaScriptクライアントの実装(ここが今回のキモ部分)
src/client.js
  1. // 生成されたコード
  2. const {AES128EncRequest} = require('./gen/rijndael_pb.js');
  3. const {AES128EncSrvClient} = require('./gen/rijndael_grpc_web_pb.js');
  4.  
  5. // gRPC-Webサービスのエンドポイントを指定
  6. const client = new AES128EncSrvClient('http://localhost:5052', null, null);
  7.  
  8. let plain=document.getElementById('plain');
  9. let key=document.getElementById('key');
  10. let out=document.getElementById('out');
  11. let button=document.getElementById('exec');
  12.  
  13. function button_clicked(){
  14.   const plain_txt=plain.value;
  15.   const key_txt=key.value;
  16.   const plain_pairs=plain_txt.match(/.{2}/g) || [];
  17.   const key_pairs=key_txt.match(/.{2}/g) || [];
  18.   const plain_arr=new Uint8Array(plain_pairs.map(byteString => parseInt(byteString, 16)));
  19.   const key_arr=new Uint8Array(key_pairs.map(byteString => parseInt(byteString, 16)));
  20.   
  21.   // リクエストを作成
  22.   const request = new AES128EncRequest();
  23.   request.setPlain(plain_arr);
  24.   request.setKey(key_arr);
  25.   
  26.   // AES128Encメソッドを呼び出し(関数名の先頭が勝手に小文字になる)
  27.   client.aES128Enc(request, {}, (err, response) => {
  28.     if (err) {
  29.       //console.error('Error: ', err.message);
  30.       console.error('gRPC error:', err.message, 'Details:', err.details, 'Code:', err.code);
  31.       return;
  32.     }
  33.     const b_encrypted = response.getEncoded();
  34.     // Uint8Arrayを16進数文字列に変換する
  35.     const txt = Array.from(b_encrypted)
  36.       .map(byte => byte.toString(16).padStart(2, '0').toUpperCase())
  37.       .join('');
  38.     console.log(txt);
  39.     out.textContent=txt;
  40.   });
  41. }
  42. button.addEventListener("click",button_clicked);

Webpackのインストールと設定
npm install --save-dev webpack webpack-cli
webpack.config.js作成
webpack.config.js
  1. // webpack.config.js
  2. const path = require('path');
  3. module.exports = {
  4.   entry: './src/client.js',
  5.   output: {
  6.     path: path.resolve(__dirname, 'dist'),
  7.     filename: 'bundle.js',
  8.   },
  9.   mode: 'development',
  10.   resolve: {
  11.     alias: {
  12.       './gen': path.resolve(__dirname, 'src', 'gen')
  13.     }
  14.   }
  15. };

バンドルを実行
npx webpack

3. ブラウザで実行
ブラウザで開く
そして、実行٩( 'ω' )و
できたーやってやったぜー

JavaScript入門は一旦おわろうかな、、、ほんと今さらだけど、まぁまぁおもしろいね(・∀・)

0 件のコメント:

コメントを投稿