CefSharpを使ってJavascriptからC#コードを呼び出す

2019年12月14日

仕事で必要に追われて調査した結果を記録しておきます。今回はプロジェクトがふたつあります。一つは、CefSharpを操作するためのWinFormsプロジェクトで、もう一つはC#コードを呼び出すJavaScriptを記述するWebアプリのプロジェクトです。

セキュリティの問題(前置き)

Webから操作端末のExeを起動したり、ファイルを操作することは、Chromeだとセキュリティ的にできない。(IEだとActiveXで実現できたりするけど)
やりたい!というニーズは結構あると思う。
ただ、考えてみればわかるが、仮にこれができてしまうと、非常に危険だとわかる。例えば、ページをロードしたタイミングで、操作端末のcmdを呼び出してcURLなりで悪意あるソフトをダウンロードして実行したり、任意のファイルを削除したり等。できちゃうよね。

さて、それでもお客様の無茶から実現しないといけないということがあります。Webサーバだけで考えると、 物理的に無理なのですが、Webにアクセスするブラウザも自前で用意すれば実現できます。

今回は、このような無茶ぶりをCefSharpを使って解決したので記録に残しています。エッセンス部分だけ抜き出して、小さなプロジェクトを二つ作って例示したいと思います。

クライアント側のプロジェクトを作成( CefSharpのインスタンスを呼ぶほう)

こちらがCefSharp側のリポジトリです。

前提

Windows フォーム アプリケーション(.NET Framework)
構成:x64
言語:C#
フレームワーク:.NET Framework 4.7.2
CefSharp:バージョン75.1.143

プロジェクト作成

AnyCPU構成だと少し面倒なのでX64構成とする。
AnyCPUで動かしたい場合はこちらを参照してください。

プロジェクトをX64構成にする。
構成マネージャを開く

アクティブソリューションプラットフォームで新規作成を選ぶ。

以下の設定でOKする。

これでx64構成ができあがり。

次にCefSharpをNugetからインストールする。
以下AnyCPU構成で使う方法が書いてあるけど、パッケージのダウンロード方法については同様なので参考にしてください。

ソースコード(C#)

C#側のソースは以下。
JavaScriptから呼び出されるメソッド等については、後ほど説明していきます。
ここで一点、注意が必要なのが、以下の部分。

_browser = new ChromiumWebBrowser("https://localhost:44315/");
呼び出すURLはそれぞれデバッグ時の環境に合わせて書き換えてください。

using CefSharp;
using CefSharp.WinForms;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace CefSharpCallFromJavaScript
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
            InitializeChromium();
        }

        private void Form1_FormClosing(object sender, FormClosingEventArgs e)
        {
            Cef.Shutdown();
        }

        private ChromiumWebBrowser _browser;

        public void InitializeChromium()
        {
            CefSharpSettings.LegacyJavascriptBindingEnabled = true;
            CefSettings settings = new CefSettings();
            // これを入れないと黒い余白が発生していまう。
            Cef.EnableHighDPISupport();
            Cef.Initialize(settings);
            // こちらの接続先はWeb側でデバッグ実行した時に表示されるローカルの接続先をコピペしてください。
            _browser = new ChromiumWebBrowser("https://localhost:44315/");
            Controls.Add(_browser);
            _browser.Dock = DockStyle.Fill;

            var eventObject = new ScriptedMethodsBoundObject();
            eventObject.EventArrived += OnJavascriptEventArrived;
            _browser.RegisterJsObject("boundEvent", eventObject, options: BindingOptions.DefaultBinder);
        }

        public class ScriptedMethodsBoundObject
        {
            public event Action<string, object> EventArrived;

            public void RaiseEvent(string eventName, object eventData = null)
            {
                if (EventArrived != null)
                {
                    EventArrived(eventName, eventData);
                }
            }
        }

        public static void OnJavascriptEventArrived(string eventName, object eventData)
        {
            var jsonString = eventData.ToString();
            string path = string.Empty;
            if (jsonString.Contains("memo"))
            {
                path = @"notepad.exe";
                Process.Start(path);
            }

            Console.WriteLine("Event arrived: {0}", eventName); // output 'click'

        }
    }
}

