Salesforce で DI できないか考えてみた
DI って?
Dependency Injection 外部依存注入の略。
DI の説明が要旨でもないので、下記 URL 参照。
Salesforce で DI ?
Salesforce のアプリケーションをパッケージで公開するとき、やってみるとわかるのですが、「global」修飾したオブジェクトしか利用者はカスタムできません。
そこは別に良いのですが、「色々カスタマイズできるようにしたい」とあれこれ global 指定してしまうと、以下のジレンマに悩まされます。
global インターフェースは絶対に変更できないので、公開すればしただけ変更ができない
ということになります。
特に、継承を許してしまうと致命的ですね。
内部挙動に依存したコードを書かれて、何か変更しようものなら「バグだゴルァ!」とクレームが飛ぶこと請け合いです。
そこで、機能を分離して、設定で上書きしたり継承したりできるようにできないか?
といったときに
インターフェースだけ公開して好きに実装しろ
と、内部挙動を勝手に実装しろ、ただしインターフェースの仕様は守れよと。
そんな感じでカスタムできる様にする手段として DI を考察しました。
基本原理
クラス名さえわかれば、引数なしコンストラクタ経由でインスタンスを生成できることがわかっているので、
- カスタム設定に「インターフェース名=実装クラス名」を実装させる
- コンテナがこれをみてインスタンスを生成する(未設定ならデフォルトの設定クラスをインスタンス化する)
- インターフェースに依存する形でアプリケーションを作成する
としておけば、顧客はカスタム設定を変更するだけで、実際の挙動を変更したりカスタムしたりできるようになるはずだ。
ということで検証した。
いざ実装
まずは検証対象クラスを作成
実際にはルートクラスと、インターフェースは global にでもするのだろうがここでは割愛。
public with sharing class SimpleExample { public interface ExampleInterface { String getMessage(); } public class DefaultImpl implements ExampleInterface { public String getMessage() { return 'Default implements'; } } }
DI 置き換え対象のクラスも作成
public with sharing class CustomImpl implements SimpleExample.ExampleInterface { public String getMessage() { return 'Custom implements'; } }
ここまでは大した難しい話ではないかも?
カスタム設定を追加する
設定の内容はあくまで検証用サンプルなので
- API 参照名
DISample__c - 項目
ExampleInterface__c : String(32)
DI コンテナマネージャの作成
DI を実際に行っていくクラスを作成する。
ここも実運用だと global だったり、ネームスペース指定とかカスタム設定を受け取るとか色々ありそうだが、今回はあくまで検証なので割愛して実装した。
public with sharing class DIManager { public class DISetting { private String interfaceName { get; private set; } private String settingName { get; private set; } private Type defaultObjectType { get; private set; } public DISetting(String interfaceName, String settingName, Type defaultObjectType) { this.interfaceName = interfaceName; this.settingName = settingName; this.defaultObjectType = defaultObjectType; } } private Map<String, DISetting> diSettings; public DIManager(List<DISetting> settings) { this.diSettings = new Map<String, DISetting>(); for (DISetting st : settings) { this.diSettings.put(st.interfaceName, st); } } public Object getObject(String interfaceName) { DISetting setting = this.diSettings.get(interfaceName); if (setting == null) { return null; } // not found DISample__c diSetting = DISample__c.getOrgDefaults(); String targetObjectName = diSetting != null ? (String) diSetting.get(setting.settingName) : null; if (String.isEmpty(targetObjectName)) { return this.getDefaultObject(interfaceName); } else { Type t = Type.forName(targetObjectName); return t.newInstance(); } } private Object getDefaultObject(String interfaceName) { DISetting setting = this.diSettings.get(interfaceName); if (setting == null) { return null; } // not found return setting.defaultObjectType.newInstance(); } }
検証
色々面倒なので匿名 Apex 実行をしてみる。
ソースは下記
DIManager.DISetting setting = new DIManager.DISetting('ExampleInterface', 'ExampleInterface__c', SimpleExample.DefaultImpl.class); DIManager manager = new DIManager(new List<DIManager.DISetting> { setting }); SimpleExample.ExampleInterface example = (SimpleExample.ExampleInterface) manager.getObject('ExampleInterface'); System.debug('Message : ' + example.getMessage());
まずは、カスタム設定を利用せずに実行する。
(前略) Execute Anonymous: System.debug('Message : ' + example.getMessage()); 17:05:25.25 (25031288)|USER_INFO|[EXTERNAL]|0052w000003PbHm|arstromeria@white-azalea.net|(GMT+09:00) 日本標準時 (Asia/Tokyo)|GMT+09:00 17:05:25.25 (25094245)|EXECUTION_STARTED 17:05:25.25 (25103087)|CODE_UNIT_STARTED|[EXTERNAL]|execute_anonymous_apex 17:05:25.25 (30135496)|USER_DEBUG|[6]|DEBUG|Message : Default implements ← 特に設定が存在しないのでデフォルト実装が動作してる 17:05:25.30 (30230158)|CUMULATIVE_LIMIT_USAGE 17:05:25.30 (30230158)|LIMIT_USAGE_FOR_NS|(default)| Number of SOQL queries: 0 out of 100 Number of query rows: 0 out of 50000 (以下略)
ではここで、カスタム設定を行ってみる。
この状態で実行すると
17:09:13.33 (33341882)|EXECUTION_STARTED 17:09:13.33 (33360008)|CODE_UNIT_STARTED|[EXTERNAL]|execute_anonymous_apex 17:09:13.33 (42787189)|USER_DEBUG|[6]|DEBUG|Message : Custom implements ← 設定によって利用されるインスタンスが書き換わった! 17:09:13.42 (42932602)|CUMULATIVE_LIMIT_USAGE 17:09:13.42 (42932602)|LIMIT_USAGE_FOR_NS|(default)|
ということができた。
ゼロから始めるCTF
勉強会
という事でこのご時世にもかかわらず突入してみました。
コワーキングスペース「Weeyble」さんのイベントですね。
CTF?
Capture The Flag.
ファイルやサーバに隠れたデータをハッキングなどの技術を用いて、引っこ抜くというスキルを競うイベントです。
この勉強会
コワーキングスペース主宰で、CTF の導入/入門を行っている勉強会です。
CTF には以下の種類がありますが
- 問題型:ファイルや暗号の中に隠されたフラッグを探す
- 対抗戦型:相手サーバ中のフラッグを奪取する
- 対抗戦型:相手サーバにフラッグを書き込み、その維持時間で得点を稼ぐ
この中の問題型をメインに取り扱ってます。
やったこと
ファイルシステムに依存する隠しファイル
代替データストリームの紹介がありました。
- NTFS 専用の隠しデータ
- 通常みることはできないが特定手段で参照できる
こういう情報もあるという。
PDF の隠し情報を取得する
PDF は過去なかなかの問題を排出したフォーマットです。
- 黒塗りしただけの情報を取得する(Libre Office を利用して黒塗りを除去)
- パスワードクラック(ubuntu のアプリケーション pdfcrack )
- フォントを分離してフォント内に隠された情報を取得する(www.pdfconverteronline.com でフォント抽出)
という様なことを行いました。
SQL Injection
SQL エスケープを怠ったチャットボットプログラムに対し、いくつかのクエリを与えてクエリを推測。
SQL を食わせてテーブルリスト取得→テーブル構造取得→データ引き抜き を行いました。
所感
普通業務では「防ぎ方」しか教わりません。
攻撃者が何を思い、どう対処するのかなどやってみなければわからない…
そんな中「合法的に攻撃をテストできる」というのはかなり新鮮。
防御方法の検討や、他のやり方についても考えさせてくれる感じでしたね。
Salesforceのログにインデントをつけるアプリを nodejs で
Salesforce
2019/4 から Salesforce エンジニアに転職したつつじーです。
半年以上 Salesforce にどっぷり浸かってました(汗
Salesforce は SaaS の大御所で、およそ企業に必要な販売管理、売り上げ管理といった機能をデフォルトで持っています。
また、カスタマイズ機能が非常に豊富で、テーブル定義、処理フロー定義、画面設定、トリガーの設定、アクセス権限設定、バッチの指定、集計の設定、統計の設定、売り上げ予測。
こうした機能が「一切プログラムせずに画面上から設定できる」という強力さがあります。
そんな Salesforce ですから、利用しよう、導入しようという「非IT開発系ベンダー」は凄まじく多いです。
一方でその需要を満たすだけの、カスタマイズサポート、カスタマイズでできない部分の開発などを行うエンジニアが圧倒的に不足中です。
私も就職してから、「なぜエンジニアが死ぬほど少ないか」を理解しました… APEX という存在です。
Apex とは
Apex は Salesforce でほぼ唯一と言っていい開発言語です。
そして 劣化 Java 1.4 と呼べる クソ 言語 です。
Apex は画面上のカスタマイズで補いきれない要件を解決するために実装された独自言語です。
Salesforce では全ての挙動がクラウド上で実行されており、当然そこには複数の企業があります。
となると、リソースの問題や安全の問題が出てくるのですね。
そこで Salesforce は開発者が好き勝手できない様に、わざと制限のある言語を実装したわけです。
ただし、汎用的に言語を作ろうとしたのではなく、あくまで「補助スクリプト」という位置付けで始まり、拡張を繰り返した挙句、一貫性のないひどい言語に仕上がってしまいました(汗
Salesforce もその酷さを理解しているので、最近は pure nodeJs で置き換えれる様に頑張っている様です。
とはいえ、それが実用になるまでは Apex と付き合わざるを得ないので、色々頑張って書いてるのが現状です。
Salesforce の Apex ログ
他の言語から入ってきた人間からみたときに
- 特にログ出力コードを書かなくても実行ログを出せる
- ヒープの確保、処理行の実行(with行番号)、メソッドコール&終了、変数への代入、処理時間、クエリの実行内容、実行結果のサマリ など
そう、およそブレークポイントで各行止めて記録したかのようになかなかに詳細なログを自動で吐いてくれます。
そのため、このログを正しく読み取れれば、問題解決もめちゃめちゃスムーズにやれます。
実際、「デバッグのためにログを埋め込む」という必要が無くなるくらいには詳細で強力なログが出ます。
(引き換えに実行されるのがクラウド上であるため、ブレークを仕掛ける…といった運用はできませんが…)
Python3 で負荷テストを簡単に
for Python 3.7.x
テスト対象のサーバを起動する
require: Docker version 19.03.+
docker-compose.yml
にざっくり記述。
version: "1" services: wordpress: image: wordpress:latest ports: - 9000:80 links: - wordpress-db environment: WORDPRESS_DB_HOST: wordpress-db:3306 WORDPRESS_DB_NAME: wordpress WORDPRESS_DB_USER: wp_user WORDPRESS_DB_PASSWORD: database_password wordpress-db: image: mysql:5.7 ports: - 3306:3306 environment: MYSQL_ROOT_PASSWORD: database_password MYSQL_DATABASE: wordpress MYSQL_USER: wp_user MYSQL_PASSWORD: database_password
今回はテスト対象サーバとして、Wordpress を用意しました。
対象の設定はこのディレクトリにある docker-compose.yml で、次のコマンドで起動します。
$ docker-compose up --detach
http://localhost:9000
にアクセスすると、Wordpress のセットアップが見れるはずです。
適当にセットアップしてしまいましょう。
ID/Password はこのとき控えておいてください。
[f:id:white-azalea:20191002221611p:plain] [f:id:white-azalea:20191002221629p:plain] [f:id:white-azalea:20191002221646p:plain]
ついでにログインできることも確認しましょう。
[f:id:white-azalea:20191002221702p:plain] [f:id:white-azalea:20191002221719p:plain]
LOCUST インストール
Python 用の負荷テストツールをインストールします。
これもライブラリとして配布されてますので、pip からインストールできます。
$ sudo pip install locustio
このツールはインストール時にコマンドも入るので、確認してみます。
$ locust --version [2019-10-02 21:12:48,992] anyone-macbookpro.local/INFO/stdout: Locust 0.11.0 [2019-10-02 21:12:48,992] anyone-macbookpro.local/INFO/stdout:続きを読む
瞬間 HTTP サーバ
for Python 3.7.x
HTTP サーバを単独起動
Python には HTTP サーバがデフォルトで樽座しています。
あらかじめ example/index.html
を用意しておきます。
その上で
$ cd example $ python -m http.server 8000 Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/)
後は Chrome でアクセスしてみます。
死ぬほど簡単でしょう?
プログラムで実行
プログラムからも指定して起動することもできます。
import http.server import socketserver PORT = 8000 Handler = http.server.SimpleHTTPRequestHandler with socketserver.TCPServer(("", PORT), Handler) as httpd: print("Start server at: ", PORT) httpd.serve_forever()
Python 3.x でのシェルとの共存
Python3.7.x で動作確認。
ちまちま記事を追加中。
シェルとの相互運用周りを行うサンプルです。
Python のシェルライク起動
Python を python
コマンド引数でなく実行する方法です。
先頭に #!/usr/bin/python
を突っ込んで実行権限を与えます。
#!/usr/bin/python print('fire')
この状態で実行権限を付与して実行します。
$ chmod +x hello.py $ ./hello.py fire
パイプから呼び出す処理を作成する
パイプから呼び出される際、その値は標準入力から設定されます。
注意点として、ある程度まとまった結果を標準入力でまとめて受け流という点です。
サンプルとして simple.py
を用意しました。
#!/usr/bin/python import os import sys value = sys.stdin.read() # パイプの入力 value = value.replace('//', '/') # / が // として入力されるのでリプレース splitted = value.split("\n") # 改行で分解し for line in splitted: if os.path.isfile(line): # それがファイルで filename, ext = os.path.splitext(line) if '.txt' == ext: # 拡張子が txt なら中身を表示します with open(line, mode='r') as rf: print(''.join(rf.readlines()))
そして実行してみる。
1行目はファイル構成。この値が標準入力に入ってきます。
$ find ./sampleTexts/ ./sampleTexts/ ./sampleTexts//dontcall.md ./sampleTexts//example.txt $ $ chmod +x simple.py $ find ./sampleTexts/ | ./simple.py Hello python: shell command.
Python からシェルコマンドを実行する
一番わかりやすいところで ls
コマンドをサブプロセスとして呼んでみます。
shell_command.py
というファイル名で作成してます。
#!/usr/local/bin/python3 import subprocess proc = subprocess.run(["ls"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) print(proc.stdout.decode("utf8"))
単純にパイプを呼び出す
パイプを実行するもっともシンプルな方法。
simple_pipe.py
として保存。
#!/usr/local/bin/python3 import subprocess res = subprocess.check_output( "ls | grep si", shell=True, stderr=subprocess.STDOUT) print(res.decode())
Python からパイプを実行してみる
といってもパイプのアウトプットを連結するだけです。
途中に Python 処理を挟みたければ挟めば?って用途ですね。
pipe_basic.py
として用意します。
#!/usr/local/bin/python3 import subprocess p1 = subprocess.Popen(["ls"], stdout=subprocess.PIPE) p2 = subprocess.Popen(["grep", "py"], stdin=p1.stdout, stdout=subprocess.PIPE) p1.stdout.close() result = p2.communicate()[0] print(result.decode())