CefSharpで呼び出される側のプロジェクトを用意する

こちらがWeb側のリポジトリです。

前提

ASP.NET Core Web アプリケーション(MVC)
構成:x64
言語:C#,HTML,CSS,JavaScript
フレームワーク:.NET Core3.1.0

プロジェクト作成

ソースコード

@{
    ViewData["Title"] = "Home Page";
}

<div class="text-center">
    <h1 class="display-4">Welcome</h1>
    <p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
</div>
<button onclick="test('memo')">メモ帳起動</button>
<script>
    console.log("test");

    function test(arg) {
        if (!window.boundEvent) {
            alert('window.boundEvent does not exist.');
            return;
        }
        window.boundEvent.raiseEvent('click', JSON.stringify({
            data1: arg,
        }));
    }

</script>

デバッグのやり方

メモ帳をWeb側で設置したボタンから起動する

上記ソースをすべて準備したら実際にデバッグして処理を追いかけてみましょう。
まず、WebAppForCefCallプロジェクトをデバッグ実行します。
すると、以下のようなページが立ち上がります。
Chromeで立ち上がるようにしてます。

この状態でメモ帳起動ボタンを押すと、
window.boundEvent does not exist.
とアラートが出るはずです。

WebAppForCefCallをデバッグ実行したままで、次にCefSharpCallFromJavaScriptをデバッグ実行します。
すると、WinFormsで作ったFormが立ち上がります。

この状態で先ほどと同様にメモ帳起動ボタンを押すと、メモ帳が起動します。

ブレークポイントを置いてみる

ブレークポイントをOnJavascriptEventArrivedメソッドの中において、再度ボタンをクリックしてみる。

すると、このブレークポイントで処理が止まるので、eventDataの中身を見てみましょう。
中には{"data1":"memo"}というのがあり、これは以下コードから渡されたものになります。

    window.boundEvent.raiseEvent('click', JSON.stringify({
        data1: arg,
    }));

JavaScript側からパラメータを渡してEventArgsとしてC#で受け取ることもできることがわかります。
HTTPリクエストからではなく、JavaScriptから直接 C#コードが呼び出されているのがわかると思います。

なんか黒い余白があるんだけど

という問題に直面したので、その対応策については以下を参考に修正してください。

ソースコードの説明

ここからはソースコードの説明をしていきます。
大体はソースコードを眺めれば理解できると思いますが、ポイントだけ自分のためにも記録しておきます。

LegacyJavascriptBindingEnabledをTrueにする

これをしないと実行時に
_browser.RegisterJsObject("boundEvent", eventObject, options: BindingOptions.DefaultBinder);部分でエラーが発生します。
以下のようなエラー。
やり方が

System.Exception: ‘CefSharpSettings.LegacyJavascriptBindingEnabled is currently false,
for legacy binding you must set CefSharpSettings.LegacyJavascriptBindingEnabled = true
before registering your first object see https://github.com/cefsharp/CefSharp/issues/2246
for details on the new binding options. If you perform cross-site navigations bound objects will
no longer be registered and you will have to migrate to the new method.’

ざっくり略すと

レガシーバインディングの場合、CefSharpSettings.LegacyJavascriptBindingEnabled = trueを設定する必要があります。新しいバインディングオプションの詳細については、
 https://github.com/cefsharp/CefSharp/issues/2246をご覧ください クロスサイトナビゲーションを実行すると、バインドされたオブジェクトは登録されなくなったため、新しい方法に移行する必要があります。

今回は、対応していませんが、こちらのやり方のほうがよさそう。
こちらは、ブランチで作業中(まだサンプルを作ってる途中)